diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/IDialogTokenGenerator.cs b/src/Digdir.Domain.Dialogporten.Application/Common/IDialogTokenGenerator.cs index f6234b31f..b14d22402 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/IDialogTokenGenerator.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/IDialogTokenGenerator.cs @@ -74,6 +74,11 @@ public string GetDialogToken(DialogEntity dialog, DialogDetailsAuthorizationResu private static string GetAuthorizedActions(DialogDetailsAuthorizationResult authorizationResult) { + if (authorizationResult.AuthorizedAltinnActions.Count == 0) + { + return string.Empty; + } + var actions = new StringBuilder(); foreach (var (action, resource) in authorizationResult.AuthorizedAltinnActions) { diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs index 601edd44a..c75b7360f 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs @@ -16,4 +16,6 @@ public Task GetAuthorizedResourcesForSearch( public Task GetAuthorizedParties(IPartyIdentifier authenticatedParty, bool flatten = false, CancellationToken cancellationToken = default); + + Task HasListAuthorizationForDialog(DialogEntity dialog, CancellationToken cancellationToken); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs index e2a9267f5..2889d9e60 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs @@ -98,7 +98,19 @@ public async Task Handle(GetDialogQuery request, CancellationTo if (!authorizationResult.HasAccessToMainResource()) { - return new EntityNotFound(request.DialogId); + // If the user for some reason does not have access to the main resource, which might be + // because they are granted access to XACML-actions besides "read" not explicitly defined in the dialog, + // we do a recheck if the user has access to the dialog via the list authorization. If this is the case, + // we return the dialog and let DecorateWithAuthorization flag the actions as unauthorized. Note that + // there might be transmissions that the user has access to, even though there are no authorized actions. + var listAuthorizationResult = await _altinnAuthorization.HasListAuthorizationForDialog( + dialog, + cancellationToken: cancellationToken); + + if (!listAuthorizationResult) + { + return new EntityNotFound(request.DialogId); + } } if (dialog.Deleted) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs index 16aeb8978..3d127348e 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs @@ -73,8 +73,9 @@ public async Task GetAuthorizedResourcesForSear ConstraintServiceResources = serviceResources }; - return await _pdpCache.GetOrSetAsync(request.GenerateCacheKey(), async token - => await PerformDialogSearchAuthorization(request, token), token: cancellationToken); + // We don't cache at this level, as the principal information is received from GetAuthorizedParties, + // which is already cached + return await PerformDialogSearchAuthorization(request, cancellationToken); } public async Task GetAuthorizedParties(IPartyIdentifier authenticatedParty, bool flatten = false, @@ -87,6 +88,16 @@ public async Task GetAuthorizedParties(IPartyIdentifier return flatten ? GetFlattenedAuthorizedParties(authorizedParties) : authorizedParties; } + public async Task HasListAuthorizationForDialog(DialogEntity dialog, CancellationToken cancellationToken) + { + var authorizedResourcesForSearch = await GetAuthorizedResourcesForSearch( + [dialog.Party], [dialog.ServiceResource], cancellationToken); + + return authorizedResourcesForSearch.ResourcesByParties.Count > 0 + || authorizedResourcesForSearch.SubjectsByParties.Count > 0 + || authorizedResourcesForSearch.DialogIds.Contains(dialog.Id); + } + private static AuthorizedPartiesResult GetFlattenedAuthorizedParties(AuthorizedPartiesResult authorizedParties) { var flattenedAuthorizedParties = new AuthorizedPartiesResult(); diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs index 2957f7714..0a9b51751 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs @@ -56,4 +56,6 @@ public async Task GetAuthorizedResourcesForSear [SuppressMessage("Performance", "CA1822:Mark members as static")] public async Task GetAuthorizedParties(IPartyIdentifier authenticatedParty, bool _ = false, CancellationToken __ = default) => await Task.FromResult(new AuthorizedPartiesResult { AuthorizedParties = [new() { Name = "Local Party", Party = authenticatedParty.FullId, IsCurrentEndUser = true }] }); + + public Task HasListAuthorizationForDialog(DialogEntity dialog, CancellationToken cancellationToken) => Task.FromResult(true); } diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs index 54f08226b..134f9270c 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs @@ -228,7 +228,8 @@ public async Task Cannot_Create_Transmission_Without_Content() response.TryPickT2(out var validationError, out _).Should().BeTrue(); validationError.Should().NotBeNull(); validationError.Errors.Should().HaveCount(1); - validationError.Errors.First().ErrorMessage.Should().Contain("'Content' must not be empty"); + // The error message might be localized, so we just check for the property name + validationError.Errors.First().ErrorMessage.Should().Contain("'Content'"); } [Fact]