Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Simplify subject attribute matching
Browse files Browse the repository at this point in the history
elsand committed Oct 25, 2024
1 parent 0c0a4da commit 1cbee77
Showing 3 changed files with 51 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using Altinn.Authorization.ABAC.Xacml.JsonProfile;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Altinn.Authorization.ABAC.Xacml.JsonProfile;
using System.Security.Claims;

using Digdir.Domain.Dialogporten.Application.Common.Extensions;
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
using Digdir.Domain.Dialogporten.Domain.Parties;
@@ -10,20 +11,23 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;
internal static class DecisionRequestHelper
{
private const string SubjectId = "s1";
private const string AltinnUrnNsPrefix = "urn:altinn:";

private const string PidClaimType = "pid";
private const string ConsumerClaimType = "consumer";
private const string UserIdClaimType = "urn:altinn:userid";
private const string RarAutorizationDetailsClaimType = "authorization_details";

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";
private const string AltinnAutorizationDetailsClaim = "authorization_details";
private const string AttributeIdOrg = "urn:altinn:org";
private const string AttributeIdApp = "urn:altinn:app";
private const string AttributeIdSystemUser = "urn:altinn:systemuser:uuid";
private const string AttributeIdUserId = "urn:altinn:userid";
private const string ReservedResourcePrefixForApps = "app_";
private const string AttributeIdAppInstance = "urn:altinn:instance-id";
private const string AttributeIdSubResource = "urn:altinn:subresource";
private const string AttributeIdUserId = "urn:altinn:userid";

private const string ReservedResourcePrefixForApps = "app_";

private const string PermitResponse = "Permit";

public static XacmlJsonRequestRoot CreateDialogDetailsRequest(DialogDetailsAuthorizationRequest request)
@@ -73,37 +77,46 @@ public static DialogDetailsAuthorizationResult CreateDialogDetailsResponse(List<

private static List<XacmlJsonCategory> CreateAccessSubjectCategory(IEnumerable<Claim> claims)
{
var attributes = claims
.Select(x => x switch
// The PDP expects for the most part only a single subject attribute, and will even fail the request
// for some types (e.g. the urn:altinn:systemuser:uuid) if there are multiple subject attributes (for
// security reasons). We therefore need to filter out the relevant attributes and only include those,
// which in essence is the pid and the systemuser uuid. In addition, we also utilize urn:altinn:userid
// if present instead of the pid as a simple optimization as this offloads the PDP from having to look up
// the user id from the pid.
XacmlJsonAttribute? selectedAttribute = null;

foreach (var claim in claims)
{
if (claim.Type == UserIdClaimType)
{
{ 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.TryGetOrganizationNumber(out var organizationNumber) => new() { AttributeId = NorwegianOrganizationIdentifier.Prefix, Value = organizationNumber },
{ Type: AltinnAutorizationDetailsClaim } => new() { AttributeId = AttributeIdSystemUser, Value = GetSystemUserId(x) },
_ => null
})
.Where(x => x is not null)
.Cast<XacmlJsonAttribute>()
.ToList();
selectedAttribute = new XacmlJsonAttribute { AttributeId = AttributeIdUserId, Value = claim.Value };
break;
}

// If we're authorizing a person (i.e. 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).
// The same goes if urn:altinn:userid is present, which might be present if using a legacy enterprise user token
if (attributes.Any(x => x.AttributeId == NorwegianPersonIdentifier.Prefix) ||
attributes.Any(x => x.AttributeId == AttributeIdUserId))
{
attributes.RemoveAll(x => x.AttributeId == NorwegianOrganizationIdentifier.Prefix);
if (claim.Type == PidClaimType)
{
selectedAttribute = new XacmlJsonAttribute { AttributeId = NorwegianPersonIdentifier.Prefix, Value = claim.Value };
break;
}

if (claim.Type == RarAutorizationDetailsClaimType)
{
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[] { claim }));
if (claimsPrincipal.TryGetSystemUserId(out var systemUserId))
{
selectedAttribute = new XacmlJsonAttribute { AttributeId = AttributeIdSystemUser, Value = systemUserId };
}
break;
}
}

var attributes = selectedAttribute != null
? [selectedAttribute]
: new List<XacmlJsonAttribute>();

return [new() { Id = SubjectId, Attribute = attributes }];
}

private static string GetSystemUserId(Claim claim)
{
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity([claim]));
claimsPrincipal.TryGetSystemUserId(out var systemUserId);
return systemUserId!;
}

private static List<XacmlJsonCategory> CreateActionCategories(
List<AltinnAction> altinnActions, out Dictionary<string, string> actionIdByName)
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequest()
("pid", "12345678901"),

// This should not be copied as subject claim since there's a "pid"-claim
("consumer", ConsumerClaimValue)
("authorization_details", AuthorizationDetailsClaimValue)
),
$"{NorwegianOrganizationIdentifier.PrefixWithSeparator}713330310");
var dialogId = request.DialogId;
@@ -42,9 +42,8 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequest()
// Check AccessSubject attributes
var accessSubject = result.Request.AccessSubject.First();
Assert.Equal("s1", accessSubject.Id);
Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:foo" && a.Value == "bar");
Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:person:identifier-no" && a.Value == "12345678901");
Assert.DoesNotContain(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:organization:identifier-no");
Assert.Single(accessSubject.Attribute);

// Check Action attributes.
var actionIdsByName = new Dictionary<string, string>();
@@ -79,15 +78,15 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequest()
}

[Fact]
public void CreateDialogDetailsRequestShouldReturnCorrectRequestForLegacyEnterpriseUsers()
public void CreateDialogDetailsRequestShouldReturnCorrectRequestForExchangedTokens()
{
// Arrange
var request = CreateDialogDetailsAuthorizationRequest(
GetAsClaims(
("urn:altinn:userid", "5678901"),

// This should not be copied as subject claim since there's a "urn:altinn:user-id"-claim
("consumer", ConsumerClaimValue)
("pid", "12345678901")
),
$"{NorwegianOrganizationIdentifier.PrefixWithSeparator}713330310");

@@ -98,7 +97,7 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequestForLegacyEnterpr
var accessSubject = result.Request.AccessSubject.First();
Assert.Equal("s1", accessSubject.Id);
Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:userid" && a.Value == "5678901");
Assert.DoesNotContain(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:organization:identifier-no");
Assert.Single(accessSubject.Attribute);
}

[Fact]
@@ -136,7 +135,9 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequestForSystemUser()
// Arrange
var request = CreateDialogDetailsAuthorizationRequest(
GetAsClaims(
("authorization_details", AuthorizationDetailsClaimValue)
("authorization_details", AuthorizationDetailsClaimValue),
("pid", "12345678901"),
("consumer", ConsumerClaimValue)
),
$"{NorwegianOrganizationIdentifier.PrefixWithSeparator}713330310"
);
@@ -151,33 +152,8 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequestForSystemUser()

var accessSubject = result.Request.AccessSubject.First();
Assert.Equal("s1", accessSubject.Id);
Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:foo" && a.Value == "bar");
Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:systemuser:uuid" && a.Value == "unique_systemuser_id");
}

[Fact]
public void CreateDialogDetailsRequestShouldReturnCorrectRequestForConsumerOrgAndPersonParty()
{
// Arrange
var request = CreateDialogDetailsAuthorizationRequest(
GetAsClaims(
// Should be copied as subject claim since there's not a "pid"-claim
("consumer", ConsumerClaimValue)
),
$"{NorwegianPersonIdentifier.PrefixWithSeparator}16073422888");

// Act
var result = DecisionRequestHelper.CreateDialogDetailsRequest(request);

// Assert
// Check that we have the organizationnumber
var accessSubject = result.Request.AccessSubject.First();
Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:organization:identifier-no" && a.Value == "991825827");

// Check that we have the ssn attribute as resource owner
var resource1 = result.Request.Resource.FirstOrDefault(r => r.Id == "r1");
Assert.NotNull(resource1);
Assert.Contains(resource1.Attribute, a => a.AttributeId == "urn:altinn:person:identifier-no" && a.Value == "16073422888");
Assert.Single(accessSubject.Attribute);
}

[Fact]

0 comments on commit 1cbee77

Please sign in to comment.