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