Skip to content

Commit

Permalink
feat: Add user types (#768)
Browse files Browse the repository at this point in the history
## Description
This implements a new abstract lookup enum for user types in order to
support legacy users/systemUsers
Middleware is added for validating the IUser to be of this new enum
Name lookup is refactored to support these different types
Migration created for the abstract lookup type

## Related Issue(s)
- #652 

## Verification

- [x] **Your** code builds clean without any errors or warnings
- [ ] Manual testing done (required)
- [ ] Relevant automated test added (if you find this hard, leave it and
we'll help out)

## Documentation

- [ ] Documentation is updated (either in `docs`-directory, Altinnpedia
or a separate linked PR in
[altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if
applicable)
  • Loading branch information
oskogstad authored Jun 5, 2024
1 parent 6490625 commit b6fd439
Show file tree
Hide file tree
Showing 31 changed files with 1,994 additions and 191 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
.AddTransient<IStringHasher, PersistentRandomSaltStringHasher>()
.AddTransient<IUserOrganizationRegistry, UserOrganizationRegistry>()
.AddTransient<IUserResourceRegistry, UserResourceRegistry>()
.AddTransient<IUserNameRegistry, UserNameRegistry>()
.AddTransient<IUserRegistry, UserRegistry>()
.AddTransient<IUserParties, UserParties>()
.AddTransient<IDialogActivityService, DialogActivityService>()
.AddTransient<IClock, Clock>()
Expand All @@ -66,7 +66,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
localDeveloperSettings.UseLocalDevelopmentUser ||
localDeveloperSettings.UseLocalDevelopmentOrganizationRegister);

services.Decorate<IUserNameRegistry, LocalDevelopmentUserNameRegistryDecorator>(
services.Decorate<IUserRegistry, LocalDevelopmentUserRegistryDecorator>(
predicate:
localDeveloperSettings.UseLocalDevelopmentUser ||
localDeveloperSettings.UseLocalDevelopmentNameRegister);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
using System.Security.Claims;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Digdir.Domain.Dialogporten.Domain.Parties;
using UserIdType = Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogUserType.Values;

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

Expand All @@ -16,28 +19,40 @@ public static class ClaimsPrincipalExtensions
private const char IdDelimiter = ':';
private const string IdPrefix = "0192";
private const string AltinnClaimPrefix = "urn:altinn:";
private const string OrgClaim = "urn:altinn:org";
private const string IdportenAuthLevelClaim = "acr";
private const string AltinnAutorizationDetailsClaim = "authorization_details";
private const string AttributeIdSystemUser = "urn:altinn:systemuser";
private const string AltinnAuthLevelClaim = "urn:altinn:authlevel";
private const string AltinnAuthenticationMethodClaim = "urn:altinn:authenticatemethod";
private const string AltinnAuthenticationEnterpriseUserMethod = "virksomhetsbruker";
private const string AltinnUserIdClaim = "urn:altinn:userid";
private const string AltinnUserNameClaim = "urn:altinn:username";
private const string ScopeClaim = "scope";
private const char ScopeClaimSeparator = ' ';
private const string PidClaim = "pid";

public static bool TryGetClaimValue(this ClaimsPrincipal claimsPrincipal, string claimType, [NotNullWhen(true)] out string? value)

// TODO: This scope is also defined in WebAPI/GQL. Can this be fetched from a common auth lib?
// https://github.com/digdir/dialogporten/issues/647
// This could be done for all claims/scopes/prefixes etc, there are duplicates
public const string ServiceProviderScope = "digdir:dialogporten.serviceprovider";

public static bool TryGetClaimValue(this ClaimsPrincipal claimsPrincipal, string claimType,
[NotNullWhen(true)] out string? value)
{
value = claimsPrincipal.FindFirst(claimType)?.Value;
return value is not null;
}

public static bool TryGetOrganizationNumber(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? orgNumber)
=> claimsPrincipal.FindFirst(ConsumerClaim).TryGetOrganizationNumber(out orgNumber);

public static bool HasScope(this ClaimsPrincipal claimsPrincipal, string scope) =>
claimsPrincipal.TryGetClaimValue(ScopeClaim, out var scopes) &&
scopes.Split(ScopeClaimSeparator).Contains(scope);

public static bool TryGetOrgNumber(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? orgNumber)
=> claimsPrincipal.FindFirst(ConsumerClaim).TryGetOrgNumber(out orgNumber);

public static bool TryGetSupplierOrgNumber(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? orgNumber)
=> claimsPrincipal.FindFirst(SupplierClaim).TryGetOrgNumber(out orgNumber);
=> claimsPrincipal.FindFirst(SupplierClaim).TryGetOrganizationNumber(out orgNumber);

public static bool TryGetPid(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? pid)
=> claimsPrincipal.FindFirst(PidClaim).TryGetPid(out pid);
Expand All @@ -58,7 +73,101 @@ public static bool TryGetPid(this Claim? pidClaim, [NotNullWhen(true)] out strin
return pid is not null;
}

public static bool TryGetOrgNumber(this Claim? consumerClaim, [NotNullWhen(true)] out string? orgNumber)
// https://docs.altinn.studio/authentication/systemauthentication/
private class SystemUserAuthorizationDetails
{
[JsonPropertyName("type")]
public string? Type { get; set; }

[JsonPropertyName("systemuser_id")]
public string[]? SystemUserIds { get; set; }
}

private static bool TryGetAuthorizationDetailsClaimValue(this ClaimsPrincipal claimsPrincipal,
[NotNullWhen(true)] out SystemUserAuthorizationDetails[]? authorizationDetails)
{
authorizationDetails = null;

if (!claimsPrincipal.TryGetClaimValue(AltinnAutorizationDetailsClaim, out var authDetailsJson))
{
return false;
}

var authDetailsJsonNode = JsonNode.Parse(authDetailsJson);
if (authDetailsJsonNode is null)
{
return false;
}

// If a claim is an array, but contains only one element, it will be deserialized as a single object by dotnet
if (authDetailsJsonNode.GetValueKind() is JsonValueKind.Array)
{
authorizationDetails = JsonSerializer.Deserialize<SystemUserAuthorizationDetails[]>(authDetailsJson);
}
else
{
var systemUserAuthorizationDetails = JsonSerializer.Deserialize<SystemUserAuthorizationDetails>(authDetailsJson);
authorizationDetails = [systemUserAuthorizationDetails!];
}

return authorizationDetails is not null;
}

public static bool TryGetSystemUserId(this ClaimsPrincipal claimsPrincipal,
[NotNullWhen(true)] out string? systemUserId)
{
systemUserId = null;

if (!claimsPrincipal.TryGetAuthorizationDetailsClaimValue(out var authorizationDetails))
{
return false;
}

if (authorizationDetails.Length == 0)
{
return false;
}

var systemUserDetails = authorizationDetails.FirstOrDefault(x => x.Type == AttributeIdSystemUser);

if (systemUserDetails?.SystemUserIds is null)
{
return false;
}

systemUserId = systemUserDetails.SystemUserIds.FirstOrDefault();

return systemUserId is not null;
}

// This is used for legacy systems using Altinn 2 enterprise users with Maskinporten authentication + token exchange
// as described in https://altinn.github.io/docs/api/rest/kom-i-gang/virksomhet/#autentisering-med-virksomhetsbruker-og-maskinporten
public static bool TryGetLegacySystemUserId(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? systemUserId)
{
systemUserId = null;
if (claimsPrincipal.TryGetClaimValue(AltinnAuthenticationMethodClaim, out var authMethod) &&
authMethod == AltinnAuthenticationEnterpriseUserMethod &&
claimsPrincipal.TryGetClaimValue(AltinnUserIdClaim, out var userId))
{
systemUserId = userId;
}

return systemUserId is not null;
}

public static bool TryGetLegacySystemUserName(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? systemUserName)
{
systemUserName = null;
if (claimsPrincipal.TryGetLegacySystemUserId(out _) &&
claimsPrincipal.TryGetClaimValue(AltinnUserNameClaim, out var claimValue))
{
systemUserName = claimValue;
}

return systemUserName is not null;
}

public static bool TryGetOrganizationNumber(this Claim? consumerClaim, [NotNullWhen(true)] out string? orgNumber)
{
orgNumber = null;
if (consumerClaim is null || consumerClaim.Type != ConsumerClaim)
Expand Down Expand Up @@ -115,27 +224,51 @@ public static IEnumerable<Claim> GetIdentifyingClaims(this List<Claim> claims) =
c.Type == PidClaim ||
c.Type == ConsumerClaim ||
c.Type == SupplierClaim ||
c.Type == OrgClaim ||
c.Type == IdportenAuthLevelClaim ||
c.Type.StartsWith(AltinnClaimPrefix, StringComparison.Ordinal)
).OrderBy(c => c.Type);

private static bool TryGetOrgShortName(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? orgShortName)
=> claimsPrincipal.FindFirst(OrgClaim).TryGetOrgShortName(out orgShortName);

private static bool TryGetOrgShortName(this Claim? orgClaim, [NotNullWhen(true)] out string? orgShortName)
public static (UserIdType, string externalId) GetUserType(this ClaimsPrincipal claimsPrincipal)
{
orgShortName = orgClaim?.Value;
if (claimsPrincipal.TryGetPid(out var externalId))
{
// ServiceOwnerOnHelfOfPerson does not work atm., since there will be no PID claim on service owner calls
// TODO: This needs to be fixed when implementing https://github.com/digdir/dialogporten/issues/386
// F.ex. a middleware that runs before UserTypeValidationMiddleware that adds the PID claim
return (claimsPrincipal.HasScope(ServiceProviderScope)
? UserIdType.ServiceOwnerOnBehalfOfPerson
: UserIdType.Person, externalId);
}

return orgShortName is not null;
}
if (claimsPrincipal.TryGetLegacySystemUserId(out externalId))
{
return (UserIdType.LegacySystemUser, externalId);
}

internal static bool TryGetOrgNumber(this IUser user, [NotNullWhen(true)] out string? orgNumber) =>
user.GetPrincipal().TryGetOrgNumber(out orgNumber);
// https://docs.altinn.studio/authentication/systemauthentication/
if (claimsPrincipal.TryGetSystemUserId(out externalId))
{
return (UserIdType.SystemUser, externalId);
}

if (claimsPrincipal.HasScope(ServiceProviderScope) &&
claimsPrincipal.TryGetOrganizationNumber(out externalId))
{
return (UserIdType.ServiceOwner, externalId);
}

internal static bool TryGetOrgShortName(this IUser user, [NotNullWhen(true)] out string? orgShortName) =>
user.GetPrincipal().TryGetOrgShortName(out orgShortName);
return (UserIdType.Unknown, string.Empty);
}

internal static bool TryGetOrganizationNumber(this IUser user, [NotNullWhen(true)] out string? orgNumber) =>
user.GetPrincipal().TryGetOrganizationNumber(out orgNumber);

internal static bool TryGetPid(this IUser user, [NotNullWhen(true)] out string? pid) =>
user.GetPrincipal().TryGetPid(out pid);

internal static bool TryGetLegacySystemUserId(this IUser user, [NotNullWhen(true)] out string? systemUserId) =>
user.GetPrincipal().TryGetLegacySystemUserId(out systemUserId);

internal static bool TryGetLegacySystemUserName(this IUser user, [NotNullWhen(true)] out string? systemUserName) =>
user.GetPrincipal().TryGetLegacySystemUserName(out systemUserName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private string GetAuthenticatedParty()
return NorwegianPersonIdentifier.PrefixWithSeparator + pid;
}

if (_user.TryGetOrgNumber(out var orgNumber))
if (_user.TryGetOrganizationNumber(out var orgNumber))
{
return NorwegianOrganizationIdentifier.PrefixWithSeparator + orgNumber;
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,40 @@ namespace Digdir.Domain.Dialogporten.Application.Common;
public interface IUserOrganizationRegistry
{
Task<string?> GetCurrentUserOrgShortName(CancellationToken cancellationToken);
Task<IList<OrganizationLongName>?> GetCurrentUserOrgLongNames(CancellationToken cancellationToken);
Task<IList<ServiceOwnerLongName>?> GetCurrentUserOrgLongNames(CancellationToken cancellationToken);
}

public class UserOrganizationRegistry : IUserOrganizationRegistry
{
private readonly IUser _user;
private readonly IOrganizationRegistry _organizationRegistry;
private readonly IServiceOwnerNameRegistry _serviceOwnerNameRegistry;

public UserOrganizationRegistry(IUser user, IOrganizationRegistry organizationRegistry)
public UserOrganizationRegistry(IUser user, IServiceOwnerNameRegistry serviceOwnerNameRegistry)
{
_user = user ?? throw new ArgumentNullException(nameof(user));
_organizationRegistry = organizationRegistry ?? throw new ArgumentNullException(nameof(organizationRegistry));
_serviceOwnerNameRegistry = serviceOwnerNameRegistry ?? throw new ArgumentNullException(nameof(serviceOwnerNameRegistry));
}

public async Task<string?> GetCurrentUserOrgShortName(CancellationToken cancellationToken)
{
if (_user.TryGetOrgShortName(out var orgShortName))
{
return orgShortName;
}

if (!_user.TryGetOrgNumber(out var orgNumber))
if (!_user.TryGetOrganizationNumber(out var orgNumber))
{
return null;
}

var orgInfo = await _organizationRegistry.GetOrgInfo(orgNumber, cancellationToken);
var orgInfo = await _serviceOwnerNameRegistry.GetServiceOwnerInfo(orgNumber, cancellationToken);

return orgInfo?.ShortName;
}

public async Task<IList<OrganizationLongName>?> GetCurrentUserOrgLongNames(CancellationToken cancellationToken)
public async Task<IList<ServiceOwnerLongName>?> GetCurrentUserOrgLongNames(CancellationToken cancellationToken)
{
if (!_user.TryGetOrgNumber(out var orgNumber))
if (!_user.TryGetOrganizationNumber(out var orgNumber))
{
return null;
}

var orgInfo = await _organizationRegistry.GetOrgInfo(orgNumber, cancellationToken);
var orgInfo = await _serviceOwnerNameRegistry.GetServiceOwnerInfo(orgNumber, cancellationToken);

return orgInfo?.LongNames.ToArray();
}
Expand All @@ -56,6 +51,6 @@ internal sealed class LocalDevelopmentUserOrganizationRegistryDecorator : IUserO
public LocalDevelopmentUserOrganizationRegistryDecorator(IUserOrganizationRegistry _) { }

public Task<string?> GetCurrentUserOrgShortName(CancellationToken cancellationToken) => Task.FromResult("digdir")!;
public Task<IList<OrganizationLongName>?> GetCurrentUserOrgLongNames(CancellationToken cancellationToken) =>
Task.FromResult<IList<OrganizationLongName>?>(new[] { new OrganizationLongName { LongName = "Digdir", Language = "nb" } });
public Task<IList<ServiceOwnerLongName>?> GetCurrentUserOrgLongNames(CancellationToken cancellationToken) =>
Task.FromResult<IList<ServiceOwnerLongName>?>(new[] { new ServiceOwnerLongName { LongName = "Digdir", Language = "nb" } });
}
Loading

0 comments on commit b6fd439

Please sign in to comment.