diff --git a/src/BuildingBlocks/Security/Authorization/AuthorizationPipeline.cs b/src/BuildingBlocks/Security/Authorization/AuthorizationPipeline.cs new file mode 100644 index 0000000..88869e0 --- /dev/null +++ b/src/BuildingBlocks/Security/Authorization/AuthorizationPipeline.cs @@ -0,0 +1,44 @@ +namespace OpenHealthOS.Security.Authorization; + +using OpenHealthOS.Contracts.Authorization; + +/// +/// Provides the application authorization pipeline by delegating +/// authorization requests to the authorization service. +/// +public sealed class AuthorizationPipeline : IAuthorizationPipeline +{ + private readonly IAuthorizationService _authorizationService; + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The authorization service used to evaluate requests. + /// + public AuthorizationPipeline( + IAuthorizationService authorizationService) + { + ArgumentNullException.ThrowIfNull(authorizationService); + + _authorizationService = authorizationService; + } + + /// + /// Evaluates an authorization request. + /// + /// + /// The authorization request to evaluate. + /// + /// + /// The authorization decision returned by the authorization service. + /// + public AuthorizationDecision Authorize( + AuthorizationRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + return _authorizationService.Authorize(request.Context); + } +} diff --git a/src/BuildingBlocks/Security/Authorization/AuthorizationRequest.cs b/src/BuildingBlocks/Security/Authorization/AuthorizationRequest.cs new file mode 100644 index 0000000..1f56283 --- /dev/null +++ b/src/BuildingBlocks/Security/Authorization/AuthorizationRequest.cs @@ -0,0 +1,29 @@ +namespace OpenHealthOS.Security.Authorization; + +using OpenHealthOS.Contracts.Authorization; + +/// +/// Represents an authorization request submitted to the authorization +/// pipeline. +/// +public sealed record AuthorizationRequest +{ + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The authorization context associated with the request. + /// + public AuthorizationRequest(AuthorizationContextDto context) + { + ArgumentNullException.ThrowIfNull(context); + + Context = context; + } + + /// + /// Gets the authorization context. + /// + public AuthorizationContextDto Context { get; } +} diff --git a/src/BuildingBlocks/Security/Authorization/IAuthorizationPipeline.cs b/src/BuildingBlocks/Security/Authorization/IAuthorizationPipeline.cs new file mode 100644 index 0000000..2e6ce80 --- /dev/null +++ b/src/BuildingBlocks/Security/Authorization/IAuthorizationPipeline.cs @@ -0,0 +1,21 @@ +namespace OpenHealthOS.Security.Authorization; + +using OpenHealthOS.Contracts.Authorization; + +/// +/// Represents the application authorization pipeline. +/// +public interface IAuthorizationPipeline +{ + /// + /// Evaluates an authorization request. + /// + /// + /// The authorization request to evaluate. + /// + /// + /// The resulting authorization decision. + /// + AuthorizationDecision Authorize( + AuthorizationRequest request); +} diff --git a/tests/UnitTests/Authorization/AuthorizationPipelineTests.cs b/tests/UnitTests/Authorization/AuthorizationPipelineTests.cs new file mode 100644 index 0000000..1ddb1dd --- /dev/null +++ b/tests/UnitTests/Authorization/AuthorizationPipelineTests.cs @@ -0,0 +1,116 @@ +namespace OpenHealthOS.UnitTests.Authorization; + +using OpenHealthOS.Contracts.Authorization; +using OpenHealthOS.Contracts.Identity; +using OpenHealthOS.Security.Authorization; +using Xunit; + +public sealed class AuthorizationPipelineTests +{ + [Fact] + public void Authorize_ShouldReturnAllow_WhenServiceAllows() + { + var service = new StubAuthorizationService( + AuthorizationDecision.Allow); + + var pipeline = new AuthorizationPipeline(service); + + var request = CreateRequest(); + + var result = pipeline.Authorize(request); + + Assert.Equal( + AuthorizationDecision.Allow, + result); + } + + [Fact] + public void Authorize_ShouldReturnDeny_WhenServiceDenies() + { + var service = new StubAuthorizationService( + AuthorizationDecision.Deny); + + var pipeline = new AuthorizationPipeline(service); + + var request = CreateRequest(); + + var result = pipeline.Authorize(request); + + Assert.Equal( + AuthorizationDecision.Deny, + result); + } + + [Fact] + public void Constructor_ShouldThrow_WhenServiceIsNull() + { + Assert.Throws( + () => new AuthorizationPipeline(null!)); + } + + [Fact] + public void Authorize_ShouldThrow_WhenRequestIsNull() + { + var service = new StubAuthorizationService( + AuthorizationDecision.Allow); + + var pipeline = new AuthorizationPipeline(service); + + Assert.Throws( + () => pipeline.Authorize(null!)); + } + + [Fact] + public void Authorize_ShouldPassContextToService() + { + var service = new StubAuthorizationService( + AuthorizationDecision.Allow); + + var pipeline = new AuthorizationPipeline(service); + + var request = CreateRequest(); + + pipeline.Authorize(request); + + Assert.Same(request.Context, service.LastContext); + } + + private static AuthorizationRequest CreateRequest() + { + var context = new AuthorizationContextDto + { + Identity = new IdentityContextDto + { + SubjectId = "test-user", + PrincipalType = PrincipalType.User, + Scopes = ["patient.read"], + }, + Permission = new Permission("patient.read"), + ResourceType = "Patient", + }; + + return new AuthorizationRequest(context); + } + + private sealed class StubAuthorizationService + : IAuthorizationService + { + private readonly AuthorizationDecision _decision; + + public StubAuthorizationService( + AuthorizationDecision decision) + { + _decision = decision; + } + + public AuthorizationContextDto? LastContext { get; private set; } + + public AuthorizationDecision Authorize( + AuthorizationContextDto context) + { + LastContext = context; + + return _decision; + } + } +}