Skip to content

Commit

Permalink
feat: change format of party identifier (#376)
Browse files Browse the repository at this point in the history
Issue #220
  • Loading branch information
knuhau authored Jan 26, 2024
1 parent 8fae1b6 commit 27e6744
Show file tree
Hide file tree
Showing 17 changed files with 198 additions and 116 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Digdir.Domain.Dialogporten.Application.Externals.Presentation;
using System.Security.Claims;
using System.Diagnostics.CodeAnalysis;
using Digdir.Domain.Dialogporten.Application.Common.Numbers;
using System.Text.Json;
using Digdir.Domain.Dialogporten.Domain.Parties;

namespace Digdir.Domain.Dialogporten.Application.Common.Extensions;

Expand Down Expand Up @@ -47,7 +47,7 @@ public static bool TryGetOrgNumber(this Claim? consumerClaim, [NotNullWhen(true)

orgNumber = id.Split(IdDelimiter) switch
{
[IdPrefix, var on] => OrganizationNumber.IsValid(on) ? on : null,
[IdPrefix, var on] => NorwegianOrganizationIdentifier.IsValid(on) ? on : null,
_ => null
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Digdir.Domain.Dialogporten.Application.Common.Numbers;
using Digdir.Domain.Dialogporten.Domain.Parties;
using FluentValidation;

namespace Digdir.Domain.Dialogporten.Application.Common.Extensions.FluentValidation;

public static class FluentValidationPartyIdentifierExtensions
{
public static IRuleBuilderOptions<T, string> IsValidPartyIdentifier<T>(this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder
.Must(identifier => identifier is null || NorwegianPersonIdentifier.TryParse(identifier, out _) || NorwegianOrganizationIdentifier.TryParse(identifier, out _))
.WithMessage(
$"'{{PropertyName}}' must be on format '{NorwegianOrganizationIdentifier.Prefix}{{norwegian org-nr}}' or " +
$"'{NorwegianPersonIdentifier.Prefix}{{{{norwegian f-nr/d-nr}}}}' with valid numbers respectively.");
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables;
using Digdir.Domain.Dialogporten.Application.Common.Extensions.FluentValidation;
using Digdir.Domain.Dialogporten.Application.Common.Numbers;
using Digdir.Domain.Dialogporten.Application.Common.Pagination;
using Digdir.Domain.Dialogporten.Domain.Localizations;
using FluentValidation;
Expand All @@ -22,6 +24,9 @@ public SearchDialogQueryValidator()
.Must(x => !x.ServiceResource.IsNullOrEmpty() || !x.Party.IsNullOrEmpty())
.WithMessage($"Either {nameof(SearchDialogQuery.ServiceResource)} or {nameof(SearchDialogQuery.Party)} must be specified.");

RuleForEach(x => x.Party)
.IsValidPartyIdentifier();

RuleFor(x => x.Org!.Count)
.LessThanOrEqualTo(20)
.When(x => x.Org is not null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,7 @@ public CreateDialogCommandValidator(
.WithMessage($"'{{PropertyName}}' must start with '{Constants.ServiceResourcePrefix}'.");

RuleFor(x => x.Party)
.Must(x => x is null || x.Split('/') switch
{
["", "org", var orgNumber] => OrganizationNumber.IsValid(orgNumber),
["", "person", var socialSecurityNumber] => SocialSecurityNumber.IsValid(socialSecurityNumber),
_ => false
}).WithMessage(
"'{PropertyName}' must be on format '/org/[orgNumber]' or " +
"'/person/[socialSecurityNumber]' with valid numbers respectively.")
.IsValidPartyIdentifier()
.NotEmpty()
.MaximumLength(Constants.DefaultMaxStringLength);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables;
using Digdir.Domain.Dialogporten.Application.Common.Numbers;
using Digdir.Domain.Dialogporten.Application.Common.Extensions.FluentValidation;
using Digdir.Domain.Dialogporten.Application.Common.Pagination;
using Digdir.Domain.Dialogporten.Domain.Localizations;
using Digdir.Domain.Dialogporten.Domain.Parties;
using FluentValidation;

namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Search;
Expand All @@ -20,12 +21,15 @@ public SearchDialogQueryValidator()
.WithMessage("'{PropertyName}' must be a valid culture code.");

RuleFor(x => x)
.Must(x => EndUserIdentifier.IsValid(x.EndUserId!))
.WithMessage($"'{nameof(SearchDialogQuery.EndUserId)}' must be a valid end user identifier. It should match the format 'urn:altinn:person:identifier-no::{{norwegian f-nr/d-nr}} or 'urn:altinn:systemuser:{{uuid}}\"")
.Must(x => NorwegianPersonIdentifier.IsValid(x.EndUserId!) || SystemUserIdentifier.IsValid(x.EndUserId))
.WithMessage($"'{{PropertyName}}' must be a valid end user identifier. It should match the format '{NorwegianPersonIdentifier.Prefix}{{norwegian f-nr/d-nr}} or '{SystemUserIdentifier.Prefix}{{uuid}}\"")
.Must(x => !x.ServiceResource.IsNullOrEmpty() || !x.Party.IsNullOrEmpty())
.WithMessage($"Either '{nameof(SearchDialogQuery.ServiceResource)}' or '{nameof(SearchDialogQuery.Party)}' must be specified if '{nameof(SearchDialogQuery.EndUserId)}' is provided.")
.When(x => x.EndUserId is not null);

RuleForEach(x => x.Party)
.IsValidPartyIdentifier();

RuleFor(x => x.ServiceResource!.Count)
.LessThanOrEqualTo(20)
.When(x => x.ServiceResource is not null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;

namespace Digdir.Domain.Dialogporten.Application.Common.Numbers;
namespace Digdir.Domain.Dialogporten.Domain.Numbers;

internal static class Mod11
{
Expand Down
12 changes: 12 additions & 0 deletions src/Digdir.Domain.Dialogporten.Domain/Parties/IPartyIdentifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;

namespace Digdir.Domain.Dialogporten.Domain.Parties;

public interface IPartyIdentifier
{
static abstract string Prefix { get; }
string Value { get; }
static abstract bool IsValid(ReadOnlySpan<char> value);
static abstract bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier);
static abstract ReadOnlySpan<char> GetIdPart(ReadOnlySpan<char> value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Digdir.Domain.Dialogporten.Domain.Numbers;

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 { get; } = "urn:altinn:organization:identifier-no::";
public string Value { get; }

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

public static bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier)
{
if (!IsValid(value))
{
identifier = null;
return false;
}

identifier = new NorwegianOrganizationIdentifier(GetIdPart(value));
return true;
}

public static bool IsValid(ReadOnlySpan<char> value)
{
var idNumberWithoutPrefix = GetIdPart(value);
return idNumberWithoutPrefix.Length == 9
&& Mod11.TryCalculateControlDigit(idNumberWithoutPrefix[..8], OrgNumberWeights, out var control)
&& control == int.Parse(idNumberWithoutPrefix[8..9], CultureInfo.InvariantCulture);
}

public static ReadOnlySpan<char> GetIdPart(ReadOnlySpan<char> value)
{
return value.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)
? value[Prefix.Length..]
: value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Digdir.Domain.Dialogporten.Domain.Numbers;

namespace Digdir.Domain.Dialogporten.Domain.Parties;

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 { get; } = "urn:altinn:person:identifier-no::";
public string Value { get; }

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

public static bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier)
{
if (!IsValid(value))
{
identifier = null;
return false;
}

identifier = new NorwegianPersonIdentifier(GetIdPart(value));
return true;
}

public static bool IsValid(ReadOnlySpan<char> value)
{
var idNumberWithoutPrefix = GetIdPart(value);
return idNumberWithoutPrefix.Length == 11
&& Mod11.TryCalculateControlDigit(idNumberWithoutPrefix[..9], SocialSecurityNumberWeights1, out var control1)
&& Mod11.TryCalculateControlDigit(idNumberWithoutPrefix[..10], SocialSecurityNumberWeights2, out var control2)
&& control1 == int.Parse(idNumberWithoutPrefix[9..10], CultureInfo.InvariantCulture)
&& control2 == int.Parse(idNumberWithoutPrefix[10..11], CultureInfo.InvariantCulture);
}

public static ReadOnlySpan<char> GetIdPart(ReadOnlySpan<char> value)
{
return value.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)
? value[Prefix.Length..]
: value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Diagnostics.CodeAnalysis;

namespace Digdir.Domain.Dialogporten.Domain.Parties;

public record SystemUserIdentifier : IPartyIdentifier
{
public static string Prefix { get; } = "urn:altinn:systemuser::";
public string Value { get; }

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

public static bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPartyIdentifier? identifier)
{
if (!IsValid(value))
{
identifier = null;
return false;
}

identifier = new SystemUserIdentifier(GetIdPart(value));
return true;
}

public static bool IsValid(ReadOnlySpan<char> value)
{
var idNumberWithoutPrefix = GetIdPart(value);
return Guid.TryParse(idNumberWithoutPrefix, out _);
}

public static ReadOnlySpan<char> GetIdPart(ReadOnlySpan<char> value)
{
return value.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)
? value[Prefix.Length..]
: value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Digdir.Domain.Dialogporten.Application.Common.Authorization;
using Digdir.Domain.Dialogporten.Application.Common.Extensions;
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
using Digdir.Domain.Dialogporten.Domain.Parties;

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

Expand All @@ -12,8 +13,6 @@ internal static class DecisionRequestHelper
private const string AltinnUrnNsPrefix = "urn:altinn:";
private const string PidClaimType = "pid";
private const string ConsumerClaimType = "consumer";
private const string PartyPrefixOrg = "/org/";
private const string PartyPrefixPerson = "/person/";
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";
Expand Down Expand Up @@ -163,18 +162,19 @@ private static (string, string) SplitNsAndValue(string serviceResource)

private static XacmlJsonAttribute? ExtractPartyAttribute(string party)
{
// TODO: This can be removed once Altinn Auth has been updated to use the new party format.
var partyAttribute = new XacmlJsonAttribute();

if (party.StartsWith(PartyPrefixOrg, StringComparison.Ordinal))
if (party.StartsWith(NorwegianOrganizationIdentifier.Prefix, StringComparison.Ordinal))
{
partyAttribute.AttributeId = AttributeIdOrganizationNumber;
partyAttribute.Value = party[PartyPrefixOrg.Length..];
partyAttribute.Value = NorwegianOrganizationIdentifier.GetIdPart(party).ToString();

}
else if (party.StartsWith(PartyPrefixPerson, StringComparison.Ordinal))
else if (party.StartsWith(NorwegianPersonIdentifier.Prefix, StringComparison.Ordinal))
{
partyAttribute.AttributeId = AttributeIdSsn;
partyAttribute.Value = party[PartyPrefixPerson.Length..];
partyAttribute.Value = NorwegianPersonIdentifier.GetIdPart(party).ToString();
}
else
{
Expand Down Expand Up @@ -267,13 +267,13 @@ public static DialogSearchAuthorizationResult CreateDialogSearchResponse(
.FirstOrDefault(a => a.AttributeId == AttributeIdOrganizationNumber);
if (partyOrgNr != null)
{
party = PartyPrefixOrg + partyOrgNr.Value;
party = NorwegianOrganizationIdentifier.Prefix + partyOrgNr.Value;
}
else
{
var partySsn = xamlJsonRequestRoot.Request.Resource.First(r => r.Id == resourceId).Attribute
.First(a => a.AttributeId == AttributeIdSsn);
party = PartyPrefixPerson + partySsn.Value;
party = NorwegianPersonIdentifier.Prefix + partySsn.Value;
}

if (!response.PartiesByResources.TryGetValue(serviceResource, out var parties))
Expand Down
Loading

0 comments on commit 27e6744

Please sign in to comment.