diff --git a/src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs b/src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs
index 01a4857442..9dd62e9547 100644
--- a/src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs
+++ b/src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs
@@ -23,6 +23,12 @@ internal sealed class SigneeContext
[JsonPropertyName("CommunicationConfig")]
public CommunicationConfig? CommunicationConfig { get; init; }
+ ///
+ /// Additional actions to delegate to this signee beyond the default read and sign.
+ ///
+ [JsonPropertyName("additionalActionsToDelegate")]
+ public List? AdditionalActionsToDelegate { get; init; }
+
///
/// The state of the signee.
///
diff --git a/src/Altinn.App.Core/Features/Signing/ProvidedSignee.cs b/src/Altinn.App.Core/Features/Signing/ProvidedSignee.cs
index ee7f1a2c2f..8b34a68eb4 100644
--- a/src/Altinn.App.Core/Features/Signing/ProvidedSignee.cs
+++ b/src/Altinn.App.Core/Features/Signing/ProvidedSignee.cs
@@ -12,6 +12,13 @@ public abstract class ProvidedSignee
///
[JsonPropertyName("communicationConfig")]
public CommunicationConfig? CommunicationConfig { get; init; }
+
+ ///
+ /// Additional actions to delegate to this signee. Read and sign are always delegated by default.
+ /// Use this to delegate additional actions, e.g. "reject". Custom action strings are also supported.
+ ///
+ [JsonPropertyName("additionalActionsToDelegate")]
+ public List? AdditionalActionsToDelegate { get; init; }
}
///
diff --git a/src/Altinn.App.Core/Features/Signing/Services/SigneeContextsManager.cs b/src/Altinn.App.Core/Features/Signing/Services/SigneeContextsManager.cs
index 1b646ca9a4..fc236752fc 100644
--- a/src/Altinn.App.Core/Features/Signing/Services/SigneeContextsManager.cs
+++ b/src/Altinn.App.Core/Features/Signing/Services/SigneeContextsManager.cs
@@ -162,6 +162,7 @@ CancellationToken ct
TaskId = taskId,
SigneeState = new SigneeContextState(),
CommunicationConfig = providedSignee.CommunicationConfig,
+ AdditionalActionsToDelegate = providedSignee.AdditionalActionsToDelegate,
Signee = signee,
};
}
diff --git a/src/Altinn.App.Core/Features/Signing/Services/SigningDelegationService.cs b/src/Altinn.App.Core/Features/Signing/Services/SigningDelegationService.cs
index e4d9f3919b..fd369854a8 100644
--- a/src/Altinn.App.Core/Features/Signing/Services/SigningDelegationService.cs
+++ b/src/Altinn.App.Core/Features/Signing/Services/SigningDelegationService.cs
@@ -68,7 +68,7 @@ CancellationToken ct
partyUuid.ToString()
?? throw new InvalidOperationException("Delegatee: PartyUuid is null"),
},
- Rights = CreateRights(appIdentifier, taskId),
+ Rights = CreateRights(appIdentifier, taskId, signeeContext.AdditionalActionsToDelegate),
};
await accessManagementClient.DelegateRights(delegationRequest, ct);
state.IsAccessDelegated = true;
@@ -125,7 +125,7 @@ CancellationToken ct
partyUuid.ToString()
?? throw new InvalidOperationException("Delegatee: PartyUuid is null"),
},
- Rights = CreateRights(appIdentifier, taskId),
+ Rights = CreateRights(appIdentifier, taskId, signeeContext.AdditionalActionsToDelegate),
};
await accessManagementClient.RevokeRights(delegationRequest, ct);
signeeContext.SigneeState.IsAccessDelegated = false;
@@ -155,7 +155,11 @@ private static Guid ParseInstanceGuid(string instanceIdCombo)
}
}
- private static List CreateRights(AppIdentifier appIdentifier, string taskId)
+ private static List CreateRights(
+ AppIdentifier appIdentifier,
+ string taskId,
+ List? additionalActions
+ )
{
var resources = new List
{
@@ -164,7 +168,7 @@ private static List CreateRights(AppIdentifier appIdentifier, stri
new TaskResource { Value = taskId },
};
- return
+ List rights =
[
new RightRequest
{
@@ -177,5 +181,21 @@ private static List CreateRights(AppIdentifier appIdentifier, stri
Action = new AltinnAction { Value = ActionType.Sign },
},
];
+
+ if (additionalActions is not null)
+ {
+ foreach (string action in additionalActions)
+ {
+ rights.Add(
+ new RightRequest
+ {
+ Resource = resources,
+ Action = new AltinnAction { Value = action },
+ }
+ );
+ }
+ }
+
+ return rights;
}
}
diff --git a/test/Altinn.App.Core.Tests/Features/Signing/SigneeContextsManagerTests.cs b/test/Altinn.App.Core.Tests/Features/Signing/SigneeContextsManagerTests.cs
index 469695512f..f25fa6baf3 100644
--- a/test/Altinn.App.Core.Tests/Features/Signing/SigneeContextsManagerTests.cs
+++ b/test/Altinn.App.Core.Tests/Features/Signing/SigneeContextsManagerTests.cs
@@ -530,4 +530,49 @@ public async Task GetSigneeContexts_WithMissingSigneeStatesDataTypeId_ThrowsAppl
Assert.Empty(result);
}
+
+ [Fact]
+ public async Task GenerateSigneeContexts_WithAdditionalActionsToDelegate_ThreadsToSigneeContext()
+ {
+ // Arrange
+ var signatureConfiguration = new AltinnSignatureConfiguration
+ {
+ SigneeProviderId = "testProvider",
+ SigneeStatesDataTypeId = SigneeStatesDataTypeId,
+ };
+
+ var instance = new Instance
+ {
+ Process = new ProcessState { CurrentTask = new ProcessElementInfo { ElementId = "Task_1" } },
+ };
+
+ var cachedInstanceMutator = new Mock();
+ cachedInstanceMutator.Setup(x => x.Instance).Returns(instance);
+
+ var personSignee = new ProvidedPerson
+ {
+ SocialSecurityNumber = "12345678901",
+ FullName = "Person One",
+ AdditionalActionsToDelegate = ["reject"],
+ };
+
+ var signeesResult = new SigneeProviderResult { Signees = [personSignee] };
+
+ _signeeProvider.Setup(x => x.Id).Returns("testProvider");
+ _signeeProvider.Setup(x => x.GetSignees(It.IsAny())).ReturnsAsync(signeesResult);
+
+ // Act
+ var result = await _signeeContextsManager.GenerateSigneeContexts(
+ cachedInstanceMutator.Object,
+ signatureConfiguration,
+ CancellationToken.None
+ );
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ var additionalActions = result[0].AdditionalActionsToDelegate;
+ Assert.NotNull(additionalActions);
+ Assert.Equal("reject", Assert.Single(additionalActions));
+ }
}
diff --git a/test/Altinn.App.Core.Tests/Features/Signing/SigningDelegationServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Signing/SigningDelegationServiceTests.cs
index dcf1b1e750..4b9b14d89c 100644
--- a/test/Altinn.App.Core.Tests/Features/Signing/SigningDelegationServiceTests.cs
+++ b/test/Altinn.App.Core.Tests/Features/Signing/SigningDelegationServiceTests.cs
@@ -540,4 +540,149 @@ public async Task RevokeSigneeRights_RecordsFailedRevocationOnError()
// Assert
Assert.False(success);
}
+
+ [Fact]
+ public async Task DelegateSigneeRights_WithAdditionalActions_DelegatesAllRights()
+ {
+ // Arrange
+ DelegationRequest? capturedRequest = null;
+ var accessManagementClient = new Mock();
+ accessManagementClient
+ .Setup(x => x.DelegateRights(It.IsAny(), It.IsAny()))
+ .Callback((req, _) => capturedRequest = req)
+ .ReturnsAsync(new DelegationResponse());
+ var logger = new Mock>();
+ var service = new SigningDelegationService(accessManagementClient.Object, logger.Object);
+ var taskId = "taskId";
+ Guid instanceGuid = Guid.NewGuid();
+ var instanceId = "instanceOwnerPartyId" + "/" + instanceGuid;
+ Guid instanceOwnerPartyUuid = Guid.NewGuid();
+ var appIdentifier = new AppIdentifier("testOrg", "testApp");
+ var signeeContexts = new List()
+ {
+ new()
+ {
+ TaskId = taskId,
+ SigneeState = new SigneeState() { IsAccessDelegated = false },
+ Signee = _signee,
+ AdditionalActionsToDelegate = ["reject"],
+ },
+ };
+ var ct = new CancellationToken();
+
+ // Act
+ (signeeContexts, var success) = await service.DelegateSigneeRights(
+ taskId,
+ instanceId,
+ instanceOwnerPartyUuid,
+ appIdentifier,
+ signeeContexts,
+ ct
+ );
+
+ // Assert
+ Assert.True(success);
+ Assert.True(signeeContexts[0].SigneeState.IsAccessDelegated);
+ Assert.NotNull(capturedRequest);
+ Assert.Equal(3, capturedRequest!.Rights.Count);
+ Assert.Equal("read", capturedRequest.Rights[0].Action!.Value);
+ Assert.Equal("sign", capturedRequest.Rights[1].Action!.Value);
+ Assert.Equal("reject", capturedRequest.Rights[2].Action!.Value);
+ }
+
+ [Fact]
+ public async Task DelegateSigneeRights_WithNullAdditionalActions_DelegatesOnlyReadAndSign()
+ {
+ // Arrange
+ DelegationRequest? capturedRequest = null;
+ var accessManagementClient = new Mock();
+ accessManagementClient
+ .Setup(x => x.DelegateRights(It.IsAny(), It.IsAny()))
+ .Callback((req, _) => capturedRequest = req)
+ .ReturnsAsync(new DelegationResponse());
+ var logger = new Mock>();
+ var service = new SigningDelegationService(accessManagementClient.Object, logger.Object);
+ var taskId = "taskId";
+ Guid instanceGuid = Guid.NewGuid();
+ var instanceId = "instanceOwnerPartyId" + "/" + instanceGuid;
+ Guid instanceOwnerPartyUuid = Guid.NewGuid();
+ var appIdentifier = new AppIdentifier("testOrg", "testApp");
+ var signeeContexts = new List()
+ {
+ new()
+ {
+ TaskId = taskId,
+ SigneeState = new SigneeState() { IsAccessDelegated = false },
+ Signee = _signee,
+ AdditionalActionsToDelegate = null,
+ },
+ };
+ var ct = new CancellationToken();
+
+ // Act
+ (_, bool success) = await service.DelegateSigneeRights(
+ taskId,
+ instanceId,
+ instanceOwnerPartyUuid,
+ appIdentifier,
+ signeeContexts,
+ ct
+ );
+
+ // Assert
+ Assert.True(success);
+ Assert.NotNull(capturedRequest);
+ Assert.Equal(2, capturedRequest!.Rights.Count);
+ Assert.Equal("read", capturedRequest.Rights[0].Action!.Value);
+ Assert.Equal("sign", capturedRequest.Rights[1].Action!.Value);
+ }
+
+ [Fact]
+ public async Task RevokeSigneeRights_WithAdditionalActions_RevokesAllRights()
+ {
+ // Arrange
+ DelegationRequest? capturedRequest = null;
+ var accessManagementClient = new Mock();
+ accessManagementClient
+ .Setup(x => x.RevokeRights(It.IsAny(), It.IsAny()))
+ .Callback((req, _) => capturedRequest = req)
+ .ReturnsAsync(new DelegationResponse());
+ var logger = new Mock>();
+ var service = new SigningDelegationService(accessManagementClient.Object, logger.Object);
+ var taskId = "taskId";
+ Guid instanceGuid = Guid.NewGuid();
+ var instanceId = "instanceOwnerPartyId" + "/" + instanceGuid;
+ Guid instanceOwnerPartyUuid = Guid.NewGuid();
+ var appIdentifier = new AppIdentifier("testOrg", "testApp");
+ var signeeContexts = new List()
+ {
+ new()
+ {
+ TaskId = taskId,
+ SigneeState = new SigneeState() { IsAccessDelegated = true },
+ Signee = _signee,
+ AdditionalActionsToDelegate = ["reject"],
+ },
+ };
+ var ct = new CancellationToken();
+
+ // Act
+ (signeeContexts, var success) = await service.RevokeSigneeRights(
+ taskId,
+ instanceId,
+ instanceOwnerPartyUuid,
+ appIdentifier,
+ signeeContexts,
+ ct
+ );
+
+ // Assert
+ Assert.True(success);
+ Assert.False(signeeContexts[0].SigneeState.IsAccessDelegated);
+ Assert.NotNull(capturedRequest);
+ Assert.Equal(3, capturedRequest!.Rights.Count);
+ Assert.Equal("read", capturedRequest.Rights[0].Action!.Value);
+ Assert.Equal("sign", capturedRequest.Rights[1].Action!.Value);
+ Assert.Equal("reject", capturedRequest.Rights[2].Action!.Value);
+ }
}
diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
index ec33a03324..9298d85727 100644
--- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
+++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
@@ -2020,6 +2020,8 @@ namespace Altinn.App.Core.Features.Signing
public abstract class ProvidedSignee
{
protected ProvidedSignee() { }
+ [System.Text.Json.Serialization.JsonPropertyName("additionalActionsToDelegate")]
+ public System.Collections.Generic.List? AdditionalActionsToDelegate { get; init; }
[System.Text.Json.Serialization.JsonPropertyName("communicationConfig")]
public Altinn.App.Core.Features.Signing.CommunicationConfig? CommunicationConfig { get; init; }
}