From d331fbc23362f92b3d441f126d9be3e5dfbf43d2 Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Sat, 17 Aug 2024 01:12:04 +0200 Subject: [PATCH 1/3] Squash --- docs/schema/V1/swagger.verified.json | 9 ++ .../Extensions/ClaimsPrincipalExtensions.cs | 3 - .../DialogDetailsAuthorizationResult.cs | 15 +-- .../IAltinnAuthorization.cs | 1 + .../Search/SearchDialogActivityQuery.cs | 2 +- .../Queries/Get/GetDialogSeenLogQuery.cs | 2 +- .../Search/SearchDialogSeenLogQuery.cs | 2 +- .../Queries/Get/GetDialogTransmissionQuery.cs | 4 +- .../Search/SearchDialogTransmissionQuery.cs | 2 +- .../Dialogs/Queries/Get/GetDialogQuery.cs | 4 +- .../Dialogs/Queries/Get/GetDialogQuery.cs | 91 +++++++++++++++++-- .../Queries/Get/GetDialogQueryValidator.cs | 18 ++++ .../Dialogs/Entities/DialogEntity.cs | 3 +- .../AltinnAuthorizationClient.cs | 6 +- .../LocalDevelopmentAltinnAuthorization.cs | 10 +- .../ServiceOwnerOnBehalfOfPersonMiddleware.cs | 79 ++++++++++++++++ .../Create/CreateDialogActivityEndpoint.cs | 6 +- .../CreateDialogTransmissionEndpoint.cs | 6 +- .../Dialogs/Get/GetDialogEndpoint.cs | 3 +- .../Program.cs | 1 + 20 files changed, 226 insertions(+), 41 deletions(-) create mode 100644 src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogQueryValidator.cs create mode 100644 src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/ServiceOwnerOnBehalfOfPersonMiddleware.cs diff --git a/docs/schema/V1/swagger.verified.json b/docs/schema/V1/swagger.verified.json index 84b1d8b7b..f5151fc7d 100644 --- a/docs/schema/V1/swagger.verified.json +++ b/docs/schema/V1/swagger.verified.json @@ -5057,6 +5057,15 @@ "format": "guid", "type": "string" } + }, + { + "description": "Filter by end user id", + "in": "query", + "name": "endUserId", + "schema": { + "nullable": true, + "type": "string" + } } ], "responses": { diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs index 615ee4c44..f0d9b6cd2 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs @@ -232,9 +232,6 @@ public static (UserIdType, string externalId) GetUserType(this ClaimsPrincipal c { if (claimsPrincipal.TryGetPid(out var externalId)) { - // ServiceOwnerOnHelfOfPerson does not work atm., since there will be no PID claim on service owner calls - // TODO: This needs to be fixed when implementing https://github.com/digdir/dialogporten/issues/386 - // F.ex. a middleware that runs before UserTypeValidationMiddleware that adds the PID claim return (claimsPrincipal.HasScope(ServiceProviderScope) ? UserIdType.ServiceOwnerOnBehalfOfPerson : UserIdType.Person, externalId); diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/DialogDetailsAuthorizationResult.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/DialogDetailsAuthorizationResult.cs index 15ff1487d..1e93c3c28 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/DialogDetailsAuthorizationResult.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/DialogDetailsAuthorizationResult.cs @@ -1,26 +1,21 @@ using Digdir.Domain.Dialogporten.Application.Common.Authorization; -using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions; +using EndUserGetDialogQuery = Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get; +using ServiceOwnerGetDialogQuery = Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get; namespace Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; public sealed class DialogDetailsAuthorizationResult { // Each action applies to a resource. This is the main resource, another subresource indicated by a authorization attribute - // eg. "urn:altinn:subresource:some-sub-resource" or "urn:altinn:task:task_1", or another resource (ie. policy) - // eg. urn:altinn:resource:some-other-resource + // e.g. "urn:altinn:subresource:some-sub-resource" or "urn:altinn:task:task_1", or another resource (i.e. policy) + // e.g. urn:altinn:resource:some-other-resource public List AuthorizedAltinnActions { get; init; } = []; public bool HasReadAccessToMainResource() => AuthorizedAltinnActions.Contains(new(Constants.ReadAction, Constants.MainResource)); - public bool HasReadAccessToDialogTransmission(DialogTransmission dialogTransmission) => - HasReadAccessToDialogTransmission(dialogTransmission.AuthorizationAttribute); - - public bool HasReadAccessToDialogTransmission(GetDialogDialogTransmissionDto dialogTransmission) => - HasReadAccessToDialogTransmission(dialogTransmission.AuthorizationAttribute); - - private bool HasReadAccessToDialogTransmission(string? authorizationAttribute) + public bool HasReadAccessToDialogTransmission(string? authorizationAttribute) { return authorizationAttribute is not null ? ( // Dialog transmissions are authorized by either the read or read action, depending on the authorization attribute type diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs index 1c93a5070..ae8d26c22 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs @@ -7,6 +7,7 @@ public interface IAltinnAuthorization { public Task GetDialogDetailsAuthorization( DialogEntity dialogEntity, + string? endUserId = null, CancellationToken cancellationToken = default); public Task GetAuthorizedResourcesForSearch( 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 6c5765204..5290e121d 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/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs index bdf5da5d4..c8780ef1f 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs @@ -58,7 +58,7 @@ public async Task Handle(GetDialogSeenLogQuery request, var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken); + cancellationToken: cancellationToken); // If we cannot read the dialog at all, we don't allow access to the seen log if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs index 92b211caa..d3c3529f1 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs @@ -56,7 +56,7 @@ public async Task Handle(SearchDialogSeenLogQuery req var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken); + cancellationToken: cancellationToken); // If we cannot read the dialog at all, we don't allow access to the seen log if (!authorizationResult.HasReadAccessToMainResource()) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogTransmissions/Queries/Get/GetDialogTransmissionQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogTransmissions/Queries/Get/GetDialogTransmissionQuery.cs index 516d93ee2..4f1eb332a 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogTransmissions/Queries/Get/GetDialogTransmissionQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogTransmissions/Queries/Get/GetDialogTransmissionQuery.cs @@ -59,7 +59,7 @@ public async Task Handle(GetDialogTransmissionQuery 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 transmissions. if (!authorizationResult.HasReadAccessToMainResource()) @@ -79,7 +79,7 @@ public async Task Handle(GetDialogTransmissionQuery } var dto = _mapper.Map(transmission); - dto.IsAuthorized = authorizationResult.HasReadAccessToDialogTransmission(transmission); + dto.IsAuthorized = authorizationResult.HasReadAccessToDialogTransmission(transmission.AuthorizationAttribute); if (dto.IsAuthorized) return dto; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogTransmissions/Queries/Search/SearchDialogTransmissionQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogTransmissions/Queries/Search/SearchDialogTransmissionQuery.cs index 2ef661d3e..ec31c7a1e 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogTransmissions/Queries/Search/SearchDialogTransmissionQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogTransmissions/Queries/Search/SearchDialogTransmissionQuery.cs @@ -55,7 +55,7 @@ public async Task Handle(SearchDialogTransmissio 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 6e0124902..8c1b8a72d 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 @@ -92,7 +92,7 @@ public async Task Handle(GetDialogQuery request, CancellationTo var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( dialog, - cancellationToken); + cancellationToken: cancellationToken); if (!authorizationResult.HasReadAccessToMainResource()) { @@ -166,7 +166,7 @@ private static void DecorateWithAuthorization(GetDialogDto dto, } } - var authorizedTransmissions = dto.Transmissions.Where(authorizationResult.HasReadAccessToDialogTransmission); + var authorizedTransmissions = dto.Transmissions.Where(t => authorizationResult.HasReadAccessToDialogTransmission(t.AuthorizationAttribute)); foreach (var transmission in authorizedTransmissions) { transmission.IsAuthorized = true; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogQuery.cs index c8258fa63..735fcbf2a 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogQuery.cs @@ -1,7 +1,10 @@ -using AutoMapper; +using System.Diagnostics; +using AutoMapper; using Digdir.Domain.Dialogporten.Application.Common; +using Digdir.Domain.Dialogporten.Application.Common.Authorization; 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 MediatR; using Microsoft.EntityFrameworkCore; @@ -12,25 +15,38 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialog public sealed class GetDialogQuery : IRequest { public Guid DialogId { get; set; } + + /// + /// Filter by end user id + /// + public string? EndUserId { get; init; } } [GenerateOneOf] -public partial class GetDialogResult : OneOfBase; +public partial class GetDialogResult : OneOfBase; internal sealed class GetDialogQueryHandler : IRequestHandler { private readonly IDialogDbContext _db; private readonly IMapper _mapper; private readonly IUserResourceRegistry _userResourceRegistry; + private readonly IAltinnAuthorization _altinnAuthorization; + private readonly IUnitOfWork _unitOfWork; + private readonly IUserRegistry _userRegistry; public GetDialogQueryHandler( IDialogDbContext db, IMapper mapper, - IUserResourceRegistry userResourceRegistry) + IUserResourceRegistry userResourceRegistry, + IAltinnAuthorization altinnAuthorization, + IUnitOfWork unitOfWork, IUserRegistry userRegistry) { _db = db ?? throw new ArgumentNullException(nameof(db)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _userResourceRegistry = userResourceRegistry ?? throw new ArgumentNullException(nameof(userResourceRegistry)); + _altinnAuthorization = altinnAuthorization ?? throw new ArgumentNullException(nameof(altinnAuthorization)); + _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + _userRegistry = userRegistry ?? throw new ArgumentNullException(nameof(userRegistry)); } public async Task Handle(GetDialogQuery request, CancellationToken cancellationToken) @@ -68,7 +84,6 @@ public async Task Handle(GetDialogQuery request, CancellationTo .OrderBy(x => x.CreatedAt)) .ThenInclude(x => x.SeenBy) .IgnoreQueryFilters() - .AsNoTracking() // TODO: Remove when #386 is implemented .Where(x => resourceIds.Contains(x.ServiceResource)) .FirstOrDefaultAsync(x => x.Id == request.DialogId, cancellationToken); @@ -77,21 +92,79 @@ public async Task Handle(GetDialogQuery request, CancellationTo return new EntityNotFound(request.DialogId); } - // TODO: Add SeenLog if optional parameter pid on behalf of end user is present - // https://github.com/digdir/dialogporten/issues/386 - var dialogDto = _mapper.Map(dialog); + if (request.EndUserId is not null) + { + var currentUserInformation = await _userRegistry.GetCurrentUserInformation(cancellationToken); + + var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization( + dialog, + request.EndUserId, + cancellationToken); + + if (!authorizationResult.HasReadAccessToMainResource()) + { + return new EntityNotFound(request.DialogId); + } + + dialog.UpdateSeenAt( + currentUserInformation.UserId.ExternalIdWithPrefix, + currentUserInformation.UserId.Type, + currentUserInformation.Name); + + var saveResult = await _unitOfWork + .WithoutAuditableSideEffects() + .SaveChangesAsync(cancellationToken); + + saveResult.Switch( + success => { }, + domainError => throw new UnreachableException("Should not get domain error when updating SeenAt."), + concurrencyError => throw new UnreachableException("Should not get concurrencyError when updating SeenAt.")); + + DecorateWithAuthorization(dialogDto, authorizationResult); + } + dialogDto.SeenSinceLastUpdate = dialog.SeenLog .Select(log => { var logDto = _mapper.Map(log); - // TODO: Set when #386 is implemented - // logDto.IsCurrentEndUser = log.EndUserId == userPid; + logDto.IsViaServiceOwner = true; return logDto; }) .ToList(); return dialogDto; } + + private static void DecorateWithAuthorization(GetDialogDto dto, + DialogDetailsAuthorizationResult authorizationResult) + { + foreach (var (action, resource) in authorizationResult.AuthorizedAltinnActions) + { + foreach (var apiAction in dto.ApiActions.Where(a => a.Action == action)) + { + if ((apiAction.AuthorizationAttribute is null && resource == Constants.MainResource) + || (apiAction.AuthorizationAttribute is not null && resource == apiAction.AuthorizationAttribute)) + { + apiAction.IsAuthorized = true; + } + } + + foreach (var guiAction in dto.GuiActions.Where(a => a.Action == action)) + { + if ((guiAction.AuthorizationAttribute is null && resource == Constants.MainResource) + || (guiAction.AuthorizationAttribute is not null && resource == guiAction.AuthorizationAttribute)) + { + guiAction.IsAuthorized = true; + } + } + + var authorizedTransmissions = dto.Transmissions.Where(t => authorizationResult.HasReadAccessToDialogTransmission(t.AuthorizationAttribute)); + foreach (var transmission in authorizedTransmissions) + { + transmission.IsAuthorized = true; + } + } + } } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogQueryValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogQueryValidator.cs new file mode 100644 index 000000000..e1dce4f8f --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogQueryValidator.cs @@ -0,0 +1,18 @@ +using Digdir.Domain.Dialogporten.Domain.Parties; +using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions; +using FluentValidation; + +namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get; + +internal sealed class GetDialogQueryValidator : AbstractValidator +{ + public GetDialogQueryValidator() + { + RuleFor(x => x.EndUserId) + .Must(x => PartyIdentifier.TryParse(x, out var id) && + id is NorwegianPersonIdentifier) + .WithMessage($"{{PropertyName}} must be a valid end user identifier. It must match the format " + + $"'{NorwegianPersonIdentifier.PrefixWithSeparator}{{norwegian f-nr/d-nr}}'.") + .When(x => x.EndUserId is not null); + } +} diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs index 97564b452..e5ce07ea8 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs @@ -1,4 +1,4 @@ -using Digdir.Domain.Dialogporten.Domain.Actors; +using Digdir.Domain.Dialogporten.Domain.Actors; using Digdir.Domain.Dialogporten.Domain.Attachments; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities; @@ -103,6 +103,7 @@ public void UpdateSeenAt(string endUserId, DialogUserType.Values userTypeId, str SeenLog.Add(new() { EndUserTypeId = userTypeId, + IsViaServiceOwner = userTypeId == DialogUserType.Values.ServiceOwnerOnBehalfOfPerson, SeenBy = new DialogSeenLogSeenByActor { ActorTypeId = ActorType.Values.PartyRepresentative, diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs index 32562db63..ee805df0f 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs @@ -43,12 +43,14 @@ public AltinnAuthorizationClient( _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public async Task GetDialogDetailsAuthorization(DialogEntity dialogEntity, + public async Task GetDialogDetailsAuthorization( + DialogEntity dialogEntity, + string? endUserId, CancellationToken cancellationToken = default) { var request = new DialogDetailsAuthorizationRequest { - Claims = _user.GetPrincipal().Claims.ToList(), + Claims = GetOrCreateClaimsBasedOnEndUserId(endUserId), ServiceResource = dialogEntity.ServiceResource, DialogId = dialogEntity.Id, Party = dialogEntity.Party, diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs index 7e4bbbcdc..53184edf6 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs @@ -18,10 +18,14 @@ public LocalDevelopmentAltinnAuthorization(IDialogDbContext db) } [SuppressMessage("Performance", "CA1822:Mark members as static")] - public Task GetDialogDetailsAuthorization(DialogEntity dialogEntity, - CancellationToken cancellationToken = default) => + public Task GetDialogDetailsAuthorization( + DialogEntity dialogEntity, + string? _, + CancellationToken __) + { // Just allow everything - Task.FromResult(new DialogDetailsAuthorizationResult { AuthorizedAltinnActions = dialogEntity.GetAltinnActions() }); + return Task.FromResult(new DialogDetailsAuthorizationResult { AuthorizedAltinnActions = dialogEntity.GetAltinnActions() }); + } public async Task GetAuthorizedResourcesForSearch(List constraintParties, List serviceResources, string? endUserId, CancellationToken cancellationToken = default) diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/ServiceOwnerOnBehalfOfPersonMiddleware.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/ServiceOwnerOnBehalfOfPersonMiddleware.cs new file mode 100644 index 000000000..06977b9a5 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/ServiceOwnerOnBehalfOfPersonMiddleware.cs @@ -0,0 +1,79 @@ +using System.Security.Claims; +using Digdir.Domain.Dialogporten.Domain.Parties; +using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; + +namespace Digdir.Domain.Dialogporten.WebApi.Common.Authentication; + +public class ServiceOwnerOnBehalfOfPersonMiddleware +{ + private readonly RequestDelegate _next; + private const string EndUserId = "enduserid"; + private const string PidClaim = "pid"; + + public ServiceOwnerOnBehalfOfPersonMiddleware(RequestDelegate next) + { + _next = next; + } + + public Task InvokeAsync(HttpContext context) + { + if (context.User.Identity is not { IsAuthenticated: true }) + { + return _next(context); + } + + if (!context.Request.Query.TryGetValue(EndUserId, out var endUserIdQuery)) + { + return _next(context); + } + + if (!NorwegianPersonIdentifier.TryParse(endUserIdQuery.First(), out var endUserId)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.WriteAsJsonAsync(context.ResponseBuilder( + context.Response.StatusCode, + [ + new("EndUserId", + "EndUserId must be a valid end user identifier. It must match the format " + + $"'{NorwegianPersonIdentifier.PrefixWithSeparator}{{norwegian f-nr/d-nr}}'." + ) + ] + )); + return Task.CompletedTask; + } + + OverrideClaim(context.User, PidClaim, endUserId.Id); + + return _next(context); + } + + private static void OverrideClaim(ClaimsPrincipal claimsPrincipal, string claimType, string newClaimValue) + { + if (claimsPrincipal.Identity is not ClaimsIdentity identity) + { + throw new InvalidOperationException("ClaimsPrincipal does not have a ClaimsIdentity."); + } + + if (!claimsPrincipal.HasClaim(claim => claim.Type == claimType)) + { + identity.AddClaim(new Claim(claimType, newClaimValue)); + return; + } + + foreach (var ident in claimsPrincipal.Identities) + { + foreach (var claim in ident.FindAll(c => c.Type == claimType)) + { + ident.RemoveClaim(claim); + } + } + + identity.AddClaim(new Claim(claimType, newClaimValue)); + } +} + +public static class ServiceOwnerOnBehalfOfPersonMiddlewareExtensions +{ + public static IApplicationBuilder UseServiceOwnerOnBehalfOfPerson(this IApplicationBuilder app) + => app.UseMiddleware(); +} diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpoint.cs index 653544b63..cad2f97e9 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpoint.cs @@ -35,9 +35,11 @@ public override void Configure() public override async Task HandleAsync(CreateDialogActivityRequest req, CancellationToken ct) { var dialogQueryResult = await _sender.Send(new GetDialogQuery { DialogId = req.DialogId }, ct); - if (dialogQueryResult.TryPickT1(out var entityNotFound, out var dialog)) + if (!dialogQueryResult.TryPickT0(out var dialog, out var errors)) { - await this.NotFoundAsync(entityNotFound, cancellationToken: ct); + await errors.Match( + notFound => this.NotFoundAsync(notFound, cancellationToken: ct), + validationError => this.BadRequestAsync(validationError, ct)); return; } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpoint.cs index 601caa738..e65b44f25 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpoint.cs @@ -35,9 +35,11 @@ public override void Configure() public override async Task HandleAsync(CreateDialogTransmissionRequest req, CancellationToken ct) { var dialogQueryResult = await _sender.Send(new GetDialogQuery { DialogId = req.DialogId }, ct); - if (dialogQueryResult.TryPickT1(out var entityNotFound, out var dialog)) + if (!dialogQueryResult.TryPickT0(out var dialog, out var errors)) { - await this.NotFoundAsync(entityNotFound, cancellationToken: ct); + await errors.Match( + notFound => this.NotFoundAsync(notFound, cancellationToken: ct), + validationError => this.BadRequestAsync(validationError, ct)); return; } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Get/GetDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Get/GetDialogEndpoint.cs index e286e4715..2026e16f8 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Get/GetDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Get/GetDialogEndpoint.cs @@ -33,6 +33,7 @@ await result.Match( HttpContext.Response.Headers.ETag = dto.Revision.ToString(); return SendOkAsync(dto, ct); }, - notFound => this.NotFoundAsync(notFound, ct)); + notFound => this.NotFoundAsync(notFound, ct), + validationError => this.BadRequestAsync(validationError, ct)); } } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs index a588294b2..96eafacea 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs @@ -152,6 +152,7 @@ static void BuildAndRun(string[] args) .UseJwtSchemeSelector() .UseAuthentication() .UseAuthorization() + .UseServiceOwnerOnBehalfOfPerson() .UseUserTypeValidation() .UseAzureConfiguration() .UseFastEndpoints(x => From 556a1225286a2dbfec8e714436a3957125ac887e Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Tue, 20 Aug 2024 18:09:00 +0200 Subject: [PATCH 2/3] Removed unused usings --- .../AltinnAuthorization/DialogDetailsAuthorizationResult.cs | 3 --- src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/DialogDetailsAuthorizationResult.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/DialogDetailsAuthorizationResult.cs index 1e93c3c28..2aa63bdf1 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/DialogDetailsAuthorizationResult.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/DialogDetailsAuthorizationResult.cs @@ -1,7 +1,4 @@ using Digdir.Domain.Dialogporten.Application.Common.Authorization; -using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions; -using EndUserGetDialogQuery = Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get; -using ServiceOwnerGetDialogQuery = Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get; namespace Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs index f15d69ff8..470355afa 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs @@ -1,7 +1,6 @@ using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; -using Digdir.Domain.Dialogporten.Infrastructure.Common.Exceptions; using Digdir.Domain.Dialogporten.Infrastructure.Persistence; using Digdir.Library.Entity.Abstractions.Features.Versionable; using Digdir.Library.Entity.EntityFrameworkCore; @@ -9,7 +8,6 @@ using OneOf.Types; using Polly; using Polly.Contrib.WaitAndRetry; -using Polly.Timeout; namespace Digdir.Domain.Dialogporten.Infrastructure; From ee027eba6a4d37f24fae9d81068a66e7d035aac0 Mon Sep 17 00:00:00 2001 From: Knut Haug Date: Wed, 21 Aug 2024 13:46:38 +0200 Subject: [PATCH 3/3] Fix Patch ValidationError handling --- .../ServiceOwner/Dialogs/Patch/PatchDialogsController.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs index 1c54f616f..bb71709d8 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs @@ -65,9 +65,14 @@ public async Task Patch( CancellationToken ct) { var dialogQueryResult = await _sender.Send(new GetDialogQuery { DialogId = dialogId }, ct); - if (dialogQueryResult.TryPickT1(out var entityNotFound, out var dialog)) + if (!dialogQueryResult.TryPickT0(out var dialog, out var errors)) { - return NotFound(HttpContext.ResponseBuilder(StatusCodes.Status404NotFound, entityNotFound.ToValidationResults())); + return errors.Match( + notFound => NotFound(HttpContext.ResponseBuilder(StatusCodes.Status404NotFound, + notFound.ToValidationResults())), + validationFailed => + BadRequest(HttpContext.ResponseBuilder(StatusCodes.Status400BadRequest, + validationFailed.Errors.ToList()))); } var updateDialogDto = _mapper.Map(dialog);