Skip to content

Commit 6048127

Browse files
Connections API | GET connections/users & Connections API | GET /resource/instances/users (#2613)
* feat: Add endpoints for available users and instance access users (#2579, #2581) Add two new GET endpoints to ConnectionsController: - GET /connections/users - retrieves available users with existing access from a party - GET /resources/instances/users - lists users with access to a specific instance New DTOs: - SimplifiedPartyDto: party information excluding sensitive personal identifiers - SimplifiedConnectionDto: simplified connection structure without access details New mapper utilities: - DtoMapper.Simplified: conversion methods for simplified DTOs with nullable reference types Both endpoints require POLICY_INSTANCE_DELEGATION authorization and exclude PersonIdentifier/SSN from responses for data privacy compliance. * fix: Add party validation and remove unused using in simplified mapper - Add ParameterValidation.Party() to GetAvailableUsers and GetInstanceUsers endpoints to reject Guid.Empty party parameter with 400 Bad Request - Remove unused Altinn.AccessMgmt.PersistenceEF.Models using directive from DtoMapper.Simplified.cs * feat: Add scope authorization to new user endpoints Add proper scope-based authorization policies to GetAvailableUsers and GetInstanceUsers endpoints to align with existing controller patterns: - GetAvailableUsers (GET /connections/users): * POLICY_ENDUSER_CONNECTIONS_WRITE_TOOTHERS - write preparation endpoint * POLICY_INSTANCE_DELEGATION - instance delegation specific - GetInstanceUsers (GET /resources/instances/users): * POLICY_ENDUSER_CONNECTIONS_BIDRECTIONAL_READ - read operation * POLICY_INSTANCE_DELEGATION - instance delegation specific This ensures proper authorization checks consistent with other endpoints like CheckInstance, GetInstanceRights, and delegation check endpoints.
1 parent 1c2e3cf commit 6048127

File tree

4 files changed

+236
-0
lines changed

4 files changed

+236
-0
lines changed

src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionsController.cs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,48 @@ public async Task<IActionResult> GetConnections(
8989
return Ok(PaginatedResult.Create(result.Value, null));
9090
}
9191

92+
/// <summary>
93+
/// Gets all available users who already have some access from the specified party and are available to receive new delegations.
94+
/// </summary>
95+
[HttpGet("users")]
96+
[Authorize(Policy = AuthzConstants.POLICY_ENDUSER_CONNECTIONS_WRITE_TOOTHERS)]
97+
[Authorize(Policy = AuthzConstants.POLICY_INSTANCE_DELEGATION)]
98+
[ProducesResponseType<PaginatedResult<SimplifiedConnectionDto>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
99+
[ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
100+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
101+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
102+
public async Task<IActionResult> GetAvailableUsers(
103+
[Required][FromQuery(Name = "party")] Guid party,
104+
[FromQuery, FromHeader] PagingInput paging,
105+
CancellationToken cancellationToken = default)
106+
{
107+
var validationErrors = ValidationComposer.Validate(
108+
ParameterValidation.Party(party.ToString()),
109+
ConnectionValidation.ValidateReadConnection(party.ToString(), party.ToString(), null));
110+
if (validationErrors is { })
111+
{
112+
return validationErrors.ToActionResult();
113+
}
114+
115+
var result = await ConnectionService.Get(
116+
party,
117+
party,
118+
null,
119+
includeClientDelegations: true,
120+
includeAgentConnections: true,
121+
ConfigureConnections,
122+
cancellationToken
123+
);
124+
125+
if (result.IsProblem)
126+
{
127+
return result.Problem.ToActionResult();
128+
}
129+
130+
var simplifiedConnections = DtoMapper.ToSimplifiedConnections(result.Value);
131+
return Ok(PaginatedResult.Create(simplifiedConnections, null));
132+
}
133+
92134
#region Assignment
93135

94136
/// <summary>
@@ -1072,5 +1114,64 @@ public async Task<IActionResult> CheckInstance(
10721114
return Ok(result.Value);
10731115
}
10741116

1117+
/// <summary>
1118+
/// Gets all users who have access to a specific instance.
1119+
/// </summary>
1120+
[HttpGet("resources/instances/users")]
1121+
[Authorize(Policy = AuthzConstants.POLICY_ENDUSER_CONNECTIONS_BIDRECTIONAL_READ)]
1122+
[Authorize(Policy = AuthzConstants.POLICY_INSTANCE_DELEGATION)]
1123+
[ProducesResponseType<PaginatedResult<SimplifiedPartyDto>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
1124+
[ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
1125+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
1126+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
1127+
public async Task<IActionResult> GetInstanceUsers(
1128+
[Required][FromQuery(Name = "party")] Guid party,
1129+
[Required][FromQuery(Name = "resource")] string resource,
1130+
[Required][FromQuery(Name = "instance")] string instance,
1131+
[FromQuery, FromHeader] PagingInput paging,
1132+
CancellationToken cancellationToken = default)
1133+
{
1134+
var validationErrors = ValidationComposer.Validate(
1135+
ParameterValidation.Party(party.ToString()),
1136+
ParameterValidation.InstanceUrn(instance));
1137+
if (validationErrors is { })
1138+
{
1139+
return validationErrors.ToActionResult();
1140+
}
1141+
1142+
var resourceObj = await resourceService.GetResource(resource, cancellationToken);
1143+
if (resourceObj is null)
1144+
{
1145+
ProblemDetails problem = Core.Errors.Problems.InvalidResource.ToProblemDetails();
1146+
problem.Extensions["resource"] = resource;
1147+
problem.Extensions["instance"] = instance;
1148+
return problem.ToActionResult();
1149+
}
1150+
1151+
var result = await ConnectionService.GetResourceInstances(
1152+
party,
1153+
fromId: party,
1154+
toId: null,
1155+
resourceId: resourceObj.Id,
1156+
instanceId: instance,
1157+
configureConnections: ConfigureConnections,
1158+
cancellationToken: cancellationToken
1159+
);
1160+
1161+
if (result.IsProblem)
1162+
{
1163+
return result.Problem.ToActionResult();
1164+
}
1165+
1166+
var users = result.Value
1167+
.SelectMany(inst => inst.Permissions ?? Enumerable.Empty<PermissionDto>())
1168+
.Where(perm => perm.To != null)
1169+
.Select(perm => DtoMapper.ToSimplifiedParty(perm.To))
1170+
.DistinctBy(p => p.Id)
1171+
.ToList();
1172+
1173+
return Ok(PaginatedResult.Create(users, null));
1174+
}
1175+
10751176
#endregion
10761177
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using Altinn.Authorization.Api.Contracts.AccessManagement;
2+
3+
namespace Altinn.AccessMgmt.Core.Utils;
4+
5+
/// <summary>
6+
/// Mapper extension for simplified connections
7+
/// </summary>
8+
public partial class DtoMapper
9+
{
10+
/// <summary>
11+
/// Converts a CompactEntityDto to SimplifiedPartyDto, excluding sensitive personal information
12+
/// </summary>
13+
/// <param name="entity">The entity to convert</param>
14+
/// <returns>A simplified party DTO without PersonIdentifier/SSN, or null if entity is null</returns>
15+
/// <remarks>This method explicitly excludes PersonIdentifier to comply with data privacy requirements</remarks>
16+
public static SimplifiedPartyDto? ToSimplifiedParty(CompactEntityDto? entity)
17+
{
18+
if (entity is null)
19+
{
20+
return null;
21+
}
22+
23+
return new SimplifiedPartyDto
24+
{
25+
Id = entity.Id,
26+
Name = entity.Name,
27+
Type = entity.Type,
28+
Variant = entity.Variant,
29+
OrganizationIdentifier = entity.OrganizationIdentifier,
30+
IsDeleted = entity.IsDeleted,
31+
DeletedAt = entity.DeletedAt
32+
};
33+
}
34+
35+
/// <summary>
36+
/// Converts a ConnectionDto to SimplifiedConnectionDto
37+
/// </summary>
38+
/// <param name="connection">The connection to convert</param>
39+
/// <returns>A simplified connection DTO, or null if connection is null</returns>
40+
public static SimplifiedConnectionDto? ToSimplifiedConnection(ConnectionDto? connection)
41+
{
42+
if (connection is null)
43+
{
44+
return null;
45+
}
46+
47+
return new SimplifiedConnectionDto
48+
{
49+
Party = ToSimplifiedParty(connection.Party),
50+
Connections = connection.Connections?.Select(ToSimplifiedConnection).ToList() ?? []
51+
};
52+
}
53+
54+
/// <summary>
55+
/// Converts a collection of ConnectionDto to simplified connections
56+
/// </summary>
57+
/// <param name="connections">The connections to convert</param>
58+
/// <returns>A collection of simplified connection DTOs</returns>
59+
public static IEnumerable<SimplifiedConnectionDto> ToSimplifiedConnections(IEnumerable<ConnectionDto>? connections)
60+
{
61+
return connections?.Select(ToSimplifiedConnection) ?? Enumerable.Empty<SimplifiedConnectionDto>();
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Altinn.Authorization.Api.Contracts.AccessManagement;
4+
5+
/// <summary>
6+
/// Simplified connection for listing available users
7+
/// </summary>
8+
public class SimplifiedConnectionDto
9+
{
10+
/// <summary>
11+
/// The party information
12+
/// </summary>
13+
[JsonPropertyName("party")]
14+
public SimplifiedPartyDto Party { get; set; }
15+
16+
/// <summary>
17+
/// Sub-connections (nested users under this party)
18+
/// </summary>
19+
[JsonPropertyName("connections")]
20+
public List<SimplifiedConnectionDto> Connections { get; set; } = new();
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Altinn.Authorization.Api.Contracts.AccessManagement;
4+
5+
/// <summary>
6+
/// Simplified party information for connections API responses
7+
/// </summary>
8+
public class SimplifiedPartyDto
9+
{
10+
/// <summary>
11+
/// The unique identifier for the party
12+
/// </summary>
13+
[JsonPropertyName("id")]
14+
public Guid Id { get; set; }
15+
16+
/// <summary>
17+
/// The name of the party
18+
/// </summary>
19+
[JsonPropertyName("name")]
20+
public string Name { get; set; }
21+
22+
/// <summary>
23+
/// The type of party (Person, Organization, etc.)
24+
/// </summary>
25+
[JsonPropertyName("type")]
26+
public string Type { get; set; }
27+
28+
/// <summary>
29+
/// The variant/subtype of the party
30+
/// </summary>
31+
[JsonPropertyName("variant")]
32+
public string Variant { get; set; }
33+
34+
/// <summary>
35+
/// Organization number (only for organizations)
36+
/// </summary>
37+
[JsonPropertyName("organizationIdentifier")]
38+
public string? OrganizationIdentifier { get; set; }
39+
40+
/// <summary>
41+
/// Indicates if the party is deleted
42+
/// </summary>
43+
[JsonPropertyName("isDeleted")]
44+
public bool IsDeleted { get; set; }
45+
46+
/// <summary>
47+
/// The timestamp when the party was deleted
48+
/// </summary>
49+
[JsonPropertyName("deletedAt")]
50+
public DateTimeOffset? DeletedAt { get; set; }
51+
}

0 commit comments

Comments
 (0)