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,44 @@
namespace OpenHealthOS.Security.Authorization;

using OpenHealthOS.Contracts.Authorization;

/// <summary>
/// Provides the application authorization pipeline by delegating
/// authorization requests to the authorization service.
/// </summary>
public sealed class AuthorizationPipeline : IAuthorizationPipeline
{
private readonly IAuthorizationService _authorizationService;

/// <summary>
/// Initializes a new instance of the
/// <see cref="AuthorizationPipeline"/> class.
/// </summary>
/// <param name="authorizationService">
/// The authorization service used to evaluate requests.
/// </param>
public AuthorizationPipeline(
IAuthorizationService authorizationService)
{
ArgumentNullException.ThrowIfNull(authorizationService);

_authorizationService = authorizationService;
}

/// <summary>
/// Evaluates an authorization request.
/// </summary>
/// <param name="request">
/// The authorization request to evaluate.
/// </param>
/// <returns>
/// The authorization decision returned by the authorization service.
/// </returns>
public AuthorizationDecision Authorize(
AuthorizationRequest request)
{
ArgumentNullException.ThrowIfNull(request);

return _authorizationService.Authorize(request.Context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace OpenHealthOS.Security.Authorization;

using OpenHealthOS.Contracts.Authorization;

/// <summary>
/// Represents an authorization request submitted to the authorization
/// pipeline.
/// </summary>
public sealed record AuthorizationRequest
{
/// <summary>
/// Initializes a new instance of the
/// <see cref="AuthorizationRequest"/> class.
/// </summary>
/// <param name="context">
/// The authorization context associated with the request.
/// </param>
public AuthorizationRequest(AuthorizationContextDto context)
{
ArgumentNullException.ThrowIfNull(context);

Context = context;
}

/// <summary>
/// Gets the authorization context.
/// </summary>
public AuthorizationContextDto Context { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace OpenHealthOS.Security.Authorization;

using OpenHealthOS.Contracts.Authorization;

/// <summary>
/// Represents the application authorization pipeline.
/// </summary>
public interface IAuthorizationPipeline
{
/// <summary>
/// Evaluates an authorization request.
/// </summary>
/// <param name="request">
/// The authorization request to evaluate.
/// </param>
/// <returns>
/// The resulting authorization decision.
/// </returns>
AuthorizationDecision Authorize(
AuthorizationRequest request);
}
116 changes: 116 additions & 0 deletions tests/UnitTests/Authorization/AuthorizationPipelineTests.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentNullException>(
() => new AuthorizationPipeline(null!));
}

[Fact]
public void Authorize_ShouldThrow_WhenRequestIsNull()
{
var service = new StubAuthorizationService(
AuthorizationDecision.Allow);

var pipeline = new AuthorizationPipeline(service);

Assert.Throws<ArgumentNullException>(
() => 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;
}
}
}
Loading