Skip to content

Commit

Permalink
feat(web-api): Add optional EndUserId param to ServiceOwner Get Dialo…
Browse files Browse the repository at this point in the history
…g details API (#1020)

Add optional EndUserId param to ServiceOwner Get Dialog details API
  • Loading branch information
knuhau authored Aug 21, 2024
1 parent e2e5976 commit 1380b33
Show file tree
Hide file tree
Showing 22 changed files with 231 additions and 46 deletions.
9 changes: 9 additions & 0 deletions docs/schema/V1/swagger.verified.json
Original file line number Diff line number Diff line change
Expand Up @@ -5057,6 +5057,15 @@
"format": "guid",
"type": "string"
}
},
{
"description": "Filter by end user id",
"in": "query",
"name": "endUserId",
"schema": {
"nullable": true,
"type": "string"
}
}
],
"responses": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
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;

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<AltinnAction> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public interface IAltinnAuthorization
{
public Task<DialogDetailsAuthorizationResult> GetDialogDetailsAuthorization(
DialogEntity dialogEntity,
string? endUserId = null,
CancellationToken cancellationToken = default);

public Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task<SearchDialogActivityResult> 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public async Task<GetDialogSeenLogResult> 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public async Task<SearchDialogSeenLogResult> 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public async Task<GetDialogTransmissionResult> 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())
Expand All @@ -79,7 +79,7 @@ public async Task<GetDialogTransmissionResult> Handle(GetDialogTransmissionQuery
}

var dto = _mapper.Map<GetDialogTransmissionDto>(transmission);
dto.IsAuthorized = authorizationResult.HasReadAccessToDialogTransmission(transmission);
dto.IsAuthorized = authorizationResult.HasReadAccessToDialogTransmission(transmission.AuthorizationAttribute);

if (dto.IsAuthorized) return dto;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public async Task<SearchDialogTransmissionResult> 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo

var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization(
dialog,
cancellationToken);
cancellationToken: cancellationToken);

if (!authorizationResult.HasReadAccessToMainResource())
{
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,25 +15,38 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialog
public sealed class GetDialogQuery : IRequest<GetDialogResult>
{
public Guid DialogId { get; set; }

/// <summary>
/// Filter by end user id
/// </summary>
public string? EndUserId { get; init; }
}

[GenerateOneOf]
public partial class GetDialogResult : OneOfBase<GetDialogDto, EntityNotFound>;
public partial class GetDialogResult : OneOfBase<GetDialogDto, EntityNotFound, ValidationError>;

internal sealed class GetDialogQueryHandler : IRequestHandler<GetDialogQuery, GetDialogResult>
{
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<GetDialogResult> Handle(GetDialogQuery request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -68,7 +84,6 @@ public async Task<GetDialogResult> 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);

Expand All @@ -77,21 +92,79 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo
return new EntityNotFound<DialogEntity>(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<GetDialogDto>(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<DialogEntity>(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<GetDialogDialogSeenLogDto>(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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<GetDialogQuery>
{
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ public AltinnAuthorizationClient(
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<DialogDetailsAuthorizationResult> GetDialogDetailsAuthorization(DialogEntity dialogEntity,
public async Task<DialogDetailsAuthorizationResult> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ public LocalDevelopmentAltinnAuthorization(IDialogDbContext db)
}

[SuppressMessage("Performance", "CA1822:Mark members as static")]
public Task<DialogDetailsAuthorizationResult> GetDialogDetailsAuthorization(DialogEntity dialogEntity,
CancellationToken cancellationToken = default) =>
public Task<DialogDetailsAuthorizationResult> 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<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(List<string> constraintParties, List<string> serviceResources, string? endUserId,
CancellationToken cancellationToken = default)
Expand Down
2 changes: 0 additions & 2 deletions src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
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;
using Microsoft.EntityFrameworkCore;
using OneOf.Types;
using Polly;
using Polly.Contrib.WaitAndRetry;
using Polly.Timeout;

namespace Digdir.Domain.Dialogporten.Infrastructure;

Expand Down
Loading

0 comments on commit 1380b33

Please sign in to comment.