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 @@ +