Skip to content

Commit 050ccbb

Browse files
elsandoskogstad
andauthoredApr 25, 2024··
feat: Authorized parties endpoint in enduser API (#661)
## Description This adds a parties endpoint in the enduser-API, proxying requests to access-management and returning a custom DTO for all parties that the user has some sort of access relation to. ## Related Issue(s) - #660 ## Verification - [x] **Your** code builds clean without any errors or warnings - [x] Manual testing done (required) - [x] Relevant automated test added (if you find this hard, leave it and we'll help out) ## Documentation - [ ] Documentation is updated (either in `docs`-directory, Altinnpedia or a separate linked PR in [altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if applicable) --------- Co-authored-by: Ole Jørgen Skogstad <skogstad@softis.net>
1 parent 771fe15 commit 050ccbb

File tree

24 files changed

+507
-34
lines changed

24 files changed

+507
-34
lines changed
 

‎docs/schema/V1/schema.verified.graphql

+20-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
schema {
2-
query: DialogQueries
2+
query: Queries
33
}
44

55
type Activity {
@@ -34,6 +34,18 @@ type ApiActionEndpoint {
3434
sunsetAt: DateTime
3535
}
3636

37+
type AuthorizedParty {
38+
party: String!
39+
name: String!
40+
partyType: String!
41+
isDeleted: Boolean!
42+
hasKeyRole: Boolean!
43+
isMainAdministrator: Boolean!
44+
isAccessManager: Boolean!
45+
hasOnlyAccessToSubParties: Boolean!
46+
subParties: [AuthorizedParty!]
47+
}
48+
3749
type Content {
3850
type: ContentType!
3951
value: [Localization!]!
@@ -63,11 +75,6 @@ type Dialog {
6375
seenSinceLastUpdate: [SeenLog!]!
6476
}
6577

66-
type DialogQueries @authorize(policy: "enduser") {
67-
dialogById(dialogId: UUID!): Dialog!
68-
searchDialogs(input: SearchDialogInput!): SearchDialogsPayload!
69-
}
70-
7178
type Element {
7279
id: UUID!
7380
type: URL
@@ -103,6 +110,12 @@ type Localization {
103110
cultureCode: String!
104111
}
105112

113+
type Queries @authorize(policy: "enduser") {
114+
dialogById(dialogId: UUID!): Dialog!
115+
searchDialogs(input: SearchDialogInput!): SearchDialogsPayload!
116+
parties: [AuthorizedParty!]!
117+
}
118+
106119
type SearchDialog {
107120
id: UUID!
108121
org: String!
@@ -239,4 +252,4 @@ scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time"
239252

240253
scalar URL @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc3986")
241254

242-
scalar UUID @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc4122")
255+
scalar UUID @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc4122")

‎docs/schema/V1/swagger.verified.json

+86-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
{
22
"openapi": "3.0.0",
33
"info": {
44
"title": "Dialogporten",
@@ -1606,6 +1606,42 @@
16061606
]
16071607
}
16081608
},
1609+
"/api/v1/enduser/parties": {
1610+
"get": {
1611+
"tags": [
1612+
"Enduser"
1613+
],
1614+
"summary": "Gets the list of authorized parties for the end user",
1615+
"description": "Gets the list of authorized parties for the end user. For more information see the documentation (link TBD).",
1616+
"operationId": "GetParties",
1617+
"responses": {
1618+
"200": {
1619+
"description": "The list of authorized parties for the end user",
1620+
"content": {
1621+
"application/json": {
1622+
"schema": {
1623+
"type": "array",
1624+
"items": {
1625+
"$ref": "#/components/schemas/GetPartiesDto"
1626+
}
1627+
}
1628+
}
1629+
}
1630+
},
1631+
"401": {
1632+
"description": "Unauthorized"
1633+
},
1634+
"403": {
1635+
"description": "Forbidden"
1636+
}
1637+
},
1638+
"security": [
1639+
{
1640+
"JWTBearerAuth": []
1641+
}
1642+
]
1643+
}
1644+
},
16091645
"/api/v1/enduser/dialogs/{dialogId}/seenlog": {
16101646
"get": {
16111647
"tags": [
@@ -3732,6 +3768,55 @@
37323768
}
37333769
}
37343770
},
3771+
"GetPartiesDto": {
3772+
"type": "object",
3773+
"additionalProperties": false,
3774+
"properties": {
3775+
"authorizedParties": {
3776+
"type": "array",
3777+
"items": {
3778+
"$ref": "#/components/schemas/AuthorizedPartyDto"
3779+
}
3780+
}
3781+
}
3782+
},
3783+
"AuthorizedPartyDto": {
3784+
"type": "object",
3785+
"additionalProperties": false,
3786+
"properties": {
3787+
"party": {
3788+
"type": "string"
3789+
},
3790+
"name": {
3791+
"type": "string"
3792+
},
3793+
"partyType": {
3794+
"type": "string"
3795+
},
3796+
"isDeleted": {
3797+
"type": "boolean"
3798+
},
3799+
"hasKeyRole": {
3800+
"type": "boolean"
3801+
},
3802+
"isMainAdministrator": {
3803+
"type": "boolean"
3804+
},
3805+
"isAccessManager": {
3806+
"type": "boolean"
3807+
},
3808+
"hasOnlyAccessToSubParties": {
3809+
"type": "boolean"
3810+
},
3811+
"subParties": {
3812+
"type": "array",
3813+
"nullable": true,
3814+
"items": {
3815+
"$ref": "#/components/schemas/AuthorizedPartyDto"
3816+
}
3817+
}
3818+
}
3819+
},
37353820
"SearchDialogSeenLogDto": {
37363821
"type": "object",
37373822
"additionalProperties": false,

‎src/Digdir.Domain.Dialogporten.Application/ApplicationExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
4444
.AddTransient<IUserOrganizationRegistry, UserOrganizationRegistry>()
4545
.AddTransient<IUserResourceRegistry, UserResourceRegistry>()
4646
.AddTransient<IUserNameRegistry, UserNameRegistry>()
47+
.AddTransient<IUserParties, UserParties>()
4748
.AddTransient<IDialogActivityService, DialogActivityService>()
4849
.AddTransient<IClock, Clock>()
4950
.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Digdir.Domain.Dialogporten.Application.Common.Extensions;
2+
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
3+
using Digdir.Domain.Dialogporten.Application.Externals.Presentation;
4+
using Digdir.Domain.Dialogporten.Domain.Parties;
5+
6+
namespace Digdir.Domain.Dialogporten.Application.Common;
7+
8+
public interface IUserParties
9+
{
10+
public Task<AuthorizedPartiesResult> GetUserParties(CancellationToken cancellationToken = default);
11+
}
12+
13+
public class UserParties : IUserParties
14+
{
15+
private readonly IUser _user;
16+
private readonly IAltinnAuthorization _altinnAuthorization;
17+
18+
public UserParties(IUser user, IAltinnAuthorization altinnAuthorization)
19+
{
20+
_user = user ?? throw new ArgumentNullException(nameof(user));
21+
_altinnAuthorization = altinnAuthorization ?? throw new ArgumentNullException(nameof(altinnAuthorization));
22+
}
23+
24+
public Task<AuthorizedPartiesResult> GetUserParties(CancellationToken cancellationToken = default) =>
25+
_user.TryGetPid(out var pid) &&
26+
NorwegianPersonIdentifier.TryParse(NorwegianPersonIdentifier.PrefixWithSeparator + pid,
27+
out var partyIdentifier)
28+
? _altinnAuthorization.GetAuthorizedParties(partyIdentifier, cancellationToken)
29+
: Task.FromResult(new AuthorizedPartiesResult());
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions;
2+
3+
namespace Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
4+
5+
public class AuthorizedPartiesResult
6+
{
7+
public List<AuthorizedParty> AuthorizedParties { get; init; } = new();
8+
}
9+
10+
public class AuthorizedParty
11+
{
12+
public string Party { get; init; } = null!;
13+
public string Name { get; init; } = null!;
14+
public AuthorizedPartyType PartyType { get; init; }
15+
public bool IsDeleted { get; init; }
16+
public bool HasKeyRole { get; init; }
17+
public bool IsMainAdministrator { get; init; }
18+
public bool IsAccessManager { get; init; }
19+
public bool HasOnlyAccessToSubParties { get; init; }
20+
public List<string> AuthorizedResources { get; init; } = new();
21+
public List<AuthorizedParty>? SubParties { get; init; }
22+
}
23+
24+
public enum AuthorizedPartyType
25+
{
26+
Person,
27+
Organization
28+
}

‎src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;
2+
using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions;
23

34
namespace Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
45

@@ -13,4 +14,7 @@ public Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(
1314
List<string> constraintServiceResources,
1415
string? endUserId = null,
1516
CancellationToken cancellationToken = default);
17+
18+
public Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticatedParty,
19+
CancellationToken cancellationToken = default);
1620
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Parties.Queries.Get;
2+
3+
public class GetPartiesDto
4+
{
5+
public List<AuthorizedPartyDto> AuthorizedParties { get; init; } = new();
6+
}
7+
8+
public class AuthorizedPartyDto
9+
{
10+
public string Party { get; init; } = null!;
11+
public string Name { get; init; } = null!;
12+
public string PartyType { get; init; } = null!;
13+
public bool IsDeleted { get; init; }
14+
public bool HasKeyRole { get; init; }
15+
public bool IsMainAdministrator { get; init; }
16+
public bool IsAccessManager { get; init; }
17+
public bool HasOnlyAccessToSubParties { get; init; }
18+
public List<AuthorizedPartyDto>? SubParties { get; init; }
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using AutoMapper;
2+
using Digdir.Domain.Dialogporten.Application.Common;
3+
using MediatR;
4+
5+
namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Parties.Queries.Get;
6+
7+
public sealed class GetPartiesQuery : IRequest<GetPartiesDto>;
8+
9+
internal sealed class GetPartiesQueryHandler : IRequestHandler<GetPartiesQuery, GetPartiesDto>
10+
{
11+
private readonly IUserParties _userParties;
12+
private readonly IMapper _mapper;
13+
14+
public GetPartiesQueryHandler(IUserParties userParties, IMapper mapper)
15+
{
16+
_userParties = userParties;
17+
_mapper = mapper;
18+
}
19+
20+
public async Task<GetPartiesDto> Handle(GetPartiesQuery request, CancellationToken cancellationToken)
21+
{
22+
var authorizedPartiesResult = await _userParties.GetUserParties(cancellationToken);
23+
return _mapper.Map<GetPartiesDto>(authorizedPartiesResult);
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using AutoMapper;
2+
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
3+
4+
namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Parties.Queries.Get;
5+
6+
internal sealed class MappingProfile : Profile
7+
{
8+
public MappingProfile()
9+
{
10+
CreateMap<AuthorizedPartiesResult, GetPartiesDto>();
11+
CreateMap<AuthorizedParty, AuthorizedPartyDto>();
12+
}
13+
}

‎src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
using AutoMapper;
22
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get;
33
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search;
4-
using Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
54
using Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById;
65
using Digdir.Domain.Dialogporten.GraphQL.EndUser.SearchDialogs;
7-
using HotChocolate.Authorization;
86
using MediatR;
97

108
namespace Digdir.Domain.Dialogporten.GraphQL.EndUser;
119

12-
[Authorize(Policy = AuthorizationPolicy.EndUser)]
13-
public class DialogQueries
10+
public partial class Queries
1411
{
1512
public async Task<Dialog> GetDialogById(
1613
[Service] ISender mediator,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using AutoMapper;
2+
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Parties.Queries.Get;
3+
4+
namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.Parties;
5+
6+
public class MappingProfile : Profile
7+
{
8+
public MappingProfile()
9+
{
10+
CreateMap<AuthorizedPartyDto, AuthorizedParty>();
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.Parties;
2+
3+
public class AuthorizedParty
4+
{
5+
public string Party { get; init; } = null!;
6+
public string Name { get; init; } = null!;
7+
public string PartyType { get; init; } = null!;
8+
public bool IsDeleted { get; init; }
9+
public bool HasKeyRole { get; init; }
10+
public bool IsMainAdministrator { get; init; }
11+
public bool IsAccessManager { get; init; }
12+
public bool HasOnlyAccessToSubParties { get; init; }
13+
public List<AuthorizedParty>? SubParties { get; init; }
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using AutoMapper;
2+
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Parties.Queries.Get;
3+
using Digdir.Domain.Dialogporten.GraphQL.EndUser.Parties;
4+
using MediatR;
5+
6+
namespace Digdir.Domain.Dialogporten.GraphQL.EndUser;
7+
8+
public partial class Queries
9+
{
10+
public async Task<List<AuthorizedParty>> GetParties(
11+
[Service] ISender mediator,
12+
[Service] IMapper mapper,
13+
CancellationToken cancellationToken)
14+
{
15+
var request = new GetPartiesQuery();
16+
var result = await mediator.Send(request, cancellationToken);
17+
18+
return mapper.Map<List<AuthorizedParty>>(result.AuthorizedParties);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
2+
using HotChocolate.Authorization;
3+
4+
namespace Digdir.Domain.Dialogporten.GraphQL.EndUser;
5+
6+
[Authorize(Policy = AuthorizationPolicy.EndUser)]
7+
public partial class Queries;

‎src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public static IServiceCollection AddDialogportenGraphQl(
1313
.AddAuthorization()
1414
.RegisterDbContext<DialogDbContext>()
1515
.AddDiagnosticEventListener<ApplicationInsightEventListener>()
16-
.AddQueryType<DialogQueries>()
16+
.AddQueryType<Queries>()
1717
.Services;
1818
}
1919
}

‎src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs

+46-21
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,23 @@
1414
using ZiggyCreatures.Caching.Fusion;
1515

1616
namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;
17-
1817
internal sealed class AltinnAuthorizationClient : IAltinnAuthorization
1918
{
19+
private const string AuthorizeUrl = "authorization/api/v1/authorize";
20+
private const string AuthorizedPartiesUrl = "/accessmanagement/api/v1/resourceowner/authorizedparties?includeAltinn2=true";
21+
2022
private readonly HttpClient _httpClient;
2123
private readonly IFusionCache _cache;
2224
private readonly IUser _user;
2325
private readonly IDialogDbContext _db;
2426
private readonly ILogger _logger;
2527

28+
private static readonly JsonSerializerOptions SerializerOptions = new()
29+
{
30+
PropertyNameCaseInsensitive = true,
31+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
32+
};
33+
2634
public AltinnAuthorizationClient(
2735
HttpClient client,
2836
IFusionCacheProvider cacheProvider,
@@ -71,7 +79,23 @@ public async Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSear
7179
=> await PerformNonScalableDialogSearchAuthorization(request, token), token: cancellationToken);
7280
}
7381

74-
private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSearchAuthorization(DialogSearchAuthorizationRequest request, CancellationToken cancellationToken)
82+
public async Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticatedParty,
83+
CancellationToken cancellationToken = default)
84+
{
85+
var authorizedPartiesRequest = new AuthorizedPartiesRequest(authenticatedParty);
86+
return await _cache.GetOrSetAsync(authorizedPartiesRequest.GenerateCacheKey(), async token
87+
=> await PerformAuthorizedPartiesRequest(authorizedPartiesRequest, token), token: cancellationToken);
88+
}
89+
90+
private async Task<AuthorizedPartiesResult> PerformAuthorizedPartiesRequest(AuthorizedPartiesRequest authorizedPartiesRequest,
91+
CancellationToken token)
92+
{
93+
var authorizedPartiesDto = await SendAuthorizedPartiesRequest(authorizedPartiesRequest, token);
94+
return AuthorizedPartiesHelper.CreateAuthorizedPartiesResult(authorizedPartiesDto);
95+
}
96+
97+
private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSearchAuthorization(
98+
DialogSearchAuthorizationRequest request, CancellationToken cancellationToken)
7599
{
76100
/*
77101
* This is a preliminary implementation as per https://github.com/digdir/dialogporten/issues/249
@@ -107,14 +131,15 @@ private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSear
107131
}
108132

109133
var xacmlJsonRequest = DecisionRequestHelper.NonScalable.CreateDialogSearchRequest(request);
110-
var xamlJsonResponse = await SendRequest(xacmlJsonRequest, cancellationToken);
134+
var xamlJsonResponse = await SendPdpRequest(xacmlJsonRequest, cancellationToken);
111135
return DecisionRequestHelper.NonScalable.CreateDialogSearchResponse(xacmlJsonRequest, xamlJsonResponse);
112136
}
113137

114-
private async Task<DialogDetailsAuthorizationResult> PerformDialogDetailsAuthorization(DialogDetailsAuthorizationRequest request, CancellationToken cancellationToken)
138+
private async Task<DialogDetailsAuthorizationResult> PerformDialogDetailsAuthorization(
139+
DialogDetailsAuthorizationRequest request, CancellationToken cancellationToken)
115140
{
116141
var xacmlJsonRequest = DecisionRequestHelper.CreateDialogDetailsRequest(request);
117-
var xamlJsonResponse = await SendRequest(xacmlJsonRequest, cancellationToken);
142+
var xamlJsonResponse = await SendPdpRequest(xacmlJsonRequest, cancellationToken);
118143
return DecisionRequestHelper.CreateDialogDetailsResponse(request.AltinnActions, xamlJsonResponse);
119144
}
120145

@@ -133,32 +158,32 @@ private List<Claim> GetOrCreateClaimsBasedOnEndUserId(string? endUserId)
133158
return claims;
134159
}
135160

136-
private static readonly JsonSerializerOptions SerializerOptions = new()
137-
{
138-
PropertyNameCaseInsensitive = true,
139-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
140-
};
161+
private async Task<XacmlJsonResponse?> SendPdpRequest(
162+
XacmlJsonRequestRoot xacmlJsonRequest, CancellationToken cancellationToken) =>
163+
await SendRequest<XacmlJsonResponse>(
164+
AuthorizeUrl, xacmlJsonRequest, cancellationToken);
165+
166+
private async Task<List<AuthorizedPartiesResultDto>?> SendAuthorizedPartiesRequest(
167+
AuthorizedPartiesRequest authorizedPartiesRequest, CancellationToken cancellationToken) =>
168+
await SendRequest<List<AuthorizedPartiesResultDto>>(
169+
AuthorizedPartiesUrl, authorizedPartiesRequest, cancellationToken);
141170

142-
private async Task<XacmlJsonResponse?> SendRequest(XacmlJsonRequestRoot xacmlJsonRequest, CancellationToken cancellationToken)
171+
private async Task<T?> SendRequest<T>(string url, object request, CancellationToken cancellationToken)
143172
{
144-
const string apiUrl = "authorization/api/v1/authorize";
145-
var requestJson = JsonSerializer.Serialize(xacmlJsonRequest, SerializerOptions);
146-
_logger.LogDebug("Generated XACML request: {RequestJson}", requestJson);
173+
var requestJson = JsonSerializer.Serialize(request, SerializerOptions);
174+
_logger.LogDebug("Authorization request to {Url}: {RequestJson}", url, requestJson);
147175
var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
148-
149-
var response = await _httpClient.PostAsync(apiUrl, httpContent, cancellationToken);
150-
176+
var response = await _httpClient.PostAsync(url, httpContent, cancellationToken);
151177
if (response.StatusCode != HttpStatusCode.OK)
152178
{
153179
var errorResponse = await response.Content.ReadAsStringAsync(cancellationToken);
154-
_logger.LogInformation(
155-
"AltinnAuthorizationClient.SendRequest failed with non-successful status code: {StatusCode} {Response}",
180+
_logger.LogWarning("AltinnAuthorizationClient.SendRequest failed with non-successful status code: {StatusCode} {Response}",
156181
response.StatusCode, errorResponse);
157182

158-
return null;
183+
return default;
159184
}
160185

161186
var responseData = await response.Content.ReadAsStringAsync(cancellationToken);
162-
return JsonSerializer.Deserialize<XacmlJsonResponse>(responseData, SerializerOptions);
187+
return JsonSerializer.Deserialize<T>(responseData, SerializerOptions);
163188
}
164189
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
2+
using Digdir.Domain.Dialogporten.Domain.Parties;
3+
4+
namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;
5+
6+
internal static class AuthorizedPartiesHelper
7+
{
8+
private const string PartyTypeOrganization = "Organization";
9+
private const string PartyTypePerson = "Person";
10+
private const string AttributeIdResource = "urn:altinn:resource";
11+
private const string AttributeIdApp = "urn:altinn:app";
12+
private const string AppIdPrefix = "app_";
13+
private const string MainAdministratorRoleCode = "HADM";
14+
private const string AccessManagerRoleCode = "ADMAI";
15+
private static readonly string[] KeyRoleCodes = ["DAGL", "LEDE", "INNH", "DTPR", "DTSO", "BEST"];
16+
public static AuthorizedPartiesResult CreateAuthorizedPartiesResult(List<AuthorizedPartiesResultDto>? authorizedPartiesDto)
17+
{
18+
var result = new AuthorizedPartiesResult();
19+
if (authorizedPartiesDto is not null)
20+
{
21+
foreach (var authorizedPartyDto in authorizedPartiesDto)
22+
{
23+
result.AuthorizedParties.Add(MapFromDto(authorizedPartyDto));
24+
}
25+
}
26+
27+
return result;
28+
}
29+
30+
private static AuthorizedParty MapFromDto(AuthorizedPartiesResultDto dto)
31+
{
32+
var party = dto.Type switch
33+
{
34+
PartyTypeOrganization => NorwegianOrganizationIdentifier.PrefixWithSeparator + dto.OrganizationNumber,
35+
PartyTypePerson => NorwegianPersonIdentifier.PrefixWithSeparator + dto.PersonId,
36+
_ => throw new ArgumentOutOfRangeException(nameof(dto))
37+
};
38+
39+
return new AuthorizedParty
40+
{
41+
Party = party,
42+
Name = dto.Name,
43+
PartyType = dto.Type switch
44+
{
45+
PartyTypeOrganization => AuthorizedPartyType.Organization,
46+
PartyTypePerson => AuthorizedPartyType.Person,
47+
_ => throw new ArgumentOutOfRangeException(nameof(dto))
48+
},
49+
IsDeleted = dto.IsDeleted,
50+
HasKeyRole = dto.AuthorizedRoles.Exists(role => KeyRoleCodes.Contains(role)),
51+
IsMainAdministrator = dto.AuthorizedRoles.Contains(MainAdministratorRoleCode),
52+
IsAccessManager = dto.AuthorizedRoles.Contains(AccessManagerRoleCode),
53+
HasOnlyAccessToSubParties = dto.OnlyHierarchyElementWithNoAccess,
54+
AuthorizedResources = GetPrefixedResources(dto.AuthorizedResources),
55+
SubParties = dto.Subunits.Count > 0 ? dto.Subunits.Select(MapFromDto).ToList() : null
56+
};
57+
}
58+
59+
private static List<string> GetPrefixedResources(List<string> dtoAuthorizedResources) =>
60+
dtoAuthorizedResources.Select(resource => resource.StartsWith(AppIdPrefix, StringComparison.Ordinal)
61+
? AttributeIdApp + ":" + resource
62+
: AttributeIdResource + ":" + resource)
63+
.ToList();
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Security.Cryptography;
2+
using System.Text;
3+
using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions;
4+
5+
namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;
6+
7+
public sealed class AuthorizedPartiesRequest(IPartyIdentifier partyIdentifier)
8+
{
9+
public string Type { get; init; } = partyIdentifier.Prefix();
10+
public string Value { get; init; } = partyIdentifier.Id;
11+
}
12+
13+
public static class AuthorizedPartiesRequestExtensions
14+
{
15+
public static string GenerateCacheKey(this AuthorizedPartiesRequest request)
16+
{
17+
var rawKey = $"{request.Type}:{request.Value}";
18+
19+
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(rawKey));
20+
var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
21+
22+
return $"auth:parties:{hashString}";
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;
2+
3+
internal sealed class AuthorizedPartiesResultDto
4+
{
5+
public required string Name { get; set; }
6+
public required string OrganizationNumber { get; set; }
7+
public string? PersonId { get; set; }
8+
public required int PartyId { get; set; }
9+
public required string Type { get; set; }
10+
public required bool IsDeleted { get; set; }
11+
public required bool OnlyHierarchyElementWithNoAccess { get; set; }
12+
public required List<string> AuthorizedResources { get; set; }
13+
public required List<string> AuthorizedRoles { get; set; }
14+
public required List<AuthorizedPartiesResultDto> Subunits { get; set; }
15+
}

‎src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Digdir.Domain.Dialogporten.Application.Externals;
44
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
55
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;
6+
using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions;
67
using Microsoft.EntityFrameworkCore;
78

89
namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;
@@ -47,4 +48,7 @@ public async Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSear
4748

4849
return authorizedResources;
4950
}
51+
52+
public async Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticatedParty, CancellationToken cancellationToken = default)
53+
=> await Task.FromResult(new AuthorizedPartiesResult { AuthorizedParties = [new() { Name = "Local Party", Party = authenticatedParty.FullId }] });
5054
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Parties.Queries.Get;
2+
using Digdir.Domain.Dialogporten.WebApi.Common.Authorization;
3+
using FastEndpoints;
4+
using MediatR;
5+
6+
namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.EndUser.Parties.Get;
7+
8+
public class GetPartiesEndpoint : EndpointWithoutRequest<GetPartiesDto>
9+
{
10+
private readonly ISender _sender;
11+
12+
public GetPartiesEndpoint(ISender sender)
13+
{
14+
_sender = sender ?? throw new ArgumentNullException(nameof(sender));
15+
}
16+
17+
public override void Configure()
18+
{
19+
Get("parties");
20+
Policies(AuthorizationPolicy.EndUser);
21+
Group<EndUserGroup>();
22+
23+
Description(d => GetPartiesSwaggerConfig.SetDescription(d));
24+
}
25+
26+
public override async Task HandleAsync(CancellationToken ct)
27+
{
28+
var result = await _sender.Send(new GetPartiesQuery(), ct);
29+
await SendOkAsync(result, ct);
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Parties.Queries.Get;
2+
using Digdir.Domain.Dialogporten.WebApi.Common.Swagger;
3+
using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.Common.Extensions;
4+
using FastEndpoints;
5+
6+
namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.EndUser.Parties.Get;
7+
8+
public class GetPartiesSwaggerConfig : ISwaggerConfig
9+
{
10+
public static string OperationId => "GetParties";
11+
12+
public static RouteHandlerBuilder SetDescription(RouteHandlerBuilder builder)
13+
=> builder.OperationId(OperationId)
14+
.Produces<List<GetPartiesDto>>();
15+
16+
public static object GetExample() => throw new NotImplementedException();
17+
}
18+
19+
public sealed class GetPartiesEndpointSummary : Summary<GetPartiesEndpoint>
20+
{
21+
public GetPartiesEndpointSummary()
22+
{
23+
Summary = "Gets the list of authorized parties for the end user";
24+
Description = """
25+
Gets the list of authorized parties for the end user. For more information see the documentation (link TBD).
26+
""";
27+
28+
Responses[StatusCodes.Status200OK] = "The list of authorized parties for the end user";
29+
}
30+
}

‎tests/k6/tests/enduser/all-tests.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// This file is generated, see "scripts" directory
22
import { default as dialogDetails } from './dialogDetails.js';
33
import { default as dialogSearch } from './dialogSearch.js';
4+
import { default as parties } from './parties.js';
45

56
export default function() {
67
dialogDetails();
78
dialogSearch();
9+
parties();
810
}

‎tests/k6/tests/enduser/parties.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { describe, expect, expectStatusFor, getEU } from '../../common/testimports.js'
2+
3+
export default function () {
4+
describe('Check if we get any parties', () => {
5+
let r = getEU("parties");
6+
expectStatusFor(r).to.equal(200);
7+
expect(r, 'response').to.have.validJsonBody();
8+
expect(r.json(), 'response json').to.have.property("authorizedParties").with.lengthOf.at.least(2);
9+
});
10+
}

0 commit comments

Comments
 (0)
Please sign in to comment.