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
This commit is contained in:
José René White Enciso 2026-02-22 03:44:44 -06:00
parent 8dd1ee67d0
commit ab4013fcf4
15 changed files with 239 additions and 7 deletions

View File

@ -5,25 +5,32 @@ package "thalos-service" {
package "Thalos.Service.Identity.Abstractions" { package "Thalos.Service.Identity.Abstractions" {
class IssueIdentityTokenRequest class IssueIdentityTokenRequest
class IssueIdentityTokenResponse class IssueIdentityTokenResponse
class EvaluateIdentityPolicyRequest
class EvaluateIdentityPolicyResponse
class IdentityPolicyContextRequest
class IdentityPolicyContextResponse
class ThalosIdentityPackageContract
interface IdentityAbstractionBoundary interface IdentityAbstractionBoundary
} }
package "Thalos.Service.Application" { package "Thalos.Service.Application" {
interface IIssueIdentityTokenUseCase interface IIssueIdentityTokenUseCase
class IssueIdentityTokenUseCase class IssueIdentityTokenUseCase
interface IEvaluateIdentityPolicyUseCase
class EvaluateIdentityPolicyUseCase
interface IIdentityTokenReadPort interface IIdentityTokenReadPort
interface IIdentityPolicyContextReadPort
interface IIdentityCapabilityContractAdapter
interface IIdentityPolicyGrpcContractAdapter
} }
package "Thalos.Service.Grpc" { package "Thalos.Service.Grpc" {
class Program class Program
} }
IssueIdentityTokenUseCase ..|> IIssueIdentityTokenUseCase
IssueIdentityTokenUseCase --> IIdentityTokenReadPort
IIssueIdentityTokenUseCase --> IssueIdentityTokenRequest
IIssueIdentityTokenUseCase --> IssueIdentityTokenResponse
} }
package "thalos-dal" as ThalosDal package "thalos-dal" as ThalosDal
IIdentityPolicyContextReadPort ..> ThalosDal
IIdentityTokenReadPort ..> ThalosDal IIdentityTokenReadPort ..> ThalosDal
@enduml @enduml

View File

@ -3,10 +3,18 @@
## Use-Case Boundaries ## Use-Case Boundaries
- `IIssueIdentityTokenUseCase`: orchestrates token issuance behavior. - `IIssueIdentityTokenUseCase`: orchestrates token issuance behavior.
- `IEvaluateIdentityPolicyUseCase`: orchestrates policy evaluation behavior.
- `IIdentityTokenReadPort`: DAL-facing identity token boundary. - `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 ## Policy Baseline
- Token issuance policy is orchestrated in service use cases. - Token issuance and policy evaluation are orchestrated in service use cases.
- Data retrieval and persistence details remain in thalos-dal. - Data retrieval and persistence details remain in thalos-dal and identity adapters.
- Protocol adaptation remains outside use-case logic. - Protocol adaptation remains outside use-case logic.

View File

@ -0,0 +1,26 @@
using Thalos.Service.Identity.Abstractions.Contracts;
namespace Thalos.Service.Application.Adapters;
/// <summary>
/// Defines adapter boundary for integrating identity contracts into policy use cases.
/// </summary>
public interface IIdentityCapabilityContractAdapter
{
/// <summary>
/// Creates a transport-neutral context request for policy evaluation.
/// </summary>
/// <param name="identityRequest">Identity policy request.</param>
/// <returns>Identity policy context request.</returns>
IdentityPolicyContextRequest CreatePolicyContext(EvaluateIdentityPolicyRequest identityRequest);
/// <summary>
/// Maps policy context response into identity policy response.
/// </summary>
/// <param name="identityRequest">Identity policy request.</param>
/// <param name="contextResponse">Identity policy context response.</param>
/// <returns>Identity policy response.</returns>
EvaluateIdentityPolicyResponse MapPolicyResponse(
EvaluateIdentityPolicyRequest identityRequest,
IdentityPolicyContextResponse contextResponse);
}

View File

@ -0,0 +1,24 @@
using Thalos.Service.Application.Grpc;
using Thalos.Service.Identity.Abstractions.Contracts;
namespace Thalos.Service.Application.Adapters;
/// <summary>
/// Defines adapter boundary for gRPC contract translation of identity policy flows.
/// </summary>
public interface IIdentityPolicyGrpcContractAdapter
{
/// <summary>
/// Maps transport-neutral request into gRPC contract shape.
/// </summary>
/// <param name="request">Identity policy request.</param>
/// <returns>gRPC policy contract.</returns>
EvaluateIdentityPolicyGrpcContract ToGrpc(EvaluateIdentityPolicyRequest request);
/// <summary>
/// Maps gRPC contract into transport-neutral request.
/// </summary>
/// <param name="contract">gRPC policy contract.</param>
/// <returns>Identity policy request.</returns>
EvaluateIdentityPolicyRequest FromGrpc(EvaluateIdentityPolicyGrpcContract contract);
}

View File

@ -0,0 +1,9 @@
namespace Thalos.Service.Application.Grpc;
/// <summary>
/// Defines minimal gRPC contract shape for identity policy adapter translation.
/// </summary>
/// <param name="SubjectId">Identity subject identifier.</param>
/// <param name="TenantId">Tenant scope identifier.</param>
/// <param name="PermissionCode">Permission code to evaluate.</param>
public sealed record EvaluateIdentityPolicyGrpcContract(string SubjectId, string TenantId, string PermissionCode);

View File

@ -0,0 +1,16 @@
using Thalos.Service.Identity.Abstractions.Contracts;
namespace Thalos.Service.Application.Ports;
/// <summary>
/// Defines DAL/integration read boundary for identity policy context contracts.
/// </summary>
public interface IIdentityPolicyContextReadPort
{
/// <summary>
/// Reads identity policy context for policy evaluation.
/// </summary>
/// <param name="request">Identity policy context request.</param>
/// <returns>Identity policy context response.</returns>
Task<IdentityPolicyContextResponse> ReadPolicyContextAsync(IdentityPolicyContextRequest request);
}

View File

@ -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;
/// <summary>
/// Default orchestration implementation for identity policy evaluation.
/// </summary>
public sealed class EvaluateIdentityPolicyUseCase(
IIdentityCapabilityContractAdapter contractAdapter,
IIdentityPolicyContextReadPort policyContextReadPort)
: IEvaluateIdentityPolicyUseCase
{
/// <inheritdoc />
public async Task<EvaluateIdentityPolicyResponse> HandleAsync(EvaluateIdentityPolicyRequest request)
{
var policyContextRequest = contractAdapter.CreatePolicyContext(request);
var policyContextResponse = await policyContextReadPort.ReadPolicyContextAsync(policyContextRequest);
return contractAdapter.MapPolicyResponse(request, policyContextResponse);
}
}

View File

@ -0,0 +1,16 @@
using Thalos.Service.Identity.Abstractions.Contracts;
namespace Thalos.Service.Application.UseCases;
/// <summary>
/// Defines orchestration boundary for identity policy evaluation.
/// </summary>
public interface IEvaluateIdentityPolicyUseCase
{
/// <summary>
/// Handles identity policy evaluation.
/// </summary>
/// <param name="request">Identity policy request contract.</param>
/// <returns>Identity policy response contract.</returns>
Task<EvaluateIdentityPolicyResponse> HandleAsync(EvaluateIdentityPolicyRequest request);
}

View File

@ -0,0 +1,9 @@
namespace Thalos.Service.Identity.Abstractions.Contracts;
/// <summary>
/// Transport-neutral request contract for identity policy evaluation.
/// </summary>
/// <param name="SubjectId">Identity subject identifier.</param>
/// <param name="TenantId">Tenant scope identifier.</param>
/// <param name="PermissionCode">Permission code to evaluate.</param>
public sealed record EvaluateIdentityPolicyRequest(string SubjectId, string TenantId, string PermissionCode);

View File

@ -0,0 +1,9 @@
namespace Thalos.Service.Identity.Abstractions.Contracts;
/// <summary>
/// Transport-neutral response contract for identity policy evaluation.
/// </summary>
/// <param name="SubjectId">Identity subject identifier.</param>
/// <param name="PermissionCode">Permission code evaluated.</param>
/// <param name="IsAllowed">Policy result.</param>
public sealed record EvaluateIdentityPolicyResponse(string SubjectId, string PermissionCode, bool IsAllowed);

View File

@ -0,0 +1,9 @@
namespace Thalos.Service.Identity.Abstractions.Contracts;
/// <summary>
/// Transport-neutral request contract for identity policy context retrieval.
/// </summary>
/// <param name="SubjectId">Identity subject identifier.</param>
/// <param name="TenantId">Tenant scope identifier.</param>
/// <param name="PermissionCode">Permission code to evaluate.</param>
public sealed record IdentityPolicyContextRequest(string SubjectId, string TenantId, string PermissionCode);

View File

@ -0,0 +1,9 @@
namespace Thalos.Service.Identity.Abstractions.Contracts;
/// <summary>
/// Transport-neutral response contract for identity policy context retrieval.
/// </summary>
/// <param name="SubjectId">Identity subject identifier.</param>
/// <param name="PermissionCode">Permission code evaluated.</param>
/// <param name="ContextSatisfied">Indicates whether context satisfies policy preconditions.</param>
public sealed record IdentityPolicyContextResponse(string SubjectId, string PermissionCode, bool ContextSatisfied);

View File

@ -0,0 +1,15 @@
using Core.Blueprint.Common.Contracts;
namespace Thalos.Service.Identity.Abstractions.Contracts;
/// <summary>
/// Defines package descriptor metadata for Thalos identity abstractions.
/// </summary>
public sealed class ThalosIdentityPackageContract : IBlueprintPackageContract
{
/// <inheritdoc />
public BlueprintPackageDescriptor Descriptor { get; } = new(
"Thalos.Service.Identity.Abstractions",
PackageVersionPolicy.Minor,
["Core.Blueprint.Common"]);
}

View File

@ -4,4 +4,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -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<IdentityPolicyContextResponse> ReadPolicyContextAsync(IdentityPolicyContextRequest request)
{
return Task.FromResult(new IdentityPolicyContextResponse(request.SubjectId, request.PermissionCode, true));
}
}
}