feat(thalos-service): propagate provider metadata through grpc flow

This commit is contained in:
José René White Enciso 2026-02-25 13:13:56 -06:00
parent 654a808c54
commit f960f0656d
10 changed files with 87 additions and 13 deletions

View File

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

View File

@ -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
/// <inheritdoc />
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());
}
/// <inheritdoc />
public EvaluateIdentityPolicyRequest FromGrpc(EvaluateIdentityPolicyGrpcContract contract)
{
return new EvaluateIdentityPolicyRequest(contract.SubjectId, contract.TenantId, contract.PermissionCode);
var provider = Enum.TryParse<IdentityAuthProvider>(contract.Provider, true, out var parsedProvider)
? parsedProvider
: IdentityAuthProvider.InternalJwt;
return new EvaluateIdentityPolicyRequest(
contract.SubjectId,
contract.TenantId,
contract.PermissionCode,
provider);
}
}

View File

@ -6,4 +6,9 @@ namespace Thalos.Service.Application.Grpc;
/// <param name="SubjectId">Identity subject identifier.</param>
/// <param name="TenantId">Tenant scope identifier.</param>
/// <param name="PermissionCode">Permission code to evaluate.</param>
public sealed record EvaluateIdentityPolicyGrpcContract(string SubjectId, string TenantId, string PermissionCode);
/// <param name="Provider">Auth provider.</param>
public sealed record EvaluateIdentityPolicyGrpcContract(
string SubjectId,
string TenantId,
string PermissionCode,
string Provider = "InternalJwt");

View File

@ -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);
}

View File

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

View File

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

View File

@ -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<IdentityAuthProvider>(provider, true, out var parsedProvider)
? parsedProvider
: IdentityAuthProvider.InternalJwt;
}
}

View File

@ -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]));
}

View File

@ -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<IdentityTokenData> ReadTokenAsync(IssueIdentityTokenRequest request)
{
return Task.FromResult(new IdentityTokenData("token-123", 3600));
return Task.FromResult(new IdentityTokenData("token-123", 3600, IdentityAuthProvider.InternalJwt));
}
}
}

View File

@ -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<IIssueIdentityTokenUseCase>();
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);
}
}