Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enforce minimum auth level requirements on dialogs #1875

Merged
merged 16 commits into from
Feb 18, 2025
Merged
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 @@ -3357,6 +3393,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 @@ -4052,6 +4106,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,8 @@ public static class Constants
public const string TransmissionReadAction = "transmissionread";
public static readonly Uri UnauthorizedUri = new("urn:dialogporten:unauthorized");

public const string AltinnAuthLevelToLow = "Altinn authentication level too low.";

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

Expand All @@ -24,7 +26,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
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);
bool UserHasRequiredAuthLevel(string serviceResource);
}
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 (!_altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource))
{
return new Forbidden(Constants.AltinnAuthLevelToLow);
}

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 (!_altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource))
{
return new Forbidden(Constants.AltinnAuthLevelToLow);
}

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 (!_altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource))
{
return new Forbidden(Constants.AltinnAuthLevelToLow);
}

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 (!_altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource))
{
return new Forbidden(Constants.AltinnAuthLevelToLow);
}

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 (!_altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource))
{
return new Forbidden(Constants.AltinnAuthLevelToLow);
}

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 (!_altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource))
{
return new Forbidden(Constants.AltinnAuthLevelToLow);
}

var transmission = dialog.Transmissions.FirstOrDefault();
if (transmission 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 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 @@ -68,6 +69,11 @@ public async Task<SearchTransmissionResult> Handle(SearchTransmissionQuery reque
return new EntityDeleted<DialogEntity>(request.DialogId);
}

if (!_altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource))
{
return new Forbidden(Constants.AltinnAuthLevelToLow);
}

return _mapper.Map<List<TransmissionDto>>(dialog.Transmissions);
}
}
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 (!_altinnAuthorization.UserHasRequiredAuthLevel(dialog.ServiceResource))
{
return new Forbidden(Constants.AltinnAuthLevelToLow);
}

// TODO: What if name lookup fails
// https://github.com/altinn/dialogporten/issues/387
var currentUserInformation = await _userRegistry.GetCurrentUserInformation(cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents;

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

internal static class DialogContentExtensions
{
public static void SetNonSensitiveContent(this List<DialogContent> content)
{
var nonSensitiveTitle = content.FirstOrDefault(x => x.TypeId == DialogContentType.Values.NonSensitiveTitle);
if (nonSensitiveTitle is not null)
{
var title = content.First(x => x.TypeId == DialogContentType.Values.Title);
title.Value = nonSensitiveTitle.Value;
}

var nonSensitiveSummary = content.FirstOrDefault(x => x.TypeId == DialogContentType.Values.NonSensitiveSummary);
if (nonSensitiveSummary is not null)
{
var summary = content.First(x => x.TypeId == DialogContentType.Values.Summary);
summary.Value = nonSensitiveSummary.Value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,27 @@ public async Task<SearchDialogResult> Handle(SearchDialogQuery request, Cancella
seenLog.IsCurrentEndUser = IdentifierMasker.GetMaybeMaskedIdentifier(_userRegistry.GetCurrentUserId().ExternalIdWithPrefix) == seenLog.SeenBy.ActorId;
}

var serviceResources = paginatedList.Items
.Select(x => x.ServiceResource)
.Distinct()
.ToList();

var resourcePolicyInformation = await _db.ResourcePolicyInformation
.Where(x => serviceResources.Contains(x.Resource))
.ToDictionaryAsync(x => x.Resource, x => x.MinimumAuthenticationLevel, cancellationToken);

foreach (var dialog in paginatedList.Items)
{
if (!resourcePolicyInformation.TryGetValue(dialog.ServiceResource, out var minimumAuthenticationLevel))
{
continue;
}

if (!_altinnAuthorization.UserHasRequiredAuthLevel(minimumAuthenticationLevel))
{
dialog.Content.SetNonSensitiveContent();
}
}
return paginatedList.ConvertTo(_mapper.Map<DialogDto>);
}
}
Loading