Align policy simulation auth passthrough at the frontdoor

This commit is contained in:
master
2026-03-10 01:55:51 +02:00
parent d16d7a1692
commit 72084355a6
7 changed files with 109 additions and 1 deletions

View File

@@ -5,6 +5,15 @@
"AllowAnonymous": true, "AllowAnonymous": true,
"EnableLegacyHeaders": true, "EnableLegacyHeaders": true,
"AllowScopeHeader": false, "AllowScopeHeader": false,
"ApprovedAuthPassthroughPrefixes": [
"/connect",
"/console",
"/authority",
"/doctor",
"/api",
"/policy/shadow",
"/policy/simulations"
],
"Authority": { "Authority": {
"Issuer": "https://authority.stella-ops.local/", "Issuer": "https://authority.stella-ops.local/",
"RequireHttpsMetadata": false, "RequireHttpsMetadata": false,
@@ -225,6 +234,12 @@
"TranslatesTo": "http://policy-gateway.stella-ops.local/policy/shadow", "TranslatesTo": "http://policy-gateway.stella-ops.local/policy/shadow",
"PreserveAuthHeaders": true "PreserveAuthHeaders": true
}, },
{
"Type": "ReverseProxy",
"Path": "/policy/simulations",
"TranslatesTo": "http://policy-gateway.stella-ops.local/policy/simulations",
"PreserveAuthHeaders": true
},
{ {
"Type": "ReverseProxy", "Type": "ReverseProxy",
"Path": "/api/v1/advisory-ai/adapters", "Path": "/api/v1/advisory-ai/adapters",

View File

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

View File

@@ -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`. - 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: - Authorization/DPoP passthrough is fail-closed:
- route must be configured with `PreserveAuthHeaders=true`, and - 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. - Tenant override attempts are logged with deterministic fields including route, actor, requested tenant, and resolved tenant.
### Connection State ### Connection State

View File

@@ -190,6 +190,19 @@ public sealed class GatewayAuthOptions
/// </summary> /// </summary>
public bool EnableTenantOverride { get; set; } = false; public bool EnableTenantOverride { get; set; } = false;
/// <summary>
/// Approved route prefixes where Authorization/DPoP passthrough may be enabled.
/// Routes still need <c>PreserveAuthHeaders=true</c> to preserve auth headers.
/// </summary>
public List<string> ApprovedAuthPassthroughPrefixes { get; set; } =
[
"/connect",
"/console",
"/authority",
"/doctor",
"/api"
];
/// <summary> /// <summary>
/// Emit signed identity envelope headers for router-dispatched requests. /// Emit signed identity envelope headers for router-dispatched requests.
/// </summary> /// </summary>

View File

@@ -131,6 +131,7 @@ builder.Services.AddSingleton(new IdentityHeaderPolicyOptions
.Where(r => r.PreserveAuthHeaders) .Where(r => r.PreserveAuthHeaders)
.Select(r => r.Path) .Select(r => r.Path)
.ToList(), .ToList(),
ApprovedAuthPassthroughPrefixes = [.. bootstrapOptions.Auth.ApprovedAuthPassthroughPrefixes],
EnableTenantOverride = bootstrapOptions.Auth.EnableTenantOverride EnableTenantOverride = bootstrapOptions.Auth.EnableTenantOverride
}); });

View File

@@ -42,6 +42,15 @@
"DpopEnabled": true, "DpopEnabled": true,
"MtlsEnabled": false, "MtlsEnabled": false,
"AllowAnonymous": true, "AllowAnonymous": true,
"ApprovedAuthPassthroughPrefixes": [
"/connect",
"/console",
"/authority",
"/doctor",
"/api",
"/policy/shadow",
"/policy/simulations"
],
"Authority": { "Authority": {
"Issuer": "https://authority.stella-ops.local", "Issuer": "https://authority.stella-ops.local",
"RequireHttpsMetadata": false, "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/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/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": "/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": "/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/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 }, { "Type": "ReverseProxy", "Path": "/api/risk", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk", "PreserveAuthHeaders": true },

View File

@@ -415,6 +415,23 @@ public sealed class IdentityHeaderPolicyMiddlewareTests
Assert.Equal("proof-value", context.Request.Headers["DPoP"].ToString()); 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] [Fact]
public async Task InvokeAsync_StripsAuthorizationHeadersWhenConfiguredPrefixIsNotApproved() public async Task InvokeAsync_StripsAuthorizationHeadersWhenConfiguredPrefixIsNotApproved()
{ {