Skip to content

Commit

Permalink
feat: add support for serviceowner admin scope (#1002)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the Title above -->

## Description

<!--- Describe your changes in detail -->

## Related Issue(s)

- #272 

## Verification

- [ ] **Your** code builds clean without any errors or warnings
- [ ] Manual testing done (required)
- [ ] Relevant automated test added (if you find this hard, leave it and
we'll help out)

## Documentation

- [ ] Documentation is updated (either in `docs`-directory, Altinnpedia
or a separate linked PR in
[altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if
applicable)

---------

Co-authored-by: Knut Haug <154342485+knuhau@users.noreply.github.com>
Co-authored-by: Knut Haug <knut.espen.haug@digdir.no>
Co-authored-by: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com>
  • Loading branch information
4 people authored Aug 20, 2024
1 parent 8e74368 commit 2638b48
Show file tree
Hide file tree
Showing 19 changed files with 414 additions and 341 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
Expand Up @@ -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";
}
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;

public interface IServiceResourceAuthorizer
{
Task<AuthorizeServiceResourcesResult> AuthorizeServiceResources(
DialogEntity dialog,
CancellationToken cancellationToken);

Task<SetResourceTypeResult> SetResourceType(
DialogEntity dialog,
CancellationToken cancellationToken);
}

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

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

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<AuthorizeServiceResourcesResult> 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<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> GetPrimaryServiceResourceReferences(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,11 +10,11 @@ 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();
}

public class UserResourceRegistry : IUserResourceRegistry
internal sealed class UserResourceRegistry : IUserResourceRegistry
{
private readonly IUser _user;
private readonly IResourceRegistry _resourceRegistry;
Expand All @@ -31,21 +31,25 @@ 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
};

public bool IsCurrentUserServiceOwnerAdmin() => _user.GetPrincipal().HasScope(Constants.ServiceOwnerAdminScope);
}

internal sealed class LocalDevelopmentUserResourceRegistryDecorator : IUserResourceRegistry
Expand All @@ -63,8 +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,6 +2,20 @@

public interface IResourceRegistry
{
Task<IReadOnlyCollection<string>> GetResourceIds(string orgNumber, CancellationToken cancellationToken);
Task<string> GetResourceType(string orgNumber, string serviceResourceId, CancellationToken cancellationToken);
Task<IReadOnlyCollection<ServiceResourceInformation>> GetResourceInformationForOrg(string orgNumber, CancellationToken cancellationToken);
Task<ServiceResourceInformation?> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -27,83 +25,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;

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)
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));
_serviceResourceAuthorizer = serviceResourceAuthorizer;
}

public async Task<CreateDialogResult> 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<DialogEntity>(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<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 @@ -115,53 +85,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 2638b48

Please sign in to comment.