diff --git a/src/Digdir.Domain.Dialogporten.Application/ApplicationExtensions.cs b/src/Digdir.Domain.Dialogporten.Application/ApplicationExtensions.cs index 519baae48..9b7f2084a 100644 --- a/src/Digdir.Domain.Dialogporten.Application/ApplicationExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Application/ApplicationExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System.Reflection; +using Digdir.Domain.Dialogporten.Application.Common.Authorization; using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Content; namespace Digdir.Domain.Dialogporten.Application; @@ -41,6 +42,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services .AddScoped() // Transient + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Authorization/Constants.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Authorization/Constants.cs index 5f6fb1495..054f4fa0d 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Authorization/Constants.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Authorization/Constants.cs @@ -7,4 +7,5 @@ public static class Constants public const string TransmissionReadAction = "transmissionread"; public static readonly Uri UnauthorizedUri = new("urn:dialogporten:unauthorized"); public const string CorrespondenceScope = "digdir:dialogporten.correspondence"; + public const string ServiceOwnerAdminScope = "digdir:dialogporten.serviceprovider.admin"; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Authorization/IServiceResourceAuthorizer.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Authorization/IServiceResourceAuthorizer.cs new file mode 100644 index 000000000..2b8afab71 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Authorization/IServiceResourceAuthorizer.cs @@ -0,0 +1,99 @@ +using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; +using Digdir.Domain.Dialogporten.Application.Externals; +using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Create; +using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; +using OneOf; +using OneOf.Types; + +namespace Digdir.Domain.Dialogporten.Application.Common.Authorization; + +public interface IServiceResourceAuthorizer +{ + Task AuthorizeServiceResources( + DialogEntity dialog, + CancellationToken cancellationToken); + + Task SetResourceType( + DialogEntity dialog, + CancellationToken cancellationToken); +} + +[GenerateOneOf] +public partial class AuthorizeServiceResourcesResult : OneOfBase; + +[GenerateOneOf] +public partial class SetResourceTypeResult : OneOfBase; + +public struct DomainContextInvalidated; + +internal sealed class ServiceResourceAuthorizer : IServiceResourceAuthorizer +{ + private readonly IUserResourceRegistry _userResourceRegistry; + private readonly IResourceRegistry _resourceRegistry; + private readonly IDomainContext _domainContext; + + public ServiceResourceAuthorizer( + IUserResourceRegistry userResourceRegistry, + IResourceRegistry resourceRegistry, + IDomainContext domainContext) + { + _userResourceRegistry = userResourceRegistry ?? throw new ArgumentNullException(nameof(userResourceRegistry)); + _resourceRegistry = resourceRegistry ?? throw new ArgumentNullException(nameof(resourceRegistry)); + _domainContext = domainContext ?? throw new ArgumentNullException(nameof(domainContext)); + } + + public async Task AuthorizeServiceResources( + DialogEntity dialog, + CancellationToken cancellationToken) + { + if (_userResourceRegistry.IsCurrentUserServiceOwnerAdmin()) + { + return new Success(); + } + + var ownedResources = await _userResourceRegistry.GetCurrentUserResourceIds(cancellationToken); + var notOwnedResources = GetPrimaryServiceResourceReferences(dialog) + .Except(ownedResources) + .ToList(); + + if (notOwnedResources.Count != 0) + { + return new Forbidden($"Not allowed to reference the following unowned resources: [{string.Join(", ", notOwnedResources)}]."); + } + + if (!_userResourceRegistry.UserCanModifyResourceType(dialog.ServiceResourceType)) + { + return new Forbidden($"User cannot create or modify a dialog with resource type {dialog.ServiceResourceType}."); + } + + return new Success(); + } + + public async Task SetResourceType(DialogEntity dialog, CancellationToken cancellationToken) + { + var serviceResourceInformation = await _resourceRegistry.GetResourceInformation(dialog.ServiceResource, cancellationToken); + if (serviceResourceInformation is null) + { + _domainContext.AddError(nameof(CreateDialogCommand.ServiceResource), + $"Service resource '{dialog.ServiceResource}' does not exist in the resource registry."); + return new DomainContextInvalidated(); + } + + dialog.ServiceResourceType = serviceResourceInformation.ResourceType; + return new Success(); + } + + private static IEnumerable GetPrimaryServiceResourceReferences(DialogEntity dialog) => + Enumerable.Empty() + .Append(dialog.ServiceResource) + .Concat(dialog.ApiActions.Select(action => action.AuthorizationAttribute!)) + .Concat(dialog.GuiActions.Select(action => action.AuthorizationAttribute!)) + .Concat(dialog.Transmissions.Select(transmission => transmission.AuthorizationAttribute!)) + .Select(x => x.ToLowerInvariant()) + .Distinct() + .Where(IsPrimaryResource); + + private static bool IsPrimaryResource(string? resource) => + resource is not null + && resource.StartsWith(Domain.Common.Constants.ServiceResourcePrefix, StringComparison.OrdinalIgnoreCase); +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/IUserResourceRegistry.cs b/src/Digdir.Domain.Dialogporten.Application/Common/IUserResourceRegistry.cs index 5e904bbe3..29a35b682 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/IUserResourceRegistry.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/IUserResourceRegistry.cs @@ -10,11 +10,11 @@ public interface IUserResourceRegistry { Task CurrentUserIsOwner(string serviceResource, CancellationToken cancellationToken); Task> GetCurrentUserResourceIds(CancellationToken cancellationToken); - Task GetResourceType(string serviceResourceId, CancellationToken cancellationToken); bool UserCanModifyResourceType(string serviceResourceType); + bool IsCurrentUserServiceOwnerAdmin(); } -public class UserResourceRegistry : IUserResourceRegistry +internal sealed class UserResourceRegistry : IUserResourceRegistry { private readonly IUser _user; private readonly IResourceRegistry _resourceRegistry; @@ -31,21 +31,25 @@ public async Task CurrentUserIsOwner(string serviceResource, CancellationT return resourceIds.Contains(serviceResource); } - public Task> GetCurrentUserResourceIds(CancellationToken cancellationToken) => - !_user.TryGetOrganizationNumber(out var orgNumber) - ? throw new UnreachableException() - : _resourceRegistry.GetResourceIds(orgNumber, cancellationToken); + public async Task> GetCurrentUserResourceIds(CancellationToken cancellationToken) + { + if (!_user.TryGetOrganizationNumber(out var orgNumber)) + { + throw new UnreachableException(); + } - public Task GetResourceType(string serviceResourceId, CancellationToken cancellationToken) => - !_user.TryGetOrganizationNumber(out var orgNumber) - ? throw new UnreachableException() - : _resourceRegistry.GetResourceType(orgNumber, serviceResourceId, cancellationToken); + var dic = await _resourceRegistry.GetResourceInformationForOrg(orgNumber, cancellationToken); + return dic.Select(x => x.ResourceId).ToList(); + } public bool UserCanModifyResourceType(string serviceResourceType) => serviceResourceType switch { ResourceRegistry.Constants.Correspondence => _user.GetPrincipal().HasScope(Constants.CorrespondenceScope), + null => false, _ => true }; + + public bool IsCurrentUserServiceOwnerAdmin() => _user.GetPrincipal().HasScope(Constants.ServiceOwnerAdminScope); } internal sealed class LocalDevelopmentUserResourceRegistryDecorator : IUserResourceRegistry @@ -63,8 +67,6 @@ public Task CurrentUserIsOwner(string serviceResource, CancellationToken c public Task> GetCurrentUserResourceIds(CancellationToken cancellationToken) => _userResourceRegistry.GetCurrentUserResourceIds(cancellationToken); - public Task GetResourceType(string serviceResourceId, CancellationToken cancellationToken) => - Task.FromResult("LocalResourceType"); - public bool UserCanModifyResourceType(string serviceResourceType) => true; + public bool IsCurrentUserServiceOwnerAdmin() => true; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/IResourceRegistry.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/IResourceRegistry.cs index d942e3911..c4204791d 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/IResourceRegistry.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/IResourceRegistry.cs @@ -2,6 +2,20 @@ public interface IResourceRegistry { - Task> GetResourceIds(string orgNumber, CancellationToken cancellationToken); - Task GetResourceType(string orgNumber, string serviceResourceId, CancellationToken cancellationToken); + Task> GetResourceInformationForOrg(string orgNumber, CancellationToken cancellationToken); + Task GetResourceInformation(string serviceResourceId, CancellationToken cancellationToken); +} + +public sealed record ServiceResourceInformation +{ + public string ResourceType { get; } + public string OwnerOrgNumber { get; } + public string ResourceId { get; } + + public ServiceResourceInformation(string resourceId, string resourceType, string ownerOrgNumber) + { + ResourceId = resourceId.ToLowerInvariant(); + ResourceType = resourceType.ToLowerInvariant(); + OwnerOrgNumber = ownerOrgNumber.ToLowerInvariant(); + } } 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 b9b9dcbe4..516d93ee2 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 @@ -1,5 +1,4 @@ using AutoMapper; -using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Common.Authorization; using Digdir.Domain.Dialogporten.Application.Externals; 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 4c14a9aee..2ef661d3e 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 @@ -1,5 +1,4 @@ using AutoMapper; -using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogDto.cs index 541d183d9..ea9abc59d 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogDto.cs @@ -1,6 +1,5 @@ using System.Text.Json.Serialization; using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Content; -using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents; namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommand.cs index 14abbaca5..387049e56 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommand.cs @@ -1,18 +1,16 @@ 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.Domain.Common; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions; -using FluentValidation.Results; using MediatR; using OneOf; using OneOf.Types; -using ResourceRegistryConstants = Digdir.Domain.Dialogporten.Application.Common.ResourceRegistry.Constants; -using AuthorizationConstants = Digdir.Domain.Dialogporten.Application.Common.Authorization.Constants; namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Create; @@ -27,75 +25,36 @@ internal sealed class CreateDialogCommandHandler : IRequestHandler Handle(CreateDialogCommand request, CancellationToken cancellationToken) { - foreach (var serviceResourceReference in GetServiceResourceReferences(request)) - { - if (!await _userResourceRegistry.CurrentUserIsOwner(serviceResourceReference, cancellationToken)) - { - return new Forbidden($"Not allowed to reference {serviceResourceReference}."); - } - } - - var serviceResourceType = await _userResourceRegistry.GetResourceType(request.ServiceResource, cancellationToken); - - if (!_userResourceRegistry.UserCanModifyResourceType(serviceResourceType)) - { - return new Forbidden($"User cannot create resource type {serviceResourceType}. Missing scope {AuthorizationConstants.CorrespondenceScope}."); - } - - if (serviceResourceType == ResourceRegistryConstants.Correspondence) - { - if (request.Progress is not null) - return new ValidationError(ProgressValidationFailure); - } + var dialog = _mapper.Map(request); - foreach (var activity in request.Activities) + await _serviceResourceAuthorizer.SetResourceType(dialog, cancellationToken); + var serviceResourceAuthorizationResult = await _serviceResourceAuthorizer.AuthorizeServiceResources(dialog, cancellationToken); + if (serviceResourceAuthorizationResult.Value is Forbidden forbiddenResult) { - if (activity.PerformedBy.ActorId is null) - { - continue; - } - - activity.PerformedBy.ActorName = await _partyNameRegistry.GetName(activity.PerformedBy.ActorId, cancellationToken); - - if (!string.IsNullOrWhiteSpace(activity.PerformedBy.ActorName)) - { - continue; - } - - var domainFailure = new DomainFailure(nameof(activity.PerformedBy.ActorId), $"Unable to look up name for actor id: {activity.PerformedBy.ActorId}"); - return new DomainError(domainFailure); + return forbiddenResult; } - var dialog = _mapper.Map(request); - - dialog.ServiceResourceType = serviceResourceType; - dialog.Org = await _userOrganizationRegistry.GetCurrentUserOrgShortName(cancellationToken) ?? string.Empty; if (string.IsNullOrWhiteSpace(dialog.Org)) { @@ -103,7 +62,18 @@ public async Task Handle(CreateDialogCommand request, Cancel "Cannot find service owner organization shortname for current user. Please ensure that you are logged in as a service owner.")); } - var existingDialogIds = await _db.GetExistingIds(new[] { dialog }, cancellationToken); + await EnsureNoExistingUserDefinedIds(dialog, cancellationToken); + await _db.Dialogs.AddAsync(dialog, cancellationToken); + var saveResult = await _unitOfWork.SaveChangesAsync(cancellationToken); + return saveResult.Match( + success => new Success(dialog.Id), + domainError => domainError, + concurrencyError => throw new UnreachableException("Should never get a concurrency error when creating a new dialog")); + } + + private async Task EnsureNoExistingUserDefinedIds(DialogEntity dialog, CancellationToken cancellationToken) + { + var existingDialogIds = await _db.GetExistingIds([dialog], cancellationToken); if (existingDialogIds.Count != 0) { _domainContext.AddError(DomainFailure.EntityExists(existingDialogIds)); @@ -115,53 +85,10 @@ public async Task Handle(CreateDialogCommand request, Cancel _domainContext.AddError(DomainFailure.EntityExists(existingActivityIds)); } - var existingAttachmentIds = await _db.GetExistingIds(dialog.Attachments, cancellationToken); - if (existingAttachmentIds.Count != 0) - { - _domainContext.AddError(DomainFailure.EntityExists(existingAttachmentIds)); - } - var existingTransmissionIds = await _db.GetExistingIds(dialog.Transmissions, cancellationToken); if (existingTransmissionIds.Count != 0) { _domainContext.AddError(DomainFailure.EntityExists(existingTransmissionIds)); } - - var transmissionAttachments = dialog.Transmissions.SelectMany(x => x.Attachments); - var existingTransmissionAttachmentIds = await _db.GetExistingIds(transmissionAttachments, cancellationToken); - if (existingTransmissionAttachmentIds.Count != 0) - { - _domainContext.AddError(DomainFailure.EntityExists(existingTransmissionAttachmentIds)); - } - - await _db.Dialogs.AddAsync(dialog, cancellationToken); - - var saveResult = await _unitOfWork.SaveChangesAsync(cancellationToken); - return saveResult.Match( - success => new Success(dialog.Id), - domainError => domainError, - concurrencyError => throw new UnreachableException("Should never get a concurrency error when creating a new dialog")); - } - - private static List GetServiceResourceReferences(CreateDialogDto request) - { - var serviceResourceReferences = new List { request.ServiceResource }; - - static bool IsExternalResource(string? resource) - { - return resource is not null && resource.StartsWith(Constants.ServiceResourcePrefix, StringComparison.OrdinalIgnoreCase); - } - - serviceResourceReferences.AddRange(request.ApiActions - .Where(action => IsExternalResource(action.AuthorizationAttribute)) - .Select(action => action.AuthorizationAttribute!)); - serviceResourceReferences.AddRange(request.GuiActions - .Where(action => IsExternalResource(action.AuthorizationAttribute)) - .Select(action => action.AuthorizationAttribute!)); - serviceResourceReferences.AddRange(request.Transmissions - .Where(transmission => IsExternalResource(transmission.AuthorizationAttribute)) - .Select(transmission => transmission.AuthorizationAttribute!)); - - return serviceResourceReferences.Distinct().ToList(); } } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/MappingProfile.cs index c6b7d44eb..7bbf6aef0 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/MappingProfile.cs @@ -48,7 +48,8 @@ public MappingProfile() .ForMember(dest => dest.HttpMethodId, opt => opt.MapFrom(src => src.HttpMethod)); CreateMap() - .IgnoreComplexDestinationProperties(); + .IgnoreComplexDestinationProperties() + .ForMember(x => x.Id, opt => opt.Ignore()); CreateMap() .IgnoreComplexDestinationProperties() @@ -81,7 +82,8 @@ public MappingProfile() .ForMember(dest => dest.TypeId, opt => opt.MapFrom(src => src.Type)); CreateMap() - .IgnoreComplexDestinationProperties(); + .IgnoreComplexDestinationProperties() + .ForMember(x => x.Id, opt => opt.Ignore()); CreateMap() .IgnoreComplexDestinationProperties() diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs index 8e8530a9f..7ac9ca025 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs @@ -1,5 +1,6 @@ using AutoMapper; using Digdir.Domain.Dialogporten.Application.Common; +using Digdir.Domain.Dialogporten.Application.Common.Authorization; using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; @@ -9,12 +10,10 @@ using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions; -using FluentValidation.Results; using MediatR; using Microsoft.EntityFrameworkCore; using OneOf; using OneOf.Types; -using ResourceRegistryConstants = Digdir.Domain.Dialogporten.Application.Common.ResourceRegistry.Constants; namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Update; @@ -35,21 +34,22 @@ internal sealed class UpdateDialogCommandHandler : IRequestHandler Handle(UpdateDialogCommand request, CancellationToken cancellationToken) @@ -81,25 +81,6 @@ public async Task Handle(UpdateDialogCommand request, Cancel return new EntityNotFound(request.Id); } - foreach (var serviceResourceReference in GetServiceResourceReferences(request.Dto)) - { - if (!await _userResourceRegistry.CurrentUserIsOwner(serviceResourceReference, cancellationToken)) - { - return new Forbidden($"Not allowed to reference {serviceResourceReference}."); - } - } - - if (!_userResourceRegistry.UserCanModifyResourceType(dialog.ServiceResourceType)) - { - return new Forbidden($"User cannot modify resource type {dialog.ServiceResourceType}."); - } - - if (dialog.ServiceResourceType == ResourceRegistryConstants.Correspondence) - { - if (request.Dto.Progress is not null) - return new ValidationError(_progressValidationFailure); - } - if (dialog.Deleted) { // TODO: When restoration is implemented, add a hint to the error message. @@ -127,14 +108,13 @@ public async Task Handle(UpdateDialogCommand request, Cancel delete: DeleteDelegate.NoOp, comparer: StringComparer.InvariantCultureIgnoreCase); - await dialog.Attachments - .MergeAsync(request.Dto.Attachments, + dialog.Attachments + .Merge(request.Dto.Attachments, destinationKeySelector: x => x.Id, sourceKeySelector: x => x.Id, create: CreateAttachments, update: UpdateAttachments, - delete: DeleteDelegate.NoOp, - cancellationToken: cancellationToken); + delete: DeleteDelegate.NoOp); dialog.GuiActions .Merge(request.Dto.GuiActions, @@ -152,6 +132,14 @@ await dialog.Attachments update: UpdateApiActions, delete: DeleteDelegate.NoOp); + var serviceResourceAuthorizationResult = await _serviceResourceAuthorizer.AuthorizeServiceResources(dialog, cancellationToken); + if (serviceResourceAuthorizationResult.Value is Forbidden forbiddenResult) + { + // Ignore the domain context errors, as they are not relevant when returning Forbidden. + _domainContext.Pop(); + return forbiddenResult; + } + var saveResult = await _unitOfWork .EnableConcurrencyCheck(dialog, request.IfMatchDialogRevision) .SaveChangesAsync(cancellationToken); @@ -162,28 +150,6 @@ await dialog.Attachments concurrencyError => concurrencyError); } - private static List GetServiceResourceReferences(UpdateDialogDto request) - { - var serviceResourceReferences = new List(); - - static bool IsExternalResource(string? resource) - { - return resource is not null && resource.StartsWith(Domain.Common.Constants.ServiceResourcePrefix, StringComparison.OrdinalIgnoreCase); - } - - serviceResourceReferences.AddRange(request.ApiActions - .Where(action => IsExternalResource(action.AuthorizationAttribute)) - .Select(action => action.AuthorizationAttribute!)); - serviceResourceReferences.AddRange(request.GuiActions - .Where(action => IsExternalResource(action.AuthorizationAttribute)) - .Select(action => action.AuthorizationAttribute!)); - serviceResourceReferences.AddRange(request.Transmissions - .Where(transmission => IsExternalResource(transmission.AuthorizationAttribute)) - .Select(transmission => transmission.AuthorizationAttribute!)); - - return serviceResourceReferences.Distinct().ToList(); - } - private void ValidateTimeFields(DialogEntity dialog) { const string errorMessage = "Must be in future or current value."; @@ -293,19 +259,7 @@ private async Task AppendTransmission(DialogEntity dialog, UpdateDialogDto dto, _domainContext.AddError(DomainFailure.EntityExists(existingIds)); } - var transmissionAttachments = newDialogTransmissions.SelectMany(x => x.Attachments); - var existingTransmissionAttachmentIds = await _db.GetExistingIds(transmissionAttachments, cancellationToken); - if (existingTransmissionAttachmentIds.Count != 0) - { - _domainContext.AddError(DomainFailure.EntityExists(existingTransmissionAttachmentIds)); - } - - if (_domainContext.Errors.Count != 0) - { - return; - } dialog.Transmissions.AddRange(newDialogTransmissions); - // Tell ef explicitly to add transmissions as new to the database. _db.DialogTransmissions.AddRange(newDialogTransmissions); } @@ -362,26 +316,17 @@ private void UpdateApiActions(IEnumerable> CreateAttachments(IEnumerable creatables, CancellationToken cancellationToken) + private IEnumerable CreateAttachments(IEnumerable creatables) { - var attachments = new List(); - foreach (var atttachmentDto in creatables) - { - var attachment = _mapper.Map(atttachmentDto); - attachment.Urls = _mapper.Map>(atttachmentDto.Urls); - attachments.Add(attachment); - } - - var existingIds = await _db.GetExistingIds(attachments, cancellationToken); - if (existingIds.Count != 0) - { - _domainContext.AddError(nameof(UpdateDialogDto.Attachments), $"Entity '{nameof(DialogAttachment)}' with the following key(s) already exists: ({string.Join(", ", existingIds)})."); - } - - return attachments; + return creatables.Select(attachmentDto => + { + var attachment = _mapper.Map(attachmentDto); + attachment.Urls = _mapper.Map>(attachmentDto.Urls); + return attachment; + }); } - private Task UpdateAttachments(IEnumerable> updateSets, CancellationToken _) + private void UpdateAttachments(IEnumerable> updateSets) { foreach (var updateSet in updateSets) { @@ -394,7 +339,5 @@ private Task UpdateAttachments(IEnumerable( apiUrl, nameLookup, @@ -63,6 +51,24 @@ public PartyNameRegistryClient(HttpClient client, IFusionCacheProvider cacheProv return nameLookupResult.PartyNames.FirstOrDefault()?.Name; } + private static bool TryParse(string externalIdWithPrefix, [NotNullWhen(true)] out NameLookup? nameLookup) + { + if (!PartyIdentifier.TryParse(externalIdWithPrefix, out var partyIdentifier)) + { + nameLookup = null; + return false; + } + + nameLookup = partyIdentifier switch + { + NorwegianPersonIdentifier personIdentifier => new() { Parties = [new() { Ssn = personIdentifier.Id }] }, + NorwegianOrganizationIdentifier organizationIdentifier => new() { Parties = [new() { OrgNo = organizationIdentifier.Id }] }, + _ => null + }; + + return nameLookup is not null; + } + private sealed class NameLookup { public List Parties { get; set; } = null!; diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/LocalDevelopmentResourceRegistry.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/LocalDevelopmentResourceRegistry.cs index c850baf40..246dabe9a 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/LocalDevelopmentResourceRegistry.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/LocalDevelopmentResourceRegistry.cs @@ -5,8 +5,9 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.ResourceRegistry; internal sealed class LocalDevelopmentResourceRegistry : IResourceRegistry { - private static readonly HashSet _cachedResourceIds = []; - private readonly string _localResourceType = "LocalResourceType"; + private const string LocalResourceType = "LocalResourceType"; + private const string LocalOrgId = "742859274"; + private static readonly HashSet CachedResourceIds = new(new ServiceResourceInformationEqualityComparer()); private readonly IDialogDbContext _db; public LocalDevelopmentResourceRegistry(IDialogDbContext db) @@ -14,23 +15,33 @@ public LocalDevelopmentResourceRegistry(IDialogDbContext db) _db = db ?? throw new ArgumentNullException(nameof(db)); } - public async Task> GetResourceIds(string orgNumber, CancellationToken cancellationToken) + public async Task> GetResourceInformationForOrg(string orgNumber, CancellationToken cancellationToken) { var newIds = await _db.Dialogs - .Where(x => !_cachedResourceIds.Contains(x.ServiceResource)) .Select(x => x.ServiceResource) .Distinct() .ToListAsync(cancellationToken); foreach (var id in newIds) { - _cachedResourceIds.Add(id); + CachedResourceIds.Add(new ServiceResourceInformation(id, LocalResourceType, orgNumber)); } - return _cachedResourceIds; + return CachedResourceIds; } - // TODO: Local testing of correspondence? - public Task GetResourceType(string _, string __, CancellationToken ___) - => Task.FromResult(_localResourceType); + public Task GetResourceInformation(string serviceResourceId, CancellationToken cancellationToken) + { + return Task.FromResult( + new ServiceResourceInformation(serviceResourceId, LocalResourceType, LocalOrgId)); + } + + private sealed class ServiceResourceInformationEqualityComparer : IEqualityComparer + { + public bool Equals(ServiceResourceInformation? x, ServiceResourceInformation? y) + => x?.ResourceId == y?.ResourceId && x?.OwnerOrgNumber == y?.OwnerOrgNumber; + + public int GetHashCode(ServiceResourceInformation obj) + => HashCode.Combine(obj.ResourceId, obj.OwnerOrgNumber); + } } diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs index 1c0b8c09b..fa87ae0ab 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs @@ -6,7 +6,8 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.ResourceRegistry; internal sealed class ResourceRegistryClient : IResourceRegistry { - private const string OrgResourceReferenceCacheKey = "OrgResourceReference"; + private const string ServiceResourceInformationByOrgCacheKey = "ServiceResourceInformationByOrgCacheKey"; + private const string ServiceResourceInformationByResourceIdCacheKey = "ServiceResourceInformationByResourceIdCacheKey"; private const string ResourceTypeGenericAccess = "GenericAccessResource"; private const string ResourceTypeAltinnApp = "AltinnApp"; private const string ResourceTypeCorrespondence = "Correspondence"; @@ -20,31 +21,57 @@ public ResourceRegistryClient(HttpClient client, IFusionCacheProvider cacheProvi _cache = cacheProvider.GetCache(nameof(ResourceRegistry)) ?? throw new ArgumentNullException(nameof(cacheProvider)); } - private async Task> GetResourceInfoByOrg(CancellationToken cancellationToken) => - await _cache.GetOrSetAsync( - OrgResourceReferenceCacheKey, - async token => await GetResourceInfoByOrgFromAltinn(token), - token: cancellationToken); + public async Task> GetResourceInformationForOrg( + string orgNumber, + CancellationToken cancellationToken) + { + var dic = await GetOrSetResourceInformationByOrg(cancellationToken); + if (!dic.TryGetValue(orgNumber, out var resources)) + { + resources = []; + } - public async Task> GetResourceIds(string org, CancellationToken cancellationToken) + return resources.AsReadOnly(); + } + + public async Task GetResourceInformation( + string serviceResourceId, + CancellationToken cancellationToken) { - var resourceIdsByOrg = await GetResourceInfoByOrg(cancellationToken); - resourceIdsByOrg.TryGetValue(org, out var resourceInfos); - return resourceInfos?.Select(x => x.ResourceId).ToList() ?? []; + var dic = await GetOrSetResourceInformationByResourceId(cancellationToken); + dic.TryGetValue(serviceResourceId, out var resource); + return resource; } - public async Task GetResourceType(string orgNumber, string serviceResourceId, CancellationToken token) + private async Task> GetOrSetResourceInformationByOrg( + CancellationToken cancellationToken) { - var resourceIdsByOrg = await GetResourceInfoByOrg(token); - resourceIdsByOrg.TryGetValue(orgNumber, out var resourceInfo); + return await _cache.GetOrSetAsync( + ServiceResourceInformationByOrgCacheKey, + async cToken => + { + var resources = await FetchServiceResourceInformation(cToken); + return resources + .GroupBy(x => x.OwnerOrgNumber) + .ToDictionary(x => x.Key, x => x.ToArray()); + }, + token: cancellationToken); + } - return resourceInfo? - .FirstOrDefault(x => x.ResourceId == serviceResourceId)? - .ResourceType ?? - throw new KeyNotFoundException(); + private async Task> GetOrSetResourceInformationByResourceId( + CancellationToken cancellationToken) + { + return await _cache.GetOrSetAsync( + ServiceResourceInformationByResourceIdCacheKey, + async cToken => + { + var resources = await FetchServiceResourceInformation(cToken); + return resources.ToDictionary(x => x.ResourceId); + }, + token: cancellationToken); } - private async Task> GetResourceInfoByOrgFromAltinn(CancellationToken cancellationToken) + private async Task FetchServiceResourceInformation(CancellationToken cancellationToken) { const string searchEndpoint = "resourceregistry/api/v1/resource/resourcelist"; @@ -52,21 +79,17 @@ private async Task> GetResourceI .GetFromJsonEnsuredAsync>(searchEndpoint, cancellationToken: cancellationToken); - var resourceInfoByOrg = response + return response .Where(x => !string.IsNullOrWhiteSpace(x.HasCompetentAuthority.Organization)) .Where(x => x.ResourceType is ResourceTypeGenericAccess or ResourceTypeAltinnApp or ResourceTypeCorrespondence) - .GroupBy(x => x.HasCompetentAuthority.Organization!) - .ToDictionary( - x => x.Key, - x => x.Select( - x => new AltinnResourceInformation($"{Constants.ServiceResourcePrefix}{x.Identifier}", x.ResourceType)) - .ToArray() - ); - - return resourceInfoByOrg; + .Select(x => new ServiceResourceInformation( + $"{Constants.ServiceResourcePrefix}{x.Identifier}", + x.ResourceType, + x.HasCompetentAuthority.Organization!)) + .ToArray(); } private sealed class ResourceRegistryResponse @@ -85,4 +108,3 @@ private sealed class CompetentAuthority } } -public sealed record AltinnResourceInformation(string ResourceId, string ResourceType); diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 1f2b19985..f91182960 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -26,6 +26,7 @@ using Digdir.Domain.Dialogporten.Infrastructure.Altinn.NameRegistry; using Digdir.Domain.Dialogporten.Infrastructure.Altinn.OrganizationRegistry; using Digdir.Domain.Dialogporten.Infrastructure.Altinn.ResourceRegistry; +using Digdir.Domain.Dialogporten.Infrastructure.Persistence.Configurations.Actors; using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.NullObjects; @@ -121,7 +122,10 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi { o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); }) - .AddInterceptors(services.GetRequiredService()); + .AddInterceptors([ + services.GetRequiredService(), + services.GetRequiredService() + ]); }) .AddHostedService() @@ -132,6 +136,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi // Transient .AddTransient() .AddTransient() + .AddTransient() // Decorate .Decorate(typeof(INotificationHandler<>), typeof(IdempotentDomainEventHandler<>)); diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Configurations/Actors/PopulateActorNameInterceptor.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Configurations/Actors/PopulateActorNameInterceptor.cs new file mode 100644 index 000000000..6b09e6d60 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Configurations/Actors/PopulateActorNameInterceptor.cs @@ -0,0 +1,97 @@ +using System.Diagnostics; +using Digdir.Domain.Dialogporten.Application.Common; +using Digdir.Domain.Dialogporten.Application.Externals; +using Digdir.Domain.Dialogporten.Domain.Actors; +using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Digdir.Domain.Dialogporten.Infrastructure.Persistence.Configurations.Actors; + +internal sealed class PopulateActorNameInterceptor : SaveChangesInterceptor +{ + private static readonly Type ActorType = typeof(Actor); + + private readonly IDomainContext _domainContext; + private readonly IPartyNameRegistry _partyNameRegistry; + private bool _hasBeenExecuted; + + public PopulateActorNameInterceptor( + IDomainContext domainContext, + IPartyNameRegistry partyNameRegistry) + { + _domainContext = domainContext ?? throw new ArgumentNullException(nameof(domainContext)); + _partyNameRegistry = partyNameRegistry ?? throw new ArgumentNullException(nameof(partyNameRegistry)); + } + + public override async ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + // If the interceptor has already run during this transaction, we don't want to run it again. + // This is to avoid doing the same work over multiple retries. + if (_hasBeenExecuted) + { + return await base.SavingChangesAsync(eventData, result, cancellationToken); + } + + var dbContext = eventData.Context; + + if (dbContext is null) + { + return await base.SavingChangesAsync(eventData, result, cancellationToken); + } + + var actors = dbContext.ChangeTracker.Entries() + .Where(x => x.Metadata.ClrType.IsAssignableTo(ActorType)) + .Where(x => x.State is EntityState.Added or EntityState.Modified) + .Select(x => + { + var actor = (Actor)x.Entity; + actor.ActorId = actor.ActorId?.ToLowerInvariant(); + return actor; + }) + .Where(x => x.ActorId is not null) + .ToList(); + + var actorNameById = new Dictionary(); + foreach (var actorId in actors + .Select(x => x.ActorId!) + .Distinct()) + { + actorNameById[actorId] = await _partyNameRegistry.GetName(actorId, cancellationToken); + } + + foreach (var actor in actors) + { + if (!actorNameById.TryGetValue(actor.ActorId!, out var actorName)) + { + throw new UnreachableException( + $"Expected {nameof(actorNameById)} to contain a record for every " + + $"actor id. Missing record for actor id: {actor.ActorId}. Is " + + $"the lookup method implemented correctly?"); + } + + if (string.IsNullOrWhiteSpace(actorName)) + { + // We don't want to fail the save operation if we are unable to look up the + // name for this particular actor, as it is used on enduser get operations. + if (actor is DialogSeenLogSeenByActor) + { + continue; + } + + _domainContext.AddError(nameof(Actor.ActorId), $"Unable to look up name for actor id: {actor.ActorId}"); + continue; + } + + actor.ActorName = actorName; + } + + _hasBeenExecuted = true; + return !_domainContext.IsValid + ? InterceptionResult.SuppressWithResult(0) + : await base.SavingChangesAsync(eventData, result, cancellationToken); + } +} diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs index 48a8d78ba..f15d69ff8 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs @@ -10,13 +10,18 @@ using Polly; using Polly.Contrib.WaitAndRetry; using Polly.Timeout; -using Polly.Wrap; namespace Digdir.Domain.Dialogporten.Infrastructure; internal sealed class UnitOfWork : IUnitOfWork { - private static readonly AsyncPolicyWrap ConcurrencyRetryPolicy; + // Fetch the db revision and retry + // https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#resolving-concurrency-conflicts + private static readonly AsyncPolicy ConcurrencyRetryPolicy = Policy + .Handle() + .WaitAndRetryAsync( + sleepDurations: Backoff.ConstantBackoff(TimeSpan.FromMilliseconds(200), 25), + onRetryAsync: FetchCurrentRevision); private readonly DialogDbContext _dialogDbContext; private readonly ITransactionTime _transactionTime; @@ -32,31 +37,6 @@ public UnitOfWork(DialogDbContext dialogDbContext, ITransactionTime transactionT _domainContext = domainContext ?? throw new ArgumentNullException(nameof(domainContext)); } - static UnitOfWork() - { - // Backoff strategy with jitter for retry policy, starting at ~5ms - const int medianFirstDelayInMs = 5; - // Total timeout for optimistic concurrency handling - const int timeoutInSeconds = 10; - - var timeoutPolicy = - Policy.TimeoutAsync(timeoutInSeconds, - TimeoutStrategy.Pessimistic, - (_, _, _) => throw new OptimisticConcurrencyTimeoutException()); - - // Fetch the db revision and retry - // https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#resolving-concurrency-conflicts - var retryPolicy = Policy - .Handle() - .WaitAndRetryAsync( - sleepDurations: Backoff.DecorrelatedJitterBackoffV2( - medianFirstRetryDelay: TimeSpan.FromMilliseconds(medianFirstDelayInMs), - retryCount: int.MaxValue), - onRetryAsync: FetchCurrentRevision); - - ConcurrencyRetryPolicy = timeoutPolicy.WrapAsync(retryPolicy); - } - public IUnitOfWork EnableConcurrencyCheck( TEntity? entity, Guid? revision) @@ -97,20 +77,23 @@ public async Task SaveChangesAsync(CancellationToken cancella { // Attempt to save changes without concurrency check await ConcurrencyRetryPolicy.ExecuteAsync(_dialogDbContext.SaveChangesAsync, cancellationToken); - - return new Success(); - } - - try - { - await _dialogDbContext.SaveChangesAsync(cancellationToken); } - catch (DbUpdateConcurrencyException) + else { - return new ConcurrencyError(); + try + { + await _dialogDbContext.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return new ConcurrencyError(); + } } - return new Success(); + // Interceptors can add domain errors, so check again + return !_domainContext.IsValid + ? new DomainError(_domainContext.Pop()) + : new Success(); } private static async Task FetchCurrentRevision(Exception exception, TimeSpan _) diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs index 76710ce87..13477addc 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs @@ -1,11 +1,12 @@ using AutoMapper; using Digdir.Domain.Dialogporten.Application.Common; -using Digdir.Domain.Dialogporten.Application.Common.ResourceRegistry; +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.Features.V1.ServiceOwner.Dialogs.Commands.Create; +using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; using Digdir.Tool.Dialogporten.GenerateFakeData; using NSubstitute; -using AuthorizationConstants = Digdir.Domain.Dialogporten.Application.Common.Authorization.Constants; namespace Digdir.Domain.Dialogporten.Application.Unit.Tests.Features.V1.ServiceOwner.Dialogs.Commands; @@ -26,69 +27,27 @@ public async Task CreateDialogCommand_Should_Return_Forbidden_When_Scope_Is_Miss var domainContextSub = Substitute.For(); var userResourceRegistrySub = Substitute.For(); var userOrganizationRegistrySub = Substitute.For(); - var partyNameRegistrySub = Substitute.For(); + var serviceAuthorizationSub = Substitute.For(); var createCommand = DialogGenerator.GenerateSimpleFakeDialog(); - userResourceRegistrySub - .CurrentUserIsOwner(createCommand.ServiceResource, Arg.Any()) - .Returns(true); - - userResourceRegistrySub.GetResourceType(createCommand.ServiceResource, Arg.Any()) - .Returns(Constants.Correspondence); - - var commandHandler = new CreateDialogCommandHandler(dialogDbContextSub, - mapper, unitOfWorkSub, domainContextSub, userResourceRegistrySub, - userOrganizationRegistrySub, partyNameRegistrySub); - - // Act - var result = await commandHandler.Handle(createCommand, CancellationToken.None); - - // Assert - Assert.True(result.IsT3); - Assert.Contains(AuthorizationConstants.CorrespondenceScope, result.AsT3.Reasons[0]); - } - - - [Fact] - public async Task CreateDialogCommand_Should_Return_ValidationError_When_Progress_Set_On_Correspondence() - { - // Arrange - var dialogDbContextSub = Substitute.For(); - - var mapper = new MapperConfiguration(cfg => - { - cfg.AddMaps(typeof(CreateDialogCommandHandler).Assembly); - }).CreateMapper(); - - var unitOfWorkSub = Substitute.For(); - var domainContextSub = Substitute.For(); - var userResourceRegistrySub = Substitute.For(); - var userOrganizationRegistrySub = Substitute.For(); - var partyNameRegistrySub = Substitute.For(); - - var createCommand = DialogGenerator.GenerateSimpleFakeDialog(); + serviceAuthorizationSub + .AuthorizeServiceResources(Arg.Any(), Arg.Any()) + .Returns(new Forbidden()); userResourceRegistrySub .CurrentUserIsOwner(createCommand.ServiceResource, Arg.Any()) .Returns(true); - userResourceRegistrySub.UserCanModifyResourceType(Arg.Any()).Returns(true); - - userResourceRegistrySub.GetResourceType(createCommand.ServiceResource, Arg.Any()) - .Returns(Constants.Correspondence); - var commandHandler = new CreateDialogCommandHandler(dialogDbContextSub, - mapper, unitOfWorkSub, domainContextSub, userResourceRegistrySub, - userOrganizationRegistrySub, partyNameRegistrySub); + mapper, unitOfWorkSub, domainContextSub, + userOrganizationRegistrySub, serviceAuthorizationSub); // Act var result = await commandHandler.Handle(createCommand, CancellationToken.None); // Assert - Assert.True(result.IsT2); // ValidationError - Assert.Equal(CreateDialogCommandHandler.ProgressValidationFailure.ErrorMessage, - result.AsT2.Errors.First().ErrorMessage); + Assert.True(result.IsT3); } [Fact] @@ -106,17 +65,21 @@ public async Task CreateDialogCommand_Should_Return_Forbidden_When_User_Is_Not_O var domainContextSub = Substitute.For(); var userResourceRegistrySub = Substitute.For(); var userOrganizationRegistrySub = Substitute.For(); - var partyNameRegistrySub = Substitute.For(); + var serviceAuthorizationSub = Substitute.For(); var createCommand = DialogGenerator.GenerateSimpleFakeDialog(); + serviceAuthorizationSub + .AuthorizeServiceResources(Arg.Any(), Arg.Any()) + .Returns(new Forbidden()); + userResourceRegistrySub .CurrentUserIsOwner(createCommand.ServiceResource, Arg.Any()) .Returns(false); var commandHandler = new CreateDialogCommandHandler(dialogDbContextSub, - mapper, unitOfWorkSub, domainContextSub, userResourceRegistrySub, - userOrganizationRegistrySub, partyNameRegistrySub); + mapper, unitOfWorkSub, domainContextSub, + userOrganizationRegistrySub, serviceAuthorizationSub); // Act var result = await commandHandler.Handle(createCommand, CancellationToken.None);