From bb75bbbe7c17a38d7cfc5ebb65325d2352bc4b89 Mon Sep 17 00:00:00 2001 From: Ponchanon Datta Rone Date: Wed, 12 Aug 2026 09:48:46 -0500 Subject: [PATCH] feat(authorization): add permission evaluator --- Directory.Packages.props | 4 + OpenHealthOS.slnx | 4 + .../Authorization/PermissionEvaluator.cs | 33 +++++++ .../Authorization/PermissionEvaluatorTests.cs | 87 +++++++++++++++++++ tests/UnitTests/OpenHealthOS.UnitTests.csproj | 19 ++++ 5 files changed, 147 insertions(+) create mode 100644 src/BuildingBlocks/Security/Authorization/PermissionEvaluator.cs create mode 100644 tests/UnitTests/Authorization/PermissionEvaluatorTests.cs create mode 100644 tests/UnitTests/OpenHealthOS.UnitTests.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 9287ced..46e12a9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,10 @@ + + + + \ No newline at end of file diff --git a/OpenHealthOS.slnx b/OpenHealthOS.slnx index 00fa214..b8bf438 100644 --- a/OpenHealthOS.slnx +++ b/OpenHealthOS.slnx @@ -28,4 +28,8 @@ + + + + diff --git a/src/BuildingBlocks/Security/Authorization/PermissionEvaluator.cs b/src/BuildingBlocks/Security/Authorization/PermissionEvaluator.cs new file mode 100644 index 0000000..2e583e0 --- /dev/null +++ b/src/BuildingBlocks/Security/Authorization/PermissionEvaluator.cs @@ -0,0 +1,33 @@ +namespace OpenHealthOS.Security.Authorization; + +using OpenHealthOS.Contracts.Authorization; + +/// +/// Evaluates authorization permissions using the scopes +/// associated with the authenticated identity. +/// +public sealed class PermissionEvaluator : IAuthorizationEvaluator +{ + /// + /// Evaluates whether the requested permission is granted. + /// + /// + /// The authorization context to evaluate. + /// + /// + /// when the permission is + /// granted; otherwise . + /// + public AuthorizationDecision Evaluate(AuthorizationContextDto context) + { + ArgumentNullException.ThrowIfNull(context); + + var permission = context.Permission.Value; + + return context.Identity.Scopes.Contains( + permission, + StringComparer.OrdinalIgnoreCase) + ? AuthorizationDecision.Allow + : AuthorizationDecision.Deny; + } +} diff --git a/tests/UnitTests/Authorization/PermissionEvaluatorTests.cs b/tests/UnitTests/Authorization/PermissionEvaluatorTests.cs new file mode 100644 index 0000000..43a2212 --- /dev/null +++ b/tests/UnitTests/Authorization/PermissionEvaluatorTests.cs @@ -0,0 +1,87 @@ +namespace OpenHealthOS.UnitTests.Authorization; + +using OpenHealthOS.Contracts.Authorization; +using OpenHealthOS.Contracts.Identity; +using OpenHealthOS.Security.Authorization; +using Xunit; + +public sealed class PermissionEvaluatorTests +{ + private readonly PermissionEvaluator _evaluator = new(); + + [Fact] + public void Evaluate_ShouldThrow_WhenContextIsNull() + { + var action = () => { _evaluator.Evaluate(null!); }; + + Assert.Throws(action); + } + + [Fact] + public void Evaluate_ShouldAllow_WhenPermissionExistsInScopes() + { + var context = CreateContext( + scopes: ["patient.read"]); + + var result = _evaluator.Evaluate(context); + + Assert.Equal( + AuthorizationDecision.Allow, + result); + } + + [Fact] + public void Evaluate_ShouldDeny_WhenPermissionIsNotGranted() + { + var context = CreateContext( + scopes: ["patient.write"]); + + var result = _evaluator.Evaluate(context); + + Assert.Equal( + AuthorizationDecision.Deny, + result); + } + + [Fact] + public void Evaluate_ShouldDeny_WhenIdentityHasNoPermissions() + { + var context = CreateContext(); + + var result = _evaluator.Evaluate(context); + + Assert.Equal( + AuthorizationDecision.Deny, + result); + } + + [Fact] + public void Evaluate_ShouldBeCaseInsensitive() + { + var context = CreateContext( + scopes: ["PATIENT.READ"]); + + var result = _evaluator.Evaluate(context); + + Assert.Equal( + AuthorizationDecision.Allow, + result); + } + + private static AuthorizationContextDto CreateContext( + IReadOnlyCollection? scopes = null) + { + return new AuthorizationContextDto + { + Identity = new IdentityContextDto + { + SubjectId = "test-user", + PrincipalType = PrincipalType.User, + Scopes = scopes ?? Array.Empty(), + Roles = Array.Empty(), + }, + Permission = new Permission("patient.read"), + ResourceType = "Patient", + }; + } +} diff --git a/tests/UnitTests/OpenHealthOS.UnitTests.csproj b/tests/UnitTests/OpenHealthOS.UnitTests.csproj new file mode 100644 index 0000000..3297996 --- /dev/null +++ b/tests/UnitTests/OpenHealthOS.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + false + true + + + + + + + + + + + + +