Skip to content

Commit

Permalink
--wip-- [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
MagnusSandgren committed Aug 15, 2024
1 parent e1b712a commit d0d2f7a
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +42,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
.AddScoped<IDialogTokenGenerator, DialogTokenGenerator>()

// Transient
.AddTransient<IServiceResourceAuthorizer, ServiceResourceAuthorizer>()
.AddTransient<IUserOrganizationRegistry, UserOrganizationRegistry>()
.AddTransient<IUserResourceRegistry, UserResourceRegistry>()
.AddTransient<IUserRegistry, UserRegistry>()
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

internal interface IServiceResourceAuthorizer
{
Task<AuthorizeServiceResourcesResult> AuthorizeServiceResources(DialogEntity dialog, CancellationToken cancellationToken);
Task<SetResourceTypeResult> SetResourceType(DialogEntity dialog, CancellationToken cancellationToken);
}

[GenerateOneOf]
internal partial class AuthorizeServiceResourcesResult : OneOfBase<Success, Forbidden, DomainContextInvalidated>;

[GenerateOneOf]
internal partial class SetResourceTypeResult : OneOfBase<Success, DomainContextInvalidated>;

internal 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<AuthorizeServiceResourcesResult> AuthorizeServiceResources(DialogEntity dialog, CancellationToken cancellationToken)
{
if (_userResourceRegistry.IsCurrentUserServiceOwnerAdmin())
{
return new Success();
}

var ownedResources = await _userResourceRegistry.GetCurrentUserResourceIds(cancellationToken);
var notOwnedResources = GetServiceResourceReferences(dialog)
.Except(ownedResources)
.ToList();

if (notOwnedResources.Count != 0)
{
return new Forbidden($"Not allowed to reference the following unowned resources: [{string.Join(", ", notOwnedResources)}].");
}

if (dialog.ServiceResourceType == ResourceRegistry.Constants.Correspondence
&& dialog.Progress is not null)
{
_domainContext.AddError(nameof(CreateDialogCommand.Progress), "Progress cannot be set for correspondence dialogs.");
return new DomainContextInvalidated();
}

if (!_userResourceRegistry.UserCanModifyResourceType(dialog.ServiceResourceType))
{
return new Forbidden($"User cannot create or modify resource type {dialog.ServiceResourceType}.");
}

return new Success();
}

public async Task<SetResourceTypeResult> 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<string> GetServiceResourceReferences(DialogEntity dialog) =>
Enumerable.Empty<string>()
.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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ public interface IUserResourceRegistry
{
Task<bool> CurrentUserIsOwner(string serviceResource, CancellationToken cancellationToken);
Task<IReadOnlyCollection<string>> GetCurrentUserResourceIds(CancellationToken cancellationToken);
Task<string> GetResourceType(string serviceResourceId, CancellationToken cancellationToken);
bool UserCanModifyResourceType(string serviceResourceType);
bool IsCurrentUserServiceOwnerAdmin();
}
Expand All @@ -32,19 +31,21 @@ public async Task<bool> CurrentUserIsOwner(string serviceResource, CancellationT
return resourceIds.Contains(serviceResource);
}

public Task<IReadOnlyCollection<string>> GetCurrentUserResourceIds(CancellationToken cancellationToken) =>
!_user.TryGetOrganizationNumber(out var orgNumber)
? throw new UnreachableException()
: _resourceRegistry.GetResourceIds(orgNumber, cancellationToken);
public async Task<IReadOnlyCollection<string>> GetCurrentUserResourceIds(CancellationToken cancellationToken)
{
if (!_user.TryGetOrganizationNumber(out var orgNumber))
{
throw new UnreachableException();
}

public Task<string> 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
};

Expand All @@ -66,9 +67,6 @@ public Task<bool> CurrentUserIsOwner(string serviceResource, CancellationToken c
public Task<IReadOnlyCollection<string>> GetCurrentUserResourceIds(CancellationToken cancellationToken) =>
_userResourceRegistry.GetCurrentUserResourceIds(cancellationToken);

public Task<string> GetResourceType(string serviceResourceId, CancellationToken cancellationToken) =>
Task.FromResult("LocalResourceType");

public bool UserCanModifyResourceType(string serviceResourceType) => true;
public bool IsCurrentUserServiceOwnerAdmin() => true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

public interface IResourceRegistry
{
Task<IReadOnlyCollection<string>> GetResourceIds(string orgNumber, CancellationToken cancellationToken);
Task<string> GetResourceType(string orgNumber, string serviceResourceId, CancellationToken cancellationToken);
Task<bool> ResourceExists(string serviceResource, CancellationToken cancellationToken);
Task<IReadOnlyCollection<ServiceResourceInformation>> GetResourceInformationForOrg(string orgNumber, CancellationToken cancellationToken);
Task<ServiceResourceInformation?> GetResourceInformation(string serviceResourceId, CancellationToken cancellationToken);
}

public sealed record ServiceResourceInformation(string ResourceId, string ResourceType, string OwnerOrgNumber);
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
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;
Expand All @@ -12,7 +14,6 @@
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;

Expand All @@ -27,98 +28,55 @@ internal sealed class CreateDialogCommandHandler : IRequestHandler<CreateDialogC
private readonly IMapper _mapper;
private readonly IUnitOfWork _unitOfWork;
private readonly IDomainContext _domainContext;
private readonly IUserResourceRegistry _userResourceRegistry;
private readonly IUserOrganizationRegistry _userOrganizationRegistry;
private readonly IPartyNameRegistry _partyNameRegistry;
private readonly IResourceRegistry _resourceRegistry;

internal static readonly ValidationFailure ProgressValidationFailure = new(nameof(CreateDialogCommand.Progress), "Progress cannot be set for correspondence dialogs.");
private readonly IServiceResourceAuthorizer _serviceResourceAuthorizer;

public CreateDialogCommandHandler(
IDialogDbContext db,
IMapper mapper,
IUnitOfWork unitOfWork,
IDomainContext domainContext,
IUserResourceRegistry userResourceRegistry,
IUserOrganizationRegistry userOrganizationRegistry,
IPartyNameRegistry partyNameRegistry,
IResourceRegistry resourceRegistry)
IServiceResourceAuthorizer serviceResourceAuthorizer)
{
_db = db ?? throw new ArgumentNullException(nameof(db));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
_domainContext = domainContext ?? throw new ArgumentNullException(nameof(domainContext));
_userResourceRegistry = userResourceRegistry ?? throw new ArgumentNullException(nameof(userResourceRegistry));
_userOrganizationRegistry = userOrganizationRegistry ?? throw new ArgumentNullException(nameof(userOrganizationRegistry));
_partyNameRegistry = partyNameRegistry ?? throw new ArgumentNullException(nameof(partyNameRegistry));
_resourceRegistry = resourceRegistry ?? throw new ArgumentNullException(nameof(resourceRegistry));
_serviceResourceAuthorizer = serviceResourceAuthorizer;
}

public async Task<CreateDialogResult> Handle(CreateDialogCommand request, CancellationToken cancellationToken)
{
if (!_userResourceRegistry.IsCurrentUserServiceOwnerAdmin())
{
foreach (var serviceResourceReference in GetServiceResourceReferences(request))
{
if (!await _userResourceRegistry.CurrentUserIsOwner(serviceResourceReference, cancellationToken))
{
return new Forbidden($"Not allowed to reference {serviceResourceReference}.");
}
}
}
else
{
var resourceExists = await _resourceRegistry.ResourceExists(request.ServiceResource, cancellationToken);
if (!resourceExists)
{
return new ValidationError(new ValidationFailure(nameof(CreateDialogCommand.ServiceResource),
$"Could not find service resource '{request.ServiceResource}' in resource registry"));
}
}

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}.");
}
var dialog = _mapper.Map<DialogEntity>(request);

if (serviceResourceType == ResourceRegistryConstants.Correspondence)
await _serviceResourceAuthorizer.SetResourceType(dialog, cancellationToken);
var serviceResourceAuthorizationResult = await _serviceResourceAuthorizer.AuthorizeServiceResources(dialog, cancellationToken);
if (serviceResourceAuthorizationResult.Value is Forbidden forbiddenResult)
{
if (request.Progress is not null)
return new ValidationError(ProgressValidationFailure);
return forbiddenResult;
}

foreach (var activity in request.Activities)
{
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);
}

var dialog = _mapper.Map<DialogEntity>(request);

dialog.ServiceResourceType = serviceResourceType;

dialog.Org = await _userOrganizationRegistry.GetCurrentUserOrgShortName(cancellationToken) ?? string.Empty;
if (string.IsNullOrWhiteSpace(dialog.Org))
{
_domainContext.AddError(new DomainFailure(nameof(DialogEntity.Org),
"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<CreateDialogResult>(
success => new Success<Guid>(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<DialogEntity>(existingDialogIds));
Expand All @@ -130,53 +88,10 @@ public async Task<CreateDialogResult> Handle(CreateDialogCommand request, Cancel
_domainContext.AddError(DomainFailure.EntityExists<DialogActivity>(existingActivityIds));
}

var existingAttachmentIds = await _db.GetExistingIds(dialog.Attachments, cancellationToken);
if (existingAttachmentIds.Count != 0)
{
_domainContext.AddError(DomainFailure.EntityExists<DialogAttachment>(existingAttachmentIds));
}

var existingTransmissionIds = await _db.GetExistingIds(dialog.Transmissions, cancellationToken);
if (existingTransmissionIds.Count != 0)
{
_domainContext.AddError(DomainFailure.EntityExists<DialogTransmission>(existingTransmissionIds));
}

var transmissionAttachments = dialog.Transmissions.SelectMany(x => x.Attachments);
var existingTransmissionAttachmentIds = await _db.GetExistingIds(transmissionAttachments, cancellationToken);
if (existingTransmissionAttachmentIds.Count != 0)
{
_domainContext.AddError(DomainFailure.EntityExists<DialogTransmissionAttachment>(existingTransmissionAttachmentIds));
}

await _db.Dialogs.AddAsync(dialog, cancellationToken);

var saveResult = await _unitOfWork.SaveChangesAsync(cancellationToken);
return saveResult.Match<CreateDialogResult>(
success => new Success<Guid>(dialog.Id),
domainError => domainError,
concurrencyError => throw new UnreachableException("Should never get a concurrency error when creating a new dialog"));
}

private static List<string> GetServiceResourceReferences(CreateDialogDto request)
{
var serviceResourceReferences = new List<string> { 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();
}
}
Loading

0 comments on commit d0d2f7a

Please sign in to comment.