Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace OpenHealthOS.Security.Authorization;

using OpenHealthOS.Contracts.Authorization;

/// <summary>
/// Evaluates a collection of authorization policies using AND semantics.
/// </summary>
public sealed class AuthorizationPolicySet : IAuthorizationPolicySet
{
private readonly IReadOnlyCollection<IAuthorizationPolicy> _policies;

/// <summary>
/// Initializes a new instance of the
/// <see cref="AuthorizationPolicySet"/> class.
/// </summary>
/// <param name="policies">
/// The policies that must all be satisfied.
/// </param>
public AuthorizationPolicySet(
IEnumerable<IAuthorizationPolicy> policies)
{
ArgumentNullException.ThrowIfNull(policies);

_policies = policies.ToArray();

if (_policies.Any(policy => policy is null))
{
throw new ArgumentException(
"Policy collection cannot contain null policies.",
nameof(policies));
}
}

/// <summary>
/// Determines whether all policies are satisfied.
/// </summary>
/// <param name="context">
/// The authorization context to evaluate.
/// </param>
/// <returns>
/// <see langword="true"/> when every policy is satisfied;
/// otherwise <see langword="false"/>.
/// </returns>
public bool IsSatisfied(AuthorizationContextDto context)
{
ArgumentNullException.ThrowIfNull(context);

return _policies.All(policy => policy.IsSatisfied(context));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace OpenHealthOS.Security.Authorization;

using OpenHealthOS.Contracts.Authorization;

/// <summary>
/// Represents a composition of authorization policies.
/// </summary>
public interface IAuthorizationPolicySet
{
/// <summary>
/// Determines whether the authorization context satisfies all policies
/// in the set.
/// </summary>
/// <param name="context">
/// The authorization context to evaluate.
/// </param>
/// <returns>
/// <see langword="true"/> when all policies are satisfied;
/// otherwise <see langword="false"/>.
/// </returns>
bool IsSatisfied(AuthorizationContextDto context);
}
88 changes: 88 additions & 0 deletions tests/UnitTests/Authorization/AuthorizationPolicySetTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace OpenHealthOS.UnitTests.Authorization;

using OpenHealthOS.Contracts.Authorization;
using OpenHealthOS.Contracts.Identity;
using OpenHealthOS.Security.Authorization;
using Xunit;

public sealed class AuthorizationPolicySetTests
{
[Fact]
public void IsSatisfied_ShouldAllow_WhenAllPoliciesAreSatisfied()
{
var context = CreateContext("patient.read");

var policySet = new AuthorizationPolicySet(
[
new PermissionPolicy(new Permission("patient.read")),
new PermissionPolicy(new Permission("PATIENT.READ")),
]);

var result = policySet.IsSatisfied(context);

Assert.True(result);
}

[Fact]
public void IsSatisfied_ShouldDeny_WhenAnyPolicyIsNotSatisfied()
{
var context = CreateContext("patient.read");

var policySet = new AuthorizationPolicySet(
[
new PermissionPolicy(new Permission("patient.read")),
new PermissionPolicy(new Permission("patient.write")),
]);

var result = policySet.IsSatisfied(context);

Assert.False(result);
}

[Fact]
public void IsSatisfied_ShouldAllow_WhenPolicySetIsEmpty()
{
var context = CreateContext("patient.read");

var policySet = new AuthorizationPolicySet(
Array.Empty<IAuthorizationPolicy>());

var result = policySet.IsSatisfied(context);

Assert.True(result);
}

[Fact]
public void IsSatisfied_ShouldThrow_WhenContextIsNull()
{
var policySet = new AuthorizationPolicySet(
[
new PermissionPolicy(new Permission("patient.read")),
]);

Assert.Throws<ArgumentNullException>(
() => policySet.IsSatisfied(null!));
}

[Fact]
public void Constructor_ShouldThrow_WhenPoliciesIsNull()
{
Assert.Throws<ArgumentNullException>(
() => new AuthorizationPolicySet(null!));
}

private static AuthorizationContextDto CreateContext(
string permission)
{
return new AuthorizationContextDto
{
Identity = new IdentityContextDto
{
SubjectId = "test-user",
PrincipalType = PrincipalType.User,
},
Permission = new Permission(permission),
ResourceType = "Patient",
};
}
}
Loading