From 72084355a610d83e46b5516f587534593730d118 Mon Sep 17 00:00:00 2001 From: master <> Date: Tue, 10 Mar 2026 01:55:51 +0200 Subject: [PATCH] Align policy simulation auth passthrough at the frontdoor --- devops/compose/router-gateway-local.json | 15 ++++++ ...policy_simulation_frontdoor_translation.md | 50 +++++++++++++++++++ docs/modules/router/architecture.md | 3 +- .../Configuration/GatewayOptions.cs | 13 +++++ .../StellaOps.Gateway.WebService/Program.cs | 1 + .../appsettings.json | 11 ++++ .../IdentityHeaderPolicyMiddlewareTests.cs | 17 +++++++ 7 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 docs/implplan/SPRINT_20260309_018_Router_policy_simulation_frontdoor_translation.md diff --git a/devops/compose/router-gateway-local.json b/devops/compose/router-gateway-local.json index 3d13e1fe8..3b7cfb38b 100644 --- a/devops/compose/router-gateway-local.json +++ b/devops/compose/router-gateway-local.json @@ -5,6 +5,15 @@ "AllowAnonymous": true, "EnableLegacyHeaders": true, "AllowScopeHeader": false, + "ApprovedAuthPassthroughPrefixes": [ + "/connect", + "/console", + "/authority", + "/doctor", + "/api", + "/policy/shadow", + "/policy/simulations" + ], "Authority": { "Issuer": "https://authority.stella-ops.local/", "RequireHttpsMetadata": false, @@ -225,6 +234,12 @@ "TranslatesTo": "http://policy-gateway.stella-ops.local/policy/shadow", "PreserveAuthHeaders": true }, + { + "Type": "ReverseProxy", + "Path": "/policy/simulations", + "TranslatesTo": "http://policy-gateway.stella-ops.local/policy/simulations", + "PreserveAuthHeaders": true + }, { "Type": "ReverseProxy", "Path": "/api/v1/advisory-ai/adapters", diff --git a/docs/implplan/SPRINT_20260309_018_Router_policy_simulation_frontdoor_translation.md b/docs/implplan/SPRINT_20260309_018_Router_policy_simulation_frontdoor_translation.md new file mode 100644 index 000000000..a23cee6f2 --- /dev/null +++ b/docs/implplan/SPRINT_20260309_018_Router_policy_simulation_frontdoor_translation.md @@ -0,0 +1,50 @@ +# Sprint 20260309-018 - Router Policy Simulation Frontdoor Translation + +## Topic & Scope +- Restore frontdoor reachability for the live Policy Simulation history tools after the backend compatibility handlers were repaired. +- Fix the actual frontdoor root cause: router auth passthrough approval drift for `/policy/shadow*` and `/policy/simulations*`, not just raw path translation. +- Verify the repaired paths with direct frontdoor probes and authenticated Playwright navigation against `https://stella-ops.local`. +- Working directory: `src/Router/StellaOps.Gateway.WebService`. +- Allowed coordination edits: `devops/compose/router-gateway-local.json`, `devops/compose/router-gateway-local.reverseproxy.json`, `docs/modules/router/architecture.md`, `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/**`. +- Expected evidence: gateway auth policy diff, router config diff, focused direct HTTP probes, authenticated Playwright route/action artifacts. + +## Dependencies & Concurrency +- Depends on `SPRINT_20260309_011_Platform_live_remaining_route_contract_repair.md` for the backend `/policy/simulations*` handlers and focused gateway tests. +- Safe parallelism: do not touch unrelated route rewrites already in progress in the router JSON files; stage only the policy simulation auth/passthrough hunks for this commit. + +## Documentation Prerequisites +- `AGENTS.md` +- `docs/code-of-conduct/CODE_OF_CONDUCT.md` +- `docs/qa/feature-checks/FLOW.md` +- `docs/modules/policy/architecture.md` + +## Delivery Tracker + +### ROUTER-POLICY-SIM-018-001 - Align policy simulation frontdoor auth passthrough +Status: DOING +Dependency: none +Owners: Developer, QA +Task description: +- Extend the canonical local router config, reverse-proxy fallback config, and source gateway defaults so authenticated frontdoor requests for Policy simulation history, compare, verify, and pin actions reach `policy-gateway.stella-ops.local` with DPoP/JWT passthrough preserved. +- Keep the gateway's approved passthrough allow-list explicit and auditable instead of silently depending on a stale hardcoded prefix set. +- Preserve auth headers and avoid disturbing unrelated dirty route edits from other agents. + +Completion criteria: +- [ ] `https://stella-ops.local/policy/shadow/results`, `.../simulations/history`, `.../compare`, and `.../{id}/verify` no longer fail because gateway auth passthrough was stripped. +- [ ] Only the policy simulation passthrough hunks are staged for the commit. +- [ ] Authenticated Playwright can load the live history page and exercise its key actions through the frontdoor. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-09 | Sprint created after live retesting proved the Policy gateway served the simulation history contract while the frontdoor still returned `404` because the canonical router config translated `/policy/shadow*` but not `/policy/simulations*`. | Developer | +| 2026-03-09 | Live router logs showed the browser was already sending auth for `/policy/shadow/results` and `/policy/simulations/history`, but the gateway stripped Authorization/DPoP because the prefixes were missing from the approved passthrough allow-list. This sprint now fixes the gateway/config drift directly. | Developer | + +## Decisions & Risks +- Decision: keep auth passthrough fail-closed, but move the approved prefix set into explicit gateway/config data so live route additions do not silently drift away from the code path that strips auth headers. +- Decision: keep the policy fix scoped to `/policy/shadow` and `/policy/simulations` rather than broadening every `/policy/*` route. +- Risk: the router JSON files are already dirty from unrelated route work; stage only the specific policy passthrough additions and leave the rest untouched. + +## Next Checkpoints +- 2026-03-09: land the gateway/config passthrough fix and redeploy the frontdoor. +- 2026-03-09: rerun authenticated Policy Simulation history navigation with Playwright. diff --git a/docs/modules/router/architecture.md b/docs/modules/router/architecture.md index f973c5813..b1ae80d40 100644 --- a/docs/modules/router/architecture.md +++ b/docs/modules/router/architecture.md @@ -301,7 +301,8 @@ Request ─►│ ForwardedHeaders │ - Per-request tenant override is disabled by default and only works when explicitly enabled with `Gateway:Auth:EnableTenantOverride=true` and the requested tenant exists in `stellaops:allowed_tenants`. - Authorization/DPoP passthrough is fail-closed: - route must be configured with `PreserveAuthHeaders=true`, and -- route prefix must also be in the approved passthrough allow-list (`/connect`, `/console`, `/authority`, `/doctor`, `/api`). +- route prefix must also be in the approved passthrough allow-list configured under `Gateway:Auth:ApprovedAuthPassthroughPrefixes`. +- local frontdoor configs approve `/connect`, `/console`, `/authority`, `/doctor`, `/api`, `/policy/shadow`, and `/policy/simulations` so live policy compatibility endpoints can preserve DPoP/JWT passthrough without broadening unrelated routes. - Tenant override attempts are logged with deterministic fields including route, actor, requested tenant, and resolved tenant. ### Connection State diff --git a/src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptions.cs b/src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptions.cs index af1a0c6c6..19e2d1735 100644 --- a/src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptions.cs +++ b/src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptions.cs @@ -190,6 +190,19 @@ public sealed class GatewayAuthOptions /// public bool EnableTenantOverride { get; set; } = false; + /// + /// Approved route prefixes where Authorization/DPoP passthrough may be enabled. + /// Routes still need PreserveAuthHeaders=true to preserve auth headers. + /// + public List ApprovedAuthPassthroughPrefixes { get; set; } = + [ + "/connect", + "/console", + "/authority", + "/doctor", + "/api" + ]; + /// /// Emit signed identity envelope headers for router-dispatched requests. /// diff --git a/src/Router/StellaOps.Gateway.WebService/Program.cs b/src/Router/StellaOps.Gateway.WebService/Program.cs index 14de2dc25..d3e510890 100644 --- a/src/Router/StellaOps.Gateway.WebService/Program.cs +++ b/src/Router/StellaOps.Gateway.WebService/Program.cs @@ -131,6 +131,7 @@ builder.Services.AddSingleton(new IdentityHeaderPolicyOptions .Where(r => r.PreserveAuthHeaders) .Select(r => r.Path) .ToList(), + ApprovedAuthPassthroughPrefixes = [.. bootstrapOptions.Auth.ApprovedAuthPassthroughPrefixes], EnableTenantOverride = bootstrapOptions.Auth.EnableTenantOverride }); diff --git a/src/Router/StellaOps.Gateway.WebService/appsettings.json b/src/Router/StellaOps.Gateway.WebService/appsettings.json index 57df8946b..a9905ea7b 100644 --- a/src/Router/StellaOps.Gateway.WebService/appsettings.json +++ b/src/Router/StellaOps.Gateway.WebService/appsettings.json @@ -42,6 +42,15 @@ "DpopEnabled": true, "MtlsEnabled": false, "AllowAnonymous": true, + "ApprovedAuthPassthroughPrefixes": [ + "/connect", + "/console", + "/authority", + "/doctor", + "/api", + "/policy/shadow", + "/policy/simulations" + ], "Authority": { "Issuer": "https://authority.stella-ops.local", "RequireHttpsMetadata": false, @@ -76,6 +85,8 @@ { "Type": "ReverseProxy", "Path": "/v1/evidence-packs", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/evidence-packs" }, { "Type": "ReverseProxy", "Path": "/v1/runs", "TranslatesTo": "http://orchestrator.stella-ops.local/v1/runs" }, { "Type": "ReverseProxy", "Path": "/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai" }, + { "Type": "ReverseProxy", "Path": "/policy/simulations", "TranslatesTo": "http://policy-gateway.stella-ops.local/policy/simulations", "PreserveAuthHeaders": true }, + { "Type": "ReverseProxy", "Path": "/policy/shadow", "TranslatesTo": "http://policy-gateway.stella-ops.local/policy/shadow", "PreserveAuthHeaders": true }, { "Type": "ReverseProxy", "Path": "/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local" }, { "Type": "ReverseProxy", "Path": "/api/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy", "PreserveAuthHeaders": true }, { "Type": "ReverseProxy", "Path": "/api/risk", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk", "PreserveAuthHeaders": true }, diff --git a/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs b/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs index 390935ac3..f4a90d9b6 100644 --- a/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs +++ b/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs @@ -415,6 +415,23 @@ public sealed class IdentityHeaderPolicyMiddlewareTests Assert.Equal("proof-value", context.Request.Headers["DPoP"].ToString()); } + [Fact] + public async Task InvokeAsync_PreservesAuthorizationHeadersForConfiguredPolicyPrefix() + { + _options.JwtPassthroughPrefixes = ["/policy/shadow", "/policy/simulations"]; + _options.ApprovedAuthPassthroughPrefixes = ["/connect", "/policy/shadow", "/policy/simulations"]; + var middleware = CreateMiddleware(); + var context = CreateHttpContext("/policy/shadow/results"); + context.Request.Headers.Authorization = "DPoP token-value"; + context.Request.Headers["DPoP"] = "proof-value"; + + await middleware.InvokeAsync(context); + + Assert.True(_nextCalled); + Assert.Equal("DPoP token-value", context.Request.Headers.Authorization.ToString()); + Assert.Equal("proof-value", context.Request.Headers["DPoP"].ToString()); + } + [Fact] public async Task InvokeAsync_StripsAuthorizationHeadersWhenConfiguredPrefixIsNotApproved() {