Skip to content

Commit 54425e7

Browse files
fix: Fallback to using list auth if details auth fails, remove double cache (#1274)
## Description This implements a fall back to using list authorization if the details authorization returns without access to the main resource. This might happen if - The XACML policy doesn't define a "read" rule - There are no GUI/API actions in the dialog referring to XACML actions the user is granted access to This ensures that dialogs that is visible in the list, also can be viewed in details view, even if the user has isn't authorized for any actions. He/she might still have access to transmissions using authorization attributes (depending on if the authorization attribute refers a subresource or external resource; either having "transmissionread" in the ServiceResource policy, or having "read" on the external resource policy) Also, this removes a redundant double caching of list authorization. This was a leftover after the non-scalable PDP-based authorization. ## Related Issue(s) - #1247 This adresses the principal problem raised in #1247, which is the discrepancy between perceived list and details authorization. We still need to consider if GetAltinnActions should be policy-based, as that will allow us to implement action-property validation in Create/Update commands. This will also let us include all authorized actions in dialog tokens in the `a` (actions) claim, not just the actions referred to in the dialog. ## Verification - [x] **Your** code builds clean without any errors or warnings - [x] 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) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new method to check list authorization for dialogs, enhancing user access control. - Added additional authorization checks for dialogs, allowing users with list access to retrieve dialogs even without main resource access. - **Bug Fixes** - Improved error handling and validation in dialog creation tests, ensuring robust and localized feedback. - **Chores** - Updated caching strategy for search authorization results to improve performance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 7267546 commit 54425e7

File tree

6 files changed

+37
-4
lines changed

6 files changed

+37
-4
lines changed

src/Digdir.Domain.Dialogporten.Application/Common/IDialogTokenGenerator.cs

+5
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ public string GetDialogToken(DialogEntity dialog, DialogDetailsAuthorizationResu
7474

7575
private static string GetAuthorizedActions(DialogDetailsAuthorizationResult authorizationResult)
7676
{
77+
if (authorizationResult.AuthorizedAltinnActions.Count == 0)
78+
{
79+
return string.Empty;
80+
}
81+
7782
var actions = new StringBuilder();
7883
foreach (var (action, resource) in authorizationResult.AuthorizedAltinnActions)
7984
{

src/Digdir.Domain.Dialogporten.Application/Externals/AltinnAuthorization/IAltinnAuthorization.cs

+2
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ public Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(
1616

1717
public Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticatedParty, bool flatten = false,
1818
CancellationToken cancellationToken = default);
19+
20+
Task<bool> HasListAuthorizationForDialog(DialogEntity dialog, CancellationToken cancellationToken);
1921
}

src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs

+13-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,19 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo
9898

9999
if (!authorizationResult.HasAccessToMainResource())
100100
{
101-
return new EntityNotFound<DialogEntity>(request.DialogId);
101+
// If the user for some reason does not have access to the main resource, which might be
102+
// because they are granted access to XACML-actions besides "read" not explicitly defined in the dialog,
103+
// we do a recheck if the user has access to the dialog via the list authorization. If this is the case,
104+
// we return the dialog and let DecorateWithAuthorization flag the actions as unauthorized. Note that
105+
// there might be transmissions that the user has access to, even though there are no authorized actions.
106+
var listAuthorizationResult = await _altinnAuthorization.HasListAuthorizationForDialog(
107+
dialog,
108+
cancellationToken: cancellationToken);
109+
110+
if (!listAuthorizationResult)
111+
{
112+
return new EntityNotFound<DialogEntity>(request.DialogId);
113+
}
102114
}
103115

104116
if (dialog.Deleted)

src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs

+13-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ public async Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSear
7373
ConstraintServiceResources = serviceResources
7474
};
7575

76-
return await _pdpCache.GetOrSetAsync(request.GenerateCacheKey(), async token
77-
=> await PerformDialogSearchAuthorization(request, token), token: cancellationToken);
76+
// We don't cache at this level, as the principal information is received from GetAuthorizedParties,
77+
// which is already cached
78+
return await PerformDialogSearchAuthorization(request, cancellationToken);
7879
}
7980

8081
public async Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticatedParty, bool flatten = false,
@@ -87,6 +88,16 @@ public async Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier
8788
return flatten ? GetFlattenedAuthorizedParties(authorizedParties) : authorizedParties;
8889
}
8990

91+
public async Task<bool> HasListAuthorizationForDialog(DialogEntity dialog, CancellationToken cancellationToken)
92+
{
93+
var authorizedResourcesForSearch = await GetAuthorizedResourcesForSearch(
94+
[dialog.Party], [dialog.ServiceResource], cancellationToken);
95+
96+
return authorizedResourcesForSearch.ResourcesByParties.Count > 0
97+
|| authorizedResourcesForSearch.SubjectsByParties.Count > 0
98+
|| authorizedResourcesForSearch.DialogIds.Contains(dialog.Id);
99+
}
100+
90101
private static AuthorizedPartiesResult GetFlattenedAuthorizedParties(AuthorizedPartiesResult authorizedParties)
91102
{
92103
var flattenedAuthorizedParties = new AuthorizedPartiesResult();

src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/LocalDevelopmentAltinnAuthorization.cs

+2
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,6 @@ public async Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSear
5656
[SuppressMessage("Performance", "CA1822:Mark members as static")]
5757
public async Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticatedParty, bool _ = false, CancellationToken __ = default)
5858
=> await Task.FromResult(new AuthorizedPartiesResult { AuthorizedParties = [new() { Name = "Local Party", Party = authenticatedParty.FullId, IsCurrentEndUser = true }] });
59+
60+
public Task<bool> HasListAuthorizationForDialog(DialogEntity dialog, CancellationToken cancellationToken) => Task.FromResult(true);
5961
}

tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ public async Task Cannot_Create_Transmission_Without_Content()
228228
response.TryPickT2(out var validationError, out _).Should().BeTrue();
229229
validationError.Should().NotBeNull();
230230
validationError.Errors.Should().HaveCount(1);
231-
validationError.Errors.First().ErrorMessage.Should().Contain("'Content' must not be empty");
231+
// The error message might be localized, so we just check for the property name
232+
validationError.Errors.First().ErrorMessage.Should().Contain("'Content'");
232233
}
233234

234235
[Fact]

0 commit comments

Comments
 (0)