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
+
+
+
+
+
+
+
+
+
+
+
+
+