From 3997d5d77e44cfb5d48f400395c81c4af17be49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Sun, 22 Feb 2026 18:18:21 -0600 Subject: [PATCH 1/3] feat(thalos-bff): wire rest runtime and grpc service adapter --- .../Adapters/IdentityEdgeContractAdapter.cs | 41 +++++++ .../IdentityEdgeGrpcContractAdapter.cs | 22 ++++ ...fApplicationServiceCollectionExtensions.cs | 30 +++++ .../Security/IdentityPermissionGuard.cs | 15 +++ .../Thalos.Bff.Application.csproj | 1 + .../ThalosServiceGrpcClientAdapter.cs | 107 ++++++++++++++++++ src/Thalos.Bff.Rest/Program.cs | 85 +++++++++++++- src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj | 14 +++ 8 files changed, 310 insertions(+), 5 deletions(-) create mode 100644 src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs create mode 100644 src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs create mode 100644 src/Thalos.Bff.Application/DependencyInjection/ThalosBffApplicationServiceCollectionExtensions.cs create mode 100644 src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs create mode 100644 src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs diff --git a/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs new file mode 100644 index 0000000..508c98e --- /dev/null +++ b/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs @@ -0,0 +1,41 @@ +using Thalos.Bff.Application.Contracts; +using Thalos.Bff.Contracts.Api; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Bff.Application.Adapters; + +/// +/// Default adapter implementation for identity edge API and service contracts. +/// +public sealed class IdentityEdgeContractAdapter : IIdentityEdgeContractAdapter +{ + /// + public EvaluateIdentityPolicyRequest ToPolicyRequest(IssueTokenApiRequest request, string permissionCode) + { + return new EvaluateIdentityPolicyRequest(request.SubjectId, request.TenantId, permissionCode); + } + + /// + public IssueIdentityTokenRequest ToIssueTokenRequest(IssueTokenApiRequest request) + { + return new IssueIdentityTokenRequest(request.SubjectId, request.TenantId); + } + + /// + public IssueTokenApiResponse ToIssueTokenApiResponse(IssueIdentityTokenResponse response) + { + return new IssueTokenApiResponse(response.Token, response.ExpiresInSeconds); + } + + /// + public RefreshIdentitySessionRequest ToRefreshSessionRequest(RefreshSessionApiRequest request) + { + return new RefreshIdentitySessionRequest(request.RefreshToken, request.CorrelationId); + } + + /// + public RefreshSessionApiResponse ToRefreshSessionApiResponse(RefreshIdentitySessionResponse response) + { + return new RefreshSessionApiResponse(response.Token, response.ExpiresInSeconds); + } +} diff --git a/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs new file mode 100644 index 0000000..c06c1ee --- /dev/null +++ b/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs @@ -0,0 +1,22 @@ +using Thalos.Bff.Application.Grpc; +using Thalos.Bff.Contracts.Api; + +namespace Thalos.Bff.Application.Adapters; + +/// +/// Default adapter implementation for identity edge gRPC contract translation. +/// +public sealed class IdentityEdgeGrpcContractAdapter : IIdentityEdgeGrpcContractAdapter +{ + /// + public IssueIdentityTokenGrpcContract ToGrpc(IssueTokenApiRequest request) + { + return new IssueIdentityTokenGrpcContract(request.SubjectId, request.TenantId, request.CorrelationId); + } + + /// + public IssueTokenApiRequest FromGrpc(IssueIdentityTokenGrpcContract contract) + { + return new IssueTokenApiRequest(contract.SubjectId, contract.TenantId, contract.CorrelationId); + } +} diff --git a/src/Thalos.Bff.Application/DependencyInjection/ThalosBffApplicationServiceCollectionExtensions.cs b/src/Thalos.Bff.Application/DependencyInjection/ThalosBffApplicationServiceCollectionExtensions.cs new file mode 100644 index 0000000..49f8db4 --- /dev/null +++ b/src/Thalos.Bff.Application/DependencyInjection/ThalosBffApplicationServiceCollectionExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Thalos.Bff.Application.Adapters; +using Thalos.Bff.Application.Handlers; +using Thalos.Bff.Application.Security; + +namespace Thalos.Bff.Application.DependencyInjection; + +/// +/// Registers application-layer runtime wiring for thalos-bff. +/// +public static class ThalosBffApplicationServiceCollectionExtensions +{ + /// + /// Adds thalos-bff application handlers and adapter implementations. + /// + /// Service collection. + /// Service collection for fluent chaining. + public static IServiceCollection AddThalosBffApplicationRuntime(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddScoped(); + services.TryAddScoped(); + + return services; + } +} diff --git a/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs b/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs new file mode 100644 index 0000000..964e64c --- /dev/null +++ b/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs @@ -0,0 +1,15 @@ +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Bff.Application.Security; + +/// +/// Default permission guard backed by thalos-service policy evaluation responses. +/// +public sealed class IdentityPermissionGuard : IPermissionGuard +{ + /// + public bool CanAccess(EvaluateIdentityPolicyResponse policyResponse) + { + return policyResponse.IsAllowed; + } +} diff --git a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj index 8edcb4b..206b1cc 100644 --- a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj +++ b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj @@ -5,6 +5,7 @@ enable + diff --git a/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs new file mode 100644 index 0000000..462f524 --- /dev/null +++ b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs @@ -0,0 +1,107 @@ +using Grpc.Core; +using Microsoft.Extensions.Primitives; +using Thalos.Bff.Application.Adapters; +using Thalos.Bff.Application.Contracts; +using Thalos.Service.Grpc; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Bff.Rest.Adapters; + +/// +/// gRPC-backed adapter for downstream thalos-service calls. +/// +public sealed class ThalosServiceGrpcClientAdapter( + IdentityRuntime.IdentityRuntimeClient grpcClient, + IHttpContextAccessor httpContextAccessor, + IConfiguration configuration) : IThalosServiceClient +{ + private const string CorrelationHeaderName = "x-correlation-id"; + private readonly string refreshTenantId = configuration["ThalosService:RefreshTenantId"] ?? "refresh"; + + /// + public async Task IssueTokenAsync(IssueIdentityTokenRequest request) + { + var correlationId = ResolveCorrelationId(); + var grpcRequest = new IssueIdentityTokenGrpcRequest + { + SubjectId = request.SubjectId, + TenantId = request.TenantId + }; + + var grpcResponse = await grpcClient.IssueIdentityTokenAsync( + grpcRequest, + headers: CreateHeaders(correlationId)); + + return new IssueIdentityTokenResponse(grpcResponse.Token, grpcResponse.ExpiresInSeconds); + } + + /// + public async Task EvaluatePolicyAsync(EvaluateIdentityPolicyRequest request) + { + var correlationId = ResolveCorrelationId(); + var grpcRequest = new EvaluateIdentityPolicyGrpcRequest + { + SubjectId = request.SubjectId, + TenantId = request.TenantId, + PermissionCode = request.PermissionCode + }; + + var grpcResponse = await grpcClient.EvaluateIdentityPolicyAsync( + grpcRequest, + headers: CreateHeaders(correlationId)); + + return new EvaluateIdentityPolicyResponse( + grpcResponse.SubjectId, + grpcResponse.PermissionCode, + grpcResponse.IsAllowed); + } + + /// + public async Task RefreshSessionAsync(RefreshIdentitySessionRequest request) + { + var correlationId = ResolveCorrelationId(request.CorrelationId); + var grpcRequest = new IssueIdentityTokenGrpcRequest + { + SubjectId = request.RefreshToken, + TenantId = refreshTenantId + }; + + var grpcResponse = await grpcClient.IssueIdentityTokenAsync( + grpcRequest, + headers: CreateHeaders(correlationId)); + + return new RefreshIdentitySessionResponse(grpcResponse.Token, grpcResponse.ExpiresInSeconds); + } + + private string ResolveCorrelationId(string? preferred = null) + { + if (!string.IsNullOrWhiteSpace(preferred)) + { + return preferred; + } + + var context = httpContextAccessor.HttpContext; + if (context?.Items.TryGetValue(CorrelationHeaderName, out var itemValue) == true && + itemValue is string itemCorrelationId && + !string.IsNullOrWhiteSpace(itemCorrelationId)) + { + return itemCorrelationId; + } + + if (context?.Request.Headers.TryGetValue(CorrelationHeaderName, out var headerValue) == true && + !StringValues.IsNullOrEmpty(headerValue)) + { + return headerValue.ToString(); + } + + return context?.TraceIdentifier ?? $"corr-{Guid.NewGuid():N}"; + } + + private static Metadata CreateHeaders(string correlationId) + { + return new Metadata + { + { CorrelationHeaderName, correlationId } + }; + } +} diff --git a/src/Thalos.Bff.Rest/Program.cs b/src/Thalos.Bff.Rest/Program.cs index df15eb4..3944eee 100644 --- a/src/Thalos.Bff.Rest/Program.cs +++ b/src/Thalos.Bff.Rest/Program.cs @@ -1,18 +1,93 @@ +using Core.Blueprint.Common.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Thalos.Bff.Application.Adapters; +using Thalos.Bff.Application.DependencyInjection; +using Thalos.Bff.Application.Handlers; using Thalos.Bff.Contracts.Api; +using Thalos.Bff.Rest.Adapters; +using Thalos.Bff.Rest.Endpoints; +using Thalos.Service.Grpc; + +const string CorrelationHeaderName = "x-correlation-id"; var builder = WebApplication.CreateBuilder(args); -// Stage 3 skeleton: single active external protocol for this deployment is REST. +builder.Services.AddHttpContextAccessor(); +builder.Services.AddHealthChecks(); +builder.Services.AddBlueprintRuntimeCore(); +builder.Services.AddThalosBffApplicationRuntime(); +builder.Services.AddScoped(); +builder.Services.AddGrpcClient(options => +{ + var serviceAddress = builder.Configuration["ThalosService:GrpcAddress"] ?? "http://localhost:5251"; + options.Address = new Uri(serviceAddress); +}); + var app = builder.Build(); -app.MapPost("/api/identity/token", (IssueTokenApiRequest request) => +app.Use(async (context, next) => { - return Results.Ok(new IssueTokenApiResponse("", 0)); + var correlationId = ResolveCorrelationId(context); + context.Items[CorrelationHeaderName] = correlationId; + context.Request.Headers[CorrelationHeaderName] = correlationId; + context.Response.Headers[CorrelationHeaderName] = correlationId; + await next(); }); -app.MapPost("/api/identity/session/refresh", (RefreshSessionApiRequest request) => +app.MapPost($"{EndpointConventions.ApiPrefix}/token", async ( + IssueTokenApiRequest request, + HttpContext context, + IIssueTokenHandler handler) => { - return Results.Ok(new RefreshSessionApiResponse("", 0)); + var normalizedRequest = request with { CorrelationId = ResolveCorrelationId(context, request.CorrelationId) }; + + try + { + var response = await handler.HandleAsync(normalizedRequest); + return Results.Ok(response); + } + catch (UnauthorizedAccessException) + { + return Results.Unauthorized(); + } }); +app.MapPost($"{EndpointConventions.ApiPrefix}/session/refresh", async ( + RefreshSessionApiRequest request, + HttpContext context, + IRefreshSessionHandler handler) => +{ + var normalizedRequest = request with { CorrelationId = ResolveCorrelationId(context, request.CorrelationId) }; + var response = await handler.HandleAsync(normalizedRequest); + return Results.Ok(response); +}); + +app.MapHealthChecks("/healthz"); + app.Run(); + +string ResolveCorrelationId(HttpContext context, string? preferred = null) +{ + if (!string.IsNullOrWhiteSpace(preferred)) + { + context.Items[CorrelationHeaderName] = preferred; + context.Request.Headers[CorrelationHeaderName] = preferred; + context.Response.Headers[CorrelationHeaderName] = preferred; + return preferred; + } + + if (context.Items.TryGetValue(CorrelationHeaderName, out var itemValue) && + itemValue is string itemCorrelationId && + !string.IsNullOrWhiteSpace(itemCorrelationId)) + { + return itemCorrelationId; + } + + if (context.Request.Headers.TryGetValue(CorrelationHeaderName, out var headerValue) && + !StringValues.IsNullOrEmpty(headerValue)) + { + return headerValue.ToString(); + } + + return context.TraceIdentifier; +} diff --git a/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj b/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj index 256722a..5188257 100644 --- a/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj +++ b/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj @@ -4,8 +4,22 @@ enable enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + From 26c6e141c016eb220a9d09c4ddf6365eeb04d42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Tue, 24 Feb 2026 05:26:54 -0600 Subject: [PATCH 2/3] refactor(thalos-bff): use identity contracts --- docs/architecture/bff-identity-boundary.md | 14 ++++++++++++++ .../building-block-identity-adoption-plan.md | 13 +++++++++++++ docs/migration/edge-compatibility-checks.md | 6 ++++++ .../Adapters/IIdentityEdgeContractAdapter.cs | 4 ++-- .../Adapters/IThalosServiceClient.cs | 4 ++-- .../Adapters/IdentityEdgeContractAdapter.cs | 4 ++-- .../Contracts/RefreshIdentitySessionRequest.cs | 8 -------- .../Contracts/RefreshIdentitySessionResponse.cs | 8 -------- .../Security/IPermissionGuard.cs | 2 +- .../Security/IdentityPermissionGuard.cs | 2 +- .../Thalos.Bff.Application.csproj | 2 +- .../Conventions/ThalosBffPackageContract.cs | 2 +- .../Adapters/ThalosServiceGrpcClientAdapter.cs | 4 ++-- .../ContractShapeTests.cs | 2 +- .../IssueTokenHandlerTests.cs | 4 ++-- .../RefreshSessionHandlerTests.cs | 4 ++-- 16 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 docs/architecture/bff-identity-boundary.md create mode 100644 docs/migration/building-block-identity-adoption-plan.md create mode 100644 docs/migration/edge-compatibility-checks.md delete mode 100644 src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionRequest.cs delete mode 100644 src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionResponse.cs diff --git a/docs/architecture/bff-identity-boundary.md b/docs/architecture/bff-identity-boundary.md new file mode 100644 index 0000000..efe43ea --- /dev/null +++ b/docs/architecture/bff-identity-boundary.md @@ -0,0 +1,14 @@ +# Thalos BFF Identity Boundary + +## Purpose +Keep thalos-bff as an edge adapter layer that consumes thalos-service and adopted identity capability contracts. + +## BFF Responsibilities +- Edge contract handling +- Service client adaptation +- Correlation/tracing propagation + +## Prohibited +- Direct DAL access +- Identity policy decision ownership +- Identity persistence concerns diff --git a/docs/migration/building-block-identity-adoption-plan.md b/docs/migration/building-block-identity-adoption-plan.md new file mode 100644 index 0000000..c46e8aa --- /dev/null +++ b/docs/migration/building-block-identity-adoption-plan.md @@ -0,0 +1,13 @@ +# Building Block Identity Adoption Plan + +## Goal +Align BFF contract usage with building-block-identity contract surface without changing behavior. + +## Steps +1. Map current BFF identity contract types to capability contract types. +2. Keep compatibility bridge active during migration window. +3. Validate edge payload behavior and service compatibility. + +## Guardrails +- BFF remains service-facing. +- No identity decision logic moves into BFF. diff --git a/docs/migration/edge-compatibility-checks.md b/docs/migration/edge-compatibility-checks.md new file mode 100644 index 0000000..86ef61a --- /dev/null +++ b/docs/migration/edge-compatibility-checks.md @@ -0,0 +1,6 @@ +# Edge Compatibility Checks + +## Checks +- Existing edge request/response behavior remains stable. +- Correlation and trace metadata pass-through remains stable. +- Service contract compatibility is preserved after identity contract adoption. diff --git a/src/Thalos.Bff.Application/Adapters/IIdentityEdgeContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IIdentityEdgeContractAdapter.cs index 924a7f9..616e017 100644 --- a/src/Thalos.Bff.Application/Adapters/IIdentityEdgeContractAdapter.cs +++ b/src/Thalos.Bff.Application/Adapters/IIdentityEdgeContractAdapter.cs @@ -1,6 +1,6 @@ -using Thalos.Bff.Application.Contracts; using Thalos.Bff.Contracts.Api; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Bff.Application.Adapters; diff --git a/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs b/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs index 83e30d5..2e33aa6 100644 --- a/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs +++ b/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs @@ -1,5 +1,5 @@ -using Thalos.Bff.Application.Contracts; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Bff.Application.Adapters; diff --git a/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs index 508c98e..3f38a68 100644 --- a/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs +++ b/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs @@ -1,6 +1,6 @@ -using Thalos.Bff.Application.Contracts; using Thalos.Bff.Contracts.Api; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Bff.Application.Adapters; diff --git a/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionRequest.cs b/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionRequest.cs deleted file mode 100644 index f4bd093..0000000 --- a/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Thalos.Bff.Application.Contracts; - -/// -/// Transport-neutral internal request contract for refresh session flow. -/// -/// Refresh token value. -/// Request correlation identifier. -public sealed record RefreshIdentitySessionRequest(string RefreshToken, string CorrelationId); diff --git a/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionResponse.cs b/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionResponse.cs deleted file mode 100644 index 5a5267c..0000000 --- a/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Thalos.Bff.Application.Contracts; - -/// -/// Transport-neutral internal response contract for refresh session flow. -/// -/// Refreshed token value. -/// Token expiration in seconds. -public sealed record RefreshIdentitySessionResponse(string Token, int ExpiresInSeconds); diff --git a/src/Thalos.Bff.Application/Security/IPermissionGuard.cs b/src/Thalos.Bff.Application/Security/IPermissionGuard.cs index 15ce8e9..2cec0fb 100644 --- a/src/Thalos.Bff.Application/Security/IPermissionGuard.cs +++ b/src/Thalos.Bff.Application/Security/IPermissionGuard.cs @@ -1,4 +1,4 @@ -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Bff.Application.Security; diff --git a/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs b/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs index 964e64c..e05ec49 100644 --- a/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs +++ b/src/Thalos.Bff.Application/Security/IdentityPermissionGuard.cs @@ -1,4 +1,4 @@ -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Bff.Application.Security; diff --git a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj index 206b1cc..76aa9d2 100644 --- a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj +++ b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj @@ -7,6 +7,6 @@ - + diff --git a/src/Thalos.Bff.Contracts/Conventions/ThalosBffPackageContract.cs b/src/Thalos.Bff.Contracts/Conventions/ThalosBffPackageContract.cs index 0cc1e4c..213b7aa 100644 --- a/src/Thalos.Bff.Contracts/Conventions/ThalosBffPackageContract.cs +++ b/src/Thalos.Bff.Contracts/Conventions/ThalosBffPackageContract.cs @@ -11,5 +11,5 @@ public sealed class ThalosBffPackageContract : IBlueprintPackageContract public BlueprintPackageDescriptor Descriptor { get; } = new( "Thalos.Bff.Contracts", PackageVersionPolicy.Minor, - ["Core.Blueprint.Common", "Thalos.Service.Identity.Abstractions"]); + ["Core.Blueprint.Common", "BuildingBlock.Identity.Contracts"]); } diff --git a/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs index 462f524..1a597db 100644 --- a/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs +++ b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs @@ -1,9 +1,9 @@ using Grpc.Core; using Microsoft.Extensions.Primitives; using Thalos.Bff.Application.Adapters; -using Thalos.Bff.Application.Contracts; using Thalos.Service.Grpc; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Bff.Rest.Adapters; diff --git a/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs b/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs index 1df0d38..8f3e5b4 100644 --- a/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs +++ b/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs @@ -24,6 +24,6 @@ public class ContractShapeTests Assert.Equal("Thalos.Bff.Contracts", contract.Descriptor.PackageId); Assert.Equal(PackageVersionPolicy.Minor, contract.Descriptor.VersionPolicy); Assert.Contains("Core.Blueprint.Common", contract.Descriptor.DependencyPackageIds); - Assert.Contains("Thalos.Service.Identity.Abstractions", contract.Descriptor.DependencyPackageIds); + Assert.Contains("BuildingBlock.Identity.Contracts", contract.Descriptor.DependencyPackageIds); } } diff --git a/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs b/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs index ffbcf0f..c14f71f 100644 --- a/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs +++ b/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs @@ -1,9 +1,9 @@ -using Thalos.Bff.Application.Contracts; using Thalos.Bff.Application.Adapters; using Thalos.Bff.Application.Handlers; using Thalos.Bff.Application.Security; using Thalos.Bff.Contracts.Api; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Bff.Application.UnitTests; diff --git a/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs b/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs index aa494ac..b805764 100644 --- a/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs +++ b/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs @@ -1,8 +1,8 @@ using Thalos.Bff.Application.Adapters; -using Thalos.Bff.Application.Contracts; using Thalos.Bff.Application.Handlers; using Thalos.Bff.Contracts.Api; -using Thalos.Service.Identity.Abstractions.Contracts; +using BuildingBlock.Identity.Contracts.Requests; +using BuildingBlock.Identity.Contracts.Responses; namespace Thalos.Bff.Application.UnitTests; From cc221eab1ad753daba8dec6aaf60bd3ed30bde00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Wed, 25 Feb 2026 13:13:56 -0600 Subject: [PATCH 3/3] feat(thalos-bff): propagate provider metadata at edge adapters --- docs/architecture/bff-identity-boundary.md | 2 ++ .../Adapters/IdentityEdgeContractAdapter.cs | 17 ++++++++++--- .../IdentityEdgeGrpcContractAdapter.cs | 24 +++++++++++++++++-- .../Grpc/IssueIdentityTokenGrpcContract.cs | 9 ++++++- .../Api/IssueTokenApiRequest.cs | 11 ++++++++- .../Api/RefreshSessionApiRequest.cs | 8 ++++++- .../Thalos.Bff.Contracts.csproj | 1 + .../ThalosServiceGrpcClientAdapter.cs | 10 +++++--- src/Thalos.Bff.Rest/Program.cs | 6 +++++ .../ContractShapeTests.cs | 2 ++ 10 files changed, 79 insertions(+), 11 deletions(-) diff --git a/docs/architecture/bff-identity-boundary.md b/docs/architecture/bff-identity-boundary.md index efe43ea..a778e74 100644 --- a/docs/architecture/bff-identity-boundary.md +++ b/docs/architecture/bff-identity-boundary.md @@ -7,6 +7,8 @@ Keep thalos-bff as an edge adapter layer that consumes thalos-service and adopte - Edge contract handling - Service client adaptation - Correlation/tracing propagation +- Single active edge protocol policy enforcement (`rest`) +- Provider metadata propagation (`InternalJwt`, `AzureAd`, `Google`) ## Prohibited - Direct DAL access diff --git a/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs index 3f38a68..22259b0 100644 --- a/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs +++ b/src/Thalos.Bff.Application/Adapters/IdentityEdgeContractAdapter.cs @@ -12,13 +12,21 @@ public sealed class IdentityEdgeContractAdapter : IIdentityEdgeContractAdapter /// public EvaluateIdentityPolicyRequest ToPolicyRequest(IssueTokenApiRequest request, string permissionCode) { - return new EvaluateIdentityPolicyRequest(request.SubjectId, request.TenantId, permissionCode); + return new EvaluateIdentityPolicyRequest( + request.SubjectId, + request.TenantId, + permissionCode, + request.Provider); } /// public IssueIdentityTokenRequest ToIssueTokenRequest(IssueTokenApiRequest request) { - return new IssueIdentityTokenRequest(request.SubjectId, request.TenantId); + return new IssueIdentityTokenRequest( + request.SubjectId, + request.TenantId, + request.Provider, + request.ExternalToken); } /// @@ -30,7 +38,10 @@ public sealed class IdentityEdgeContractAdapter : IIdentityEdgeContractAdapter /// public RefreshIdentitySessionRequest ToRefreshSessionRequest(RefreshSessionApiRequest request) { - return new RefreshIdentitySessionRequest(request.RefreshToken, request.CorrelationId); + return new RefreshIdentitySessionRequest( + request.RefreshToken, + request.CorrelationId, + request.Provider); } /// diff --git a/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs index c06c1ee..7f2fc4f 100644 --- a/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs +++ b/src/Thalos.Bff.Application/Adapters/IdentityEdgeGrpcContractAdapter.cs @@ -11,12 +11,32 @@ public sealed class IdentityEdgeGrpcContractAdapter : IIdentityEdgeGrpcContractA /// public IssueIdentityTokenGrpcContract ToGrpc(IssueTokenApiRequest request) { - return new IssueIdentityTokenGrpcContract(request.SubjectId, request.TenantId, request.CorrelationId); + return new IssueIdentityTokenGrpcContract( + request.SubjectId, + request.TenantId, + request.CorrelationId, + request.Provider.ToString(), + request.ExternalToken); } /// public IssueTokenApiRequest FromGrpc(IssueIdentityTokenGrpcContract contract) { - return new IssueTokenApiRequest(contract.SubjectId, contract.TenantId, contract.CorrelationId); + return new IssueTokenApiRequest( + contract.SubjectId, + contract.TenantId, + contract.CorrelationId, + ParseProvider(contract.Provider), + contract.ExternalToken); + } + + private static BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider ParseProvider(string provider) + { + return Enum.TryParse( + provider, + true, + out var parsedProvider) + ? parsedProvider + : BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider.InternalJwt; } } diff --git a/src/Thalos.Bff.Application/Grpc/IssueIdentityTokenGrpcContract.cs b/src/Thalos.Bff.Application/Grpc/IssueIdentityTokenGrpcContract.cs index ac57ab0..e49ccc1 100644 --- a/src/Thalos.Bff.Application/Grpc/IssueIdentityTokenGrpcContract.cs +++ b/src/Thalos.Bff.Application/Grpc/IssueIdentityTokenGrpcContract.cs @@ -6,4 +6,11 @@ namespace Thalos.Bff.Application.Grpc; /// Identity subject identifier. /// Tenant identifier. /// Request correlation identifier. -public sealed record IssueIdentityTokenGrpcContract(string SubjectId, string TenantId, string CorrelationId); +/// Identity provider. +/// External provider token when applicable. +public sealed record IssueIdentityTokenGrpcContract( + string SubjectId, + string TenantId, + string CorrelationId, + string Provider = "InternalJwt", + string ExternalToken = ""); diff --git a/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs b/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs index 7014cc6..aea9771 100644 --- a/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs +++ b/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.Bff.Contracts.Api; /// @@ -6,4 +8,11 @@ namespace Thalos.Bff.Contracts.Api; /// Identity subject identifier. /// Tenant identifier. /// Request correlation identifier. -public sealed record IssueTokenApiRequest(string SubjectId, string TenantId, string CorrelationId = ""); +/// Identity auth provider. +/// External provider token when applicable. +public sealed record IssueTokenApiRequest( + string SubjectId, + string TenantId, + string CorrelationId = "", + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt, + string ExternalToken = ""); diff --git a/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs b/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs index a1765d3..43980d3 100644 --- a/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs +++ b/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.Bff.Contracts.Api; /// @@ -5,4 +7,8 @@ namespace Thalos.Bff.Contracts.Api; /// /// Refresh token value. /// Request correlation identifier. -public sealed record RefreshSessionApiRequest(string RefreshToken, string CorrelationId = ""); +/// Identity auth provider. +public sealed record RefreshSessionApiRequest( + string RefreshToken, + string CorrelationId = "", + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt); diff --git a/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj b/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj index 04a4bbc..9a5109e 100644 --- a/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj +++ b/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj @@ -6,5 +6,6 @@ + diff --git a/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs index 1a597db..01f3724 100644 --- a/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs +++ b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs @@ -25,7 +25,9 @@ public sealed class ThalosServiceGrpcClientAdapter( var grpcRequest = new IssueIdentityTokenGrpcRequest { SubjectId = request.SubjectId, - TenantId = request.TenantId + TenantId = request.TenantId, + Provider = request.Provider.ToString(), + ExternalToken = request.ExternalToken }; var grpcResponse = await grpcClient.IssueIdentityTokenAsync( @@ -43,7 +45,8 @@ public sealed class ThalosServiceGrpcClientAdapter( { SubjectId = request.SubjectId, TenantId = request.TenantId, - PermissionCode = request.PermissionCode + PermissionCode = request.PermissionCode, + Provider = request.Provider.ToString() }; var grpcResponse = await grpcClient.EvaluateIdentityPolicyAsync( @@ -63,7 +66,8 @@ public sealed class ThalosServiceGrpcClientAdapter( var grpcRequest = new IssueIdentityTokenGrpcRequest { SubjectId = request.RefreshToken, - TenantId = refreshTenantId + TenantId = refreshTenantId, + Provider = request.Provider.ToString() }; var grpcResponse = await grpcClient.IssueIdentityTokenAsync( diff --git a/src/Thalos.Bff.Rest/Program.cs b/src/Thalos.Bff.Rest/Program.cs index 3944eee..0c31b59 100644 --- a/src/Thalos.Bff.Rest/Program.cs +++ b/src/Thalos.Bff.Rest/Program.cs @@ -11,6 +11,12 @@ using Thalos.Service.Grpc; const string CorrelationHeaderName = "x-correlation-id"; var builder = WebApplication.CreateBuilder(args); +var edgeProtocol = builder.Configuration["ThalosBff:EdgeProtocol"] ?? "rest"; +if (!string.Equals(edgeProtocol, "rest", StringComparison.OrdinalIgnoreCase)) +{ + throw new InvalidOperationException( + $"Thalos BFF supports one active edge protocol per deployment. Configured: '{edgeProtocol}'. Expected: 'rest'."); +} builder.Services.AddHttpContextAccessor(); builder.Services.AddHealthChecks(); diff --git a/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs b/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs index 8f3e5b4..99f4d1c 100644 --- a/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs +++ b/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs @@ -1,4 +1,5 @@ using Core.Blueprint.Common.Contracts; +using BuildingBlock.Identity.Contracts.Conventions; using Thalos.Bff.Contracts.Api; using Thalos.Bff.Contracts.Conventions; @@ -14,6 +15,7 @@ public class ContractShapeTests Assert.Equal("user-1", request.SubjectId); Assert.Equal("tenant-1", request.TenantId); Assert.Equal("corr-123", request.CorrelationId); + Assert.Equal(IdentityAuthProvider.InternalJwt, request.Provider); } [Fact]