Skip to content

Commit

Permalink
Feature/flattenpolicy (#498)
Browse files Browse the repository at this point in the history
* Added endpoint for flatten rules.

* Updated to urn type

* Updates

* Feedback

* Added endpoint for rules

* Feedback

* Feedback

* Feedback

* Fixed test after policy update

* Update algoritm

* Model updates

* Mapped to external object

* Moved mapping

* FAilure

* chore: right key suggestion

* Rename

* Optimized

* Fix test

* Fixed test

* Feedback

* Fixed hash value

* Fixed naming

---------

Co-authored-by: Aleksander Heintz <[email protected]>
  • Loading branch information
TheTechArch and Alxandr authored Oct 24, 2024
1 parent 7398d71 commit 771a415
Show file tree
Hide file tree
Showing 13 changed files with 1,423 additions and 592 deletions.
156 changes: 156 additions & 0 deletions src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System.Buffers;
using System.Data;
using System.Xml;
using Altinn.Authorization.ABAC.Constants;
using Altinn.Authorization.ABAC.Utils;
using Altinn.Authorization.ABAC.Xacml;
using Altinn.ResourceRegistry.Core.Constants;
using Altinn.ResourceRegistry.Core.Extensions;
using Altinn.ResourceRegistry.Core.Models;
using Altinn.Urn;
using Altinn.Urn.Json;
using Nerdbank.Streams;
using static Altinn.ResourceRegistry.Core.Constants.AltinnXacmlConstants;

Expand Down Expand Up @@ -153,6 +156,158 @@ public static string GetAltinnAppsPolicyPath(string org, string app)
return $"{org.AsFileName()}/{app.AsFileName()}/policy.xml";
}

/// <summary>
/// Converts a XACML policy to a list of PolicyRule
/// </summary>
public static List<PolicyRule> ConvertToPolicyRules(XacmlPolicy xacmlPolicy)
{
List<PolicyRule> rules = new List<PolicyRule>();
foreach (XacmlRule xacmlRule in xacmlPolicy.Rules)
{
FlattenXacmlRule(xacmlRule, rules);
}

return rules;
}

/// <summary>
/// Returns a list of rights for a resource. A right is a combination of resource and action. The response list the subjects in policy that is granted the right.
/// Response is grouped by right.
/// </summary>
public static List<PolicyRight> ConvertToPolicyRight(XacmlPolicy policy)
{
List<PolicyRule> policyRules = ConvertToPolicyRules(policy);
List<PolicyRight> policyRights = new(policyRules.Count);

Dictionary<string, (PolicyRight Rights, List<PolicySubject> Subjects)> resourceActions = new();

foreach (PolicyRule rule in policyRules)
{
List<PolicySubject> subjects = [new PolicySubject { SubjectAttributes = rule.Subject }];
PolicyRight policyResourceAction = new PolicyRight()
{
Action = rule.Action,
Resource = rule.Resource,
Subjects = subjects,
};

if (resourceActions.ContainsKey(policyResourceAction.RightKey))
{
resourceActions[policyResourceAction.RightKey].Subjects.AddRange(policyResourceAction.Subjects);
}
else
{
resourceActions.Add(policyResourceAction.RightKey, (policyResourceAction, subjects));
policyRights.Add(policyResourceAction);
}
}

return policyRights;
}

/// <summary>
/// This method will flatten the XACML rule into a list of PolicyRule where each PolicyRule contains a list of KeyValueUrn for subject, action and resource
/// The list will cotain duplicates if there is duplicate rules in XACML.
///
/// The code also enforce some extra rules:
/// For each of the three categories (subject, action, resource) they need to be in a separate AnyOf element in the Target element.
/// Action can only be one match in a AllOf element.(you cant do both read and write at the same time)
/// Subject have multiple matches in a AllOf element, but it is not used in the current implementation in Altinn. (requiring a user to have multiple roles to access a resource)
/// A resource can have multiple matched in a AllOf element to be able to match on multiple attributes. (app, task1 , task2 etc)
/// </summary>
private static void FlattenXacmlRule(XacmlRule xacmlRule, List<PolicyRule> policyRules)
{
XacmlAnyOf anyOfSubjects = null;
XacmlAnyOf anyOfActions = null;
XacmlAnyOf anyOfResourcs = null;

foreach (XacmlAnyOf anyOf in xacmlRule.Target.AnyOf)
{
string category = null;

foreach (XacmlAllOf allOf in anyOf.AllOf)
{
foreach (XacmlMatch match in allOf.Matches)
{
if (category == null)
{
category = match.AttributeDesignator.Category.ToString();
}
else if (!category.Equals(match.AttributeDesignator.Category.ToString()))
{
throw new ArgumentException("All matches in a all must have the same category ruleId " + xacmlRule.RuleId);
}
}
}

if (category.Equals(XacmlConstants.MatchAttributeCategory.Action))
{
anyOfActions = anyOf;
}
else if (category.Equals(XacmlConstants.MatchAttributeCategory.Subject))
{
anyOfSubjects = anyOf;
}
else if (category.Equals(XacmlConstants.MatchAttributeCategory.Resource))
{
anyOfResourcs = anyOf;
}
}

foreach (XacmlAllOf allOfSubject in anyOfSubjects.AllOf)
{
foreach (XacmlAllOf allOfAction in anyOfActions.AllOf)
{
foreach (XacmlAllOf allOfResource in anyOfResourcs.AllOf)
{
PolicyRule policyRule = new PolicyRule()
{
Subject = GetMatchValuesFromAllOff(allOfSubject),
Action = GetMatchValueFromAllOff(allOfAction),
Resource = GetMatchValuesFromAllOff(allOfResource)
};
policyRules.Add(policyRule);
}
}
}
}

/// <summary>
/// Convert all matches in a allOf to a list of KeyValueUrn
/// </summary>
private static List<UrnJsonTypeValue> GetMatchValuesFromAllOff(XacmlAllOf allOfs)
{
List<UrnJsonTypeValue> subjectMatches = new List<UrnJsonTypeValue>();

foreach (XacmlMatch match in allOfs.Matches)
{
string attributeId = match.AttributeDesignator.AttributeId.ToString();
subjectMatches.Add(KeyValueUrn.Create($"{attributeId}:{match.AttributeValue.Value}", attributeId.Length + 1));
}

return subjectMatches;
}

/// <summary>
/// Convert all matches in a allOf to a list of KeyValueUrn
/// </summary>
private static UrnJsonTypeValue GetMatchValueFromAllOff(XacmlAllOf allOfs)
{
if (allOfs.Matches.Count > 1)
{
throw new ArgumentException("Only one match is allowed in a allOf for action category");
}

if (allOfs.Matches.Count == 0)
{
throw new ArgumentException("No match found in allOf for action category");
}

XacmlMatch match = allOfs.Matches.First();
string matchId = match.AttributeDesignator.AttributeId.ToString();
return KeyValueUrn.Create($"{matchId}:{match.AttributeValue.Value}", matchId.Length + 1);
}

private static AttributeMatch GetActionValueFromRule(XacmlRule rule)

Check warning on line 311 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs

View workflow job for this annotation

GitHub Actions / Analyze

Remove the unused private method 'GetActionValueFromRule'. (https://rules.sonarsource.com/csharp/RSPEC-1144)
{
foreach (XacmlAnyOf anyOf in rule.Target.AnyOf)
Expand Down Expand Up @@ -192,5 +347,6 @@ private static List<AttributeMatch> GetResourceFromXacmlRule(XacmlRule rule)

return result;
}

}

Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs

View workflow job for this annotation

GitHub Actions / Analyze

Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

}
161 changes: 161 additions & 0 deletions src/Altinn.ResourceRegistry.Core/Models/PolicyRight.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#nullable enable

using System.Buffers;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Altinn.Urn.Json;

namespace Altinn.ResourceRegistry.Core.Models
{
/// <summary>
/// Defines a flatten Policy Rule
/// </summary>
public class PolicyRight
{
private string? _rightKey;
private SortedSet<string>? _subjectTypes;

/// <summary>
/// Defines the action that the subject is allowed to perform on the resource
/// </summary>
public required UrnJsonTypeValue Action { get; init; }

/// <summary>
/// The Resource attributes that identy one unique resource
/// </summary>
public required IReadOnlyList<UrnJsonTypeValue> Resource { get; init; }

/// <summary>
/// List of subjects that is allowed to perform the action on the resource
/// </summary>
public required IReadOnlyList<PolicySubject> Subjects { get; init; }

/// <summary>
/// Returns the right key for the right part of policy resource action
/// </summary>
public string RightKey
=> _rightKey ??= CalculateRightKey();

/// <summary>
/// Returns a list of subject types that is allowed to perform the action on the resource
/// IS used for filtering the
/// </summary>
public IReadOnlySet<string> SubjectTypes
=> _subjectTypes ??= CalculateSubjectTypes();

private SortedSet<string> CalculateSubjectTypes()
{
SortedSet<string> subjectTypes = new SortedSet<string>();

foreach (var subject in Subjects)
{
foreach (var attr in subject.SubjectAttributes)
{
subjectTypes.Add(attr.Value.PrefixSpan.ToString().ToLowerInvariant());
}
}

return subjectTypes;
}

private string CalculateRightKey()
{
var sb = _stringBuilder ?? new();

UrnJsonTypeValue[]? resourcesArray = null;
byte[]? rentedBytes = null;

try
{
resourcesArray = ArrayPool<UrnJsonTypeValue>.Shared.Rent(Resource.Count);

// copy all of the resources so we can sort them
var resources = CopyUrns(Resource, resourcesArray);
resources.Sort(static (x, y) => x.Value.Urn.CompareTo(y.Value.Urn));

// first, fill with the string to be hashed
sb.Append(Action.Value.ValueSpan);
foreach (var resource in resources)
{
sb.Append(';').Append(resource.Value.Urn);
}

// compute the MD5 hash
Span<byte> hash = stackalloc byte[MD5.HashSizeInBytes];
rentedBytes = ArrayPool<byte>.Shared.Rent(sb.Length * 2); // every char is 2 bytes
var toHash = CopyBytes(sb, rentedBytes);
MD5.HashData(toHash, hash);

// build the final key
sb.Clear();
sb.Append(Action.Value.ValueSpan);
foreach (var resource in resources)
{
sb.Append(';').Append(resource.Value.ValueSpan);
}

var success = true;
sb.Append(';');
Span<char> dest = stackalloc char[2];
for (var i = 0; i < MD5.HashSizeInBytes; i++)
{
var b = hash[i];
success |= b.TryFormat(dest, out _, "x2");
sb.Append(dest);
}

Debug.Assert(success);
return sb.ToString();
}
finally
{
if (resourcesArray is not null)
{
ArrayPool<UrnJsonTypeValue>.Shared.Return(resourcesArray);
}

if (rentedBytes is not null)
{
ArrayPool<byte>.Shared.Return(rentedBytes);
}

sb.Clear();
_stringBuilder = sb;
}

static Span<UrnJsonTypeValue> CopyUrns(IReadOnlyList<UrnJsonTypeValue> from, UrnJsonTypeValue[] to)
{
if (from is ICollection<UrnJsonTypeValue> collection)
{
collection.CopyTo(to, 0);
return to.AsSpan(0, from.Count);
}

for (int i = 0; i < from.Count; i++)
{
to[i] = from[i];
}

return to.AsSpan(0, from.Count);
}

static ReadOnlySpan<byte> CopyBytes(StringBuilder from, byte[] to)
{
var position = 0;
foreach (var chunk in from.GetChunks())
{
var bytes = MemoryMarshal.AsBytes(chunk.Span);
bytes.CopyTo(to.AsSpan(position));
position += bytes.Length;
}

return to.AsSpan(0, position);
}
}

[ThreadStatic]
private static StringBuilder? _stringBuilder;
}
}
26 changes: 26 additions & 0 deletions src/Altinn.ResourceRegistry.Core/Models/PolicyRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Altinn.Urn;
using Altinn.Urn.Json;

namespace Altinn.ResourceRegistry.Core.Models
{
/// <summary>
/// Definees a flatten Policy Rule
/// </summary>
public class PolicyRule
{
/// <summary>
/// The Subject target in rule
/// </summary>
public required IReadOnlyList<UrnJsonTypeValue> Subject { get; init; }

/// <summary>
/// Defines the action that the subject is allowed to perform on the resource
/// </summary>
public required UrnJsonTypeValue Action { get; init; }

/// <summary>
/// The Resource attributes that identy one unique resource
/// </summary>
public required IReadOnlyList<UrnJsonTypeValue> Resource { get; init; }
}
}
15 changes: 15 additions & 0 deletions src/Altinn.ResourceRegistry.Core/Models/PolicySubject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Altinn.Urn.Json;

namespace Altinn.ResourceRegistry.Core.Models
{
/// <summary>
/// Defines a Policy Subject
/// </summary>
public class PolicySubject
{
/// <summary>
/// Subject attributes that defines the subject
/// </summary>
public required IReadOnlyList<UrnJsonTypeValue> SubjectAttributes { get; init; }
}
}
Loading

0 comments on commit 771a415

Please sign in to comment.