From 9ca6b4c110bf06114e502efdf0847b9fb2b97baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Sun, 22 Feb 2026 01:30:02 -0600 Subject: [PATCH] feat(stage3): scaffold task-001 baseline - WHY: establish Stage 3 task-001 execution baseline per repo intent - WHAT: add minimal solution/project skeleton and boundary docs - RULE: apply stage3 execution runtime and repository workflow directives --- Thalos.Bff.slnx | 10 +++++ docs/api/identity-edge-api.md | 16 ++++++++ docs/architecture/thalos-bff-flow.puml | 27 +++++++++++++ docs/security/permission-enforcement-map.md | 11 ++++++ .../Adapters/IThalosServiceClient.cs | 23 +++++++++++ .../Handlers/IIssueTokenHandler.cs | 16 ++++++++ .../Handlers/IRefreshSessionHandler.cs | 16 ++++++++ .../Handlers/IssueTokenHandler.cs | 23 +++++++++++ .../Handlers/RefreshSessionHandler.cs | 17 +++++++++ .../Security/IPermissionGuard.cs | 14 +++++++ .../Thalos.Bff.Application.csproj | 10 +++++ .../Api/IssueTokenApiRequest.cs | 8 ++++ .../Api/IssueTokenApiResponse.cs | 8 ++++ .../Api/RefreshSessionApiRequest.cs | 7 ++++ .../Api/RefreshSessionApiResponse.cs | 8 ++++ .../Thalos.Bff.Contracts.csproj | 7 ++++ .../Endpoints/EndpointConventions.cs | 12 ++++++ src/Thalos.Bff.Rest/Program.cs | 18 +++++++++ src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj | 11 ++++++ .../IssueTokenHandlerTests.cs | 38 +++++++++++++++++++ .../Thalos.Bff.Application.UnitTests.csproj | 21 ++++++++++ 21 files changed, 321 insertions(+) create mode 100644 Thalos.Bff.slnx create mode 100644 docs/api/identity-edge-api.md create mode 100644 docs/architecture/thalos-bff-flow.puml create mode 100644 docs/security/permission-enforcement-map.md create mode 100644 src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs create mode 100644 src/Thalos.Bff.Application/Handlers/IIssueTokenHandler.cs create mode 100644 src/Thalos.Bff.Application/Handlers/IRefreshSessionHandler.cs create mode 100644 src/Thalos.Bff.Application/Handlers/IssueTokenHandler.cs create mode 100644 src/Thalos.Bff.Application/Handlers/RefreshSessionHandler.cs create mode 100644 src/Thalos.Bff.Application/Security/IPermissionGuard.cs create mode 100644 src/Thalos.Bff.Application/Thalos.Bff.Application.csproj create mode 100644 src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs create mode 100644 src/Thalos.Bff.Contracts/Api/IssueTokenApiResponse.cs create mode 100644 src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs create mode 100644 src/Thalos.Bff.Contracts/Api/RefreshSessionApiResponse.cs create mode 100644 src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj create mode 100644 src/Thalos.Bff.Rest/Endpoints/EndpointConventions.cs create mode 100644 src/Thalos.Bff.Rest/Program.cs create mode 100644 src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj create mode 100644 tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs create mode 100644 tests/Thalos.Bff.Application.UnitTests/Thalos.Bff.Application.UnitTests.csproj diff --git a/Thalos.Bff.slnx b/Thalos.Bff.slnx new file mode 100644 index 0000000..e0195c7 --- /dev/null +++ b/Thalos.Bff.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/api/identity-edge-api.md b/docs/api/identity-edge-api.md new file mode 100644 index 0000000..c61beab --- /dev/null +++ b/docs/api/identity-edge-api.md @@ -0,0 +1,16 @@ +# Identity Edge API + +## Active External Protocol + +- REST is the active external protocol for this BFF deployment. + +## Entrypoints + +- `POST /api/identity/token` +- `POST /api/identity/session/refresh` + +## Boundary Notes + +- Endpoint handlers perform edge validation and permission checks. +- Business orchestration remains in thalos-service. +- Identity abstractions remain owned by Thalos repositories. diff --git a/docs/architecture/thalos-bff-flow.puml b/docs/architecture/thalos-bff-flow.puml new file mode 100644 index 0000000..ec2a6b0 --- /dev/null +++ b/docs/architecture/thalos-bff-flow.puml @@ -0,0 +1,27 @@ +@startuml +skinparam packageStyle rectangle + +package "thalos-bff" { + class Program + interface IIssueTokenHandler + class IssueTokenHandler + interface IRefreshSessionHandler + class RefreshSessionHandler + interface IPermissionGuard + interface IThalosServiceClient + + IssueTokenHandler ..|> IIssueTokenHandler + RefreshSessionHandler ..|> IRefreshSessionHandler + IssueTokenHandler --> IPermissionGuard + IssueTokenHandler --> IThalosServiceClient + RefreshSessionHandler --> IThalosServiceClient +} + +package "Clients" as Clients +package "thalos-service" as ThalosService + +Clients --> Program : REST +Program --> IIssueTokenHandler +Program --> IRefreshSessionHandler +IThalosServiceClient ..> ThalosService : gRPC/internal +@enduml diff --git a/docs/security/permission-enforcement-map.md b/docs/security/permission-enforcement-map.md new file mode 100644 index 0000000..e6592db --- /dev/null +++ b/docs/security/permission-enforcement-map.md @@ -0,0 +1,11 @@ +# Permission Enforcement Map + +## Enforcement Points + +- `identity.token.issue` evaluated at token issuance handler. +- Session refresh guarded by edge session validation policy. + +## Guardrail + +- Permission checks happen at BFF entrypoints before downstream calls. +- Authorization decisions are explicit and traceable at edge boundaries. diff --git a/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs b/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs new file mode 100644 index 0000000..667d149 --- /dev/null +++ b/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs @@ -0,0 +1,23 @@ +using Thalos.Bff.Contracts.Api; + +namespace Thalos.Bff.Application.Adapters; + +/// +/// Adapter boundary for downstream thalos-service calls. +/// +public interface IThalosServiceClient +{ + /// + /// Requests token issuance from thalos-service. + /// + /// Token issuance request. + /// Token issuance response. + Task IssueTokenAsync(IssueTokenApiRequest request); + + /// + /// Requests token refresh from thalos-service. + /// + /// Session refresh request. + /// Session refresh response. + Task RefreshSessionAsync(RefreshSessionApiRequest request); +} diff --git a/src/Thalos.Bff.Application/Handlers/IIssueTokenHandler.cs b/src/Thalos.Bff.Application/Handlers/IIssueTokenHandler.cs new file mode 100644 index 0000000..557b5c7 --- /dev/null +++ b/src/Thalos.Bff.Application/Handlers/IIssueTokenHandler.cs @@ -0,0 +1,16 @@ +using Thalos.Bff.Contracts.Api; + +namespace Thalos.Bff.Application.Handlers; + +/// +/// Edge handler boundary for token issuance entrypoint. +/// +public interface IIssueTokenHandler +{ + /// + /// Handles token issuance flow. + /// + /// Token issuance request. + /// Token issuance response. + Task HandleAsync(IssueTokenApiRequest request); +} diff --git a/src/Thalos.Bff.Application/Handlers/IRefreshSessionHandler.cs b/src/Thalos.Bff.Application/Handlers/IRefreshSessionHandler.cs new file mode 100644 index 0000000..e0d75e4 --- /dev/null +++ b/src/Thalos.Bff.Application/Handlers/IRefreshSessionHandler.cs @@ -0,0 +1,16 @@ +using Thalos.Bff.Contracts.Api; + +namespace Thalos.Bff.Application.Handlers; + +/// +/// Edge handler boundary for session refresh entrypoint. +/// +public interface IRefreshSessionHandler +{ + /// + /// Handles session refresh flow. + /// + /// Session refresh request. + /// Session refresh response. + Task HandleAsync(RefreshSessionApiRequest request); +} diff --git a/src/Thalos.Bff.Application/Handlers/IssueTokenHandler.cs b/src/Thalos.Bff.Application/Handlers/IssueTokenHandler.cs new file mode 100644 index 0000000..2e3ec62 --- /dev/null +++ b/src/Thalos.Bff.Application/Handlers/IssueTokenHandler.cs @@ -0,0 +1,23 @@ +using Thalos.Bff.Application.Adapters; +using Thalos.Bff.Application.Security; +using Thalos.Bff.Contracts.Api; + +namespace Thalos.Bff.Application.Handlers; + +/// +/// Default edge handler for token issuance. +/// +public sealed class IssueTokenHandler(IThalosServiceClient serviceClient, IPermissionGuard permissionGuard) + : IIssueTokenHandler +{ + /// + public Task HandleAsync(IssueTokenApiRequest request) + { + if (!permissionGuard.CanAccess("identity.token.issue")) + { + throw new UnauthorizedAccessException("Permission denied."); + } + + return serviceClient.IssueTokenAsync(request); + } +} diff --git a/src/Thalos.Bff.Application/Handlers/RefreshSessionHandler.cs b/src/Thalos.Bff.Application/Handlers/RefreshSessionHandler.cs new file mode 100644 index 0000000..0ffda49 --- /dev/null +++ b/src/Thalos.Bff.Application/Handlers/RefreshSessionHandler.cs @@ -0,0 +1,17 @@ +using Thalos.Bff.Application.Adapters; +using Thalos.Bff.Contracts.Api; + +namespace Thalos.Bff.Application.Handlers; + +/// +/// Default edge handler for refresh session flow. +/// +public sealed class RefreshSessionHandler(IThalosServiceClient serviceClient) + : IRefreshSessionHandler +{ + /// + public Task HandleAsync(RefreshSessionApiRequest request) + { + return serviceClient.RefreshSessionAsync(request); + } +} diff --git a/src/Thalos.Bff.Application/Security/IPermissionGuard.cs b/src/Thalos.Bff.Application/Security/IPermissionGuard.cs new file mode 100644 index 0000000..ac2600d --- /dev/null +++ b/src/Thalos.Bff.Application/Security/IPermissionGuard.cs @@ -0,0 +1,14 @@ +namespace Thalos.Bff.Application.Security; + +/// +/// Edge permission enforcement contract. +/// +public interface IPermissionGuard +{ + /// + /// Evaluates whether a permission is satisfied for the current request context. + /// + /// Permission code to evaluate. + /// True when access is allowed. + bool CanAccess(string permissionCode); +} diff --git a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj new file mode 100644 index 0000000..99d234a --- /dev/null +++ b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj @@ -0,0 +1,10 @@ + + + net10.0 + enable + enable + + + + + diff --git a/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs b/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs new file mode 100644 index 0000000..a5b423d --- /dev/null +++ b/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs @@ -0,0 +1,8 @@ +namespace Thalos.Bff.Contracts.Api; + +/// +/// External API request for identity token issuance. +/// +/// Identity subject identifier. +/// Tenant identifier. +public sealed record IssueTokenApiRequest(string SubjectId, string TenantId); diff --git a/src/Thalos.Bff.Contracts/Api/IssueTokenApiResponse.cs b/src/Thalos.Bff.Contracts/Api/IssueTokenApiResponse.cs new file mode 100644 index 0000000..44704b6 --- /dev/null +++ b/src/Thalos.Bff.Contracts/Api/IssueTokenApiResponse.cs @@ -0,0 +1,8 @@ +namespace Thalos.Bff.Contracts.Api; + +/// +/// External API response for identity token issuance. +/// +/// Issued access token. +/// Token expiration in seconds. +public sealed record IssueTokenApiResponse(string AccessToken, int ExpiresInSeconds); diff --git a/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs b/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs new file mode 100644 index 0000000..c9c8e76 --- /dev/null +++ b/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs @@ -0,0 +1,7 @@ +namespace Thalos.Bff.Contracts.Api; + +/// +/// External API request for refresh token session flow. +/// +/// Refresh token value. +public sealed record RefreshSessionApiRequest(string RefreshToken); diff --git a/src/Thalos.Bff.Contracts/Api/RefreshSessionApiResponse.cs b/src/Thalos.Bff.Contracts/Api/RefreshSessionApiResponse.cs new file mode 100644 index 0000000..ebbace3 --- /dev/null +++ b/src/Thalos.Bff.Contracts/Api/RefreshSessionApiResponse.cs @@ -0,0 +1,8 @@ +namespace Thalos.Bff.Contracts.Api; + +/// +/// External API response for refresh token session flow. +/// +/// Refreshed access token. +/// New token expiration in seconds. +public sealed record RefreshSessionApiResponse(string AccessToken, int ExpiresInSeconds); diff --git a/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj b/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj new file mode 100644 index 0000000..6c3a887 --- /dev/null +++ b/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj @@ -0,0 +1,7 @@ + + + net10.0 + enable + enable + + diff --git a/src/Thalos.Bff.Rest/Endpoints/EndpointConventions.cs b/src/Thalos.Bff.Rest/Endpoints/EndpointConventions.cs new file mode 100644 index 0000000..19062a5 --- /dev/null +++ b/src/Thalos.Bff.Rest/Endpoints/EndpointConventions.cs @@ -0,0 +1,12 @@ +namespace Thalos.Bff.Rest.Endpoints; + +/// +/// Defines endpoint conventions for the identity edge API. +/// +public static class EndpointConventions +{ + /// + /// Prefix used by identity edge API routes. + /// + public const string ApiPrefix = "/api/identity"; +} diff --git a/src/Thalos.Bff.Rest/Program.cs b/src/Thalos.Bff.Rest/Program.cs new file mode 100644 index 0000000..df15eb4 --- /dev/null +++ b/src/Thalos.Bff.Rest/Program.cs @@ -0,0 +1,18 @@ +using Thalos.Bff.Contracts.Api; + +var builder = WebApplication.CreateBuilder(args); + +// Stage 3 skeleton: single active external protocol for this deployment is REST. +var app = builder.Build(); + +app.MapPost("/api/identity/token", (IssueTokenApiRequest request) => +{ + return Results.Ok(new IssueTokenApiResponse("", 0)); +}); + +app.MapPost("/api/identity/session/refresh", (RefreshSessionApiRequest request) => +{ + return Results.Ok(new RefreshSessionApiResponse("", 0)); +}); + +app.Run(); diff --git a/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj b/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj new file mode 100644 index 0000000..256722a --- /dev/null +++ b/src/Thalos.Bff.Rest/Thalos.Bff.Rest.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs b/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs new file mode 100644 index 0000000..b5bf048 --- /dev/null +++ b/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs @@ -0,0 +1,38 @@ +using Thalos.Bff.Application.Adapters; +using Thalos.Bff.Application.Handlers; +using Thalos.Bff.Application.Security; +using Thalos.Bff.Contracts.Api; + +namespace Thalos.Bff.Application.UnitTests; + +public class IssueTokenHandlerTests +{ + [Fact] + public async Task HandleAsync_WhenPermissionAllowed_DelegatesToServiceClient() + { + var handler = new IssueTokenHandler(new FakeThalosServiceClient(), new AllowPermissionGuard()); + + var response = await handler.HandleAsync(new IssueTokenApiRequest("user-1", "tenant-1")); + + Assert.Equal("token-xyz", response.AccessToken); + Assert.Equal(1800, response.ExpiresInSeconds); + } + + private sealed class FakeThalosServiceClient : IThalosServiceClient + { + public Task IssueTokenAsync(IssueTokenApiRequest request) + { + return Task.FromResult(new IssueTokenApiResponse("token-xyz", 1800)); + } + + public Task RefreshSessionAsync(RefreshSessionApiRequest request) + { + return Task.FromResult(new RefreshSessionApiResponse("token-refreshed", 1800)); + } + } + + private sealed class AllowPermissionGuard : IPermissionGuard + { + public bool CanAccess(string permissionCode) => true; + } +} diff --git a/tests/Thalos.Bff.Application.UnitTests/Thalos.Bff.Application.UnitTests.csproj b/tests/Thalos.Bff.Application.UnitTests/Thalos.Bff.Application.UnitTests.csproj new file mode 100644 index 0000000..ecfabe1 --- /dev/null +++ b/tests/Thalos.Bff.Application.UnitTests/Thalos.Bff.Application.UnitTests.csproj @@ -0,0 +1,21 @@ + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + +