Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ internal sealed class SigneeContext
[JsonPropertyName("CommunicationConfig")]
public CommunicationConfig? CommunicationConfig { get; init; }

/// <summary>
/// Additional actions to delegate to this signee beyond the default read and sign.
/// </summary>
[JsonPropertyName("additionalActionsToDelegate")]
public List<string>? AdditionalActionsToDelegate { get; init; }

/// <summary>
/// The state of the signee.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Altinn.App.Core/Features/Signing/ProvidedSignee.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ public abstract class ProvidedSignee
/// </summary>
[JsonPropertyName("communicationConfig")]
public CommunicationConfig? CommunicationConfig { get; init; }

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("additionalActionsToDelegate")]
public List<string>? AdditionalActionsToDelegate { get; init; }
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ CancellationToken ct
TaskId = taskId,
SigneeState = new SigneeContextState(),
CommunicationConfig = providedSignee.CommunicationConfig,
AdditionalActionsToDelegate = providedSignee.AdditionalActionsToDelegate,
Signee = signee,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -155,7 +155,11 @@ private static Guid ParseInstanceGuid(string instanceIdCombo)
}
}

private static List<RightRequest> CreateRights(AppIdentifier appIdentifier, string taskId)
private static List<RightRequest> CreateRights(
AppIdentifier appIdentifier,
string taskId,
List<string>? additionalActions
)
{
var resources = new List<Resource>
{
Expand All @@ -164,7 +168,7 @@ private static List<RightRequest> CreateRights(AppIdentifier appIdentifier, stri
new TaskResource { Value = taskId },
};

return
List<RightRequest> rights =
[
new RightRequest
{
Expand All @@ -177,5 +181,21 @@ private static List<RightRequest> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<IInstanceDataMutator>();
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<GetSigneesParameters>())).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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<IAccessManagementClient>();
accessManagementClient
.Setup(x => x.DelegateRights(It.IsAny<DelegationRequest>(), It.IsAny<CancellationToken>()))
.Callback<DelegationRequest, CancellationToken>((req, _) => capturedRequest = req)
.ReturnsAsync(new DelegationResponse());
var logger = new Mock<ILogger<SigningDelegationService>>();
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<SigneeContext>()
{
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<IAccessManagementClient>();
accessManagementClient
.Setup(x => x.DelegateRights(It.IsAny<DelegationRequest>(), It.IsAny<CancellationToken>()))
.Callback<DelegationRequest, CancellationToken>((req, _) => capturedRequest = req)
.ReturnsAsync(new DelegationResponse());
var logger = new Mock<ILogger<SigningDelegationService>>();
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<SigneeContext>()
{
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<IAccessManagementClient>();
accessManagementClient
.Setup(x => x.RevokeRights(It.IsAny<DelegationRequest>(), It.IsAny<CancellationToken>()))
.Callback<DelegationRequest, CancellationToken>((req, _) => capturedRequest = req)
.ReturnsAsync(new DelegationResponse());
var logger = new Mock<ILogger<SigningDelegationService>>();
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<SigneeContext>()
{
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>? AdditionalActionsToDelegate { get; init; }
[System.Text.Json.Serialization.JsonPropertyName("communicationConfig")]
public Altinn.App.Core.Features.Signing.CommunicationConfig? CommunicationConfig { get; init; }
}
Expand Down
Loading