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

fix: Update Altinn Authorization integration (#457) #469

Merged
merged 7 commits into from
Mar 12, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public interface IPartyIdentifier
string FullId { get; }
string Id { get; }
static abstract string Prefix { get; }
static abstract string PrefixWithSeparator { get; }
static abstract bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ public static class PartyIdentifier
{
private delegate bool TryParseDelegate(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier);
private static readonly Dictionary<string, TryParseDelegate> TryParseByPrefix = CreateTryParseByPrefix();
private const string Separator = "::";
public const string Separator = "::";

public static string Prefix(this IPartyIdentifier identifier)
=> identifier.FullId[..(identifier.FullId.IndexOf(identifier.Id, StringComparison.Ordinal) - Separator.Length)];

public static bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier)
{
Expand Down Expand Up @@ -41,10 +44,12 @@ private static Dictionary<string, TryParseDelegate> CreateTryParseByPrefix()
(
Type: partyIdentifierType,
Prefix: (string)partyIdentifierType
.GetProperty(nameof(IPartyIdentifier.Prefix), BindingFlags.Static | BindingFlags.Public)!
.GetProperty(nameof(IPartyIdentifier.PrefixWithSeparator),
BindingFlags.Static | BindingFlags.Public)!
.GetValue(null)!,
TryParse: partyIdentifierType
.GetMethod(nameof(IPartyIdentifier.TryParse), [typeof(ReadOnlySpan<char>), typeof(IPartyIdentifier).MakeByRefType()
.GetMethod(nameof(IPartyIdentifier.TryParse), [
typeof(ReadOnlySpan<char>), typeof(IPartyIdentifier).MakeByRefType()
])!
.CreateDelegate<TryParseDelegate>()
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ namespace Digdir.Domain.Dialogporten.Domain.Parties;
public record NorwegianOrganizationIdentifier : IPartyIdentifier
{
private static readonly int[] OrgNumberWeights = [3, 2, 7, 6, 5, 4, 3, 2];
public static string Prefix => "urn:altinn:organization:identifier-no::";
public static string Prefix => "urn:altinn:organization:identifier-no";
public static string PrefixWithSeparator => Prefix + PartyIdentifier.Separator;
public string FullId { get; }
public string Id { get; }

private NorwegianOrganizationIdentifier(ReadOnlySpan<char> value)
{
Id = value.ToString();
FullId = Prefix + Id;
FullId = PrefixWithSeparator + Id;
}

public static bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ public class NorwegianPersonIdentifier : IPartyIdentifier
private static readonly int[] SocialSecurityNumberWeights1 = [3, 7, 6, 1, 8, 9, 4, 5, 2, 1];
private static readonly int[] SocialSecurityNumberWeights2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1];

public static string Prefix => "urn:altinn:person:identifier-no::";
public static string Prefix => "urn:altinn:person:identifier-no";
public static string PrefixWithSeparator => Prefix + PartyIdentifier.Separator;
public string FullId { get; }
public string Id { get; }

private NorwegianPersonIdentifier(ReadOnlySpan<char> value)
{
Id = value.ToString();
FullId = Prefix + Id;
FullId = PrefixWithSeparator + Id;
}

public static bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ namespace Digdir.Domain.Dialogporten.Domain.Parties;

public record SystemUserIdentifier : IPartyIdentifier
{
public static string Prefix => "urn:altinn:systemuser::";
public static string Prefix => "urn:altinn:systemuser";
public static string PrefixWithSeparator => Prefix + PartyIdentifier.Separator;
public string FullId { get; }
public string Id { get; }

private SystemUserIdentifier(ReadOnlySpan<char> value)
{
Id = value.ToString();
FullId = Prefix + Id;
FullId = PrefixWithSeparator + Id;
}

public static bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
using Digdir.Domain.Dialogporten.Application.Externals.Presentation;
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;
using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;

internal sealed class AltinnAuthorizationClient : IAltinnAuthorization
{
private const string AttributePidClaim = "urn:altinn:ssn";

private readonly HttpClient _httpClient;
private readonly IUser _user;
private readonly IDialogDbContext _db;
Expand Down Expand Up @@ -81,6 +80,7 @@ private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSear
.Where(dialog => request.ConstraintServiceResources.Contains(dialog.ServiceResource))
.Select(dialog => dialog.Party)
.Distinct()
.Take(20) // Limit to 20 parties to limit request size
.ToListAsync(cancellationToken: cancellationToken);
}

Expand All @@ -89,7 +89,9 @@ private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSear
request.ConstraintServiceResources = await _db.Dialogs
.Where(dialog => request.ConstraintParties.Contains(dialog.Party))
.Select(x => x.ServiceResource)
.Distinct().ToListAsync(cancellationToken: cancellationToken);
.Distinct()
.Take(20) // Limit to 20 resources to limit request size
.ToListAsync(cancellationToken: cancellationToken);
}

var xacmlJsonRequest = DecisionRequestHelper.NonScalable.CreateDialogSearchRequest(request);
Expand All @@ -107,20 +109,18 @@ private async Task<DialogDetailsAuthorizationResult> PerformDialogDetailsAuthori
private List<Claim> GetOrCreateClaimsBasedOnEndUserId(string? endUserId)
{
List<Claim> claims = [];
if (endUserId is not null)
if (endUserId is not null && PartyIdentifier.TryParse(endUserId, out var partyIdentifier))
{
claims.Add(new Claim(AttributePidClaim, ExtractEndUserIdNumber(endUserId)!));
claims.Add(new Claim(partyIdentifier.Prefix(), partyIdentifier.Id));
}
else
{
claims.AddRange(_user.GetPrincipal().Claims);
}

return claims;
}

private static string ExtractEndUserIdNumber(string endUserId) =>
endUserId.Split("::").LastOrDefault() ?? string.Empty;

private static readonly JsonSerializerOptions _serializerOptions = new()
{
PropertyNameCaseInsensitive = true,
Expand All @@ -129,7 +129,7 @@ private static string ExtractEndUserIdNumber(string endUserId) =>

private async Task<XacmlJsonResponse?> SendRequest(XacmlJsonRequestRoot xacmlJsonRequest, CancellationToken cancellationToken)
{
const string apiUrl = "authorization/api/v1/Decision";
const string apiUrl = "authorization/api/v1/authorize";
var requestJson = JsonSerializer.Serialize(xacmlJsonRequest, _serializerOptions);
_logger.LogDebug("Generated XACML request: {RequestJson}", requestJson);
var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ internal static class DecisionRequestHelper
private const string AltinnUrnNsPrefix = "urn:altinn:";
private const string PidClaimType = "pid";
private const string ConsumerClaimType = "consumer";
private const string AttributeIdSsn = "urn:altinn:ssn";
private const string AttributeIdOrganizationNumber = "urn:altinn:organizationnumber";
private const string AttributeIdAction = "urn:oasis:names:tc:xacml:1.0:action:action-id";
private const string AttributeIdResource = "urn:altinn:resource";
private const string AttributeIdResourceInstance = "urn:altinn:resourceinstance";
Expand Down Expand Up @@ -56,18 +54,20 @@ private static List<XacmlJsonCategory> CreateAccessSubjectCategory(IEnumerable<C
var attributes = claims
.Select(x => x switch
{
{ Type: PidClaimType } => new XacmlJsonAttribute { AttributeId = AttributeIdSsn, Value = x.Value },
{ Type: PidClaimType } => new XacmlJsonAttribute { AttributeId = NorwegianPersonIdentifier.Prefix, Value = x.Value },
{ Type: var type } when type.StartsWith(AltinnUrnNsPrefix, StringComparison.Ordinal) => new() { AttributeId = type, Value = x.Value },
{ Type: ConsumerClaimType } when x.TryGetOrgNumber(out var organizationNumber) => new() { AttributeId = AttributeIdOrganizationNumber, Value = organizationNumber },
{ Type: ConsumerClaimType } when x.TryGetOrgNumber(out var organizationNumber) => new() { AttributeId = NorwegianOrganizationIdentifier.Prefix, Value = organizationNumber },
_ => null
})
.Where(x => x is not null)
.Cast<XacmlJsonAttribute>()
.ToList();

if (attributes.Any(x => x.AttributeId == AttributeIdSsn))
// If we're authorizing a person (ie. ID-porten token), we are not interested in the consumer-claim (organization number)
// as that is not relevant for the authorization decision (it's just the organization owning the OAuth client).
if (attributes.Any(x => x.AttributeId == NorwegianPersonIdentifier.Prefix))
{
attributes.RemoveAll(x => x.AttributeId == AttributeIdOrganizationNumber);
attributes.RemoveAll(x => x.AttributeId == NorwegianOrganizationIdentifier.Prefix);
}

return [new() { Id = SubjectId, Attribute = attributes }];
Expand Down Expand Up @@ -108,7 +108,7 @@ private static List<XacmlJsonCategory> CreateResourceCategories(
x => x.id);


var partyAttribute = ExtractPartyAttribute(party);
var partyAttribute = GetPartyAttribute(party);
return resourceIdByName
.Select(x =>
CreateResourceCategory(x.Value, serviceResource, dialogId, partyAttribute, x.Key))
Expand Down Expand Up @@ -161,16 +161,18 @@ private static (string, string) SplitNsAndValue(string serviceResource)
return (ns, value);
}

private static XacmlJsonAttribute? ExtractPartyAttribute(string party)
private static XacmlJsonAttribute? GetPartyAttribute(string party)
{
// TODO: This can be removed once Altinn Auth has been updated to use the new party format.
var _ = PartyIdentifier.TryParse(party, out var partyIdentifier);
return partyIdentifier switch
if (PartyIdentifier.TryParse(party, out var partyIdentifier))
{
NorwegianOrganizationIdentifier => new XacmlJsonAttribute { AttributeId = AttributeIdOrganizationNumber, Value = partyIdentifier.Id },
NorwegianPersonIdentifier => new() { AttributeId = AttributeIdSsn, Value = partyIdentifier.Id },
_ => null
};
return new XacmlJsonAttribute
{
AttributeId = partyIdentifier.Prefix(),
Value = partyIdentifier.Id
};
}

return null;
}

private static XacmlJsonMultiRequests CreateMultiRequests(
Expand Down Expand Up @@ -241,7 +243,7 @@ public static DialogSearchAuthorizationResult CreateDialogSearchResponse(

for (var i = 0; i < xamlJsonRequestRoot.Request.MultiRequests.RequestReference.Count; i++)
{
if (xamlJsonResponse.Response[i].Decision != PermitResponse)
if (i >= xamlJsonResponse.Response.Count || xamlJsonResponse.Response[i].Decision != PermitResponse)
{
continue;
}
Expand All @@ -253,16 +255,16 @@ public static DialogSearchAuthorizationResult CreateDialogSearchResponse(

string party;
var partyOrgNr = xamlJsonRequestRoot.Request.Resource.First(r => r.Id == resourceId).Attribute
.FirstOrDefault(a => a.AttributeId == AttributeIdOrganizationNumber);
.FirstOrDefault(a => a.AttributeId == NorwegianOrganizationIdentifier.Prefix);
if (partyOrgNr != null)
{
party = NorwegianOrganizationIdentifier.Prefix + partyOrgNr.Value;
party = NorwegianOrganizationIdentifier.PrefixWithSeparator + partyOrgNr.Value;
}
else
{
var partySsn = xamlJsonRequestRoot.Request.Resource.First(r => r.Id == resourceId).Attribute
.First(a => a.AttributeId == AttributeIdSsn);
party = NorwegianPersonIdentifier.Prefix + partySsn.Value;
.First(a => a.AttributeId == NorwegianPersonIdentifier.Prefix);
party = NorwegianPersonIdentifier.PrefixWithSeparator + partySsn.Value;
}

if (!response.PartiesByResources.TryGetValue(serviceResource, out var parties))
Expand All @@ -286,7 +288,7 @@ private static List<XacmlJsonCategory> CreateResourceCategoriesForSearch(
var resourceCounter = 0;
foreach (var party in parties)
{
var partyAttribute = ExtractPartyAttribute(party);
var partyAttribute = GetPartyAttribute(party);

foreach (var serviceResource in serviceResources)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<ItemGroup>
<PackageReference Include="Altinn.ApiClients.Maskinporten" Version="9.1.0" />
<PackageReference Include="Altinn.Authorization.ABAC" Version="0.0.7" />
<PackageReference Include="Altinn.Authorization.ABAC" Version="0.0.8" />
<PackageReference Include="Bogus" Version="35.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,14 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
})
.AddPolicyHandlerFromRegistry(PollyPolicy.DefaultHttpRetryPolicy);

services.AddHttpClient<IAltinnAuthorization, AltinnAuthorizationClient>((services, client) =>
services.AddMaskinportenHttpClient<IAltinnAuthorization, AltinnAuthorizationClient, SettingsJwkClientDefinition>(
infrastructureConfigurationSection,
x => x.ClientSettings.ExhangeToAltinnToken = true)
.ConfigureHttpClient((services, client) =>
{
var altinnSettings = services.GetRequiredService<IOptions<InfrastructureSettings>>().Value.Altinn;
client.BaseAddress = altinnSettings.BaseUri;
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", altinnSettings.SubscriptionKey);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
})
// TODO! Add cache policy based on request body
.AddPolicyHandlerFromRegistry(PollyPolicy.DefaultHttpRetryPolicy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// 2. Client Id/integration as configured in Maskinporten
"ClientId": "TODO: Add to local secrets",
// 3. Scope(s) requested, space seperated. Must be provisioned on supplied client id.
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin",
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin altinn:authorization:pdp",
// --------------------------
// Any additional settings are specific for the selected client definition type.
// See below for examples using other types.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"ClientId": "TODO: Add to local secrets",

// 3. Scope(s) requested, space seperated. Must be provisioned on supplied client id.
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin",
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin altinn:authorization:pdp",

// --------------------------
// Any additional settings are specific for the selected client definition type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"ClientId": "TODO: Add to local secrets",

// 3. Scope(s) requested, space seperated. Must be provisioned on supplied client id.
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin",
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin altinn:authorization:pdp",

// --------------------------
// Any additional settings are specific for the selected client definition type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"ClientId": "TODO: Add to local secrets",

// 3. Scope(s) requested, space seperated. Must be provisioned on supplied client id.
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin",
"Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin altinn:authorization:pdp",

// --------------------------
// Any additional settings are specific for the selected client definition type.
Expand Down
Loading