diff --git a/src/BuildingBlocks/Security/Authorization/AuthorizationService.cs b/src/BuildingBlocks/Security/Authorization/AuthorizationService.cs new file mode 100644 index 0000000..d641e7a --- /dev/null +++ b/src/BuildingBlocks/Security/Authorization/AuthorizationService.cs @@ -0,0 +1,43 @@ +namespace OpenHealthOS.Security.Authorization; + +using OpenHealthOS.Contracts.Authorization; + +/// +/// Provides application-facing authorization by delegating evaluation +/// to an authorization evaluator. +/// +public sealed class AuthorizationService : IAuthorizationService +{ + private readonly IAuthorizationEvaluator _evaluator; + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The authorization evaluator used to evaluate requests. + /// + public AuthorizationService(IAuthorizationEvaluator evaluator) + { + ArgumentNullException.ThrowIfNull(evaluator); + + _evaluator = evaluator; + } + + /// + /// Evaluates whether the specified authorization context is allowed. + /// + /// + /// The authorization context to evaluate. + /// + /// + /// The authorization decision returned by the evaluator. + /// + public AuthorizationDecision Authorize( + AuthorizationContextDto context) + { + ArgumentNullException.ThrowIfNull(context); + + return _evaluator.Evaluate(context); + } +} diff --git a/src/BuildingBlocks/Security/Authorization/IAuthorizationService.cs b/src/BuildingBlocks/Security/Authorization/IAuthorizationService.cs new file mode 100644 index 0000000..b856878 --- /dev/null +++ b/src/BuildingBlocks/Security/Authorization/IAuthorizationService.cs @@ -0,0 +1,20 @@ +namespace OpenHealthOS.Security.Authorization; + +using OpenHealthOS.Contracts.Authorization; + +/// +/// Provides the application-facing authorization service. +/// +public interface IAuthorizationService +{ + /// + /// Evaluates whether the specified authorization context is allowed. + /// + /// + /// The authorization context to evaluate. + /// + /// + /// The authorization decision. + /// + AuthorizationDecision Authorize(AuthorizationContextDto context); +} diff --git a/tests/UnitTests/Authorization/AuthorizationServiceTests.cs b/tests/UnitTests/Authorization/AuthorizationServiceTests.cs new file mode 100644 index 0000000..10a3820 --- /dev/null +++ b/tests/UnitTests/Authorization/AuthorizationServiceTests.cs @@ -0,0 +1,110 @@ +namespace OpenHealthOS.UnitTests.Authorization; + +using OpenHealthOS.Contracts.Authorization; +using OpenHealthOS.Contracts.Identity; +using OpenHealthOS.Security.Authorization; +using Xunit; + +public sealed class AuthorizationServiceTests +{ + [Fact] + public void Authorize_ShouldReturnAllow_WhenEvaluatorAllows() + { + var evaluator = new StubAuthorizationEvaluator( + AuthorizationDecision.Allow); + + var service = new AuthorizationService(evaluator); + + var result = service.Authorize(CreateContext()); + + Assert.Equal( + AuthorizationDecision.Allow, + result); + } + + [Fact] + public void Authorize_ShouldReturnDeny_WhenEvaluatorDenies() + { + var evaluator = new StubAuthorizationEvaluator( + AuthorizationDecision.Deny); + + var service = new AuthorizationService(evaluator); + + var result = service.Authorize(CreateContext()); + + Assert.Equal( + AuthorizationDecision.Deny, + result); + } + + [Fact] + public void Constructor_ShouldThrow_WhenEvaluatorIsNull() + { + Assert.Throws( + () => new AuthorizationService(null!)); + } + + [Fact] + public void Authorize_ShouldThrow_WhenContextIsNull() + { + var evaluator = new StubAuthorizationEvaluator( + AuthorizationDecision.Allow); + + var service = new AuthorizationService(evaluator); + + Assert.Throws( + () => service.Authorize(null!)); + } + + [Fact] + public void Authorize_ShouldDelegateToEvaluator() + { + var evaluator = new StubAuthorizationEvaluator( + AuthorizationDecision.Allow); + + var service = new AuthorizationService(evaluator); + + var context = CreateContext(); + + service.Authorize(context); + + Assert.Same(context, evaluator.LastContext); + } + + private static AuthorizationContextDto CreateContext() + { + return new AuthorizationContextDto + { + Identity = new IdentityContextDto + { + SubjectId = "test-user", + PrincipalType = PrincipalType.User, + Scopes = ["patient.read"], + }, + Permission = new Permission("patient.read"), + ResourceType = "Patient", + }; + } + + private sealed class StubAuthorizationEvaluator + : IAuthorizationEvaluator + { + private readonly AuthorizationDecision _decision; + + public StubAuthorizationEvaluator( + AuthorizationDecision decision) + { + _decision = decision; + } + + public AuthorizationContextDto? LastContext { get; private set; } + + public AuthorizationDecision Evaluate( + AuthorizationContextDto context) + { + LastContext = context; + + return _decision; + } + } +}