Skip to content

Commit

Permalink
feat: Enforce minimum auth level requirements on dialogs (#1875)
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)

- #1804 

## 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)
  • Loading branch information
oskogstad authored Feb 18, 2025
1 parent f68c112 commit 37febf6
Show file tree
Hide file tree
Showing 38 changed files with 2,673 additions and 63 deletions.
6 changes: 5 additions & 1 deletion docs/schema/V1/schema.verified.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ type DialogByIdForbidden implements DialogByIdError {
message: String!
}

type DialogByIdForbiddenAuthLevelToLow implements DialogByIdError {
message: String!
}

type DialogByIdNotFound implements DialogByIdError {
message: String!
}
Expand Down Expand Up @@ -466,4 +470,4 @@ scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time"

scalar URL @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc3986")

scalar UUID @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc4122")
scalar UUID @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc4122")
72 changes: 72 additions & 0 deletions docs/schema/V1/swagger.verified.json
Original file line number Diff line number Diff line change
Expand Up @@ -2258,6 +2258,24 @@
}
]
},
"nonSensitiveSummary": {
"description": "An optional non-sensitive summary of the dialog and its current state.\nUsed for search and list views if the user authorization does not meet the required eIDAS level",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"nonSensitiveTitle": {
"description": "An optional non-sensitive title of the dialog.\nUsed for search and list views if the user authorization does not meet the required eIDAS level",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"senderName": {
"description": "Overridden sender name. If not supplied, assume \u0022org\u0022 as the sender name. Must be text/plain if supplied.\nSupported media types: text/plain",
"nullable": true,
Expand Down Expand Up @@ -2912,6 +2930,24 @@
}
]
},
"nonSensitiveSummary": {
"description": "An optional non-sensitive summary of the dialog and its current state.\nUsed for search and list views if the user authorization does not meet the required eIDAS level",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"nonSensitiveTitle": {
"description": "An optional non-sensitive title of the dialog.\nUsed for search and list views if the user authorization does not meet the required eIDAS level",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"senderName": {
"description": "Overridden sender name. If not supplied, assume \u0022org\u0022 as the sender name. Must be text/plain if supplied.",
"nullable": true,
Expand Down Expand Up @@ -3367,6 +3403,24 @@
}
]
},
"nonSensitiveSummary": {
"description": "An optional non-sensitive summary of the dialog and its current state.\nUsed for search and list views if the user authorization does not meet the required eIDAS level",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"nonSensitiveTitle": {
"description": "An optional non-sensitive title of the dialog.\nUsed for search and list views if the user authorization does not meet the required eIDAS level",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"senderName": {
"description": "Overridden sender name. If not supplied, assume \u0022org\u0022 as the sender name.",
"nullable": true,
Expand Down Expand Up @@ -4062,6 +4116,24 @@
}
]
},
"nonSensitiveSummary": {
"description": "An optional non-sensitive summary of the dialog and its current state.\nUsed for search and list views if the user authorization does not meet the required eIDAS level",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"nonSensitiveTitle": {
"description": "An optional non-sensitive title of the dialog.\nUsed for search and list views if the user authorization does not meet the required eIDAS level",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"senderName": {
"description": "Overridden sender name. If not supplied, assume \u0022org\u0022 as the sender name.",
"nullable": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public static class Constants
public const string TransmissionReadAction = "transmissionread";
public static readonly Uri UnauthorizedUri = new("urn:dialogporten:unauthorized");

public const string IdportenLoaSubstantial = "idporten-loa-substantial";
public const string IdportenLoaHigh = "idporten-loa-high";
public const string AltinnAuthLevelTooLow = "Altinn authentication level too low.";

public const string DisableAltinnEventsRequiresAdminScope =
"Disabling Altinn events requires service owner admin scope.";

Expand All @@ -24,7 +28,7 @@ public static class Constants
public static class AuthorizationScope
{
/// <summary>
/// Needed to be able to modify (create/update/delete) correspondence service resources. Primarily used by the correspondence service.
/// Needed to be able to modify (create/update/delete) correspondence service resources. Primarily used by the correspondence service.
/// </summary>
public const string CorrespondenceScope = "digdir:dialogporten.correspondence";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Digdir.Domain.Dialogporten.Application.Externals.Presentation;
using System.Diagnostics;
using Digdir.Domain.Dialogporten.Application.Externals.Presentation;
using System.Security.Claims;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
Expand Down Expand Up @@ -175,30 +176,26 @@ public static bool TryGetOrganizationNumber(this Claim? consumerClaim, [NotNullW
return orgNumber is not null;
}

public static bool TryGetAuthenticationLevel(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out int? authenticationLevel)
public static int GetAuthenticationLevel(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.TryGetClaimValue(AltinnAuthLevelClaim, out var claimValue) && int.TryParse(claimValue, out var level))
{
authenticationLevel = level;
return true;
return level;
}

if (claimsPrincipal.TryGetClaimValue(IdportenAuthLevelClaim, out claimValue))
{
// The acr claim value is either "idporten-loa-substantial" (previously "Level3") or "idporten-loa-high" (previously "Level4")
// https://docs.digdir.no/docs/idporten/oidc/oidc_protocol_new_idporten#new-acr-values
authenticationLevel = claimValue switch
return claimValue switch
{
"idporten-loa-substantial" => 3,
"idporten-loa-high" => 4,
_ => null
Constants.IdportenLoaSubstantial => 3,
Constants.IdportenLoaHigh => 4,
_ => throw new ArgumentException("Unknown acr value")
};

return authenticationLevel.HasValue;
}

authenticationLevel = null;
return false;
throw new UnreachableException("No authentication level claim found");
}

public static IEnumerable<Claim> GetIdentifyingClaims(this IEnumerable<Claim> claims)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ public string GetDialogToken(DialogEntity dialog, DialogDetailsAuthorizationResu
{
{ DialogTokenClaimTypes.JwtId, Guid.NewGuid() },
{ DialogTokenClaimTypes.AuthenticatedParty, GetAuthenticatedParty() },
{ DialogTokenClaimTypes.AuthenticationLevel,
claimsPrincipal.TryGetAuthenticationLevel(out var authenticationLevel)
? authenticationLevel.Value
: 0 },
{ DialogTokenClaimTypes.AuthenticationLevel, claimsPrincipal.GetAuthenticationLevel() },
{ DialogTokenClaimTypes.DialogParty, dialog.Party },
{ DialogTokenClaimTypes.ServiceResource, dialog.ServiceResource },
{ DialogTokenClaimTypes.DialogId, dialog.Id },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticate
CancellationToken cancellationToken = default);

Task<bool> HasListAuthorizationForDialog(DialogEntity dialog, CancellationToken cancellationToken);

bool UserHasRequiredAuthLevel(int minimumAuthenticationLevel);
Task<bool> UserHasRequiredAuthLevel(string serviceResource, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AutoMapper;
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.Externals.AltinnAuthorization;
Expand All @@ -17,7 +18,7 @@ public sealed class GetActivityQuery : IRequest<GetActivityResult>
}

[GenerateOneOf]
public sealed partial class GetActivityResult : OneOfBase<ActivityDto, EntityNotFound, EntityDeleted>;
public sealed partial class GetActivityResult : OneOfBase<ActivityDto, EntityNotFound, EntityDeleted, Forbidden>;

internal sealed class GetActivityQueryHandler : IRequestHandler<GetActivityQuery, GetActivityResult>
{
Expand Down Expand Up @@ -67,6 +68,11 @@ public async Task<GetActivityResult> Handle(GetActivityQuery request,
return new EntityDeleted<DialogEntity>(request.DialogId);
}

if (!await _altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource, cancellationToken))
{
return new Forbidden(Constants.AltinnAuthLevelTooLow);
}

var activity = dialog.Activities.FirstOrDefault();

if (activity is null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AutoMapper;
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.Externals.AltinnAuthorization;
Expand All @@ -15,7 +16,7 @@ public sealed class SearchActivityQuery : IRequest<SearchActivityResult>
}

[GenerateOneOf]
public sealed partial class SearchActivityResult : OneOfBase<List<ActivityDto>, EntityNotFound, EntityDeleted>;
public sealed partial class SearchActivityResult : OneOfBase<List<ActivityDto>, EntityNotFound, EntityDeleted, Forbidden>;

internal sealed class SearchActivityQueryHandler : IRequestHandler<SearchActivityQuery, SearchActivityResult>
{
Expand Down Expand Up @@ -61,6 +62,11 @@ public async Task<SearchActivityResult> Handle(SearchActivityQuery request, Canc
return new EntityDeleted<DialogEntity>(request.DialogId);
}

if (!await _altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource, cancellationToken))
{
return new Forbidden(Constants.AltinnAuthLevelTooLow);
}

return _mapper.Map<List<ActivityDto>>(dialog.Activities);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AutoMapper;
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.Externals.AltinnAuthorization;
Expand All @@ -15,7 +16,7 @@ public sealed class SearchLabelAssignmentLogQuery : IRequest<SearchLabelAssignme
}

[GenerateOneOf]
public sealed partial class SearchLabelAssignmentLogResult : OneOfBase<List<LabelAssignmentLogDto>, EntityNotFound, EntityDeleted>;
public sealed partial class SearchLabelAssignmentLogResult : OneOfBase<List<LabelAssignmentLogDto>, EntityNotFound, EntityDeleted, Forbidden>;

internal sealed class SearchLabelAssignmentLogQueryHandler : IRequestHandler<SearchLabelAssignmentLogQuery, SearchLabelAssignmentLogResult>
{
Expand Down Expand Up @@ -55,6 +56,11 @@ public async Task<SearchLabelAssignmentLogResult> Handle(SearchLabelAssignmentLo
return new EntityDeleted<DialogEntity>(request.DialogId);
}

if (!await _altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource, cancellationToken))
{
return new Forbidden(Constants.AltinnAuthLevelTooLow);
}

return _mapper.Map<List<LabelAssignmentLogDto>>(dialog.DialogEndUserContext.LabelAssignmentLogs);
}
}
Original file line number Diff line number Diff line change
@@ -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.ReturnTypes;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
Expand Down Expand Up @@ -77,6 +78,11 @@ public async Task<GetSeenLogResult> Handle(GetSeenLogQuery request,
return new EntityNotFound<DialogSeenLog>(request.SeenLogId);
}

if (!await _altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource, cancellationToken))
{
return new Forbidden(Constants.AltinnAuthLevelTooLow);
}

var dto = _mapper.Map<SeenLogDto>(seenLog);
dto.IsCurrentEndUser = currentUserInformation.UserId.ExternalIdWithPrefix == seenLog.SeenBy.ActorId;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using OneOf;
using Microsoft.EntityFrameworkCore;
using Digdir.Domain.Dialogporten.Application.Common;
using Digdir.Domain.Dialogporten.Application.Common.Authorization;

namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.DialogSeenLogs.Queries.Search;

Expand Down Expand Up @@ -69,6 +70,11 @@ public async Task<SearchSeenLogResult> Handle(SearchSeenLogQuery request, Cancel
return new EntityDeleted<DialogEntity>(request.DialogId);
}

if (!await _altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource, cancellationToken))
{
return new Forbidden(Constants.AltinnAuthLevelTooLow);
}

return dialog.SeenLog
.Select(x =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed class GetTransmissionQuery : IRequest<GetTransmissionResult>
}

[GenerateOneOf]
public sealed partial class GetTransmissionResult : OneOfBase<TransmissionDto, EntityNotFound, EntityDeleted>;
public sealed partial class GetTransmissionResult : OneOfBase<TransmissionDto, EntityNotFound, EntityDeleted, Forbidden>;

internal sealed class GetTransmissionQueryHandler : IRequestHandler<GetTransmissionQuery, GetTransmissionResult>
{
Expand Down Expand Up @@ -72,6 +72,11 @@ public async Task<GetTransmissionResult> Handle(GetTransmissionQuery request,
return new EntityDeleted<DialogEntity>(request.DialogId);
}

if (!await _altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource, cancellationToken))
{
return new Forbidden(Constants.AltinnAuthLevelTooLow);
}

var transmission = dialog.Transmissions.FirstOrDefault();
if (transmission is null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed class SearchTransmissionQuery : IRequest<SearchTransmissionResult>
}

[GenerateOneOf]
public sealed partial class SearchTransmissionResult : OneOfBase<List<TransmissionDto>, EntityNotFound, EntityDeleted>;
public sealed partial class SearchTransmissionResult : OneOfBase<List<TransmissionDto>, EntityNotFound, EntityDeleted, Forbidden>;

internal sealed class SearchTransmissionQueryHandler : IRequestHandler<SearchTransmissionQuery, SearchTransmissionResult>
{
Expand Down Expand Up @@ -69,6 +69,11 @@ public async Task<SearchTransmissionResult> Handle(SearchTransmissionQuery reque
return new EntityDeleted<DialogEntity>(request.DialogId);
}

if (!await _altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource, cancellationToken))
{
return new Forbidden(Constants.AltinnAuthLevelTooLow);
}

var dto = _mapper.Map<List<TransmissionDto>>(dialog.Transmissions);

foreach (var transmission in dto)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo
return new EntityDeleted<DialogEntity>(request.DialogId);
}

if (!await _altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource, cancellationToken))
{
return new Forbidden(Constants.AltinnAuthLevelTooLow);
}

// TODO: What if name lookup fails
// https://github.com/altinn/dialogporten/issues/387
var currentUserInformation = await _userRegistry.GetCurrentUserInformation(cancellationToken);
Expand Down
Loading

0 comments on commit 37febf6

Please sign in to comment.