-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
* 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
There are no files selected for viewing
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; | ||
|
||
|
@@ -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) | ||
{ | ||
foreach (XacmlAnyOf anyOf in rule.Target.AnyOf) | ||
|
@@ -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 GitHub Actions / Analyze (csharp)
Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs GitHub Actions / Analyze (csharp)
Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs GitHub Actions / Analyze
Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs GitHub Actions / Build and Test
Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs GitHub Actions / Build and Test
Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs GitHub Actions / Analyze (csharp)
Check warning on line 351 in src/Altinn.ResourceRegistry.Core/Helpers/PolicyHelper.cs GitHub Actions / Analyze (csharp)
|
||
} |
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; | ||
} | ||
} |
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; } | ||
} | ||
} |
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; } | ||
} | ||
} |