From 5974ce6fa638f4f26e9d4a556c6b346ce8dba3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Sun, 22 Feb 2026 17:47:35 -0600 Subject: [PATCH 1/3] feat(thalos-service): wire grpc runtime and dal adapters --- .../IdentityCapabilityContractAdapter.cs | 29 ++++++++ .../IdentityPolicyGrpcContractAdapter.cs | 22 ++++++ ...rviceRuntimeServiceCollectionExtensions.cs | 37 ++++++++++ ...IdentityPolicyContextReadPortDalAdapter.cs | 49 +++++++++++++ .../Ports/IdentityTokenReadPortDalAdapter.cs | 36 +++++++++ .../Thalos.Service.Application.csproj | 2 + src/Thalos.Service.Grpc/Program.cs | 11 ++- .../Protos/identity_runtime.proto | 32 ++++++++ .../Services/IdentityRuntimeGrpcService.cs | 62 ++++++++++++++++ .../Thalos.Service.Grpc.csproj | 8 ++ .../RuntimeWiringTests.cs | 73 +++++++++++++++++++ ...halos.Service.Application.UnitTests.csproj | 1 + 12 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 src/Thalos.Service.Application/Adapters/IdentityCapabilityContractAdapter.cs create mode 100644 src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs create mode 100644 src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs create mode 100644 src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs create mode 100644 src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs create mode 100644 src/Thalos.Service.Grpc/Protos/identity_runtime.proto create mode 100644 src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs create mode 100644 tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs diff --git a/src/Thalos.Service.Application/Adapters/IdentityCapabilityContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IdentityCapabilityContractAdapter.cs new file mode 100644 index 0000000..45c8f37 --- /dev/null +++ b/src/Thalos.Service.Application/Adapters/IdentityCapabilityContractAdapter.cs @@ -0,0 +1,29 @@ +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.Adapters; + +/// +/// Default adapter implementation for identity policy contract composition. +/// +public sealed class IdentityCapabilityContractAdapter : 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); + } +} diff --git a/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs new file mode 100644 index 0000000..fe4435d --- /dev/null +++ b/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs @@ -0,0 +1,22 @@ +using Thalos.Service.Application.Grpc; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.Adapters; + +/// +/// Default adapter implementation for identity policy gRPC contract translation. +/// +public sealed class IdentityPolicyGrpcContractAdapter : IIdentityPolicyGrpcContractAdapter +{ + /// + public EvaluateIdentityPolicyGrpcContract ToGrpc(EvaluateIdentityPolicyRequest request) + { + return new EvaluateIdentityPolicyGrpcContract(request.SubjectId, request.TenantId, request.PermissionCode); + } + + /// + public EvaluateIdentityPolicyRequest FromGrpc(EvaluateIdentityPolicyGrpcContract contract) + { + return new EvaluateIdentityPolicyRequest(contract.SubjectId, contract.TenantId, contract.PermissionCode); + } +} diff --git a/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs b/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs new file mode 100644 index 0000000..9d53fc0 --- /dev/null +++ b/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs @@ -0,0 +1,37 @@ +using Core.Blueprint.Common.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Thalos.DAL.DependencyInjection; +using Thalos.Service.Application.Adapters; +using Thalos.Service.Application.Ports; +using Thalos.Service.Application.UseCases; + +namespace Thalos.Service.Application.DependencyInjection; + +/// +/// Registers thalos-service runtime orchestration and DAL adapters. +/// +public static class ThalosServiceRuntimeServiceCollectionExtensions +{ + /// + /// Adds thalos-service runtime wiring aligned with blueprint runtime and thalos-dal runtime. + /// + /// Service collection. + /// Service collection for fluent chaining. + public static IServiceCollection AddThalosServiceRuntime(this IServiceCollection services) + { + services.AddBlueprintRuntimeCore(); + services.AddThalosDalRuntime(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs b/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs new file mode 100644 index 0000000..1e1fe2e --- /dev/null +++ b/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs @@ -0,0 +1,49 @@ +using Core.Blueprint.Common.Runtime; +using Thalos.DAL.Contracts; +using Thalos.DAL.Repositories; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.Ports; + +/// +/// Default DAL adapter for identity policy context read port. +/// +public sealed class IdentityPolicyContextReadPortDalAdapter( + IIdentityRepository identityRepository, + IBlueprintSystemClock clock) : IIdentityPolicyContextReadPort +{ + /// + public async Task ReadPolicyContextAsync(IdentityPolicyContextRequest request) + { + var policyLookupRequest = new IdentityPolicyLookupRequest( + CreateEnvelope(), + request.SubjectId, + request.TenantId, + request.PermissionCode); + + var policyRecord = await identityRepository.ReadIdentityPolicyAsync(policyLookupRequest); + if (policyRecord is null) + { + return new IdentityPolicyContextResponse(request.SubjectId, request.PermissionCode, false); + } + + var permissionSetRequest = new IdentityPermissionSetLookupRequest( + policyLookupRequest.Envelope, + request.SubjectId, + request.TenantId); + + var permissions = await identityRepository.ReadPermissionSetAsync(permissionSetRequest); + var permissionMatched = permissions.Any(permission => + string.Equals(permission.PermissionCode, request.PermissionCode, StringComparison.OrdinalIgnoreCase)); + + return new IdentityPolicyContextResponse( + request.SubjectId, + request.PermissionCode, + policyRecord.ContextSatisfied && permissionMatched); + } + + private IdentityContractEnvelope CreateEnvelope() + { + return new IdentityContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}"); + } +} diff --git a/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs b/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs new file mode 100644 index 0000000..0cc98ed --- /dev/null +++ b/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs @@ -0,0 +1,36 @@ +using Core.Blueprint.Common.Runtime; +using Thalos.DAL.Contracts; +using Thalos.DAL.Repositories; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.Ports; + +/// +/// Default DAL adapter for identity token read port. +/// +public sealed class IdentityTokenReadPortDalAdapter( + IIdentityRepository identityRepository, + IBlueprintSystemClock clock) : IIdentityTokenReadPort +{ + /// + public async Task IssueTokenAsync(IssueIdentityTokenRequest request) + { + var lookupRequest = new IdentityTokenLookupRequest( + CreateEnvelope(), + request.SubjectId, + request.TenantId); + + var tokenRecord = await identityRepository.ReadIdentityTokenAsync(lookupRequest); + if (tokenRecord is null) + { + return new IssueIdentityTokenResponse(string.Empty, 0); + } + + return new IssueIdentityTokenResponse(tokenRecord.Token, tokenRecord.ExpiresInSeconds); + } + + private IdentityContractEnvelope CreateEnvelope() + { + return new IdentityContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}"); + } +} diff --git a/src/Thalos.Service.Application/Thalos.Service.Application.csproj b/src/Thalos.Service.Application/Thalos.Service.Application.csproj index 032d28e..bdb0db0 100644 --- a/src/Thalos.Service.Application/Thalos.Service.Application.csproj +++ b/src/Thalos.Service.Application/Thalos.Service.Application.csproj @@ -5,6 +5,8 @@ enable + + diff --git a/src/Thalos.Service.Grpc/Program.cs b/src/Thalos.Service.Grpc/Program.cs index 5af6dbd..f52b4c2 100644 --- a/src/Thalos.Service.Grpc/Program.cs +++ b/src/Thalos.Service.Grpc/Program.cs @@ -1,6 +1,15 @@ +using Thalos.Service.Application.DependencyInjection; +using Thalos.Service.Grpc.Services; + var builder = WebApplication.CreateBuilder(args); -// Stage 3 skeleton: single active internal protocol policy is gRPC-first. +builder.Services.AddGrpc(); +builder.Services.AddHealthChecks(); +builder.Services.AddThalosServiceRuntime(); + var app = builder.Build(); +app.MapGrpcService(); +app.MapHealthChecks("/healthz"); + app.Run(); diff --git a/src/Thalos.Service.Grpc/Protos/identity_runtime.proto b/src/Thalos.Service.Grpc/Protos/identity_runtime.proto new file mode 100644 index 0000000..80fcbce --- /dev/null +++ b/src/Thalos.Service.Grpc/Protos/identity_runtime.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +option csharp_namespace = "Thalos.Service.Grpc"; + +package thalos.service.grpc; + +service IdentityRuntime { + rpc IssueIdentityToken (IssueIdentityTokenGrpcRequest) returns (IssueIdentityTokenGrpcResponse); + rpc EvaluateIdentityPolicy (EvaluateIdentityPolicyGrpcRequest) returns (EvaluateIdentityPolicyGrpcResponse); +} + +message IssueIdentityTokenGrpcRequest { + string subject_id = 1; + string tenant_id = 2; +} + +message IssueIdentityTokenGrpcResponse { + string token = 1; + int32 expires_in_seconds = 2; +} + +message EvaluateIdentityPolicyGrpcRequest { + string subject_id = 1; + string tenant_id = 2; + string permission_code = 3; +} + +message EvaluateIdentityPolicyGrpcResponse { + string subject_id = 1; + string permission_code = 2; + bool is_allowed = 3; +} diff --git a/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs b/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs new file mode 100644 index 0000000..948cfb6 --- /dev/null +++ b/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs @@ -0,0 +1,62 @@ +using Grpc.Core; +using Thalos.Service.Application.Adapters; +using Thalos.Service.Application.Grpc; +using Thalos.Service.Application.UseCases; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Grpc.Services; + +/// +/// Internal gRPC endpoint implementation for identity runtime operations. +/// +public sealed class IdentityRuntimeGrpcService( + IIssueIdentityTokenUseCase issueIdentityTokenUseCase, + IEvaluateIdentityPolicyUseCase evaluateIdentityPolicyUseCase, + IIdentityPolicyGrpcContractAdapter grpcContractAdapter) : IdentityRuntime.IdentityRuntimeBase +{ + /// + /// Issues identity token through service use-case orchestration. + /// + /// gRPC token issuance request. + /// gRPC server call context. + /// gRPC token issuance response. + public override async Task IssueIdentityToken( + IssueIdentityTokenGrpcRequest request, + ServerCallContext context) + { + var useCaseRequest = new IssueIdentityTokenRequest(request.SubjectId, request.TenantId); + var useCaseResponse = await issueIdentityTokenUseCase.HandleAsync(useCaseRequest); + + return new IssueIdentityTokenGrpcResponse + { + Token = useCaseResponse.Token, + ExpiresInSeconds = useCaseResponse.ExpiresInSeconds + }; + } + + /// + /// Evaluates identity policy through service use-case orchestration. + /// + /// gRPC policy evaluation request. + /// gRPC server call context. + /// gRPC policy evaluation response. + public override async Task EvaluateIdentityPolicy( + EvaluateIdentityPolicyGrpcRequest request, + ServerCallContext context) + { + var grpcContract = new EvaluateIdentityPolicyGrpcContract( + request.SubjectId, + request.TenantId, + request.PermissionCode); + + var useCaseRequest = grpcContractAdapter.FromGrpc(grpcContract); + var useCaseResponse = await evaluateIdentityPolicyUseCase.HandleAsync(useCaseRequest); + + return new EvaluateIdentityPolicyGrpcResponse + { + SubjectId = useCaseResponse.SubjectId, + PermissionCode = useCaseResponse.PermissionCode, + IsAllowed = useCaseResponse.IsAllowed + }; + } +} diff --git a/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj b/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj index b7f7de2..4db7838 100644 --- a/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj +++ b/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj @@ -5,6 +5,14 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs b/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs new file mode 100644 index 0000000..fee3f25 --- /dev/null +++ b/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.DependencyInjection; +using Thalos.Service.Application.Adapters; +using Thalos.Service.Application.DependencyInjection; +using Thalos.Service.Application.Grpc; +using Thalos.Service.Application.UseCases; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Service.Application.UnitTests; + +public class RuntimeWiringTests +{ + [Fact] + public async Task AddThalosServiceRuntime_WhenInvoked_ResolvesUseCases() + { + var services = new ServiceCollection(); + services.AddThalosServiceRuntime(); + + using var provider = services.BuildServiceProvider(); + var issueTokenUseCase = provider.GetRequiredService(); + var evaluatePolicyUseCase = provider.GetRequiredService(); + + var tokenResponse = await issueTokenUseCase.HandleAsync(new IssueIdentityTokenRequest("user-1", "tenant-1")); + var policyResponse = await evaluatePolicyUseCase.HandleAsync( + new EvaluateIdentityPolicyRequest("user-1", "tenant-1", "identity.token.issue")); + + Assert.Equal("user-1:tenant-1:token", tokenResponse.Token); + Assert.Equal(1800, tokenResponse.ExpiresInSeconds); + Assert.True(policyResponse.IsAllowed); + } + + [Fact] + public async Task AddThalosServiceRuntime_WhenSubjectMissing_ReturnsEmptyToken() + { + var services = new ServiceCollection(); + services.AddThalosServiceRuntime(); + + using var provider = services.BuildServiceProvider(); + var issueTokenUseCase = provider.GetRequiredService(); + + var tokenResponse = await issueTokenUseCase.HandleAsync( + new IssueIdentityTokenRequest("missing-user", "tenant-1")); + + Assert.Equal(string.Empty, tokenResponse.Token); + Assert.Equal(0, tokenResponse.ExpiresInSeconds); + } + + [Fact] + public void IdentityPolicyGrpcContractAdapter_WhenMapped_PreservesValues() + { + var adapter = new IdentityPolicyGrpcContractAdapter(); + var useCaseRequest = new EvaluateIdentityPolicyRequest("user-2", "tenant-2", "identity.policy.evaluate"); + + var grpcContract = adapter.ToGrpc(useCaseRequest); + var roundtrip = adapter.FromGrpc(grpcContract); + + Assert.Equal("user-2", roundtrip.SubjectId); + Assert.Equal("tenant-2", roundtrip.TenantId); + Assert.Equal("identity.policy.evaluate", roundtrip.PermissionCode); + } + + [Fact] + public void IdentityPolicyGrpcContractAdapter_WhenFromGrpc_UsesExpectedContractShape() + { + var adapter = new IdentityPolicyGrpcContractAdapter(); + var contract = new EvaluateIdentityPolicyGrpcContract("subject-9", "tenant-9", "identity.token.issue"); + + var request = adapter.FromGrpc(contract); + + Assert.Equal("subject-9", request.SubjectId); + Assert.Equal("tenant-9", request.TenantId); + Assert.Equal("identity.token.issue", request.PermissionCode); + } +} diff --git a/tests/Thalos.Service.Application.UnitTests/Thalos.Service.Application.UnitTests.csproj b/tests/Thalos.Service.Application.UnitTests/Thalos.Service.Application.UnitTests.csproj index a887a0c..a08db62 100644 --- a/tests/Thalos.Service.Application.UnitTests/Thalos.Service.Application.UnitTests.csproj +++ b/tests/Thalos.Service.Application.UnitTests/Thalos.Service.Application.UnitTests.csproj @@ -7,6 +7,7 @@ + From 654a808c54a2e9648470edd507dcccf7aa4fc0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Tue, 24 Feb 2026 05:26:54 -0600 Subject: [PATCH 2/3] refactor(thalos-service): delegate to domain --- .../service-orchestration-boundary.md | 13 +++++++ docs/migration/domain-delegation-plan.md | 10 ++++++ .../identity-service-regression-checks.md | 10 ++++++ .../IIdentityCapabilityContractAdapter.cs | 26 -------------- .../IIdentityPolicyGrpcContractAdapter.cs | 2 +- .../IdentityCapabilityContractAdapter.cs | 29 ---------------- .../IdentityPolicyGrpcContractAdapter.cs | 2 +- ...rviceRuntimeServiceCollectionExtensions.cs | 4 ++- .../Ports/IIdentityPolicyContextReadPort.cs | 5 +-- .../Ports/IIdentityTokenReadPort.cs | 5 +-- ...IdentityPolicyContextReadPortDalAdapter.cs | 21 ++++++++---- .../Ports/IdentityTokenReadPortDalAdapter.cs | 9 ++--- .../Thalos.Service.Application.csproj | 3 +- .../UseCases/EvaluateIdentityPolicyUseCase.cs | 13 +++---- .../IEvaluateIdentityPolicyUseCase.cs | 3 +- .../UseCases/IIssueIdentityTokenUseCase.cs | 3 +- .../UseCases/IssueIdentityTokenUseCase.cs | 13 ++++--- .../Services/IdentityRuntimeGrpcService.cs | 2 +- .../Thalos.Service.Grpc.csproj | 1 - .../EvaluateIdentityPolicyUseCaseTests.cs | 34 +++++++++++-------- .../IssueIdentityTokenUseCaseTests.cs | 11 +++--- .../RuntimeWiringTests.cs | 2 +- ...halos.Service.Application.UnitTests.csproj | 1 - 23 files changed, 114 insertions(+), 108 deletions(-) create mode 100644 docs/architecture/service-orchestration-boundary.md create mode 100644 docs/migration/domain-delegation-plan.md create mode 100644 docs/migration/identity-service-regression-checks.md delete mode 100644 src/Thalos.Service.Application/Adapters/IIdentityCapabilityContractAdapter.cs delete mode 100644 src/Thalos.Service.Application/Adapters/IdentityCapabilityContractAdapter.cs diff --git a/docs/architecture/service-orchestration-boundary.md b/docs/architecture/service-orchestration-boundary.md new file mode 100644 index 0000000..5fb9d1d --- /dev/null +++ b/docs/architecture/service-orchestration-boundary.md @@ -0,0 +1,13 @@ +# Thalos Service Orchestration Boundary + +## Purpose +Constrain thalos-service to orchestration responsibilities after thalos-domain extraction. + +## Service Responsibilities +- Coordinate identity use-case flow +- Delegate policy/token decisions to thalos-domain abstractions +- Adapt transport contracts + +## Prohibited Responsibilities +- Owning identity decision policies +- Owning persistence decision concerns diff --git a/docs/migration/domain-delegation-plan.md b/docs/migration/domain-delegation-plan.md new file mode 100644 index 0000000..d62f61d --- /dev/null +++ b/docs/migration/domain-delegation-plan.md @@ -0,0 +1,10 @@ +# Thalos Domain Delegation Plan + +## Delegation Model +- Use cases invoke thalos-domain abstractions for policy and token decisions. +- Service adapters retain technical contract mapping only. + +## Transition Steps +1. Replace in-service decision branches with domain calls. +2. Keep service contract shapes stable. +3. Validate orchestration-only responsibilities. diff --git a/docs/migration/identity-service-regression-checks.md b/docs/migration/identity-service-regression-checks.md new file mode 100644 index 0000000..37bf0a8 --- /dev/null +++ b/docs/migration/identity-service-regression-checks.md @@ -0,0 +1,10 @@ +# Identity Service Regression Checks + +## Checks +- Service no longer contains policy/token decision branches. +- Service still orchestrates required dependencies. +- Transport contract outputs remain stable. + +## Evidence +- Updated architecture docs +- Delegation map confirmation diff --git a/src/Thalos.Service.Application/Adapters/IIdentityCapabilityContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IIdentityCapabilityContractAdapter.cs deleted file mode 100644 index 8ac98a7..0000000 --- a/src/Thalos.Service.Application/Adapters/IIdentityCapabilityContractAdapter.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 index 1eee486..d34f77c 100644 --- a/src/Thalos.Service.Application/Adapters/IIdentityPolicyGrpcContractAdapter.cs +++ b/src/Thalos.Service.Application/Adapters/IIdentityPolicyGrpcContractAdapter.cs @@ -1,5 +1,5 @@ using Thalos.Service.Application.Grpc; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; namespace Thalos.Service.Application.Adapters; diff --git a/src/Thalos.Service.Application/Adapters/IdentityCapabilityContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IdentityCapabilityContractAdapter.cs deleted file mode 100644 index 45c8f37..0000000 --- a/src/Thalos.Service.Application/Adapters/IdentityCapabilityContractAdapter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Thalos.Service.Identity.Abstractions.Contracts; - -namespace Thalos.Service.Application.Adapters; - -/// -/// Default adapter implementation for identity policy contract composition. -/// -public sealed class IdentityCapabilityContractAdapter : 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); - } -} diff --git a/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs index fe4435d..ebfde41 100644 --- a/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs +++ b/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs @@ -1,5 +1,5 @@ using Thalos.Service.Application.Grpc; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; namespace Thalos.Service.Application.Adapters; diff --git a/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs b/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs index 9d53fc0..27d9023 100644 --- a/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs +++ b/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Core.Blueprint.Common.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Thalos.Domain.Decisions; using Thalos.DAL.DependencyInjection; using Thalos.Service.Application.Adapters; using Thalos.Service.Application.Ports; @@ -22,8 +23,9 @@ public static class ThalosServiceRuntimeServiceCollectionExtensions { services.AddBlueprintRuntimeCore(); services.AddThalosDalRuntime(); + services.TryAddSingleton(); + services.TryAddSingleton(); - services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Thalos.Service.Application/Ports/IIdentityPolicyContextReadPort.cs b/src/Thalos.Service.Application/Ports/IIdentityPolicyContextReadPort.cs index 45f8b4e..3801c54 100644 --- a/src/Thalos.Service.Application/Ports/IIdentityPolicyContextReadPort.cs +++ b/src/Thalos.Service.Application/Ports/IIdentityPolicyContextReadPort.cs @@ -1,4 +1,5 @@ -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using Thalos.Domain.Contracts; namespace Thalos.Service.Application.Ports; @@ -12,5 +13,5 @@ public interface IIdentityPolicyContextReadPort /// /// Identity policy context request. /// Identity policy context response. - Task ReadPolicyContextAsync(IdentityPolicyContextRequest request); + Task ReadPolicyContextAsync(IdentityPolicyContextRequest request); } diff --git a/src/Thalos.Service.Application/Ports/IIdentityTokenReadPort.cs b/src/Thalos.Service.Application/Ports/IIdentityTokenReadPort.cs index c513044..fc4b8bf 100644 --- a/src/Thalos.Service.Application/Ports/IIdentityTokenReadPort.cs +++ b/src/Thalos.Service.Application/Ports/IIdentityTokenReadPort.cs @@ -1,4 +1,5 @@ -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using Thalos.Domain.Contracts; namespace Thalos.Service.Application.Ports; @@ -12,5 +13,5 @@ public interface IIdentityTokenReadPort /// /// Token request contract. /// Token response contract. - Task IssueTokenAsync(IssueIdentityTokenRequest request); + Task ReadTokenAsync(IssueIdentityTokenRequest request); } diff --git a/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs b/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs index 1e1fe2e..d66caa9 100644 --- a/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs +++ b/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs @@ -1,7 +1,8 @@ using Core.Blueprint.Common.Runtime; +using BuildingBlock.Identity.Contracts.Requests; using Thalos.DAL.Contracts; using Thalos.DAL.Repositories; -using Thalos.Service.Identity.Abstractions.Contracts; +using Thalos.Domain.Contracts; namespace Thalos.Service.Application.Ports; @@ -13,7 +14,7 @@ public sealed class IdentityPolicyContextReadPortDalAdapter( IBlueprintSystemClock clock) : IIdentityPolicyContextReadPort { /// - public async Task ReadPolicyContextAsync(IdentityPolicyContextRequest request) + public async Task ReadPolicyContextAsync(IdentityPolicyContextRequest request) { var policyLookupRequest = new IdentityPolicyLookupRequest( CreateEnvelope(), @@ -24,7 +25,11 @@ public sealed class IdentityPolicyContextReadPortDalAdapter( var policyRecord = await identityRepository.ReadIdentityPolicyAsync(policyLookupRequest); if (policyRecord is null) { - return new IdentityPolicyContextResponse(request.SubjectId, request.PermissionCode, false); + return new IdentityPolicyContextData( + request.SubjectId, + request.PermissionCode, + false, + []); } var permissionSetRequest = new IdentityPermissionSetLookupRequest( @@ -33,13 +38,15 @@ public sealed class IdentityPolicyContextReadPortDalAdapter( request.TenantId); var permissions = await identityRepository.ReadPermissionSetAsync(permissionSetRequest); - var permissionMatched = permissions.Any(permission => - string.Equals(permission.PermissionCode, request.PermissionCode, StringComparison.OrdinalIgnoreCase)); + var grantedPermissions = permissions + .Select(permission => permission.PermissionCode) + .ToArray(); - return new IdentityPolicyContextResponse( + return new IdentityPolicyContextData( request.SubjectId, request.PermissionCode, - policyRecord.ContextSatisfied && permissionMatched); + policyRecord.ContextSatisfied, + grantedPermissions); } private IdentityContractEnvelope CreateEnvelope() diff --git a/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs b/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs index 0cc98ed..834ec48 100644 --- a/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs +++ b/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs @@ -1,7 +1,8 @@ using Core.Blueprint.Common.Runtime; +using BuildingBlock.Identity.Contracts.Requests; using Thalos.DAL.Contracts; using Thalos.DAL.Repositories; -using Thalos.Service.Identity.Abstractions.Contracts; +using Thalos.Domain.Contracts; namespace Thalos.Service.Application.Ports; @@ -13,7 +14,7 @@ public sealed class IdentityTokenReadPortDalAdapter( IBlueprintSystemClock clock) : IIdentityTokenReadPort { /// - public async Task IssueTokenAsync(IssueIdentityTokenRequest request) + public async Task ReadTokenAsync(IssueIdentityTokenRequest request) { var lookupRequest = new IdentityTokenLookupRequest( CreateEnvelope(), @@ -23,10 +24,10 @@ public sealed class IdentityTokenReadPortDalAdapter( var tokenRecord = await identityRepository.ReadIdentityTokenAsync(lookupRequest); if (tokenRecord is null) { - return new IssueIdentityTokenResponse(string.Empty, 0); + return new IdentityTokenData(null, null); } - return new IssueIdentityTokenResponse(tokenRecord.Token, tokenRecord.ExpiresInSeconds); + return new IdentityTokenData(tokenRecord.Token, tokenRecord.ExpiresInSeconds); } private IdentityContractEnvelope CreateEnvelope() diff --git a/src/Thalos.Service.Application/Thalos.Service.Application.csproj b/src/Thalos.Service.Application/Thalos.Service.Application.csproj index bdb0db0..e2f9a55 100644 --- a/src/Thalos.Service.Application/Thalos.Service.Application.csproj +++ b/src/Thalos.Service.Application/Thalos.Service.Application.csproj @@ -6,7 +6,8 @@ + + - diff --git a/src/Thalos.Service.Application/UseCases/EvaluateIdentityPolicyUseCase.cs b/src/Thalos.Service.Application/UseCases/EvaluateIdentityPolicyUseCase.cs index 4255e88..ef645c4 100644 --- a/src/Thalos.Service.Application/UseCases/EvaluateIdentityPolicyUseCase.cs +++ b/src/Thalos.Service.Application/UseCases/EvaluateIdentityPolicyUseCase.cs @@ -1,6 +1,7 @@ -using Thalos.Service.Application.Adapters; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; using Thalos.Service.Application.Ports; -using Thalos.Service.Identity.Abstractions.Contracts; +using Thalos.Domain.Decisions; namespace Thalos.Service.Application.UseCases; @@ -8,16 +9,16 @@ namespace Thalos.Service.Application.UseCases; /// Default orchestration implementation for identity policy evaluation. /// public sealed class EvaluateIdentityPolicyUseCase( - IIdentityCapabilityContractAdapter contractAdapter, + IIdentityPolicyDecisionService decisionService, IIdentityPolicyContextReadPort policyContextReadPort) : IEvaluateIdentityPolicyUseCase { /// public async Task HandleAsync(EvaluateIdentityPolicyRequest request) { - var policyContextRequest = contractAdapter.CreatePolicyContext(request); - var policyContextResponse = await policyContextReadPort.ReadPolicyContextAsync(policyContextRequest); + var policyContextRequest = decisionService.BuildPolicyContextRequest(request); + var policyContextData = await policyContextReadPort.ReadPolicyContextAsync(policyContextRequest); - return contractAdapter.MapPolicyResponse(request, policyContextResponse); + return decisionService.Evaluate(request, policyContextData); } } diff --git a/src/Thalos.Service.Application/UseCases/IEvaluateIdentityPolicyUseCase.cs b/src/Thalos.Service.Application/UseCases/IEvaluateIdentityPolicyUseCase.cs index e503608..f51c09a 100644 --- a/src/Thalos.Service.Application/UseCases/IEvaluateIdentityPolicyUseCase.cs +++ b/src/Thalos.Service.Application/UseCases/IEvaluateIdentityPolicyUseCase.cs @@ -1,4 +1,5 @@ -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Service.Application.UseCases; diff --git a/src/Thalos.Service.Application/UseCases/IIssueIdentityTokenUseCase.cs b/src/Thalos.Service.Application/UseCases/IIssueIdentityTokenUseCase.cs index a7a4a06..34ddc03 100644 --- a/src/Thalos.Service.Application/UseCases/IIssueIdentityTokenUseCase.cs +++ b/src/Thalos.Service.Application/UseCases/IIssueIdentityTokenUseCase.cs @@ -1,4 +1,5 @@ -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Service.Application.UseCases; diff --git a/src/Thalos.Service.Application/UseCases/IssueIdentityTokenUseCase.cs b/src/Thalos.Service.Application/UseCases/IssueIdentityTokenUseCase.cs index a94c145..2fb6caf 100644 --- a/src/Thalos.Service.Application/UseCases/IssueIdentityTokenUseCase.cs +++ b/src/Thalos.Service.Application/UseCases/IssueIdentityTokenUseCase.cs @@ -1,17 +1,22 @@ +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; using Thalos.Service.Application.Ports; -using Thalos.Service.Identity.Abstractions.Contracts; +using Thalos.Domain.Decisions; namespace Thalos.Service.Application.UseCases; /// /// Default orchestration implementation for identity token issuance. /// -public sealed class IssueIdentityTokenUseCase(IIdentityTokenReadPort readPort) +public sealed class IssueIdentityTokenUseCase( + IIdentityTokenReadPort readPort, + IIdentityTokenDecisionService decisionService) : IIssueIdentityTokenUseCase { /// - public Task HandleAsync(IssueIdentityTokenRequest request) + public async Task HandleAsync(IssueIdentityTokenRequest request) { - return readPort.IssueTokenAsync(request); + var tokenData = await readPort.ReadTokenAsync(request); + return decisionService.BuildIssuedTokenResponse(tokenData); } } diff --git a/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs b/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs index 948cfb6..84c5991 100644 --- a/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs +++ b/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs @@ -2,7 +2,7 @@ using Grpc.Core; using Thalos.Service.Application.Adapters; using Thalos.Service.Application.Grpc; using Thalos.Service.Application.UseCases; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; namespace Thalos.Service.Grpc.Services; diff --git a/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj b/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj index 4db7838..c10fc1d 100644 --- a/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj +++ b/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj @@ -14,6 +14,5 @@ - diff --git a/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs b/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs index 8cb71a9..b9f7697 100644 --- a/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs @@ -1,7 +1,9 @@ -using Thalos.Service.Application.Adapters; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; using Thalos.Service.Application.Ports; using Thalos.Service.Application.UseCases; -using Thalos.Service.Identity.Abstractions.Contracts; +using Thalos.Domain.Contracts; +using Thalos.Domain.Decisions; namespace Thalos.Service.Application.UnitTests; @@ -11,7 +13,7 @@ public class EvaluateIdentityPolicyUseCaseTests public async Task HandleAsync_WhenCalled_UsesIdentityContractsAndReturnsMappedResponse() { var useCase = new EvaluateIdentityPolicyUseCase( - new FakeIdentityCapabilityContractAdapter(), + new FakeIdentityPolicyDecisionService(), new FakeIdentityPolicyContextReadPort()); var response = await useCase.HandleAsync(new EvaluateIdentityPolicyRequest("subject-1", "tenant-1", "perm.read")); @@ -21,29 +23,33 @@ public class EvaluateIdentityPolicyUseCaseTests Assert.True(response.IsAllowed); } - private sealed class FakeIdentityCapabilityContractAdapter : IIdentityCapabilityContractAdapter + private sealed class FakeIdentityPolicyDecisionService : IIdentityPolicyDecisionService { - public IdentityPolicyContextRequest CreatePolicyContext(EvaluateIdentityPolicyRequest identityRequest) + public IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request) { - return new IdentityPolicyContextRequest(identityRequest.SubjectId, identityRequest.TenantId, identityRequest.PermissionCode); + return new IdentityPolicyContextRequest(request.SubjectId, request.TenantId, request.PermissionCode); } - public EvaluateIdentityPolicyResponse MapPolicyResponse( - EvaluateIdentityPolicyRequest identityRequest, - IdentityPolicyContextResponse contextResponse) + public EvaluateIdentityPolicyResponse Evaluate( + EvaluateIdentityPolicyRequest request, + IdentityPolicyContextData policyContextData) { return new EvaluateIdentityPolicyResponse( - identityRequest.SubjectId, - identityRequest.PermissionCode, - contextResponse.ContextSatisfied); + request.SubjectId, + request.PermissionCode, + policyContextData.ContextSatisfied); } } private sealed class FakeIdentityPolicyContextReadPort : IIdentityPolicyContextReadPort { - public Task ReadPolicyContextAsync(IdentityPolicyContextRequest request) + public Task ReadPolicyContextAsync(IdentityPolicyContextRequest request) { - return Task.FromResult(new IdentityPolicyContextResponse(request.SubjectId, request.PermissionCode, true)); + return Task.FromResult(new IdentityPolicyContextData( + request.SubjectId, + request.PermissionCode, + true, + [request.PermissionCode])); } } } diff --git a/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs b/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs index 50685bf..49282b3 100644 --- a/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs @@ -1,6 +1,8 @@ +using BuildingBlock.Identity.Contracts.Requests; using Thalos.Service.Application.Ports; using Thalos.Service.Application.UseCases; -using Thalos.Service.Identity.Abstractions.Contracts; +using Thalos.Domain.Contracts; +using Thalos.Domain.Decisions; namespace Thalos.Service.Application.UnitTests; @@ -9,8 +11,9 @@ public class IssueIdentityTokenUseCaseTests [Fact] public async Task HandleAsync_WhenCalled_DelegatesToReadPort() { + var decisionService = new IdentityTokenDecisionService(); var port = new FakeIdentityTokenReadPort(); - var useCase = new IssueIdentityTokenUseCase(port); + var useCase = new IssueIdentityTokenUseCase(port, decisionService); var response = await useCase.HandleAsync(new IssueIdentityTokenRequest("user-1", "tenant-1")); @@ -20,9 +23,9 @@ public class IssueIdentityTokenUseCaseTests private sealed class FakeIdentityTokenReadPort : IIdentityTokenReadPort { - public Task IssueTokenAsync(IssueIdentityTokenRequest request) + public Task ReadTokenAsync(IssueIdentityTokenRequest request) { - return Task.FromResult(new IssueIdentityTokenResponse("token-123", 3600)); + return Task.FromResult(new IdentityTokenData("token-123", 3600)); } } } diff --git a/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs b/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs index fee3f25..ca99bce 100644 --- a/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.DependencyInjection; +using BuildingBlock.Identity.Contracts.Requests; using Thalos.Service.Application.Adapters; using Thalos.Service.Application.DependencyInjection; using Thalos.Service.Application.Grpc; using Thalos.Service.Application.UseCases; -using Thalos.Service.Identity.Abstractions.Contracts; namespace Thalos.Service.Application.UnitTests; diff --git a/tests/Thalos.Service.Application.UnitTests/Thalos.Service.Application.UnitTests.csproj b/tests/Thalos.Service.Application.UnitTests/Thalos.Service.Application.UnitTests.csproj index a08db62..0453bd8 100644 --- a/tests/Thalos.Service.Application.UnitTests/Thalos.Service.Application.UnitTests.csproj +++ b/tests/Thalos.Service.Application.UnitTests/Thalos.Service.Application.UnitTests.csproj @@ -17,6 +17,5 @@ - From f960f0656da2fe0a156f1a1beedaf3f27075d1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Wed, 25 Feb 2026 13:13:56 -0600 Subject: [PATCH 3/3] feat(thalos-service): propagate provider metadata through grpc flow --- .../service-orchestration-boundary.md | 1 + .../IdentityPolicyGrpcContractAdapter.cs | 17 +++++++++-- .../EvaluateIdentityPolicyGrpcContract.cs | 7 ++++- ...IdentityPolicyContextReadPortDalAdapter.cs | 8 ++++-- .../Ports/IdentityTokenReadPortDalAdapter.cs | 8 ++++-- .../Protos/identity_runtime.proto | 3 ++ .../Services/IdentityRuntimeGrpcService.cs | 17 +++++++++-- .../EvaluateIdentityPolicyUseCaseTests.cs | 8 +++++- .../IssueIdentityTokenUseCaseTests.cs | 3 +- .../RuntimeWiringTests.cs | 28 ++++++++++++++++++- 10 files changed, 87 insertions(+), 13 deletions(-) diff --git a/docs/architecture/service-orchestration-boundary.md b/docs/architecture/service-orchestration-boundary.md index 5fb9d1d..bc5b098 100644 --- a/docs/architecture/service-orchestration-boundary.md +++ b/docs/architecture/service-orchestration-boundary.md @@ -7,6 +7,7 @@ Constrain thalos-service to orchestration responsibilities after thalos-domain e - Coordinate identity use-case flow - Delegate policy/token decisions to thalos-domain abstractions - Adapt transport contracts +- Route provider metadata (`InternalJwt`, `AzureAd`, `Google`) between edge/service/dal boundaries ## Prohibited Responsibilities - Owning identity decision policies diff --git a/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs index ebfde41..25cfde3 100644 --- a/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs +++ b/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs @@ -1,4 +1,5 @@ using Thalos.Service.Application.Grpc; +using BuildingBlock.Identity.Contracts.Conventions; using BuildingBlock.Identity.Contracts.Requests; namespace Thalos.Service.Application.Adapters; @@ -11,12 +12,24 @@ public sealed class IdentityPolicyGrpcContractAdapter : IIdentityPolicyGrpcContr /// public EvaluateIdentityPolicyGrpcContract ToGrpc(EvaluateIdentityPolicyRequest request) { - return new EvaluateIdentityPolicyGrpcContract(request.SubjectId, request.TenantId, request.PermissionCode); + return new EvaluateIdentityPolicyGrpcContract( + request.SubjectId, + request.TenantId, + request.PermissionCode, + request.Provider.ToString()); } /// public EvaluateIdentityPolicyRequest FromGrpc(EvaluateIdentityPolicyGrpcContract contract) { - return new EvaluateIdentityPolicyRequest(contract.SubjectId, contract.TenantId, contract.PermissionCode); + var provider = Enum.TryParse(contract.Provider, true, out var parsedProvider) + ? parsedProvider + : IdentityAuthProvider.InternalJwt; + + return new EvaluateIdentityPolicyRequest( + contract.SubjectId, + contract.TenantId, + contract.PermissionCode, + provider); } } diff --git a/src/Thalos.Service.Application/Grpc/EvaluateIdentityPolicyGrpcContract.cs b/src/Thalos.Service.Application/Grpc/EvaluateIdentityPolicyGrpcContract.cs index 8fd286d..2dfacea 100644 --- a/src/Thalos.Service.Application/Grpc/EvaluateIdentityPolicyGrpcContract.cs +++ b/src/Thalos.Service.Application/Grpc/EvaluateIdentityPolicyGrpcContract.cs @@ -6,4 +6,9 @@ namespace Thalos.Service.Application.Grpc; /// Identity subject identifier. /// Tenant scope identifier. /// Permission code to evaluate. -public sealed record EvaluateIdentityPolicyGrpcContract(string SubjectId, string TenantId, string PermissionCode); +/// Auth provider. +public sealed record EvaluateIdentityPolicyGrpcContract( + string SubjectId, + string TenantId, + string PermissionCode, + string Provider = "InternalJwt"); diff --git a/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs b/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs index d66caa9..e3f7825 100644 --- a/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs +++ b/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs @@ -20,7 +20,8 @@ public sealed class IdentityPolicyContextReadPortDalAdapter( CreateEnvelope(), request.SubjectId, request.TenantId, - request.PermissionCode); + request.PermissionCode, + request.Provider); var policyRecord = await identityRepository.ReadIdentityPolicyAsync(policyLookupRequest); if (policyRecord is null) @@ -28,6 +29,7 @@ public sealed class IdentityPolicyContextReadPortDalAdapter( return new IdentityPolicyContextData( request.SubjectId, request.PermissionCode, + request.Provider, false, []); } @@ -35,7 +37,8 @@ public sealed class IdentityPolicyContextReadPortDalAdapter( var permissionSetRequest = new IdentityPermissionSetLookupRequest( policyLookupRequest.Envelope, request.SubjectId, - request.TenantId); + request.TenantId, + request.Provider); var permissions = await identityRepository.ReadPermissionSetAsync(permissionSetRequest); var grantedPermissions = permissions @@ -45,6 +48,7 @@ public sealed class IdentityPolicyContextReadPortDalAdapter( return new IdentityPolicyContextData( request.SubjectId, request.PermissionCode, + policyRecord.Provider, policyRecord.ContextSatisfied, grantedPermissions); } diff --git a/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs b/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs index 834ec48..1b40dcc 100644 --- a/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs +++ b/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs @@ -19,15 +19,17 @@ public sealed class IdentityTokenReadPortDalAdapter( var lookupRequest = new IdentityTokenLookupRequest( CreateEnvelope(), request.SubjectId, - request.TenantId); + request.TenantId, + request.Provider, + request.ExternalToken); var tokenRecord = await identityRepository.ReadIdentityTokenAsync(lookupRequest); if (tokenRecord is null) { - return new IdentityTokenData(null, null); + return new IdentityTokenData(null, null, request.Provider); } - return new IdentityTokenData(tokenRecord.Token, tokenRecord.ExpiresInSeconds); + return new IdentityTokenData(tokenRecord.Token, tokenRecord.ExpiresInSeconds, tokenRecord.Provider); } private IdentityContractEnvelope CreateEnvelope() diff --git a/src/Thalos.Service.Grpc/Protos/identity_runtime.proto b/src/Thalos.Service.Grpc/Protos/identity_runtime.proto index 80fcbce..02b620b 100644 --- a/src/Thalos.Service.Grpc/Protos/identity_runtime.proto +++ b/src/Thalos.Service.Grpc/Protos/identity_runtime.proto @@ -12,6 +12,8 @@ service IdentityRuntime { message IssueIdentityTokenGrpcRequest { string subject_id = 1; string tenant_id = 2; + string provider = 3; + string external_token = 4; } message IssueIdentityTokenGrpcResponse { @@ -23,6 +25,7 @@ message EvaluateIdentityPolicyGrpcRequest { string subject_id = 1; string tenant_id = 2; string permission_code = 3; + string provider = 4; } message EvaluateIdentityPolicyGrpcResponse { diff --git a/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs b/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs index 84c5991..43677db 100644 --- a/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs +++ b/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs @@ -1,4 +1,5 @@ using Grpc.Core; +using BuildingBlock.Identity.Contracts.Conventions; using Thalos.Service.Application.Adapters; using Thalos.Service.Application.Grpc; using Thalos.Service.Application.UseCases; @@ -24,7 +25,11 @@ public sealed class IdentityRuntimeGrpcService( IssueIdentityTokenGrpcRequest request, ServerCallContext context) { - var useCaseRequest = new IssueIdentityTokenRequest(request.SubjectId, request.TenantId); + var useCaseRequest = new IssueIdentityTokenRequest( + request.SubjectId, + request.TenantId, + ParseProvider(request.Provider), + request.ExternalToken); var useCaseResponse = await issueIdentityTokenUseCase.HandleAsync(useCaseRequest); return new IssueIdentityTokenGrpcResponse @@ -47,7 +52,8 @@ public sealed class IdentityRuntimeGrpcService( var grpcContract = new EvaluateIdentityPolicyGrpcContract( request.SubjectId, request.TenantId, - request.PermissionCode); + request.PermissionCode, + request.Provider); var useCaseRequest = grpcContractAdapter.FromGrpc(grpcContract); var useCaseResponse = await evaluateIdentityPolicyUseCase.HandleAsync(useCaseRequest); @@ -59,4 +65,11 @@ public sealed class IdentityRuntimeGrpcService( IsAllowed = useCaseResponse.IsAllowed }; } + + private static IdentityAuthProvider ParseProvider(string provider) + { + return Enum.TryParse(provider, true, out var parsedProvider) + ? parsedProvider + : IdentityAuthProvider.InternalJwt; + } } diff --git a/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs b/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs index b9f7697..4660373 100644 --- a/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs @@ -1,5 +1,6 @@ using BuildingBlock.Identity.Contracts.Requests; using BuildingBlock.Identity.Contracts.Responses; +using BuildingBlock.Identity.Contracts.Conventions; using Thalos.Service.Application.Ports; using Thalos.Service.Application.UseCases; using Thalos.Domain.Contracts; @@ -27,7 +28,11 @@ public class EvaluateIdentityPolicyUseCaseTests { public IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request) { - return new IdentityPolicyContextRequest(request.SubjectId, request.TenantId, request.PermissionCode); + return new IdentityPolicyContextRequest( + request.SubjectId, + request.TenantId, + request.PermissionCode, + request.Provider); } public EvaluateIdentityPolicyResponse Evaluate( @@ -48,6 +53,7 @@ public class EvaluateIdentityPolicyUseCaseTests return Task.FromResult(new IdentityPolicyContextData( request.SubjectId, request.PermissionCode, + IdentityAuthProvider.InternalJwt, true, [request.PermissionCode])); } diff --git a/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs b/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs index 49282b3..888ee25 100644 --- a/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs @@ -1,4 +1,5 @@ using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Conventions; using Thalos.Service.Application.Ports; using Thalos.Service.Application.UseCases; using Thalos.Domain.Contracts; @@ -25,7 +26,7 @@ public class IssueIdentityTokenUseCaseTests { public Task ReadTokenAsync(IssueIdentityTokenRequest request) { - return Task.FromResult(new IdentityTokenData("token-123", 3600)); + return Task.FromResult(new IdentityTokenData("token-123", 3600, IdentityAuthProvider.InternalJwt)); } } } diff --git a/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs b/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs index ca99bce..60c2aa3 100644 --- a/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using BuildingBlock.Identity.Contracts.Conventions; using BuildingBlock.Identity.Contracts.Requests; using Thalos.Service.Application.Adapters; using Thalos.Service.Application.DependencyInjection; @@ -44,6 +45,26 @@ public class RuntimeWiringTests Assert.Equal(0, tokenResponse.ExpiresInSeconds); } + [Fact] + public async Task AddThalosServiceRuntime_WhenAzureProviderUsed_IssuesProviderToken() + { + var services = new ServiceCollection(); + services.AddThalosServiceRuntime(); + + using var provider = services.BuildServiceProvider(); + var issueTokenUseCase = provider.GetRequiredService(); + + var tokenResponse = await issueTokenUseCase.HandleAsync( + new IssueIdentityTokenRequest( + string.Empty, + "tenant-2", + IdentityAuthProvider.AzureAd, + "azure-id-token")); + + Assert.StartsWith("azure:", tokenResponse.Token); + Assert.True(tokenResponse.ExpiresInSeconds > 0); + } + [Fact] public void IdentityPolicyGrpcContractAdapter_WhenMapped_PreservesValues() { @@ -62,12 +83,17 @@ public class RuntimeWiringTests public void IdentityPolicyGrpcContractAdapter_WhenFromGrpc_UsesExpectedContractShape() { var adapter = new IdentityPolicyGrpcContractAdapter(); - var contract = new EvaluateIdentityPolicyGrpcContract("subject-9", "tenant-9", "identity.token.issue"); + var contract = new EvaluateIdentityPolicyGrpcContract( + "subject-9", + "tenant-9", + "identity.token.issue", + IdentityAuthProvider.Google.ToString()); var request = adapter.FromGrpc(contract); Assert.Equal("subject-9", request.SubjectId); Assert.Equal("tenant-9", request.TenantId); Assert.Equal("identity.token.issue", request.PermissionCode); + Assert.Equal(IdentityAuthProvider.Google, request.Provider); } }