Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,48 @@ public async Task<IActionResult> GetConnections(
return Ok(PaginatedResult.Create(result.Value, null));
}

/// <summary>
/// Gets all available users who already have some access from the specified party and are available to receive new delegations.
/// </summary>
[HttpGet("users")]
[Authorize(Policy = AuthzConstants.POLICY_ENDUSER_CONNECTIONS_WRITE_TOOTHERS)]
[Authorize(Policy = AuthzConstants.POLICY_INSTANCE_DELEGATION)]
[ProducesResponseType<PaginatedResult<SimplifiedConnectionDto>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
[ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> GetAvailableUsers(
[Required][FromQuery(Name = "party")] Guid party,
[FromQuery, FromHeader] PagingInput paging,
CancellationToken cancellationToken = default)
{
var validationErrors = ValidationComposer.Validate(
ParameterValidation.Party(party.ToString()),
ConnectionValidation.ValidateReadConnection(party.ToString(), party.ToString(), null));
if (validationErrors is { })
{
return validationErrors.ToActionResult();
}

var result = await ConnectionService.Get(
party,
party,
null,
includeClientDelegations: true,
includeAgentConnections: true,
ConfigureConnections,
cancellationToken
);

if (result.IsProblem)
{
return result.Problem.ToActionResult();
}

var simplifiedConnections = DtoMapper.ToSimplifiedConnections(result.Value);
return Ok(PaginatedResult.Create(simplifiedConnections, null));
}

#region Assignment

/// <summary>
Expand Down Expand Up @@ -1072,5 +1114,64 @@ public async Task<IActionResult> CheckInstance(
return Ok(result.Value);
}

/// <summary>
/// Gets all users who have access to a specific instance.
/// </summary>
[HttpGet("resources/instances/users")]
[Authorize(Policy = AuthzConstants.POLICY_ENDUSER_CONNECTIONS_BIDRECTIONAL_READ)]
[Authorize(Policy = AuthzConstants.POLICY_INSTANCE_DELEGATION)]
[ProducesResponseType<PaginatedResult<SimplifiedPartyDto>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
[ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> GetInstanceUsers(
[Required][FromQuery(Name = "party")] Guid party,
[Required][FromQuery(Name = "resource")] string resource,
[Required][FromQuery(Name = "instance")] string instance,
[FromQuery, FromHeader] PagingInput paging,
CancellationToken cancellationToken = default)
{
var validationErrors = ValidationComposer.Validate(
ParameterValidation.Party(party.ToString()),
ParameterValidation.InstanceUrn(instance));
if (validationErrors is { })
{
return validationErrors.ToActionResult();
}

var resourceObj = await resourceService.GetResource(resource, cancellationToken);
if (resourceObj is null)
{
ProblemDetails problem = Core.Errors.Problems.InvalidResource.ToProblemDetails();
problem.Extensions["resource"] = resource;
problem.Extensions["instance"] = instance;
return problem.ToActionResult();
}

var result = await ConnectionService.GetResourceInstances(
party,
fromId: party,
toId: null,
resourceId: resourceObj.Id,
instanceId: instance,
configureConnections: ConfigureConnections,
cancellationToken: cancellationToken
);

if (result.IsProblem)
{
return result.Problem.ToActionResult();
}

var users = result.Value
.SelectMany(inst => inst.Permissions ?? Enumerable.Empty<PermissionDto>())
.Where(perm => perm.To != null)
.Select(perm => DtoMapper.ToSimplifiedParty(perm.To))
.DistinctBy(p => p.Id)
.ToList();

return Ok(PaginatedResult.Create(users, null));
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Altinn.Authorization.Api.Contracts.AccessManagement;

namespace Altinn.AccessMgmt.Core.Utils;

/// <summary>
/// Mapper extension for simplified connections
/// </summary>
public partial class DtoMapper
{
/// <summary>
/// Converts a CompactEntityDto to SimplifiedPartyDto, excluding sensitive personal information
/// </summary>
/// <param name="entity">The entity to convert</param>
/// <returns>A simplified party DTO without PersonIdentifier/SSN, or null if entity is null</returns>
/// <remarks>This method explicitly excludes PersonIdentifier to comply with data privacy requirements</remarks>
public static SimplifiedPartyDto? ToSimplifiedParty(CompactEntityDto? entity)
{
if (entity is null)
{
return null;
}

return new SimplifiedPartyDto
{
Id = entity.Id,
Name = entity.Name,
Type = entity.Type,
Variant = entity.Variant,
OrganizationIdentifier = entity.OrganizationIdentifier,
IsDeleted = entity.IsDeleted,
DeletedAt = entity.DeletedAt
};
}

/// <summary>
/// Converts a ConnectionDto to SimplifiedConnectionDto
/// </summary>
/// <param name="connection">The connection to convert</param>
/// <returns>A simplified connection DTO, or null if connection is null</returns>
public static SimplifiedConnectionDto? ToSimplifiedConnection(ConnectionDto? connection)
{
if (connection is null)
{
return null;
}

return new SimplifiedConnectionDto
{
Party = ToSimplifiedParty(connection.Party),
Connections = connection.Connections?.Select(ToSimplifiedConnection).ToList() ?? []
};
}

/// <summary>
/// Converts a collection of ConnectionDto to simplified connections
/// </summary>
/// <param name="connections">The connections to convert</param>
/// <returns>A collection of simplified connection DTOs</returns>
public static IEnumerable<SimplifiedConnectionDto> ToSimplifiedConnections(IEnumerable<ConnectionDto>? connections)
{
return connections?.Select(ToSimplifiedConnection) ?? Enumerable.Empty<SimplifiedConnectionDto>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;

namespace Altinn.Authorization.Api.Contracts.AccessManagement;

/// <summary>
/// Simplified connection for listing available users
/// </summary>
public class SimplifiedConnectionDto
{
/// <summary>
/// The party information
/// </summary>
[JsonPropertyName("party")]
public SimplifiedPartyDto Party { get; set; }

/// <summary>
/// Sub-connections (nested users under this party)
/// </summary>
[JsonPropertyName("connections")]
public List<SimplifiedConnectionDto> Connections { get; set; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Text.Json.Serialization;

namespace Altinn.Authorization.Api.Contracts.AccessManagement;

/// <summary>
/// Simplified party information for connections API responses
/// </summary>
public class SimplifiedPartyDto
{
/// <summary>
/// The unique identifier for the party
/// </summary>
[JsonPropertyName("id")]
public Guid Id { get; set; }

/// <summary>
/// The name of the party
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }

/// <summary>
/// The type of party (Person, Organization, etc.)
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }

/// <summary>
/// The variant/subtype of the party
/// </summary>
[JsonPropertyName("variant")]
public string Variant { get; set; }

/// <summary>
/// Organization number (only for organizations)
/// </summary>
[JsonPropertyName("organizationIdentifier")]
public string? OrganizationIdentifier { get; set; }

/// <summary>
/// Indicates if the party is deleted
/// </summary>
[JsonPropertyName("isDeleted")]
public bool IsDeleted { get; set; }

/// <summary>
/// The timestamp when the party was deleted
/// </summary>
[JsonPropertyName("deletedAt")]
public DateTimeOffset? DeletedAt { get; set; }
}
Loading