diff --git a/src/Altinn.App.Core/Features/Action/InitializeDelegatedSigningUserAction.cs b/src/Altinn.App.Core/Features/Action/InitializeDelegatedSigningUserAction.cs index ab2005566..ed65e742a 100644 --- a/src/Altinn.App.Core/Features/Action/InitializeDelegatedSigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/InitializeDelegatedSigningUserAction.cs @@ -93,7 +93,13 @@ await _appMetadata.GetApplicationMetadata(), ct ); - await _signingService.ProcessSignees(cachedDataMutator, signeeContexts, signatureConfiguration, ct); + await _signingService.ProcessSignees( + currentTask.Id, + cachedDataMutator, + signeeContexts, + signatureConfiguration, + ct + ); var changes = cachedDataMutator.GetDataElementChanges(false); await cachedDataMutator.SaveChanges(changes); diff --git a/src/Altinn.App.Core/Features/Signing/Interfaces/ISigningDelegationService.cs b/src/Altinn.App.Core/Features/Signing/Interfaces/ISigningDelegationService.cs index 621a05f4b..57867485b 100644 --- a/src/Altinn.App.Core/Features/Signing/Interfaces/ISigningDelegationService.cs +++ b/src/Altinn.App.Core/Features/Signing/Interfaces/ISigningDelegationService.cs @@ -1,14 +1,18 @@ using Altinn.App.Core.Features.Signing.Models; -using Altinn.Platform.Storage.Interface.Models; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; namespace Altinn.App.Core.Features.Signing.Interfaces; internal interface ISigningDelegationService { - internal Task> DelegateSigneeRights( + internal Task<(List, bool success)> DelegateSigneeRights( string taskId, - Instance instance, + string instanceId, + Party delegatorParty, + AppIdentifier appIdentifier, List signeeContexts, - CancellationToken ct + CancellationToken ct, + Telemetry? telemetry = null ); } diff --git a/src/Altinn.App.Core/Features/Signing/Interfaces/ISigningService.cs b/src/Altinn.App.Core/Features/Signing/Interfaces/ISigningService.cs index fc705a895..405be2efb 100644 --- a/src/Altinn.App.Core/Features/Signing/Interfaces/ISigningService.cs +++ b/src/Altinn.App.Core/Features/Signing/Interfaces/ISigningService.cs @@ -15,6 +15,7 @@ CancellationToken ct ); Task> ProcessSignees( + string taskId, IInstanceDataMutator instanceMutator, List signeeContexts, AltinnSignatureConfiguration signatureConfiguration, diff --git a/src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs b/src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs index aaf725885..3aa48b668 100644 --- a/src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs +++ b/src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Altinn.Platform.Register.Models; namespace Altinn.App.Core.Features.Signing.Models; @@ -7,9 +8,10 @@ namespace Altinn.App.Core.Features.Signing.Models; /// internal sealed class SigneeContext { - /// The identifier of the signee. - [JsonPropertyName("partyId")] - public required int PartyId { get; init; } + /// + /// The party associated with the signee state. + /// + public required Party Party { get; set; } /// The task associated with the signee state. [JsonPropertyName("taskId")] @@ -21,12 +23,6 @@ internal sealed class SigneeContext [JsonPropertyName("signeeState")] public required SigneeState SigneeState { get; set; } - // /// - // /// The signee. - // /// - // [JsonPropertyName("signeeParty")] - // public required SigneeParty SigneeParty { get; set; } - /// /// The organisation signee. /// diff --git a/src/Altinn.App.Core/Features/Signing/SigningDelegationService.cs b/src/Altinn.App.Core/Features/Signing/SigningDelegationService.cs index 44388529f..602e71ab0 100644 --- a/src/Altinn.App.Core/Features/Signing/SigningDelegationService.cs +++ b/src/Altinn.App.Core/Features/Signing/SigningDelegationService.cs @@ -1,25 +1,33 @@ -using System.Globalization; using Altinn.App.Core.Features.Signing.Interfaces; using Altinn.App.Core.Features.Signing.Models; -using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.AccessManagement; -using Altinn.App.Core.Internal.AccessManagement.Builders; using Altinn.App.Core.Internal.AccessManagement.Models; using Altinn.App.Core.Internal.AccessManagement.Models.Shared; -using Altinn.Platform.Storage.Interface.Models; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; +using Microsoft.Extensions.Logging; +using static Altinn.App.Core.Features.Telemetry.DelegationConst; namespace Altinn.App.Core.Features.Signing; -internal sealed class SigningDelegationService(IAccessManagementClient accessManagementClient) - : ISigningDelegationService +internal sealed class SigningDelegationService( + IAccessManagementClient accessManagementClient, + ILogger logger +) : ISigningDelegationService { - public async Task> DelegateSigneeRights( + public async Task<(List, bool success)> DelegateSigneeRights( string taskId, - Instance instance, + string instanceId, + Party delegatorParty, + AppIdentifier appIdentifier, List signeeContexts, - CancellationToken ct + CancellationToken ct, + Telemetry? telemetry = null ) { + var instanceGuid = instanceId.Split("/")[1]; + var appResourceId = AppResourceId.FromAppIdentifier(appIdentifier); + bool success = true; foreach (SigneeContext signeeContext in signeeContexts) { SigneeState state = signeeContext.SigneeState; @@ -28,53 +36,60 @@ CancellationToken ct { if (state.IsAccessDelegated is false) { - DelegationRequest delegationRequest = DelegationBuilder - .Create() - .WithApplicationId(instance.AppId) - .WithInstanceId(instance.Id) - .WithDelegator(new Delegator { IdType = DelegationConst.Party, Id = "" }) - .WithRecipient( - new Delegatee + var dr = new DelegationRequest + { + From = new Delegator + { + Type = DelegationConst.Party, + Value = + delegatorParty.PartyUuid.ToString() + ?? throw new InvalidOperationException("Delegator: PartyUuid is null"), + }, + To = new Delegatee + { + Type = DelegationConst.Party, + Value = + signeeContext.Party.PartyUuid.ToString() + ?? throw new InvalidOperationException("Delegatee: PartyUuid is null"), + }, + ResourceId = appResourceId.Value, + InstanceId = instanceGuid, + Rights = + [ + new RightRequest { - IdType = DelegationConst.Party, - Id = signeeContext.PartyId.ToString(CultureInfo.InvariantCulture), - } - ) - .WithRights( - [ - AccessRightBuilder - .Create() - .WithAction(ActionType.Read) - .WithResources( - [ - new Resource { Value = AppIdHelper.ToResourceId(instance.AppId) }, - new Resource { Type = DelegationConst.Task, Value = taskId }, - ] - ) - .Build(), - AccessRightBuilder - .Create() - .WithAction(ActionType.Sign) - .WithResources( - [ - new Resource { Value = AppIdHelper.ToResourceId(instance.AppId) }, - new Resource { Type = DelegationConst.Task, Value = taskId }, - ] - ) - .Build(), - ] - ) - .Build(); - var response = await accessManagementClient.DelegateRights(delegationRequest, ct); + Resource = + [ + new Resource { Type = DelegationConst.Resource, Value = appResourceId.Value }, + new Resource { Type = DelegationConst.Task, Value = taskId }, + ], + Action = new AltinnAction { Type = DelegationConst.ActionId, Value = "read" }, + }, + new RightRequest + { + Resource = + [ + new Resource { Type = DelegationConst.Resource, Value = appResourceId.Value }, + new Resource { Type = DelegationConst.Task, Value = taskId }, + ], + Action = new AltinnAction { Type = DelegationConst.ActionId, Value = "sign" }, + }, + ], + }; + var response = await accessManagementClient.DelegateRights(dr, ct); state.IsAccessDelegated = await Task.FromResult(true); + telemetry?.RecordDelegation(DelegationResult.Success); } } catch (Exception ex) { + logger.LogError(ex, "Failed to delegate signee rights"); state.DelegationFailedReason = "Failed to delegate signee rights: " + ex.Message; + telemetry?.RecordDelegation(DelegationResult.Error); + success = false; } } - return signeeContexts; + return (signeeContexts, success); } } diff --git a/src/Altinn.App.Core/Features/Signing/SigningService.cs b/src/Altinn.App.Core/Features/Signing/SigningService.cs index e0d084b21..c1200cf15 100644 --- a/src/Altinn.App.Core/Features/Signing/SigningService.cs +++ b/src/Altinn.App.Core/Features/Signing/SigningService.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Globalization; using System.Text; using System.Text.Json; using Altinn.App.Core.Features.Signing.Exceptions; @@ -22,7 +23,7 @@ internal sealed class SigningService( IPersonClient personClient, IOrganizationClient organisationClient, IAltinnPartyClient altinnPartyClient, - // ISigningDelegationService signingDelegationService, + ISigningDelegationService signingDelegationService, // ISigningNotificationService signingNotificationService, IEnumerable signeeProviders, IDataClient dataClient, @@ -84,6 +85,7 @@ CancellationToken ct } public async Task> ProcessSignees( + string taskId, IInstanceDataMutator instanceMutator, List signeeContexts, AltinnSignatureConfiguration signatureConfiguration, @@ -91,13 +93,30 @@ CancellationToken ct ) { using Activity? activity = telemetry?.StartAssignSigneesActivity(); - string taskId = instanceMutator.Instance.Process.CurrentTask.ElementId; - - // await signingDelegationService.DelegateSigneeRights(taskId, instanceMutator.Instance, signeeContexts, ct); - - //TODO: If something fails inside DelegateSigneeRights, abort and don't send notifications. Set error state in SigneeState. + string instanceOwnerPartyId = instanceMutator.Instance.InstanceOwner.PartyId; + + var instanceOwnerPartyIdAsInt = int.TryParse(instanceOwnerPartyId, out int partyId) + ? partyId + : throw new InvalidOperationException($"Party ID {instanceOwnerPartyId} is not a valid integer."); + Party? delegatorParty = await altinnPartyClient.GetParty(instanceOwnerPartyIdAsInt); + + string instanceId = instanceMutator.Instance.Id; + + AppIdentifier appIdentifier = new(instanceMutator.Instance.AppId); + (signeeContexts, var delegateSuccess) = await signingDelegationService.DelegateSigneeRights( + taskId, + instanceId, + delegatorParty ?? throw new InvalidOperationException("Delegator party is null"), + appIdentifier, + signeeContexts, + ct, + telemetry + ); - // await signingNotificationService.NotifySignatureTask(signeeContexts, ct); + if (delegateSuccess) + { + // await signingNotificationService.NotifySignatureTask(signeeContexts, ct); + } // ! TODO: Remove nullable instanceMutator.AddBinaryDataElement( @@ -202,21 +221,18 @@ CancellationToken ct List personSigneeContexts = []; foreach (PersonSignee personSignee in signeeResult.PersonSignees) { + var lastName = personSignee.LastName.Split(" ").First().ToLower(CultureInfo.InvariantCulture); _logger.LogInformation( "Looking up person with SSN {SocialSecurityNumber} and last name {LastName}.", personSignee.SocialSecurityNumber, - personSignee.LastName.Split(" ").Last() + lastName ); Person? person = - await personClient.GetPerson( - personSignee.SocialSecurityNumber, - personSignee.LastName.Split(" ").Last(), - ct - ) + await personClient.GetPerson(personSignee.SocialSecurityNumber, lastName, ct) ?? throw new SignaturePartyNotValidException( - $"The given SSN and last name did not match any person in the registry." + $"The given SSN and last name: {lastName} did not match any person in the registry." ); - Party? party = await altinnPartyClient.LookupParty( + Party party = await altinnPartyClient.LookupParty( new PartyLookup { Ssn = personSignee.SocialSecurityNumber } ); @@ -230,7 +246,7 @@ await personClient.GetPerson( new SigneeContext { TaskId = taskId, - PartyId = party.PartyId, + Party = party, PersonSignee = personSignee, SigneeState = new SigneeState(), } @@ -249,19 +265,12 @@ private async Task> GetOrganisationSigneeContexts( List organisationSigneeContexts = []; //TODO rename foreach (OrganisationSignee organisationSignee in signeeResult.OrganisationSignees) { - Organization? organisation = await organisationClient.GetOrganization( - organisationSignee.OrganisationNumber - ); - - if (organisation is null) - { - //TODO: persist state and throw - throw new SignaturePartyNotValidException( + Organization? organisation = + await organisationClient.GetOrganization(organisationSignee.OrganisationNumber) + ?? throw new SignaturePartyNotValidException( $"Signature party with organisation number {organisationSignee.OrganisationNumber} was not found in the registry." ); - } - - Party? party = await altinnPartyClient.LookupParty( + Party party = await altinnPartyClient.LookupParty( new PartyLookup { OrgNo = organisationSignee.OrganisationNumber } ); @@ -282,7 +291,7 @@ private async Task> GetOrganisationSigneeContexts( new SigneeContext { TaskId = taskId, - PartyId = party.PartyId, + Party = party, OrganisationSignee = organisationSignee, SigneeState = new SigneeState(), } diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.SigningService.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.SigningService.cs index 85f7a9bbd..3aae48615 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.SigningService.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.SigningService.cs @@ -1,12 +1,56 @@ +using System.ComponentModel.DataAnnotations; using System.Diagnostics; +using NetEscapades.EnumGenerators; +using static Altinn.App.Core.Features.Telemetry.DelegationConst; +using Tag = System.Collections.Generic.KeyValuePair; namespace Altinn.App.Core.Features; partial class Telemetry { + /// + /// Prometheus' increase and rate functions do not register the first value as an increase, but rather as the registration.
+ /// This means that going from none (non-existant) to 1 on a counter will register as an increase of 0.
+ /// In order to workaround this, we initialize to 0 for all metrics here.
+ /// Github issue can be found here. + ///
+ /// + private void InitSigning(InitContext context) + { + InitMetricCounter( + context, + MetricNameDelegation, + init: static m => + { + foreach (var result in DelegationResultExtensions.GetValues()) + { + m.Add(0, new Tag(InternalLabels.Result, result.ToStringFast())); + } + } + ); + } + + internal void RecordDelegation(DelegationResult result) => + _counters[MetricNameDelegation].Add(1, new Tag(InternalLabels.Result, result.ToStringFast())); + internal Activity? StartAssignSigneesActivity() => ActivitySource.StartActivity("SigningService.AssignSignees"); internal Activity? StartReadSigneesActivity() => ActivitySource.StartActivity("SigningService.ReadSignees"); internal Activity? StartSignActivity() => ActivitySource.StartActivity("SigningService.Sign"); // TODO: expand to include signee id + + internal static class DelegationConst + { + internal static readonly string MetricNameDelegation = Metrics.CreateLibName("signing_delegations"); + + [EnumExtensions] + internal enum DelegationResult + { + [Display(Name = "success")] + Success, + + [Display(Name = "error")] + Error, + } + } } diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs index 6732e0894..99b5c9ffb 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs @@ -73,6 +73,7 @@ internal void Init() InitData(context); InitInstances(context); InitNotifications(context); + InitSigning(context); InitProcesses(context); InitValidation(context); InitMaskinporten(context); diff --git a/src/Altinn.App.Core/Helpers/AppIdHelper.cs b/src/Altinn.App.Core/Helpers/AppIdHelper.cs deleted file mode 100644 index d232370ba..000000000 --- a/src/Altinn.App.Core/Helpers/AppIdHelper.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Altinn.App.Core.Helpers; - -internal sealed class AppIdHelper -{ - internal static string ToResourceId(string appId) - { - return ""; //TODO - } - - internal static bool IsResourceId(string appId) - { - return false; //TODO - } - - internal static bool TryGetResourceId(string appId, [NotNullWhen(true)] out string? resourceId) - { - if (string.IsNullOrEmpty(appId)) - { - resourceId = null; - return false; - } - - if (IsResourceId(appId)) - { - resourceId = appId; - return true; - } - - resourceId = ToResourceId(appId); - - if (IsResourceId(resourceId)) - { - return true; - } - else - { - resourceId = null; - return false; - } - } -} diff --git a/src/Altinn.App.Core/Internal/AccessManagement/AccessManagementClient.cs b/src/Altinn.App.Core/Internal/AccessManagement/AccessManagementClient.cs index 6c7022b64..f0759d889 100644 --- a/src/Altinn.App.Core/Internal/AccessManagement/AccessManagementClient.cs +++ b/src/Altinn.App.Core/Internal/AccessManagement/AccessManagementClient.cs @@ -5,8 +5,10 @@ using Altinn.App.Core.Internal.AccessManagement.Exceptions; using Altinn.App.Core.Internal.AccessManagement.Helpers; using Altinn.App.Core.Internal.AccessManagement.Models; +using Altinn.App.Core.Internal.AccessManagement.Models.Shared; using Altinn.App.Core.Internal.App; using Altinn.Common.AccessTokenClient.Services; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -43,7 +45,10 @@ public async Task DelegateRights(DelegationRequest delegatio var application = await appMetadata.GetApplicationMetadata(); var uri = urlHelper.CreateInstanceDelegationUrl(delegation.ResourceId, delegation.InstanceId); - var body = JsonSerializer.Serialize(delegation); + AppsInstanceDelegationRequestDto dto = GetDto(delegation); + var body = JsonSerializer.Serialize(dto); + logger.LogInformation($"------------------------------------------------------------------------"); + logger.LogInformation($"Delegating rights to {uri} with body {body}"); using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, uri) { @@ -57,6 +62,7 @@ public async Task DelegateRights(DelegationRequest delegatio httpResponseMessage = await httpClient.SendAsync(httpRequestMessage, ct); httpContent = await httpResponseMessage.Content.ReadAsStringAsync(ct); + logger.LogInformation($"Response from delegation: {httpContent}"); DelegationResponse? response; if (httpResponseMessage.IsSuccessStatusCode) { @@ -66,19 +72,44 @@ public async Task DelegateRights(DelegationRequest delegatio } else { + try + { + var problemDetails = JsonSerializer.Deserialize(httpContent); + if (problemDetails is not null) + { + logger.LogError( + "Got error status code for access management request. Status code: {StatusCode}. Problem details: {ProblemDetails}", + httpResponseMessage.StatusCode, + JsonSerializer.Serialize(problemDetails) + ); + throw new AccessManagementRequestException( + "Got error status code for access management request.", + problemDetails, + httpResponseMessage.StatusCode, + httpContent + ); + } + } + catch (JsonException) + { + response = null; + } throw new HttpRequestException("Got error status code for access management request."); } return response; } catch (Exception e) { - var ex = new AccessManagementRequestException( - $"Something went wrong when processing the access management request.", - null, - httpResponseMessage?.StatusCode, - httpContent, - e - ); + var ex = + e is AccessManagementRequestException + ? e + : new AccessManagementRequestException( + $"Something went wrong when processing the access management request.", + null, + httpResponseMessage?.StatusCode, + httpContent, + e + ); logger.LogError(ex, "Error when processing access management request."); // TODO: metrics @@ -90,4 +121,38 @@ public async Task DelegateRights(DelegationRequest delegatio httpResponseMessage?.Dispose(); } } + + private static AppsInstanceDelegationRequestDto GetDto(DelegationRequest delegation) + { + return new AppsInstanceDelegationRequestDto + { + From = new Delegator + { + Type = delegation.From is not null + ? delegation.From.Type + : throw new AccessManagementArgumentException("From is required"), + Value = delegation.From.Value, + }, + To = new Delegatee + { + Type = delegation.To is not null + ? delegation.To.Type + : throw new AccessManagementArgumentException("To is required"), + Value = delegation.To.Value, + }, + Rights = delegation + .Rights.Select(r => new RightDto + { + Resource = r.Resource.Select(rr => new Resource { Type = rr.Type, Value = rr.Value }).ToList(), + Action = new AltinnAction + { + Type = r.Action is not null + ? r.Action.Type + : throw new AccessManagementArgumentException("Action is required"), + Value = r.Action.Value, + }, + }) + .ToList(), + }; + } } diff --git a/src/Altinn.App.Core/Internal/AccessManagement/Builders/DelegationBuilder.cs b/src/Altinn.App.Core/Internal/AccessManagement/Builders/DelegationBuilder.cs index d8f0e73da..a7274eba7 100644 --- a/src/Altinn.App.Core/Internal/AccessManagement/Builders/DelegationBuilder.cs +++ b/src/Altinn.App.Core/Internal/AccessManagement/Builders/DelegationBuilder.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; -using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.AccessManagement.Exceptions; using Altinn.App.Core.Internal.AccessManagement.Models; using Altinn.App.Core.Internal.AccessManagement.Models.Shared; +using Altinn.App.Core.Models; namespace Altinn.App.Core.Internal.AccessManagement.Builders; @@ -23,7 +23,7 @@ value is null internal interface IDelegationBuilderStart { - IDelegationBuilderApplicationId WithApplicationId(string applicationId); + IDelegationBuilderApplicationId WithApplicationId(AppIdentifier appIdentifier); } internal interface IDelegationBuilderApplicationId @@ -38,7 +38,7 @@ internal interface IDelegationBuilderInstanceId internal interface IDelegationBuilderDelegator { - IDelegationBuilderRecipient WithRecipient(Delegatee recipient); + IDelegationBuilderRecipient WithDelegatee(Delegatee recipient); } internal interface IDelegationBuilderRecipient @@ -69,12 +69,10 @@ private DelegationBuilder() { } public static IDelegationBuilderStart Create() => new DelegationBuilder(); - public IDelegationBuilderApplicationId WithApplicationId(string applicationId) + public IDelegationBuilderApplicationId WithApplicationId(AppIdentifier appIdentifier) { - NotNullOrEmpty(applicationId, nameof(applicationId)); - _applicationId = AppIdHelper.TryGetResourceId(applicationId, out string? resourceId) - ? resourceId - : throw new ArgumentException("Invalid application ID", nameof(applicationId)); + AppResourceId appResourceId = AppResourceId.FromAppIdentifier(appIdentifier); + _applicationId = appResourceId.Value; return this; } @@ -92,7 +90,7 @@ public IDelegationBuilderDelegator WithDelegator(Delegator delegator) return this; } - public IDelegationBuilderRecipient WithRecipient(Delegatee recipient) + public IDelegationBuilderRecipient WithDelegatee(Delegatee recipient) { NotNullOrEmpty(recipient, nameof(recipient)); _recipient = recipient; diff --git a/src/Altinn.App.Core/Internal/AccessManagement/Models/DelegationRequest.cs b/src/Altinn.App.Core/Internal/AccessManagement/Models/DelegationRequest.cs index 83cea9894..daae701c9 100644 --- a/src/Altinn.App.Core/Internal/AccessManagement/Models/DelegationRequest.cs +++ b/src/Altinn.App.Core/Internal/AccessManagement/Models/DelegationRequest.cs @@ -3,32 +3,64 @@ namespace Altinn.App.Core.Internal.AccessManagement.Models; -internal sealed class DelegationRequest +// add params to summary +/// +/// Represents a request to delegate rights to a user. +/// \ + +public sealed class DelegationRequest { + /// + /// Gets or sets the delegator. + /// [JsonPropertyName("from")] - internal Delegator? From { get; set; } + public Delegator? From { get; set; } + /// + /// Gets or sets the delegatee. + /// [JsonPropertyName("to")] - internal Delegatee? To { get; set; } + public Delegatee? To { get; set; } + /// + /// Gets or sets the resource id. + /// [JsonPropertyName("resourceId")] - internal required string ResourceId { get; set; } + public required string ResourceId { get; set; } + /// + /// Gets or sets the instance id. + /// [JsonPropertyName("instanceId")] - internal required string InstanceId { get; set; } + public required string InstanceId { get; set; } + /// + /// Gets or sets the rights. + /// [JsonPropertyName("rights")] - internal List Rights { get; set; } = []; + public List Rights { get; set; } = []; } -internal sealed class RightRequest +/// +/// Represents the rights to delegate. +/// +public class RightRequest { + /// + /// Gets or sets the resource. + /// [JsonPropertyName("resource")] - internal List Resource { get; set; } = []; + public List Resource { get; set; } = []; + /// + /// Gets or sets the action. + /// [JsonPropertyName("action")] - internal AltinnAction? Action { get; set; } + public AltinnAction? Action { get; set; } + /// + /// Gets or sets the task id. + /// [JsonPropertyName("taskId")] - internal string? TaskId { get; set; } + public string? TaskId { get; set; } } diff --git a/src/Altinn.App.Core/Internal/AccessManagement/Models/DelegationResponse.cs b/src/Altinn.App.Core/Internal/AccessManagement/Models/DelegationResponse.cs index 8f758ddc8..f76fba513 100644 --- a/src/Altinn.App.Core/Internal/AccessManagement/Models/DelegationResponse.cs +++ b/src/Altinn.App.Core/Internal/AccessManagement/Models/DelegationResponse.cs @@ -3,32 +3,62 @@ namespace Altinn.App.Core.Internal.AccessManagement.Models; -internal sealed class DelegationResponse +/// +/// Represents a response to a delegation request. +/// +public sealed class DelegationResponse { + /// + /// Gets or sets the delegator. + /// [JsonPropertyName("from")] - internal Delegator? Delegator { get; set; } + public Delegator? Delegator { get; set; } + /// + /// Gets or sets the delegatee. + /// [JsonPropertyName("to")] - internal Delegatee? Delegatee { get; set; } + public Delegatee? Delegatee { get; set; } + /// + /// Gets or sets the resource id. + /// [JsonPropertyName("resourceId")] - internal string? ResourceId { get; set; } + public string? ResourceId { get; set; } + /// + /// Gets or sets the instance id. + /// [JsonPropertyName("instanceId")] - internal string? InstanceId { get; set; } + public string? InstanceId { get; set; } + /// + /// Gets or sets the rights. + /// [JsonPropertyName("rights")] - internal List Rights { get; set; } = []; + public List Rights { get; set; } = []; } -internal sealed class RightResponse +/// +/// Represents the rights to delegate. +/// +public sealed class RightResponse { + /// + /// Gets or sets the resource. + /// [JsonPropertyName("resource")] - internal List Resource { get; set; } = []; + public List Resource { get; set; } = []; + /// + /// Gets or sets the action. + /// [JsonPropertyName("action")] - internal AltinnAction? Action { get; set; } + public AltinnAction? Action { get; set; } + /// + /// Gets or sets the status. + /// [JsonPropertyName("status")] - internal string? Status { get; set; } + public string? Status { get; set; } } diff --git a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/AltinnAction.cs b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/AltinnAction.cs index 109246fc1..a4c7084ea 100644 --- a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/AltinnAction.cs +++ b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/AltinnAction.cs @@ -2,11 +2,20 @@ namespace Altinn.App.Core.Internal.AccessManagement.Models.Shared; -internal sealed class AltinnAction +/// +/// Represents an action. +/// +public class AltinnAction { + /// + /// Gets or sets the type of the action. + /// [JsonPropertyName("type")] - internal required string Type { get; set; } + public required string Type { get; set; } + /// + /// Gets or sets the value. + /// [JsonPropertyName("value")] - internal required string Value { get; set; } + public required string Value { get; set; } } diff --git a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/ApiInstanceDelegationRequestDto.cs b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/ApiInstanceDelegationRequestDto.cs new file mode 100644 index 000000000..16dcf34c5 --- /dev/null +++ b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/ApiInstanceDelegationRequestDto.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Internal.AccessManagement.Models.Shared; + +/// +/// Request model for performing delegation of access to an app instance from Apps +/// +public class AppsInstanceDelegationRequestDto +{ + /// + /// Gets or sets the urn identifying the party to delegate from + /// + [JsonPropertyName("from")] + public required Delegator From { get; set; } + + /// + /// Gets or sets the urn identifying the party to be delegated to + /// + [JsonPropertyName("to")] + public required Delegatee To { get; set; } + + /// + /// Gets or sets the rights to delegate + /// + [JsonPropertyName("rights")] + public required IEnumerable Rights { get; set; } +} + +/// +/// Represents the rights to delegate. +/// +public class RightDto +{ + /// + /// Gets or sets the resource. + /// + [JsonPropertyName("resource")] + public required List Resource { get; set; } + + /// + /// Gets or sets the action. + /// + [JsonPropertyName("action")] + public required AltinnAction Action { get; set; } +} diff --git a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Delegatee.cs b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Delegatee.cs index 7aff4afb6..6a6acf4bf 100644 --- a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Delegatee.cs +++ b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Delegatee.cs @@ -2,11 +2,20 @@ namespace Altinn.App.Core.Internal.AccessManagement.Models.Shared; -internal sealed class Delegatee +/// +/// Represents the delegatee. +/// +public class Delegatee { + /// + /// Gets or sets the type of the id. + /// [JsonPropertyName("type")] - internal required string IdType { get; set; } + public required string Type { get; set; } + /// + /// Gets or sets the id. + /// [JsonPropertyName("value")] - internal required string Id { get; set; } + public required string Value { get; set; } } diff --git a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Delegator.cs b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Delegator.cs index e356f63c7..d6e6120cc 100644 --- a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Delegator.cs +++ b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Delegator.cs @@ -2,11 +2,20 @@ namespace Altinn.App.Core.Internal.AccessManagement.Models.Shared; -internal sealed class Delegator +/// +/// Represents the delegator. +/// +public class Delegator { + /// + /// Gets or sets the type of the id. + /// [JsonPropertyName("type")] - internal required string IdType { get; set; } + public required string Type { get; set; } + /// + /// Gets or sets the id. + /// [JsonPropertyName("value")] - internal required string Id { get; set; } + public required string Value { get; set; } } diff --git a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Resource.cs b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Resource.cs index f25902638..a345f1d49 100644 --- a/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Resource.cs +++ b/src/Altinn.App.Core/Internal/AccessManagement/Models/Shared/Resource.cs @@ -2,11 +2,20 @@ namespace Altinn.App.Core.Internal.AccessManagement.Models.Shared; -internal sealed class Resource +/// +/// Represents a resource. +/// +public class Resource { + /// + /// Gets or sets the type of the resource. Default is "resource". + /// [JsonPropertyName("type")] - internal string Type { get; set; } = DelegationConst.Resource; + public string Type { get; set; } = DelegationConst.Resource; + /// + /// Gets or sets the value. + /// [JsonPropertyName("value")] - internal required string Value { get; set; } + public required string Value { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs index ef8de0aa6..d872d14a5 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs @@ -82,7 +82,13 @@ public async Task Start(string taskId, Instance instance) cts.Token ); - await _signingService.ProcessSignees(cachedDataMutator, signeeContexts, signatureConfiguration, cts.Token); + await _signingService.ProcessSignees( + taskId, + cachedDataMutator, + signeeContexts, + signatureConfiguration, + cts.Token + ); DataElementChanges changes = cachedDataMutator.GetDataElementChanges(false); await cachedDataMutator.UpdateInstanceData(changes); diff --git a/src/Altinn.App.Core/Models/AppResourceId.cs b/src/Altinn.App.Core/Models/AppResourceId.cs new file mode 100644 index 000000000..74460c727 --- /dev/null +++ b/src/Altinn.App.Core/Models/AppResourceId.cs @@ -0,0 +1,43 @@ +using System.Text.RegularExpressions; +using Altinn.App.Core.Models; + +internal partial class AppResourceId +{ + [GeneratedRegex("app_[a-zA-Z0-9]+_[a-zA-Z0-9]+")] + private static partial Regex AppIdRegex(); + + internal AppResourceId(string org, string app) + { + Org = org; + App = app; + } + + internal AppResourceId(AppIdentifier appIdentifier) + { + Org = appIdentifier.Org; + App = appIdentifier.App; + } + + internal AppResourceId(string appId) + { + var deconstructed = appId.Split("/"); + Org = deconstructed[0]; + App = deconstructed[1]; + } + + internal string Org { get; init; } + + internal string App { get; init; } + + internal string Value => $"app_{Org}_{App}"; + + internal static bool IsResourceId(AppResourceId? resourceId) + { + return resourceId != null && AppIdRegex().IsMatch(resourceId.Value); + } + + internal static AppResourceId FromAppIdentifier(AppIdentifier appIdentifier) + { + return new AppResourceId(appIdentifier); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/AccessManagement/Builders/DelegationBuilderTests.cs b/test/Altinn.App.Core.Tests/Internal/AccessManagement/Builders/DelegationBuilderTests.cs new file mode 100644 index 000000000..09dbad992 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/AccessManagement/Builders/DelegationBuilderTests.cs @@ -0,0 +1,111 @@ +namespace Altinn.App.Core.Tests.Internal.AccessManagement.Builders; + +using Altinn.App.Core.Internal.AccessManagement.Builders; +using Altinn.App.Core.Internal.AccessManagement.Models; +using Altinn.App.Core.Internal.AccessManagement.Models.Shared; +using Altinn.App.Core.Models; +using FluentAssertions; + +public class DelegationBuilderTests +{ + [Fact] + public void ShouldBuildValidDelegationRequest() + { + // Arrange + var testAppId = "testOrg/testApp"; + AppIdentifier appIdentifier = new(testAppId); + AppResourceId appResourceId = AppResourceId.FromAppIdentifier(appIdentifier); + string instanceId = "61c2fe1d-7ff7-4009-9e96-506c56ea3d5e"; + string instanceOwnerPartyId = "50000000"; + string delegateeId = "50000001"; + string taskId = "Task_1"; + + var builder = DelegationBuilder + .Create() + .WithApplicationId(appIdentifier) + .WithInstanceId(instanceId) + .WithDelegator(new Delegator { Type = DelegationConst.Party, Value = instanceOwnerPartyId }) + .WithDelegatee(new Delegatee { Type = DelegationConst.Party, Value = delegateeId }) + .WithRights( + [ + AccessRightBuilder + .Create() + .WithAction(ActionType.Read) + .WithResources( + [ + new Resource { Value = appResourceId.Value }, + new Resource { Type = DelegationConst.Task, Value = taskId }, + ] + ) + .Build(), + ] + ); + + var expected = new DelegationRequest + { + From = new Delegator { Type = "urn:altinn:party:uuid", Value = "50000000" }, + To = new Delegatee { Type = "urn:altinn:party:uuid", Value = "50000001" }, + ResourceId = "app_testOrg_testApp", + InstanceId = "61c2fe1d-7ff7-4009-9e96-506c56ea3d5e", + Rights = + [ + new RightRequest + { + Resource = + [ + new Resource { Value = "app_testOrg_testApp" }, + new Resource { Type = "urn:altinn:task", Value = "Task_1" }, + ], + Action = new AltinnAction + { + Type = "urn:oasis:names:tc:xacml:1.0:action:action-id", + Value = "read", + }, + }, + ], + }; + + // Act + var actual = builder.Build(); + + // Assert + // Compare top-level properties + actual.From!.Type.Should().Be(expected.From.Type); + actual.From.Value.Should().Be(expected.From.Value); + + actual.To!.Type.Should().Be(expected.To.Type); + actual.To.Value.Should().Be(expected.To.Value); + + actual.ResourceId.Should().Be(expected.ResourceId); + actual.InstanceId.Should().Be(expected.InstanceId); + + // Compare the Rights collection + actual + .Rights.Should() + .HaveCount(expected.Rights.Count, "they should contain the same number of right requests"); + + for (int i = 0; i < actual.Rights.Count; i++) + { + var requestRight = actual.Rights[i]; + var expectedRight = expected.Rights[i]; + + // Compare the Action + requestRight.Action!.Type.Should().Be(expectedRight.Action!.Type); + requestRight.Action.Value.Should().Be(expectedRight.Action.Value); + + // Compare the Resources + requestRight + .Resource.Should() + .HaveCount(expectedRight.Resource.Count, "they should reference the same number of resources"); + + for (int j = 0; j < requestRight.Resource.Count; j++) + { + var requestRes = requestRight.Resource[j]; + var expectedRes = expectedRight.Resource[j]; + + requestRes.Type.Should().Be(expectedRes.Type); + requestRes.Value.Should().Be(expectedRes.Value); + } + } + } +}