Skip to content

Commit

Permalink
Feature/delegation client (#874)
Browse files Browse the repository at this point in the history
* add model for delegation request

* add step builder for delegation request

* add scaffold for delegation client

* add scaffold to signing delegation service

* temp solution for const instead of magic strings

* add delegation client

* weird state

* update handling of party id to use party uuid

* rm sign delegate rights from access management client
  • Loading branch information
HauklandJ authored Oct 31, 2024
1 parent 6786722 commit 0fbec33
Show file tree
Hide file tree
Showing 20 changed files with 430 additions and 37 deletions.
2 changes: 1 addition & 1 deletion src/Altinn.App.Core/Altinn.App.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<PackageReference Include="Altinn.Common.EFormidlingClient" Version="1.3.3" />
<PackageReference Include="Altinn.Common.PEP" Version="4.0.0" />
<PackageReference Include="Altinn.Platform.Models" Version="1.6.1" />
<PackageReference Include="Altinn.Platform.Storage.Interface" Version="3.30.0"/>
<PackageReference Include="Altinn.Platform.Storage.Interface" Version="3.30.0" />
<PackageReference Include="JsonPatch.Net" Version="3.1.1" />
<PackageReference Include="JWTCookieAuthentication" Version="3.0.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
Expand Down
7 changes: 6 additions & 1 deletion src/Altinn.App.Core/Configuration/PlatformSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class PlatformSettings
public string ApiEventsEndpoint { get; set; } = "http://localhost:5101/events/api/v1/";

/// <summary>
/// Gets or sets tue url for the new browser based PDF API endpoint.
/// Gets or sets the url for the new browser based PDF API endpoint.
/// </summary>
public string ApiPdf2Endpoint { get; set; } = "http://localhost:5300/pdf";

Expand All @@ -46,6 +46,11 @@ public class PlatformSettings
/// </summary>
public string ApiNotificationEndpoint { get; set; } = "http://localhost:5101/notifications/api/v1/";

/// <summary>
/// Gets or sets the url for the Access Management (Delegation) API endpoint.
/// </summary>
public string ApiAccessManagementEndpoint { get; set; } = "http://localhost:5101/accessmanagement/api/v1/";

/// <summary>
/// Gets or sets the subscription key value to use in requests against the platform.
/// A new subscription key is generated automatically every time an app is deployed to an environment. The new key is then automatically
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ public async Task<UserActionResult> HandleAction(UserActionContext context)
currentTask.Id
);

List<SigneeContext> signeeContexts = await _signingService.InitializeSignees(currentTask.Id);
signeeContexts = await _signingService.ProcessSignees(signeeContexts);
CancellationToken ct = new();

List<SigneeContext> signeeContexts = await _signingService.InitializeSignees(currentTask.Id, ct);
signeeContexts = await _signingService.ProcessSignees(currentTask.Id, context.Instance, signeeContexts, ct);

//TODO: Return failure result if something failed.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using Altinn.App.Core.Features.Signing.Models;
using Altinn.Platform.Storage.Interface.Models;

namespace Altinn.App.Core.Features.Signing.Interfaces;

internal interface ISigningDelegationService
{
internal Task<List<SigneeContext>> DelegateSigneeRights(
string taskId,
Instance instance,
List<SigneeContext> signeeContexts,
CancellationToken? ct = null
CancellationToken ct
);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
using Altinn.App.Core.Features.Signing.Models;
using Altinn.App.Core.Internal.Sign;
using Altinn.Platform.Storage.Interface.Models;

namespace Altinn.App.Core.Features.Signing.Interfaces;

internal interface ISigningService
{
Task<List<SigneeContext>> InitializeSignees(string taskId, CancellationToken? ct = null);
Task<List<SigneeContext>> InitializeSignees(string taskId, CancellationToken ct);

Task<List<SigneeContext>> ProcessSignees(List<SigneeContext> signeeContexts, CancellationToken? ct = null);
Task<List<SigneeContext>> ProcessSignees(
string taskId,
Instance instance,
List<SigneeContext> signeeContexts,
CancellationToken ct
);

List<Signee> ReadSignees();
List<SigneeContext> ReadSignees();
}
4 changes: 2 additions & 2 deletions src/Altinn.App.Core/Features/Signing/Models/SigneeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Altinn.App.Core.Features.Signing.Models;

internal sealed class SigneeContext
{
internal SigneeContext(string taskId, int partyId, SigneeParty signeeParty, SigneeState signeeState)
internal SigneeContext(string taskId, Guid partyId, SigneeParty signeeParty, SigneeState signeeState)
{
SigneeState = signeeState;
SigneeParty = signeeParty;
Expand All @@ -13,7 +13,7 @@ internal SigneeContext(string taskId, int partyId, SigneeParty signeeParty, Sign
}

/// <summary>The identifier of the signee.</summary>
internal int PartyId { get; }
internal Guid PartyId { get; }

/// <summary>The task associated with the signee state.</summary>
internal string TaskId { get; set; }
Expand Down
33 changes: 30 additions & 3 deletions src/Altinn.App.Core/Features/Signing/SigningDelegationService.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
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.Models;
using Altinn.App.Core.Internal.AccessManagement.Models.Shared;
using Altinn.Platform.Storage.Interface.Models;

namespace Altinn.App.Core.Features.Signing;

internal sealed class SigningDelegationService() : ISigningDelegationService
internal sealed class SigningDelegationService(IAccessManagementClient accessManagementClient)
: ISigningDelegationService
{
public async Task<List<SigneeContext>> DelegateSigneeRights(
string taskId,
Instance instance,
List<SigneeContext> signeeContexts,
CancellationToken? ct = null
CancellationToken ct
)
{
foreach (SigneeContext signeeContext in signeeContexts)
{
SigneeState state = signeeContext.SigneeState;
AppIdHelper appIdHelper = new();
try
{
if (state.IsAccessDelegated is false)
{
//TODO: delegateSignAction
// csharpier-ignore-start
string appResourceId = appIdHelper.ToResourceId(instance.AppId);
DelegationRequest delegation = DelegationRequestBuilder
.CreateBuilder(appResourceId, instance.Id)
.WithDelegator(new Delegator { IdType = DelegationConst.Party, Id = "" }) // TODO: assign delegator
.WithRecipient(new Delegatee { IdType = DelegationConst.Party, Id = signeeContext.PartyId.ToString() })
.AddRight()
.WithAction(DelegationConst.ActionId, ActionType.Read)
.AddResource(DelegationConst.Resource, appResourceId) // TODO: translate app id to altinn resource id
.AddResource(DelegationConst.Task, taskId)
.BuildRight()
.AddRight()
.WithAction(DelegationConst.ActionId, ActionType.Sign)
.AddResource(DelegationConst.Resource, appResourceId) // TODO: translate app id to altinn resource id
.AddResource(DelegationConst.Task, taskId)
.BuildRight()
.Build();
// csharpier-ignore-end
var response = await accessManagementClient.DelegateRights(delegation, ct);
state.IsAccessDelegated = await Task.FromResult(true);
}
}
Expand Down
43 changes: 19 additions & 24 deletions src/Altinn.App.Core/Features/Signing/SigningService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Altinn.App.Core.Internal.Registers;
using Altinn.App.Core.Internal.Sign;
using Altinn.Platform.Register.Models;
using Altinn.Platform.Storage.Interface.Models;

namespace Altinn.App.Core.Features.Signing;

Expand All @@ -18,7 +19,7 @@ internal sealed class SigningService(
Telemetry? telemetry
) : ISigningService
{
public async Task<List<SigneeContext>> InitializeSignees(string taskId, CancellationToken? ct = null)
public async Task<List<SigneeContext>> InitializeSignees(string taskId, CancellationToken ct)
{
using var activity = telemetry?.StartAssignSigneesActivity();

Expand All @@ -33,13 +34,15 @@ public async Task<List<SigneeContext>> InitializeSignees(string taskId, Cancella
}

public async Task<List<SigneeContext>> ProcessSignees(
string taskId,
Instance instance,
List<SigneeContext> signeeContexts,
CancellationToken? ct = null
CancellationToken ct
)
{
using var activity = telemetry?.StartAssignSigneesActivity();

await signingDelegationService.DelegateSigneeRights(signeeContexts, ct);
await signingDelegationService.DelegateSigneeRights(taskId, instance, signeeContexts, ct);

//TODO: If something fails inside DelegateSigneeRights, abort and don't send notifications. Set error state in SigneeState.

Expand All @@ -52,17 +55,13 @@ public async Task<List<SigneeContext>> ProcessSignees(
private async Task<List<SigneeContext>> GetPersonSigneeContexts(
string taskId,
SigneesResult signeeResult,
CancellationToken? ct = null
CancellationToken ct
)
{
List<SigneeContext> personSigneeContexts = [];
foreach (PersonSignee personSignee in signeeResult.PersonSignees)
{
Person? person = await personClient.GetPerson(
personSignee.SocialSecurityNumber,
personSignee.LastName,
ct ?? new CancellationToken()
);
Person? person = await personClient.GetPerson(personSignee.SocialSecurityNumber, personSignee.LastName, ct);

if (person is null)
{
Expand All @@ -76,20 +75,19 @@ private async Task<List<SigneeContext>> GetPersonSigneeContexts(
new PartyLookup { Ssn = personSignee.SocialSecurityNumber }
);

if (party is null)
{
throw new SignaturePartyNotValidException(
$"No partyId found for signature party with social security number {personSignee.SocialSecurityNumber}."
Guid partyUuid =
party.PartyUuid
?? throw new SignaturePartyNotValidException(
$"No partyUuid found for signature party with social security number {personSignee.SocialSecurityNumber}." // TODO: ikke gi ut ssn her?!
);
}

Sms? smsNotification = personSignee.Notifications?.OnSignatureTaskReceived?.Sms;
if (smsNotification is not null && smsNotification.MobileNumber is null)
{
smsNotification.MobileNumber = person.MobileNumber;
}

personSigneeContexts.Add(new SigneeContext(taskId, party.PartyId, personSignee, new SigneeState()));
personSigneeContexts.Add(new SigneeContext(taskId, partyUuid, personSignee, new SigneeState()));
}

return personSigneeContexts;
Expand Down Expand Up @@ -120,12 +118,11 @@ private async Task<List<SigneeContext>> GetOrganisationSigneeContexts(
new PartyLookup { OrgNo = organisationSignee.OrganisationNumber }
);

if (party is null)
{
throw new SignaturePartyNotValidException(
Guid partyUuid =
party.PartyUuid
?? throw new SignaturePartyNotValidException(
$"No partyId found for signature party with organisation number {organisationSignee.OrganisationNumber}."
);
}

//TODO: Is this the correct place to set email to registry fallback? Maybe move it to notification service?
Email? emailNotification = organisationSignee.Notifications?.OnSignatureTaskReceived?.Email;
Expand All @@ -140,15 +137,13 @@ private async Task<List<SigneeContext>> GetOrganisationSigneeContexts(
smsNotification.MobileNumber = organisation.MobileNumber;
}

organisationSigneeContexts.Add(
new SigneeContext(taskId, party.PartyId, organisationSignee, new SigneeState())
);
organisationSigneeContexts.Add(new SigneeContext(taskId, partyUuid, organisationSignee, new SigneeState()));
}

return organisationSigneeContexts;
}

public List<Signee> ReadSignees()
public List<SigneeContext> ReadSignees()
{
using var activity = telemetry?.StartReadSigneesActivity();
// TODO: Get signees from state
Expand All @@ -159,7 +154,7 @@ public List<Signee> ReadSignees()
}

//TODO: There is already logic for the sign action in the SigningUserAction class. Maybe move most of it here?
internal async Task Sign(Signee signee)
internal async Task Sign(SigneeContext signee)
{
using var activity = telemetry?.StartSignActivity();
// var state = StorageClient.GetSignState(...);
Expand Down
9 changes: 9 additions & 0 deletions src/Altinn.App.Core/Helpers/AppIdHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Altinn.App.Core.Helpers;

internal sealed class AppIdHelper
{
internal string ToResourceId(string appId)
{
return ""; //TODO
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Net.Http.Headers;
using System.Text.Json;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Features;
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 Altinn.Platform.Storage.Interface.Models;
using Microsoft.Extensions.Logging;

namespace Altinn.App.Core.Internal.AccessManagement;

internal interface IAccessManagementClient
{
public Task<DelegationResponse> DelegateRights(DelegationRequest delegation, CancellationToken ct);
}

internal sealed class AccessManagementClient(
ILogger<AccessManagementClient> logger,
HttpClient httpClient,
IAppMetadata appMetadata,
IAccessTokenGenerator accessTokenGenerator,
PlatformSettings platformSettings,
Telemetry? telemetry = null
) : IAccessManagementClient
{
internal void DelegationCheck() { }

public async Task<DelegationResponse> DelegateRights(DelegationRequest delegation, CancellationToken ct)
{
// TODO: telemetry
HttpResponseMessage? httpResponseMessage = null;
string? httpContent = null;
UrlHelper urlHelper = new(platformSettings);
try
{
var application = await appMetadata.GetApplicationMetadata();

var uri = urlHelper.CreateInstanceDelegationUrl(delegation.ResourceId, delegation.InstanceId);
var body = JsonSerializer.Serialize(delegation);

using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new StringContent(body, new MediaTypeHeaderValue("application/json")),
};
httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequestMessage.Headers.Add(
"PlatformAccessToken",
accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App)
);

httpResponseMessage = await httpClient.SendAsync(httpRequestMessage, ct);
httpContent = await httpResponseMessage.Content.ReadAsStringAsync(ct);
DelegationResponse? response;
if (httpResponseMessage.IsSuccessStatusCode)
{
response = JsonSerializer.Deserialize<DelegationResponse>(httpContent);
if (response is null)
throw new JsonException("Couldn't deserialize access management response.");
}
else
{
throw new HttpRequestException("Got error status code for access management request.");
}
return response;
}
catch (Exception e)
{
var ex = new DelegationException(
$"Something went wrong when processing the access management request.",
httpResponseMessage,
httpContent,
e
);
logger.LogError(ex, "Error when processing access management request.");

// TODO: metrics

throw ex;
}
finally
{
httpResponseMessage?.Dispose();
}
}
}
Loading

0 comments on commit 0fbec33

Please sign in to comment.