diff --git a/docs/architecture/service-orchestration-boundary.md b/docs/architecture/service-orchestration-boundary.md new file mode 100644 index 0000000..bc5b098 --- /dev/null +++ b/docs/architecture/service-orchestration-boundary.md @@ -0,0 +1,14 @@ +# 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 +- Route provider metadata (`InternalJwt`, `AzureAd`, `Google`) between edge/service/dal boundaries + +## 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/IdentityPolicyGrpcContractAdapter.cs b/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs new file mode 100644 index 0000000..25cfde3 --- /dev/null +++ b/src/Thalos.Service.Application/Adapters/IdentityPolicyGrpcContractAdapter.cs @@ -0,0 +1,35 @@ +using Thalos.Service.Application.Grpc; +using BuildingBlock.Identity.Contracts.Conventions; +using BuildingBlock.Identity.Contracts.Requests; + +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, + request.Provider.ToString()); + } + + /// + public EvaluateIdentityPolicyRequest FromGrpc(EvaluateIdentityPolicyGrpcContract contract) + { + 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/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs b/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs new file mode 100644 index 0000000..27d9023 --- /dev/null +++ b/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs @@ -0,0 +1,39 @@ +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; +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(); + services.TryAddSingleton(); + + return services; + } +} 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/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 new file mode 100644 index 0000000..e3f7825 --- /dev/null +++ b/src/Thalos.Service.Application/Ports/IdentityPolicyContextReadPortDalAdapter.cs @@ -0,0 +1,60 @@ +using Core.Blueprint.Common.Runtime; +using BuildingBlock.Identity.Contracts.Requests; +using Thalos.DAL.Contracts; +using Thalos.DAL.Repositories; +using Thalos.Domain.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, + request.Provider); + + var policyRecord = await identityRepository.ReadIdentityPolicyAsync(policyLookupRequest); + if (policyRecord is null) + { + return new IdentityPolicyContextData( + request.SubjectId, + request.PermissionCode, + request.Provider, + false, + []); + } + + var permissionSetRequest = new IdentityPermissionSetLookupRequest( + policyLookupRequest.Envelope, + request.SubjectId, + request.TenantId, + request.Provider); + + var permissions = await identityRepository.ReadPermissionSetAsync(permissionSetRequest); + var grantedPermissions = permissions + .Select(permission => permission.PermissionCode) + .ToArray(); + + return new IdentityPolicyContextData( + request.SubjectId, + request.PermissionCode, + policyRecord.Provider, + policyRecord.ContextSatisfied, + grantedPermissions); + } + + 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..1b40dcc --- /dev/null +++ b/src/Thalos.Service.Application/Ports/IdentityTokenReadPortDalAdapter.cs @@ -0,0 +1,39 @@ +using Core.Blueprint.Common.Runtime; +using BuildingBlock.Identity.Contracts.Requests; +using Thalos.DAL.Contracts; +using Thalos.DAL.Repositories; +using Thalos.Domain.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 ReadTokenAsync(IssueIdentityTokenRequest request) + { + var lookupRequest = new IdentityTokenLookupRequest( + CreateEnvelope(), + request.SubjectId, + request.TenantId, + request.Provider, + request.ExternalToken); + + var tokenRecord = await identityRepository.ReadIdentityTokenAsync(lookupRequest); + if (tokenRecord is null) + { + return new IdentityTokenData(null, null, request.Provider); + } + + return new IdentityTokenData(tokenRecord.Token, tokenRecord.ExpiresInSeconds, tokenRecord.Provider); + } + + 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..e2f9a55 100644 --- a/src/Thalos.Service.Application/Thalos.Service.Application.csproj +++ b/src/Thalos.Service.Application/Thalos.Service.Application.csproj @@ -5,6 +5,9 @@ enable - + + + + 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/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..02b620b --- /dev/null +++ b/src/Thalos.Service.Grpc/Protos/identity_runtime.proto @@ -0,0 +1,35 @@ +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; + string provider = 3; + string external_token = 4; +} + +message IssueIdentityTokenGrpcResponse { + string token = 1; + int32 expires_in_seconds = 2; +} + +message EvaluateIdentityPolicyGrpcRequest { + string subject_id = 1; + string tenant_id = 2; + string permission_code = 3; + string provider = 4; +} + +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..43677db --- /dev/null +++ b/src/Thalos.Service.Grpc/Services/IdentityRuntimeGrpcService.cs @@ -0,0 +1,75 @@ +using Grpc.Core; +using BuildingBlock.Identity.Contracts.Conventions; +using Thalos.Service.Application.Adapters; +using Thalos.Service.Application.Grpc; +using Thalos.Service.Application.UseCases; +using BuildingBlock.Identity.Contracts.Requests; + +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, + ParseProvider(request.Provider), + request.ExternalToken); + 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, + request.Provider); + + var useCaseRequest = grpcContractAdapter.FromGrpc(grpcContract); + var useCaseResponse = await evaluateIdentityPolicyUseCase.HandleAsync(useCaseRequest); + + return new EvaluateIdentityPolicyGrpcResponse + { + SubjectId = useCaseResponse.SubjectId, + PermissionCode = useCaseResponse.PermissionCode, + IsAllowed = useCaseResponse.IsAllowed + }; + } + + private static IdentityAuthProvider ParseProvider(string provider) + { + return Enum.TryParse(provider, true, out var parsedProvider) + ? parsedProvider + : IdentityAuthProvider.InternalJwt; + } +} diff --git a/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj b/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj index b7f7de2..c10fc1d 100644 --- a/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj +++ b/src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj @@ -5,7 +5,14 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + - diff --git a/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs b/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs index 8cb71a9..4660373 100644 --- a/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/EvaluateIdentityPolicyUseCaseTests.cs @@ -1,7 +1,10 @@ -using Thalos.Service.Application.Adapters; +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.Service.Identity.Abstractions.Contracts; +using Thalos.Domain.Contracts; +using Thalos.Domain.Decisions; namespace Thalos.Service.Application.UnitTests; @@ -11,7 +14,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 +24,38 @@ 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, + request.Provider); } - 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, + IdentityAuthProvider.InternalJwt, + true, + [request.PermissionCode])); } } } diff --git a/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs b/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs index 50685bf..888ee25 100644 --- a/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/IssueIdentityTokenUseCaseTests.cs @@ -1,6 +1,9 @@ +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Conventions; 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 +12,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 +24,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, IdentityAuthProvider.InternalJwt)); } } } diff --git a/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs b/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs new file mode 100644 index 0000000..60c2aa3 --- /dev/null +++ b/tests/Thalos.Service.Application.UnitTests/RuntimeWiringTests.cs @@ -0,0 +1,99 @@ +using Microsoft.Extensions.DependencyInjection; +using BuildingBlock.Identity.Contracts.Conventions; +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; + +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 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() + { + 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", + 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); + } +} 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..0453bd8 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 @@ + @@ -16,6 +17,5 @@ -