From ab4013fcf4792ce5f3238c352e4d059615641dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Sun, 22 Feb 2026 03:44:44 -0600 Subject: [PATCH] feat(identity): add policy contract integration - WHY: enforce identity-only contract boundaries for policy orchestration - WHAT: add thalos-owned policy contracts, adapters, and grpc translation surfaces - RULE: apply workspace dependency graph and identity ownership constraints --- docs/architecture/thalos-service-modules.puml | 17 +++++-- docs/identity/token-policy-and-use-cases.md | 12 ++++- .../IIdentityCapabilityContractAdapter.cs | 26 ++++++++++ .../IIdentityPolicyGrpcContractAdapter.cs | 24 +++++++++ .../EvaluateIdentityPolicyGrpcContract.cs | 9 ++++ .../Ports/IIdentityPolicyContextReadPort.cs | 16 ++++++ .../UseCases/EvaluateIdentityPolicyUseCase.cs | 23 +++++++++ .../IEvaluateIdentityPolicyUseCase.cs | 16 ++++++ .../EvaluateIdentityPolicyRequest.cs | 9 ++++ .../EvaluateIdentityPolicyResponse.cs | 9 ++++ .../Contracts/IdentityPolicyContextRequest.cs | 9 ++++ .../IdentityPolicyContextResponse.cs | 9 ++++ .../ThalosIdentityPackageContract.cs | 15 ++++++ ...halos.Service.Identity.Abstractions.csproj | 3 ++ .../EvaluateIdentityPolicyUseCaseTests.cs | 49 +++++++++++++++++++ 15 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 src/Thalos.Service.Application/Adapters/IIdentityCapabilityContractAdapter.cs create mode 100644 src/Thalos.Service.Application/Adapters/IIdentityPolicyGrpcContractAdapter.cs create mode 100644 src/Thalos.Service.Application/Grpc/EvaluateIdentityPolicyGrpcContract.cs create mode 100644 src/Thalos.Service.Application/Ports/IIdentityPolicyContextReadPort.cs create mode 100644 src/Thalos.Service.Application/UseCases/EvaluateIdentityPolicyUseCase.cs create mode 100644 src/Thalos.Service.Application/UseCases/IEvaluateIdentityPolicyUseCase.cs create mode 100644 src/Thalos.Service.Identity.Abstractions/Contracts/EvaluateIdentityPolicyRequest.cs create mode 100644 src/Thalos.Service.Identity.Abstractions/Contracts/EvaluateIdentityPolicyResponse.cs create mode 100644 src/Thalos.Service.Identity.Abstractions/Contracts/IdentityPolicyContextRequest.cs create mode 100644 src/Thalos.Service.Identity.Abstractions/Contracts/IdentityPolicyContextResponse.cs create mode 100644 src/Thalos.Service.Identity.Abstractions/Contracts/ThalosIdentityPackageContract.cs create mode 100644 tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs diff --git a/docs/architecture/thalos-service-modules.puml b/docs/architecture/thalos-service-modules.puml index bc81619..e168b89 100644 --- a/docs/architecture/thalos-service-modules.puml +++ b/docs/architecture/thalos-service-modules.puml @@ -5,25 +5,32 @@ package "thalos-service" { package "Thalos.Service.Identity.Abstractions" { class IssueIdentityTokenRequest class IssueIdentityTokenResponse + class EvaluateIdentityPolicyRequest + class EvaluateIdentityPolicyResponse + class IdentityPolicyContextRequest + class IdentityPolicyContextResponse + class ThalosIdentityPackageContract interface IdentityAbstractionBoundary } package "Thalos.Service.Application" { interface IIssueIdentityTokenUseCase class IssueIdentityTokenUseCase + interface IEvaluateIdentityPolicyUseCase + class EvaluateIdentityPolicyUseCase interface IIdentityTokenReadPort + interface IIdentityPolicyContextReadPort + interface IIdentityCapabilityContractAdapter + interface IIdentityPolicyGrpcContractAdapter } package "Thalos.Service.Grpc" { class Program } - - IssueIdentityTokenUseCase ..|> IIssueIdentityTokenUseCase - IssueIdentityTokenUseCase --> IIdentityTokenReadPort - IIssueIdentityTokenUseCase --> IssueIdentityTokenRequest - IIssueIdentityTokenUseCase --> IssueIdentityTokenResponse } package "thalos-dal" as ThalosDal + +IIdentityPolicyContextReadPort ..> ThalosDal IIdentityTokenReadPort ..> ThalosDal @enduml diff --git a/docs/identity/token-policy-and-use-cases.md b/docs/identity/token-policy-and-use-cases.md index 15559fe..0469f0d 100644 --- a/docs/identity/token-policy-and-use-cases.md +++ b/docs/identity/token-policy-and-use-cases.md @@ -3,10 +3,18 @@ ## Use-Case Boundaries - `IIssueIdentityTokenUseCase`: orchestrates token issuance behavior. +- `IEvaluateIdentityPolicyUseCase`: orchestrates policy evaluation behavior. - `IIdentityTokenReadPort`: DAL-facing identity token boundary. +- `IIdentityPolicyContextReadPort`: DAL/integration-facing identity policy context boundary. + +## Contract Integration + +- Policy orchestration uses Thalos-owned transport-neutral identity contracts. +- gRPC translation boundaries are isolated behind `IIdentityPolicyGrpcContractAdapter`. +- Service contracts remain transport-neutral at the application boundary. ## Policy Baseline -- Token issuance policy is orchestrated in service use cases. -- Data retrieval and persistence details remain in thalos-dal. +- Token issuance and policy evaluation are orchestrated in service use cases. +- Data retrieval and persistence details remain in thalos-dal and identity adapters. - Protocol adaptation remains outside use-case logic. diff --git a/src/Thalos.Service.Application/Adapters/IIdentityCapabilityContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IIdentityCapabilityContractAdapter.cs new file mode 100644 index 0000000..8ac98a7 --- /dev/null +++ b/src/Thalos.Service.Application/Adapters/IIdentityCapabilityContractAdapter.cs @@ -0,0 +1,26 @@ +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.Adapters; + +/// +/// Defines adapter boundary for integrating identity contracts into policy use cases. +/// +public interface IIdentityCapabilityContractAdapter +{ + /// + /// Creates a transport-neutral context request for policy evaluation. + /// + /// Identity policy request. + /// Identity policy context request. + IdentityPolicyContextRequest CreatePolicyContext(EvaluateIdentityPolicyRequest identityRequest); + + /// + /// Maps policy context response into identity policy response. + /// + /// Identity policy request. + /// Identity policy context response. + /// Identity policy response. + EvaluateIdentityPolicyResponse MapPolicyResponse( + EvaluateIdentityPolicyRequest identityRequest, + IdentityPolicyContextResponse contextResponse); +} diff --git a/src/Thalos.Service.Application/Adapters/IIdentityPolicyGrpcContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IIdentityPolicyGrpcContractAdapter.cs new file mode 100644 index 0000000..1eee486 --- /dev/null +++ b/src/Thalos.Service.Application/Adapters/IIdentityPolicyGrpcContractAdapter.cs @@ -0,0 +1,24 @@ +using Thalos.Service.Application.Grpc; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.Adapters; + +/// +/// Defines adapter boundary for gRPC contract translation of identity policy flows. +/// +public interface IIdentityPolicyGrpcContractAdapter +{ + /// + /// Maps transport-neutral request into gRPC contract shape. + /// + /// Identity policy request. + /// gRPC policy contract. + EvaluateIdentityPolicyGrpcContract ToGrpc(EvaluateIdentityPolicyRequest request); + + /// + /// Maps gRPC contract into transport-neutral request. + /// + /// gRPC policy contract. + /// Identity policy request. + EvaluateIdentityPolicyRequest FromGrpc(EvaluateIdentityPolicyGrpcContract contract); +} diff --git a/src/Thalos.Service.Application/Grpc/EvaluateIdentityPolicyGrpcContract.cs b/src/Thalos.Service.Application/Grpc/EvaluateIdentityPolicyGrpcContract.cs new file mode 100644 index 0000000..8fd286d --- /dev/null +++ b/src/Thalos.Service.Application/Grpc/EvaluateIdentityPolicyGrpcContract.cs @@ -0,0 +1,9 @@ +namespace Thalos.Service.Application.Grpc; + +/// +/// Defines minimal gRPC contract shape for identity policy adapter translation. +/// +/// Identity subject identifier. +/// Tenant scope identifier. +/// Permission code to evaluate. +public sealed record EvaluateIdentityPolicyGrpcContract(string SubjectId, string TenantId, string PermissionCode); diff --git a/src/Thalos.Service.Application/Ports/IIdentityPolicyContextReadPort.cs b/src/Thalos.Service.Application/Ports/IIdentityPolicyContextReadPort.cs new file mode 100644 index 0000000..45f8b4e --- /dev/null +++ b/src/Thalos.Service.Application/Ports/IIdentityPolicyContextReadPort.cs @@ -0,0 +1,16 @@ +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.Ports; + +/// +/// Defines DAL/integration read boundary for identity policy context contracts. +/// +public interface IIdentityPolicyContextReadPort +{ + /// + /// Reads identity policy context for policy evaluation. + /// + /// Identity policy context request. + /// Identity policy context response. + Task ReadPolicyContextAsync(IdentityPolicyContextRequest request); +} diff --git a/src/Thalos.Service.Application/UseCases/EvaluateIdentityPolicyUseCase.cs b/src/Thalos.Service.Application/UseCases/EvaluateIdentityPolicyUseCase.cs new file mode 100644 index 0000000..4255e88 --- /dev/null +++ b/src/Thalos.Service.Application/UseCases/EvaluateIdentityPolicyUseCase.cs @@ -0,0 +1,23 @@ +using Thalos.Service.Application.Adapters; +using Thalos.Service.Application.Ports; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.UseCases; + +/// +/// Default orchestration implementation for identity policy evaluation. +/// +public sealed class EvaluateIdentityPolicyUseCase( + IIdentityCapabilityContractAdapter contractAdapter, + IIdentityPolicyContextReadPort policyContextReadPort) + : IEvaluateIdentityPolicyUseCase +{ + /// + public async Task HandleAsync(EvaluateIdentityPolicyRequest request) + { + var policyContextRequest = contractAdapter.CreatePolicyContext(request); + var policyContextResponse = await policyContextReadPort.ReadPolicyContextAsync(policyContextRequest); + + return contractAdapter.MapPolicyResponse(request, policyContextResponse); + } +} diff --git a/src/Thalos.Service.Application/UseCases/IEvaluateIdentityPolicyUseCase.cs b/src/Thalos.Service.Application/UseCases/IEvaluateIdentityPolicyUseCase.cs new file mode 100644 index 0000000..e503608 --- /dev/null +++ b/src/Thalos.Service.Application/UseCases/IEvaluateIdentityPolicyUseCase.cs @@ -0,0 +1,16 @@ +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.UseCases; + +/// +/// Defines orchestration boundary for identity policy evaluation. +/// +public interface IEvaluateIdentityPolicyUseCase +{ + /// + /// Handles identity policy evaluation. + /// + /// Identity policy request contract. + /// Identity policy response contract. + Task HandleAsync(EvaluateIdentityPolicyRequest request); +} diff --git a/src/Thalos.Service.Identity.Abstractions/Contracts/EvaluateIdentityPolicyRequest.cs b/src/Thalos.Service.Identity.Abstractions/Contracts/EvaluateIdentityPolicyRequest.cs new file mode 100644 index 0000000..1f8be61 --- /dev/null +++ b/src/Thalos.Service.Identity.Abstractions/Contracts/EvaluateIdentityPolicyRequest.cs @@ -0,0 +1,9 @@ +namespace Thalos.Service.Identity.Abstractions.Contracts; + +/// +/// Transport-neutral request contract for identity policy evaluation. +/// +/// Identity subject identifier. +/// Tenant scope identifier. +/// Permission code to evaluate. +public sealed record EvaluateIdentityPolicyRequest(string SubjectId, string TenantId, string PermissionCode); diff --git a/src/Thalos.Service.Identity.Abstractions/Contracts/EvaluateIdentityPolicyResponse.cs b/src/Thalos.Service.Identity.Abstractions/Contracts/EvaluateIdentityPolicyResponse.cs new file mode 100644 index 0000000..e32eb0a --- /dev/null +++ b/src/Thalos.Service.Identity.Abstractions/Contracts/EvaluateIdentityPolicyResponse.cs @@ -0,0 +1,9 @@ +namespace Thalos.Service.Identity.Abstractions.Contracts; + +/// +/// Transport-neutral response contract for identity policy evaluation. +/// +/// Identity subject identifier. +/// Permission code evaluated. +/// Policy result. +public sealed record EvaluateIdentityPolicyResponse(string SubjectId, string PermissionCode, bool IsAllowed); diff --git a/src/Thalos.Service.Identity.Abstractions/Contracts/IdentityPolicyContextRequest.cs b/src/Thalos.Service.Identity.Abstractions/Contracts/IdentityPolicyContextRequest.cs new file mode 100644 index 0000000..3041628 --- /dev/null +++ b/src/Thalos.Service.Identity.Abstractions/Contracts/IdentityPolicyContextRequest.cs @@ -0,0 +1,9 @@ +namespace Thalos.Service.Identity.Abstractions.Contracts; + +/// +/// Transport-neutral request contract for identity policy context retrieval. +/// +/// Identity subject identifier. +/// Tenant scope identifier. +/// Permission code to evaluate. +public sealed record IdentityPolicyContextRequest(string SubjectId, string TenantId, string PermissionCode); diff --git a/src/Thalos.Service.Identity.Abstractions/Contracts/IdentityPolicyContextResponse.cs b/src/Thalos.Service.Identity.Abstractions/Contracts/IdentityPolicyContextResponse.cs new file mode 100644 index 0000000..05baa75 --- /dev/null +++ b/src/Thalos.Service.Identity.Abstractions/Contracts/IdentityPolicyContextResponse.cs @@ -0,0 +1,9 @@ +namespace Thalos.Service.Identity.Abstractions.Contracts; + +/// +/// Transport-neutral response contract for identity policy context retrieval. +/// +/// Identity subject identifier. +/// Permission code evaluated. +/// Indicates whether context satisfies policy preconditions. +public sealed record IdentityPolicyContextResponse(string SubjectId, string PermissionCode, bool ContextSatisfied); diff --git a/src/Thalos.Service.Identity.Abstractions/Contracts/ThalosIdentityPackageContract.cs b/src/Thalos.Service.Identity.Abstractions/Contracts/ThalosIdentityPackageContract.cs new file mode 100644 index 0000000..160aacd --- /dev/null +++ b/src/Thalos.Service.Identity.Abstractions/Contracts/ThalosIdentityPackageContract.cs @@ -0,0 +1,15 @@ +using Core.Blueprint.Common.Contracts; + +namespace Thalos.Service.Identity.Abstractions.Contracts; + +/// +/// Defines package descriptor metadata for Thalos identity abstractions. +/// +public sealed class ThalosIdentityPackageContract : IBlueprintPackageContract +{ + /// + public BlueprintPackageDescriptor Descriptor { get; } = new( + "Thalos.Service.Identity.Abstractions", + PackageVersionPolicy.Minor, + ["Core.Blueprint.Common"]); +} diff --git a/src/Thalos.Service.Identity.Abstractions/Thalos.Service.Identity.Abstractions.csproj b/src/Thalos.Service.Identity.Abstractions/Thalos.Service.Identity.Abstractions.csproj index 6c3a887..04a4bbc 100644 --- a/src/Thalos.Service.Identity.Abstractions/Thalos.Service.Identity.Abstractions.csproj +++ b/src/Thalos.Service.Identity.Abstractions/Thalos.Service.Identity.Abstractions.csproj @@ -4,4 +4,7 @@ enable enable + + + diff --git a/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs b/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs new file mode 100644 index 0000000..8cb71a9 --- /dev/null +++ b/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs @@ -0,0 +1,49 @@ +using Thalos.Service.Application.Adapters; +using Thalos.Service.Application.Ports; +using Thalos.Service.Application.UseCases; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.UnitTests; + +public class EvaluateIdentityPolicyUseCaseTests +{ + [Fact] + public async Task HandleAsync_WhenCalled_UsesIdentityContractsAndReturnsMappedResponse() + { + var useCase = new EvaluateIdentityPolicyUseCase( + new FakeIdentityCapabilityContractAdapter(), + new FakeIdentityPolicyContextReadPort()); + + var response = await useCase.HandleAsync(new EvaluateIdentityPolicyRequest("subject-1", "tenant-1", "perm.read")); + + Assert.Equal("subject-1", response.SubjectId); + Assert.Equal("perm.read", response.PermissionCode); + Assert.True(response.IsAllowed); + } + + private sealed class FakeIdentityCapabilityContractAdapter : IIdentityCapabilityContractAdapter + { + public IdentityPolicyContextRequest CreatePolicyContext(EvaluateIdentityPolicyRequest identityRequest) + { + return new IdentityPolicyContextRequest(identityRequest.SubjectId, identityRequest.TenantId, identityRequest.PermissionCode); + } + + public EvaluateIdentityPolicyResponse MapPolicyResponse( + EvaluateIdentityPolicyRequest identityRequest, + IdentityPolicyContextResponse contextResponse) + { + return new EvaluateIdentityPolicyResponse( + identityRequest.SubjectId, + identityRequest.PermissionCode, + contextResponse.ContextSatisfied); + } + } + + private sealed class FakeIdentityPolicyContextReadPort : IIdentityPolicyContextReadPort + { + public Task ReadPolicyContextAsync(IdentityPolicyContextRequest request) + { + return Task.FromResult(new IdentityPolicyContextResponse(request.SubjectId, request.PermissionCode, true)); + } + } +}