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 - Coordinate identity use-case flow
- Delegate policy/token decisions to thalos-domain abstractions - Delegate policy/token decisions to thalos-domain abstractions
- Adapt transport contracts - Adapt transport contracts
- Route provider metadata (`InternalJwt`, `AzureAd`, `Google`) between edge/service/dal boundaries
## Prohibited Responsibilities ## Prohibited Responsibilities
- Owning identity decision policies - Owning identity decision policies

View File

@ -1,4 +1,5 @@
using Thalos.Service.Application.Grpc; using Thalos.Service.Application.Grpc;
using BuildingBlock.Identity.Contracts.Conventions;
using BuildingBlock.Identity.Contracts.Requests; using BuildingBlock.Identity.Contracts.Requests;
namespace Thalos.Service.Application.Adapters; namespace Thalos.Service.Application.Adapters;
@ -11,12 +12,24 @@ public sealed class IdentityPolicyGrpcContractAdapter : IIdentityPolicyGrpcContr
/// <inheritdoc /> /// <inheritdoc />
public EvaluateIdentityPolicyGrpcContract ToGrpc(EvaluateIdentityPolicyRequest request) 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 /> /// <inheritdoc />
public EvaluateIdentityPolicyRequest FromGrpc(EvaluateIdentityPolicyGrpcContract contract) 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="SubjectId">Identity subject identifier.</param>
/// <param name="TenantId">Tenant scope identifier.</param> /// <param name="TenantId">Tenant scope identifier.</param>
/// <param name="PermissionCode">Permission code to evaluate.</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(), CreateEnvelope(),
request.SubjectId, request.SubjectId,
request.TenantId, request.TenantId,
request.PermissionCode); request.PermissionCode,
request.Provider);
var policyRecord = await identityRepository.ReadIdentityPolicyAsync(policyLookupRequest); var policyRecord = await identityRepository.ReadIdentityPolicyAsync(policyLookupRequest);
if (policyRecord is null) if (policyRecord is null)
@ -28,6 +29,7 @@ public sealed class IdentityPolicyContextReadPortDalAdapter(
return new IdentityPolicyContextData( return new IdentityPolicyContextData(
request.SubjectId, request.SubjectId,
request.PermissionCode, request.PermissionCode,
request.Provider,
false, false,
[]); []);
} }
@ -35,7 +37,8 @@ public sealed class IdentityPolicyContextReadPortDalAdapter(
var permissionSetRequest = new IdentityPermissionSetLookupRequest( var permissionSetRequest = new IdentityPermissionSetLookupRequest(
policyLookupRequest.Envelope, policyLookupRequest.Envelope,
request.SubjectId, request.SubjectId,
request.TenantId); request.TenantId,
request.Provider);
var permissions = await identityRepository.ReadPermissionSetAsync(permissionSetRequest); var permissions = await identityRepository.ReadPermissionSetAsync(permissionSetRequest);
var grantedPermissions = permissions var grantedPermissions = permissions
@ -45,6 +48,7 @@ public sealed class IdentityPolicyContextReadPortDalAdapter(
return new IdentityPolicyContextData( return new IdentityPolicyContextData(
request.SubjectId, request.SubjectId,
request.PermissionCode, request.PermissionCode,
policyRecord.Provider,
policyRecord.ContextSatisfied, policyRecord.ContextSatisfied,
grantedPermissions); grantedPermissions);
} }

View File

@ -19,15 +19,17 @@ public sealed class IdentityTokenReadPortDalAdapter(
var lookupRequest = new IdentityTokenLookupRequest( var lookupRequest = new IdentityTokenLookupRequest(
CreateEnvelope(), CreateEnvelope(),
request.SubjectId, request.SubjectId,
request.TenantId); request.TenantId,
request.Provider,
request.ExternalToken);
var tokenRecord = await identityRepository.ReadIdentityTokenAsync(lookupRequest); var tokenRecord = await identityRepository.ReadIdentityTokenAsync(lookupRequest);
if (tokenRecord is null) 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() private IdentityContractEnvelope CreateEnvelope()

View File

@ -12,6 +12,8 @@ service IdentityRuntime {
message IssueIdentityTokenGrpcRequest { message IssueIdentityTokenGrpcRequest {
string subject_id = 1; string subject_id = 1;
string tenant_id = 2; string tenant_id = 2;
string provider = 3;
string external_token = 4;
} }
message IssueIdentityTokenGrpcResponse { message IssueIdentityTokenGrpcResponse {
@ -23,6 +25,7 @@ message EvaluateIdentityPolicyGrpcRequest {
string subject_id = 1; string subject_id = 1;
string tenant_id = 2; string tenant_id = 2;
string permission_code = 3; string permission_code = 3;
string provider = 4;
} }
message EvaluateIdentityPolicyGrpcResponse { message EvaluateIdentityPolicyGrpcResponse {

View File

@ -1,4 +1,5 @@
using Grpc.Core; using Grpc.Core;
using BuildingBlock.Identity.Contracts.Conventions;
using Thalos.Service.Application.Adapters; using Thalos.Service.Application.Adapters;
using Thalos.Service.Application.Grpc; using Thalos.Service.Application.Grpc;
using Thalos.Service.Application.UseCases; using Thalos.Service.Application.UseCases;
@ -24,7 +25,11 @@ public sealed class IdentityRuntimeGrpcService(
IssueIdentityTokenGrpcRequest request, IssueIdentityTokenGrpcRequest request,
ServerCallContext context) 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); var useCaseResponse = await issueIdentityTokenUseCase.HandleAsync(useCaseRequest);
return new IssueIdentityTokenGrpcResponse return new IssueIdentityTokenGrpcResponse
@ -47,7 +52,8 @@ public sealed class IdentityRuntimeGrpcService(
var grpcContract = new EvaluateIdentityPolicyGrpcContract( var grpcContract = new EvaluateIdentityPolicyGrpcContract(
request.SubjectId, request.SubjectId,
request.TenantId, request.TenantId,
request.PermissionCode); request.PermissionCode,
request.Provider);
var useCaseRequest = grpcContractAdapter.FromGrpc(grpcContract); var useCaseRequest = grpcContractAdapter.FromGrpc(grpcContract);
var useCaseResponse = await evaluateIdentityPolicyUseCase.HandleAsync(useCaseRequest); var useCaseResponse = await evaluateIdentityPolicyUseCase.HandleAsync(useCaseRequest);
@ -59,4 +65,11 @@ public sealed class IdentityRuntimeGrpcService(
IsAllowed = useCaseResponse.IsAllowed 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.Requests;
using BuildingBlock.Identity.Contracts.Responses; using BuildingBlock.Identity.Contracts.Responses;
using BuildingBlock.Identity.Contracts.Conventions;
using Thalos.Service.Application.Ports; using Thalos.Service.Application.Ports;
using Thalos.Service.Application.UseCases; using Thalos.Service.Application.UseCases;
using Thalos.Domain.Contracts; using Thalos.Domain.Contracts;
@ -27,7 +28,11 @@ public class EvaluateIdentityPolicyUseCaseTests
{ {
public IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request) 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( public EvaluateIdentityPolicyResponse Evaluate(
@ -48,6 +53,7 @@ public class EvaluateIdentityPolicyUseCaseTests
return Task.FromResult(new IdentityPolicyContextData( return Task.FromResult(new IdentityPolicyContextData(
request.SubjectId, request.SubjectId,
request.PermissionCode, request.PermissionCode,
IdentityAuthProvider.InternalJwt,
true, true,
[request.PermissionCode])); [request.PermissionCode]));
} }

View File

@ -1,4 +1,5 @@
using BuildingBlock.Identity.Contracts.Requests; using BuildingBlock.Identity.Contracts.Requests;
using BuildingBlock.Identity.Contracts.Conventions;
using Thalos.Service.Application.Ports; using Thalos.Service.Application.Ports;
using Thalos.Service.Application.UseCases; using Thalos.Service.Application.UseCases;
using Thalos.Domain.Contracts; using Thalos.Domain.Contracts;
@ -25,7 +26,7 @@ public class IssueIdentityTokenUseCaseTests
{ {
public Task<IdentityTokenData> ReadTokenAsync(IssueIdentityTokenRequest request) 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 Microsoft.Extensions.DependencyInjection;
using BuildingBlock.Identity.Contracts.Conventions;
using BuildingBlock.Identity.Contracts.Requests; using BuildingBlock.Identity.Contracts.Requests;
using Thalos.Service.Application.Adapters; using Thalos.Service.Application.Adapters;
using Thalos.Service.Application.DependencyInjection; using Thalos.Service.Application.DependencyInjection;
@ -44,6 +45,26 @@ public class RuntimeWiringTests
Assert.Equal(0, tokenResponse.ExpiresInSeconds); 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] [Fact]
public void IdentityPolicyGrpcContractAdapter_WhenMapped_PreservesValues() public void IdentityPolicyGrpcContractAdapter_WhenMapped_PreservesValues()
{ {
@ -62,12 +83,17 @@ public class RuntimeWiringTests
public void IdentityPolicyGrpcContractAdapter_WhenFromGrpc_UsesExpectedContractShape() public void IdentityPolicyGrpcContractAdapter_WhenFromGrpc_UsesExpectedContractShape()
{ {
var adapter = new IdentityPolicyGrpcContractAdapter(); 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); var request = adapter.FromGrpc(contract);
Assert.Equal("subject-9", request.SubjectId); Assert.Equal("subject-9", request.SubjectId);
Assert.Equal("tenant-9", request.TenantId); Assert.Equal("tenant-9", request.TenantId);
Assert.Equal("identity.token.issue", request.PermissionCode); Assert.Equal("identity.token.issue", request.PermissionCode);
Assert.Equal(IdentityAuthProvider.Google, request.Provider);
} }
} }