Align policy simulation auth passthrough at the frontdoor
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user