From 2c0c132d6a948e03b29b246372d587df6a6f5e68 Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Tue, 9 Jan 2024 21:24:06 +0100 Subject: [PATCH 1/9] Added initial code for using fnr/dnr to filter SO dialog search --- .../Dialogs/Queries/Search/SearchDialogDto.cs | 1 + .../Queries/Search/SearchDialogQuery.cs | 34 ++++++++++++++++--- .../Search/SearchDialogQueryValidator.cs | 13 ++++++- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogDto.cs index e4c8b46b8..bdf2a289f 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogDto.cs @@ -9,6 +9,7 @@ public sealed class SearchDialogDto public Guid Id { get; set; } public string Org { get; set; } = null!; public string ServiceResource { get; set; } = null!; + public string? AuthEndUserPid { get; set; } = null!; public string Party { get; set; } = null!; public int? Progress { get; set; } public string? ExtendedStatus { get; set; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs index 53e42c2e3..bb1499264 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -7,6 +7,7 @@ using Digdir.Domain.Dialogporten.Application.Common.Pagination.OrderOption; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; +using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; using Digdir.Domain.Dialogporten.Domain.Localizations; using MediatR; @@ -29,6 +30,11 @@ public sealed class SearchDialogQuery : SortablePaginationParameter public List? Party { get; init; } + /// + /// Filter by national identifier + /// + public string? AuthEndUserPid { get; init; } + /// /// Filter by one or more extended statuses /// @@ -116,25 +122,31 @@ internal sealed class SearchDialogQueryHandler : IRequestHandler Handle(SearchDialogQuery request, CancellationToken cancellationToken) { var resourceIds = await _userService.GetCurrentUserResourceIds(cancellationToken); var searchExpression = Expressions.LocalizedSearchExpression(request.Search, request.SearchCultureCode); - return await _db.Dialogs - .WhereIf(!request.ServiceResource.IsNullOrEmpty(), x => request.ServiceResource!.Contains(x.ServiceResource)) + + var query = _db.Dialogs + .WhereIf(!request.ServiceResource.IsNullOrEmpty(), + x => request.ServiceResource!.Contains(x.ServiceResource)) .WhereIf(!request.Party.IsNullOrEmpty(), x => request.Party!.Contains(x.Party)) - .WhereIf(!request.ExtendedStatus.IsNullOrEmpty(), x => x.ExtendedStatus != null && request.ExtendedStatus!.Contains(x.ExtendedStatus)) + .WhereIf(!request.ExtendedStatus.IsNullOrEmpty(), + x => x.ExtendedStatus != null && request.ExtendedStatus!.Contains(x.ExtendedStatus)) .WhereIf(!string.IsNullOrWhiteSpace(request.ExternalReference), x => x.ExternalReference != null && request.ExternalReference == x.ExternalReference) .WhereIf(!request.Status.IsNullOrEmpty(), x => request.Status!.Contains(x.StatusId)) @@ -150,7 +162,19 @@ public async Task Handle(SearchDialogQuery request, Cancella x.Content.Any(x => x.Value.Localizations.AsQueryable().Any(searchExpression)) || x.SearchTags.Any(x => EF.Functions.ILike(x.Value, request.Search!)) ) - .Where(x => resourceIds.Contains(x.ServiceResource)) + .Where(x => resourceIds.Contains(x.ServiceResource)); + + if (request.AuthEndUserPid is not null) + { + //todo: include authEndUserPid in GetAuthorizedResourcesForSearch + var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch( + request.Party ?? new List(), + request.ServiceResource ?? new List(), + cancellationToken); + query.WhereUserIsAuthorizedFor(authorizedResources); + } + + return await query .ProjectTo(_mapper.ConfigurationProvider) .ToPaginatedListAsync(request, cancellationToken: cancellationToken); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs index 287e0b4fb..1ec1cc8da 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs @@ -1,4 +1,6 @@ -using Digdir.Domain.Dialogporten.Application.Common.Pagination; +using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables; +using Digdir.Domain.Dialogporten.Application.Common.Numbers; +using Digdir.Domain.Dialogporten.Application.Common.Pagination; using Digdir.Domain.Dialogporten.Domain.Localizations; using FluentValidation; @@ -17,6 +19,15 @@ public SearchDialogQueryValidator() .Must(x => x is null || Localization.IsValidCultureCode(x)) .WithMessage("'{PropertyName}' must be a valid culture code."); + RuleFor(x => x) + .Must(x => !x.ServiceResource.IsNullOrEmpty() || !x.Party.IsNullOrEmpty()) + .WithMessage($"Either {nameof(SearchDialogQuery.ServiceResource)} or {nameof(SearchDialogQuery.Party)} must be specified.") + .When(x => x.AuthEndUserPid is not null); + + RuleFor(x => x.AuthEndUserPid) + .Must(x => SocialSecurityNumber.IsValid(x)) + .When(x => x.AuthEndUserPid is not null); + RuleFor(x => x.ServiceResource!.Count) .LessThanOrEqualTo(20) .When(x => x.ServiceResource is not null); From 24197a67b104dd47effd597c763724ba66f2704a Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Wed, 10 Jan 2024 10:21:15 +0100 Subject: [PATCH 2/9] Reduce code duplication --- .../Dialogs/Queries/Search/SearchDialogQuery.cs | 2 +- .../Dialogs/Queries/Search/SearchDialogQueryValidator.cs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs index bb1499264..730ad17c8 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -31,7 +31,7 @@ public sealed class SearchDialogQuery : SortablePaginationParameter? Party { get; init; } /// - /// Filter by national identifier + /// Filter by national identity number /// public string? AuthEndUserPid { get; init; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs index 1ec1cc8da..72c2ce097 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs @@ -20,14 +20,12 @@ public SearchDialogQueryValidator() .WithMessage("'{PropertyName}' must be a valid culture code."); RuleFor(x => x) + .Must(x => SocialSecurityNumber.IsValid(x.AuthEndUserPid)) + .WithMessage($"'{nameof(SearchDialogQuery.AuthEndUserPid)}' must be a valid national identity number.") .Must(x => !x.ServiceResource.IsNullOrEmpty() || !x.Party.IsNullOrEmpty()) .WithMessage($"Either {nameof(SearchDialogQuery.ServiceResource)} or {nameof(SearchDialogQuery.Party)} must be specified.") .When(x => x.AuthEndUserPid is not null); - RuleFor(x => x.AuthEndUserPid) - .Must(x => SocialSecurityNumber.IsValid(x)) - .When(x => x.AuthEndUserPid is not null); - RuleFor(x => x.ServiceResource!.Count) .LessThanOrEqualTo(20) .When(x => x.ServiceResource is not null); From 77405748ecc88dcdf35efddc13a9e8d20a7ad5da Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Tue, 16 Jan 2024 13:37:45 +0100 Subject: [PATCH 3/9] add ssn claim if authEndUserPid is present --- .../AltinnAuthorization/IAltinnAuthorization.cs | 1 + .../Dialogs/Queries/Search/SearchDialogQuery.cs | 2 +- .../Dialogs/Queries/Search/SearchDialogQuery.cs | 4 ++-- .../Authorization/AltinnAuthorizationClient.cs | 15 ++++++++++++--- .../LocalDevelopmentAltinnAuthorization.cs | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs index 95dd94c5b..0db86e12b 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs @@ -11,5 +11,6 @@ public Task GetDialogDetailsAuthorization( public Task GetAuthorizedResourcesForSearch( List constraintParties, List constraintServiceResources, + string? authEndUserPid = null, CancellationToken cancellationToken = default); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs index 51265c60e..855a4d17f 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -134,7 +134,7 @@ public async Task Handle(SearchDialogQuery request, Cancella var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch( request.Party ?? new List(), request.ServiceResource ?? new List(), - cancellationToken); + cancellationToken: cancellationToken); if (authorizedResources.HasNoAuthorizations) { diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs index 730ad17c8..02f83020e 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -166,12 +166,12 @@ public async Task Handle(SearchDialogQuery request, Cancella if (request.AuthEndUserPid is not null) { - //todo: include authEndUserPid in GetAuthorizedResourcesForSearch var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch( request.Party ?? new List(), request.ServiceResource ?? new List(), + request.AuthEndUserPid, cancellationToken); - query.WhereUserIsAuthorizedFor(authorizedResources); + query = query.WhereUserIsAuthorizedFor(authorizedResources); } return await query diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs index 63e177577..070c18592 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Security.Claims; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -45,13 +46,21 @@ await PerformDialogDetailsAuthorization(new DialogDetailsAuthorizationRequest public async Task GetAuthorizedResourcesForSearch( List constraintParties, List serviceResources, - CancellationToken cancellationToken = default) => - await PerformNonScalableDialogSearchAuthorization(new DialogSearchAuthorizationRequest + string? authEndUserPid, + CancellationToken cancellationToken = default) + { + var principal = _user.GetPrincipal(); + if (authEndUserPid is not null && principal.Identity is ClaimsIdentity claimsIdentity) { - ClaimsPrincipal = _user.GetPrincipal(), + claimsIdentity.AddClaim(new Claim("urn:altinn:ssn", authEndUserPid)); + } + return await PerformNonScalableDialogSearchAuthorization(new DialogSearchAuthorizationRequest + { + ClaimsPrincipal = principal, ConstraintParties = constraintParties, ConstraintServiceResources = serviceResources }, cancellationToken); + } private async Task PerformNonScalableDialogSearchAuthorization(DialogSearchAuthorizationRequest request, CancellationToken cancellationToken) { diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs index 9bb92d92e..17a861993 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs @@ -21,7 +21,7 @@ public Task GetDialogDetailsAuthorization(Dial // Just allow everything Task.FromResult(new DialogDetailsAuthorizationResult { AuthorizedAltinnActions = dialogEntity.GetAltinnActions() }); - public async Task GetAuthorizedResourcesForSearch(List constraintParties, List serviceResources, + public async Task GetAuthorizedResourcesForSearch(List constraintParties, List serviceResources, string? authEndUserPid, CancellationToken cancellationToken = default) { // Allow all resources for all parties From e7909e400defb679e7ce3ccd615234612ecf0473 Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Wed, 17 Jan 2024 16:15:37 +0100 Subject: [PATCH 4/9] changed from ClaimsPrincipal to list of Claims --- .../Queries/Get/GetDialogActivityQuery.cs | 2 +- .../Search/SearchDialogActivityQuery.cs | 2 +- .../Queries/Get/GetDialogElementQuery.cs | 2 +- .../Search/SearchDialogElementQuery.cs | 2 +- .../Dialogs/Queries/Get/GetDialogQuery.cs | 2 +- .../AltinnAuthorizationClient.cs | 26 ++++++++++++++----- .../Authorization/DecisionRequestHelper.cs | 4 +-- .../DialogDetailsAuthorizationRequest.cs | 2 +- .../DialogSearchAuthorizationRequest.cs | 2 +- .../DecisionRequestHelperTests.cs | 2 +- 10 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityQuery.cs index 7c1f2140e..589d9b0c7 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityQuery.cs @@ -53,7 +53,7 @@ public async Task Handle(GetDialogActivityQuery request var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken); + cancellationToken: cancellationToken); // If we cannot read the dialog at all, we don't allow access to any of the activity history if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs index 3da553e5d..d70c44005 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs @@ -48,7 +48,7 @@ public async Task Handle(SearchDialogActivityQuery r var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken); + cancellationToken: cancellationToken); // If we cannot read the dialog at all, we don't allow access to any of the activity history if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs index ec739bedf..d00c65711 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs @@ -58,7 +58,7 @@ public async Task Handle(GetDialogElementQuery request, var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken); + cancellationToken: cancellationToken); // If we cannot read the dialog at all, we don't allow access to any of the dialog elements if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Search/SearchDialogElementQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Search/SearchDialogElementQuery.cs index f13af2bc0..1e0a9f02c 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Search/SearchDialogElementQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Search/SearchDialogElementQuery.cs @@ -49,7 +49,7 @@ public async Task Handle(SearchDialogElementQuery req var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken); + cancellationToken: cancellationToken); // If we cannot read the dialog at all, we don't allow access to any of the activity history if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs index 99d681f0a..a1c390bcf 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs @@ -75,7 +75,7 @@ public async Task Handle(GetDialogQuery request, CancellationTo var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken); + cancellationToken: cancellationToken); if (!authorizationResult.HasReadAccessToMainResource()) { diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs index 070c18592..cdeea1d52 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs @@ -15,6 +15,8 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization; internal sealed class AltinnAuthorizationClient : IAltinnAuthorization { + private const string AttributeIdSsn = "urn:altinn:ssn"; + private readonly HttpClient _httpClient; private readonly IUser _user; private readonly IDialogDbContext _db; @@ -36,7 +38,7 @@ public async Task GetDialogDetailsAuthorizatio CancellationToken cancellationToken = default) => await PerformDialogDetailsAuthorization(new DialogDetailsAuthorizationRequest { - ClaimsPrincipal = _user.GetPrincipal(), + Claims = _user.GetPrincipal().Claims.ToList(), ServiceResource = dialogEntity.ServiceResource, DialogId = dialogEntity.Id, Party = dialogEntity.Party, @@ -49,14 +51,10 @@ public async Task GetAuthorizedResourcesForSear string? authEndUserPid, CancellationToken cancellationToken = default) { - var principal = _user.GetPrincipal(); - if (authEndUserPid is not null && principal.Identity is ClaimsIdentity claimsIdentity) - { - claimsIdentity.AddClaim(new Claim("urn:altinn:ssn", authEndUserPid)); - } + var claims = GetOrCreateClaimsBasedOnEndUserPid(authEndUserPid); return await PerformNonScalableDialogSearchAuthorization(new DialogSearchAuthorizationRequest { - ClaimsPrincipal = principal, + Claims = claims, ConstraintParties = constraintParties, ConstraintServiceResources = serviceResources }, cancellationToken); @@ -106,6 +104,20 @@ private async Task PerformDialogDetailsAuthori return DecisionRequestHelper.CreateDialogDetailsResponse(request.AltinnActions, xamlJsonResponse); } + private List GetOrCreateClaimsBasedOnEndUserPid(string? endUserPid) + { + List claims = new(); + if (endUserPid is not null) + { + claims.Add(new Claim(AttributeIdSsn, endUserPid)); + } + else + { + claims.AddRange(_user.GetPrincipal().Claims); + } + return claims; + } + private static readonly JsonSerializerOptions _serializerOptions = new() { PropertyNameCaseInsensitive = true, diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs index 9409b8102..6bcc0f35a 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs @@ -24,7 +24,7 @@ internal static class DecisionRequestHelper public static XacmlJsonRequestRoot CreateDialogDetailsRequest(DialogDetailsAuthorizationRequest request) { - var accessSubject = CreateAccessSubjectCategory(request.ClaimsPrincipal.Claims); + var accessSubject = CreateAccessSubjectCategory(request.Claims); var actions = CreateActionCategories(request.AltinnActions, out var actionIdByName); var resources = CreateResourceCategories(request.ServiceResource, request.DialogId, request.Party, request.AltinnActions, out var resourceIdByName); @@ -223,7 +223,7 @@ public static XacmlJsonRequestRoot CreateDialogSearchRequest(DialogSearchAuthori new (Constants.ReadAction, Constants.MainResource) }; - var accessSubject = CreateAccessSubjectCategory(request.ClaimsPrincipal.Claims); + var accessSubject = CreateAccessSubjectCategory(request.Claims); var actions = CreateActionCategories(requestActions, out _); var resources = CreateResourceCategoriesForSearch(request.ConstraintServiceResources, request.ConstraintParties); diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DialogDetailsAuthorizationRequest.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DialogDetailsAuthorizationRequest.cs index 50c7adcf2..845eda79a 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DialogDetailsAuthorizationRequest.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DialogDetailsAuthorizationRequest.cs @@ -5,7 +5,7 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization; public sealed class DialogDetailsAuthorizationRequest { - public required ClaimsPrincipal ClaimsPrincipal { get; init; } + public required List Claims { get; init; } public required string ServiceResource { get; init; } public required Guid DialogId { get; init; } public required string Party { get; init; } diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DialogSearchAuthorizationRequest.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DialogSearchAuthorizationRequest.cs index 8652f3c74..b0846b56e 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DialogSearchAuthorizationRequest.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DialogSearchAuthorizationRequest.cs @@ -4,7 +4,7 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization; public sealed class DialogSearchAuthorizationRequest { - public required ClaimsPrincipal ClaimsPrincipal { get; init; } + public required List Claims { get; init; } public List ConstraintParties { get; set; } = new(); public List ConstraintServiceResources { get; set; } = new(); } diff --git a/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/DecisionRequestHelperTests.cs b/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/DecisionRequestHelperTests.cs index bd42b0109..4fb5fb489 100644 --- a/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/DecisionRequestHelperTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/DecisionRequestHelperTests.cs @@ -138,7 +138,7 @@ private static DialogDetailsAuthorizationRequest CreateDialogDetailsAuthorizatio allClaims.AddRange(principalClaims); return new DialogDetailsAuthorizationRequest { - ClaimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(allClaims, "test")), + Claims = allClaims, ServiceResource = "urn:altinn:resource:some-service", DialogId = Guid.NewGuid(), From bfde4ca733399d107b5548f7e21ffcdfec9fc546 Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Thu, 18 Jan 2024 08:55:13 +0100 Subject: [PATCH 5/9] Cleanup --- .../Queries/Search/SearchDialogActivityQuery.cs | 2 +- .../EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs | 2 +- .../DialogElements/Queries/Search/SearchDialogElementQuery.cs | 2 +- .../Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs index d70c44005..3da553e5d 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs @@ -48,7 +48,7 @@ public async Task Handle(SearchDialogActivityQuery r var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken: cancellationToken); + cancellationToken); // If we cannot read the dialog at all, we don't allow access to any of the activity history if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs index d00c65711..ec739bedf 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Get/GetDialogElementQuery.cs @@ -58,7 +58,7 @@ public async Task Handle(GetDialogElementQuery request, var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken: cancellationToken); + cancellationToken); // If we cannot read the dialog at all, we don't allow access to any of the dialog elements if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Search/SearchDialogElementQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Search/SearchDialogElementQuery.cs index 1e0a9f02c..f13af2bc0 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Search/SearchDialogElementQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogElements/Queries/Search/SearchDialogElementQuery.cs @@ -49,7 +49,7 @@ public async Task Handle(SearchDialogElementQuery req var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken: cancellationToken); + cancellationToken); // If we cannot read the dialog at all, we don't allow access to any of the activity history if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs index a1c390bcf..99d681f0a 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs @@ -75,7 +75,7 @@ public async Task Handle(GetDialogQuery request, CancellationTo var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken: cancellationToken); + cancellationToken); if (!authorizationResult.HasReadAccessToMainResource()) { From 7abc0d76e60d04518d9df6a0e9f13f0d0901abb2 Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Thu, 18 Jan 2024 16:18:37 +0100 Subject: [PATCH 6/9] changed authEndUserPid to EndUserID, require EndUserId to be on the format 'urn:altinn:person:identifier-no::12345678901' --- .../Common/Numbers/PersonIdentifier.cs | 16 ++++++++++++++++ .../AltinnAuthorization/IAltinnAuthorization.cs | 2 +- .../Dialogs/Queries/Search/SearchDialogDto.cs | 2 +- .../Dialogs/Queries/Search/SearchDialogQuery.cs | 9 +++++---- .../Queries/Search/SearchDialogQueryValidator.cs | 10 ++++++---- .../Authorization/AltinnAuthorizationClient.cs | 15 +++++++++------ .../LocalDevelopmentAltinnAuthorization.cs | 2 +- 7 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs new file mode 100644 index 000000000..ecbfa6dd6 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs @@ -0,0 +1,16 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Digdir.Domain.Dialogporten.Application.Common.Numbers; + +internal static class PersonIdentifier +{ + public static bool IsValid(ReadOnlySpan personIdentifier) + { + var regex = new Regex(@"^urn:altinn:person:[\w-]+::\d+$"); + return regex.IsMatch(personIdentifier.ToString()); + } + + public static string? ExtractEndUserIdNumber(string? endUserId) => + endUserId?.Split("::").LastOrDefault(); +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs index 0db86e12b..0a4e1fdd8 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs @@ -11,6 +11,6 @@ public Task GetDialogDetailsAuthorization( public Task GetAuthorizedResourcesForSearch( List constraintParties, List constraintServiceResources, - string? authEndUserPid = null, + string? endUserId = null, CancellationToken cancellationToken = default); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogDto.cs index bdf2a289f..21d3b82cb 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogDto.cs @@ -9,8 +9,8 @@ public sealed class SearchDialogDto public Guid Id { get; set; } public string Org { get; set; } = null!; public string ServiceResource { get; set; } = null!; - public string? AuthEndUserPid { get; set; } = null!; public string Party { get; set; } = null!; + public string? EndUserId { get; set; } = null!; public int? Progress { get; set; } public string? ExtendedStatus { get; set; } public DateTimeOffset CreatedAt { get; set; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs index 02f83020e..16fd2db88 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -3,6 +3,7 @@ using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.Extensions; using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables; +using Digdir.Domain.Dialogporten.Application.Common.Numbers; using Digdir.Domain.Dialogporten.Application.Common.Pagination; using Digdir.Domain.Dialogporten.Application.Common.Pagination.OrderOption; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; @@ -31,9 +32,9 @@ public sealed class SearchDialogQuery : SortablePaginationParameter? Party { get; init; } /// - /// Filter by national identity number + /// Filter by end user id /// - public string? AuthEndUserPid { get; init; } + public string? EndUserId { get; init; } /// /// Filter by one or more extended statuses @@ -164,12 +165,12 @@ public async Task Handle(SearchDialogQuery request, Cancella ) .Where(x => resourceIds.Contains(x.ServiceResource)); - if (request.AuthEndUserPid is not null) + if (request.EndUserId is not null) { var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch( request.Party ?? new List(), request.ServiceResource ?? new List(), - request.AuthEndUserPid, + request.EndUserId, cancellationToken); query = query.WhereUserIsAuthorizedFor(authorizedResources); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs index 72c2ce097..c44e0025a 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs @@ -20,11 +20,13 @@ public SearchDialogQueryValidator() .WithMessage("'{PropertyName}' must be a valid culture code."); RuleFor(x => x) - .Must(x => SocialSecurityNumber.IsValid(x.AuthEndUserPid)) - .WithMessage($"'{nameof(SearchDialogQuery.AuthEndUserPid)}' must be a valid national identity number.") + .Must(x => PersonIdentifier.IsValid(x.EndUserId)) + .WithMessage($"'{nameof(SearchDialogQuery.EndUserId)}' must be a valid person identifier. It should match the format 'urn:altinn:person:identifier-no::12345678901'.") + .Must(x => SocialSecurityNumber.IsValid(PersonIdentifier.ExtractEndUserIdNumber(x.EndUserId!))) + .WithMessage($"'{nameof(SearchDialogQuery.EndUserId)}' must include a valid national identity number.") .Must(x => !x.ServiceResource.IsNullOrEmpty() || !x.Party.IsNullOrEmpty()) - .WithMessage($"Either {nameof(SearchDialogQuery.ServiceResource)} or {nameof(SearchDialogQuery.Party)} must be specified.") - .When(x => x.AuthEndUserPid is not null); + .WithMessage($"Either '{nameof(SearchDialogQuery.ServiceResource)}' or '{nameof(SearchDialogQuery.Party)}' must be specified if '{nameof(SearchDialogQuery.EndUserId)}' is provided.") + .When(x => x.EndUserId is not null); RuleFor(x => x.ServiceResource!.Count) .LessThanOrEqualTo(20) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs index cdeea1d52..d714e3c20 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs @@ -15,7 +15,7 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization; internal sealed class AltinnAuthorizationClient : IAltinnAuthorization { - private const string AttributeIdSsn = "urn:altinn:ssn"; + private const string AttributePidClaim = "urn:altinn:ssn"; private readonly HttpClient _httpClient; private readonly IUser _user; @@ -48,10 +48,10 @@ await PerformDialogDetailsAuthorization(new DialogDetailsAuthorizationRequest public async Task GetAuthorizedResourcesForSearch( List constraintParties, List serviceResources, - string? authEndUserPid, + string? endUserId, CancellationToken cancellationToken = default) { - var claims = GetOrCreateClaimsBasedOnEndUserPid(authEndUserPid); + var claims = GetOrCreateClaimsBasedOnEndUserId(endUserId); return await PerformNonScalableDialogSearchAuthorization(new DialogSearchAuthorizationRequest { Claims = claims, @@ -104,12 +104,12 @@ private async Task PerformDialogDetailsAuthori return DecisionRequestHelper.CreateDialogDetailsResponse(request.AltinnActions, xamlJsonResponse); } - private List GetOrCreateClaimsBasedOnEndUserPid(string? endUserPid) + private List GetOrCreateClaimsBasedOnEndUserId(string? endUserId) { List claims = new(); - if (endUserPid is not null) + if (endUserId is not null) { - claims.Add(new Claim(AttributeIdSsn, endUserPid)); + claims.Add(new Claim(AttributePidClaim, ExtractEndUserIdNumber(endUserId)!)); } else { @@ -118,6 +118,9 @@ private List GetOrCreateClaimsBasedOnEndUserPid(string? endUserPid) return claims; } + private static string ExtractEndUserIdNumber(string endUserId) => + endUserId.Split("::").LastOrDefault() ?? string.Empty; + private static readonly JsonSerializerOptions _serializerOptions = new() { PropertyNameCaseInsensitive = true, diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs index 17a861993..9f1d493d1 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs @@ -21,7 +21,7 @@ public Task GetDialogDetailsAuthorization(Dial // Just allow everything Task.FromResult(new DialogDetailsAuthorizationResult { AuthorizedAltinnActions = dialogEntity.GetAltinnActions() }); - public async Task GetAuthorizedResourcesForSearch(List constraintParties, List serviceResources, string? authEndUserPid, + public async Task GetAuthorizedResourcesForSearch(List constraintParties, List serviceResources, string? endUserId, CancellationToken cancellationToken = default) { // Allow all resources for all parties From 20a1b79804229395f2b1e88d7b401cb76ed7dede Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Thu, 18 Jan 2024 16:59:35 +0100 Subject: [PATCH 7/9] set regex timeout --- .../Common/Numbers/PersonIdentifier.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs index ecbfa6dd6..57f64b22a 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs @@ -3,12 +3,14 @@ namespace Digdir.Domain.Dialogporten.Application.Common.Numbers; -internal static class PersonIdentifier +internal static partial class PersonIdentifier { + [GeneratedRegex(@"^urn:altinn:person:[\w-]+::\d+$", RegexOptions.None, matchTimeoutMilliseconds: 1000)] + private static partial Regex ValidateIdentifierRegex(); + public static bool IsValid(ReadOnlySpan personIdentifier) { - var regex = new Regex(@"^urn:altinn:person:[\w-]+::\d+$"); - return regex.IsMatch(personIdentifier.ToString()); + return ValidateIdentifierRegex().IsMatch(personIdentifier.ToString()); } public static string? ExtractEndUserIdNumber(string? endUserId) => From ee1ddf8d3f9988dca1fe0bef2cc224da59136b3f Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Fri, 19 Jan 2024 14:04:24 +0100 Subject: [PATCH 8/9] refactored PersonIdentifier to EndUserIdentifer --- .../Common/Numbers/EndUserIdentifier.cs | 52 +++++++++++++++++++ .../Common/Numbers/PersonIdentifier.cs | 18 ------- .../Search/SearchDialogQueryValidator.cs | 6 +-- 3 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs delete mode 100644 src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs new file mode 100644 index 000000000..5a82241a9 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs @@ -0,0 +1,52 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Digdir.Domain.Dialogporten.Application.Common.Numbers; + +internal static partial class EndUserIdentifier +{ + private static readonly int[] NorwegianIdentifierNumberWeights1 = [3, 7, 6, 1, 8, 9, 4, 5, 2, 1]; + private static readonly int[] NorwegianIdentifierNumberWeights2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1]; + + [GeneratedRegex(@"urn:altinn:([\w-]{5,20}):([\w-]{4,20})::([\w-]{11,36})", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex IdRegex(); + + public static bool IsValid(string identifier) + { + var match = IdRegex().Match(identifier); + + var namespacePart = match.Groups[1].Value; + var type = match.Groups[2].Value; + var value = match.Groups[3].Value; + + return namespacePart switch + { + "person" => ValidatePerson(type, value), + "systemuser" => ValidateSystemUser(type, value), + _ => false + }; + } + + private static bool ValidatePerson(string type, string value) + { + return type switch + { + "identifier-no" => ValidateNorwegianIdentifier(value), + _ => false, + }; + } + + private static bool ValidateSystemUser(string type, string value) + { + return type == "uuid" && Guid.TryParse(value, out _); + } + + private static bool ValidateNorwegianIdentifier(ReadOnlySpan norwegianIdentifier) + { + return norwegianIdentifier.Length == 11 + && Mod11.TryCalculateControlDigit(norwegianIdentifier[..9], NorwegianIdentifierNumberWeights1, out var control1) + && Mod11.TryCalculateControlDigit(norwegianIdentifier[..10], NorwegianIdentifierNumberWeights2, out var control2) + && control1 == int.Parse(norwegianIdentifier[9..10], CultureInfo.InvariantCulture) + && control2 == int.Parse(norwegianIdentifier[10..11], CultureInfo.InvariantCulture); + } +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs deleted file mode 100644 index 57f64b22a..000000000 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/PersonIdentifier.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Globalization; -using System.Text.RegularExpressions; - -namespace Digdir.Domain.Dialogporten.Application.Common.Numbers; - -internal static partial class PersonIdentifier -{ - [GeneratedRegex(@"^urn:altinn:person:[\w-]+::\d+$", RegexOptions.None, matchTimeoutMilliseconds: 1000)] - private static partial Regex ValidateIdentifierRegex(); - - public static bool IsValid(ReadOnlySpan personIdentifier) - { - return ValidateIdentifierRegex().IsMatch(personIdentifier.ToString()); - } - - public static string? ExtractEndUserIdNumber(string? endUserId) => - endUserId?.Split("::").LastOrDefault(); -} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs index c44e0025a..a5144755d 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Search/SearchDialogQueryValidator.cs @@ -20,10 +20,8 @@ public SearchDialogQueryValidator() .WithMessage("'{PropertyName}' must be a valid culture code."); RuleFor(x => x) - .Must(x => PersonIdentifier.IsValid(x.EndUserId)) - .WithMessage($"'{nameof(SearchDialogQuery.EndUserId)}' must be a valid person identifier. It should match the format 'urn:altinn:person:identifier-no::12345678901'.") - .Must(x => SocialSecurityNumber.IsValid(PersonIdentifier.ExtractEndUserIdNumber(x.EndUserId!))) - .WithMessage($"'{nameof(SearchDialogQuery.EndUserId)}' must include a valid national identity number.") + .Must(x => EndUserIdentifier.IsValid(x.EndUserId!)) + .WithMessage($"'{nameof(SearchDialogQuery.EndUserId)}' must be a valid end user identifier. It should match the format 'urn:altinn:person:identifier-no::{{norwegian f-nr/d-nr}} or 'urn:altinn:systemuser:{{uuid}}\"") .Must(x => !x.ServiceResource.IsNullOrEmpty() || !x.Party.IsNullOrEmpty()) .WithMessage($"Either '{nameof(SearchDialogQuery.ServiceResource)}' or '{nameof(SearchDialogQuery.Party)}' must be specified if '{nameof(SearchDialogQuery.EndUserId)}' is provided.") .When(x => x.EndUserId is not null); From 4b46bdb904bf13173df34d401df89f1461f12f29 Mon Sep 17 00:00:00 2001 From: Knut Haug <154342485+knuhau@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:43:17 +0100 Subject: [PATCH 9/9] Update src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set identifier mimimum length from 11 to 5 Co-authored-by: Bjørn Dybvik Langfors --- .../Common/Numbers/EndUserIdentifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs index 5a82241a9..3d87ec590 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Numbers/EndUserIdentifier.cs @@ -8,7 +8,7 @@ internal static partial class EndUserIdentifier private static readonly int[] NorwegianIdentifierNumberWeights1 = [3, 7, 6, 1, 8, 9, 4, 5, 2, 1]; private static readonly int[] NorwegianIdentifierNumberWeights2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1]; - [GeneratedRegex(@"urn:altinn:([\w-]{5,20}):([\w-]{4,20})::([\w-]{11,36})", RegexOptions.None, matchTimeoutMilliseconds: 100)] + [GeneratedRegex(@"urn:altinn:([\w-]{5,20}):([\w-]{4,20})::([\w-]{5,36})", RegexOptions.None, matchTimeoutMilliseconds: 100)] private static partial Regex IdRegex(); public static bool IsValid(string identifier)