diff --git a/devops/compose/router-gateway-local.json b/devops/compose/router-gateway-local.json index 3b7cfb38b..cf61478ca 100644 --- a/devops/compose/router-gateway-local.json +++ b/devops/compose/router-gateway-local.json @@ -229,7 +229,7 @@ "PreserveAuthHeaders": true }, { - "Type": "Microservice", + "Type": "ReverseProxy", "Path": "/policy/shadow", "TranslatesTo": "http://policy-gateway.stella-ops.local/policy/shadow", "PreserveAuthHeaders": true diff --git a/docs/implplan/SPRINT_20260309_019_FE_policy_simulation_active_tenant_runtime.md b/docs/implplan/SPRINT_20260309_019_FE_policy_simulation_active_tenant_runtime.md new file mode 100644 index 000000000..31b6d55ae --- /dev/null +++ b/docs/implplan/SPRINT_20260309_019_FE_policy_simulation_active_tenant_runtime.md @@ -0,0 +1,49 @@ +# Sprint 20260309-019 - FE Policy Simulation Active Tenant Runtime + +## Topic & Scope +- Remove the remaining mock-era tenant placeholder behavior from live Policy Simulation runtime calls. +- Ensure live policy simulation surfaces use the active shell tenant context when older callers still pass the legacy `'default'` placeholder. +- Verify the repaired behavior with focused client tests, a web rebuild, and authenticated Playwright against `https://stella-ops.local`. +- Working directory: `src/Web/StellaOps.Web/src/app/core/api`. +- Allowed coordination edits: `src/Web/StellaOps.Web/src/app/features/policy-simulation/**`, `docs/modules/ui/**`. +- Expected evidence: focused client spec pass, live Playwright policy sweep artifact, rebuilt web bundle. + +## Dependencies & Concurrency +- Depends on `SPRINT_20260309_018_Router_policy_simulation_frontdoor_translation.md` so the frontdoor preserves auth/DPoP for policy simulation requests. +- Safe parallelism: avoid touching unrelated search and setup slices; keep this sprint scoped to policy simulation tenant resolution. + +## Documentation Prerequisites +- `AGENTS.md` +- `docs/code-of-conduct/CODE_OF_CONDUCT.md` +- `docs/qa/feature-checks/FLOW.md` +- `docs/modules/ui/README.md` + +## Delivery Tracker + +### FE-POLICY-SIM-019-001 - Normalize legacy placeholder tenants to the active shell context +Status: DOING +Dependency: none +Owners: Developer, QA +Task description: +- Repair the live Policy Simulation client seam so runtime requests stop sending `tenant=default` when the shell is actually scoped to a real tenant such as `demo-prod`. +- Preserve explicit tenant overrides for legitimate cross-tenant/admin flows while treating the legacy `'default'` value as a placeholder whenever an active context tenant is available. +- Cover the behavior with focused tests and live Playwright verification on the shadow results/history flows. + +Completion criteria: +- [ ] Policy Simulation history, pin, compare, verify, and shadow-results requests no longer fail with tenant override rejection in live router logs. +- [ ] Focused client tests prove placeholder tenant resolution prefers active runtime tenant while explicit custom tenants still win. +- [ ] Authenticated Playwright on `/ops/policy/simulation` and `/ops/policy/simulation/history` completes without `403` responses for `/policy/shadow/results` or `/policy/simulations/history`. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-09 | Sprint created after live Playwright and router logs showed Policy Simulation pages were reachable, but background requests still failed with `403` because the feature passed `tenant=default` while the live context resolved to `demo-prod`. | Developer | +| 2026-03-10 | Focused `policy-simulation.client.spec.ts` passed with the new placeholder-tenant normalization. Live recheck confirmed `/policy/simulations/history` moved from `403` to `200`, then exposed remaining local gateway drift where `/policy/shadow` was still typed as `Microservice` and returned frontdoor `404`s. | Developer | + +## Decisions & Risks +- Decision: normalize the legacy `'default'` tenant at the shared client seam instead of patching only the currently failing components; this protects the whole Policy Simulation feature cluster against the same runtime drift. +- Risk: a real tenant literally named `default` would still be ambiguous; preserve it only when no active tenant context exists. + +## Next Checkpoints +- 2026-03-09: land the client normalization and focused regression test. +- 2026-03-09: rebuild the web bundle and re-run authenticated Playwright on the affected policy routes. diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts index af0603814..4fdf12b11 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts @@ -30,7 +30,9 @@ describe('PolicySimulationHttpClient', () => { beforeEach(() => { authSessionStoreMock = jasmine.createSpyObj('AuthSessionStore', ['getActiveTenantId']); - tenantServiceMock = jasmine.createSpyObj('TenantActivationService', ['getActiveTenant']); + tenantServiceMock = { + activeTenantId: jasmine.createSpy('activeTenantId').and.returnValue(null), + }; authSessionStoreMock.getActiveTenantId.and.returnValue('tenant-001'); TestBed.configureTestingModule({ @@ -726,6 +728,7 @@ describe('PolicySimulationHttpClient', () => { it('should use provided tenant over active tenant', async () => { const customTenant = 'custom-tenant-001'; + tenantServiceMock.activeTenantId.and.returnValue('tenant-context-001'); const promise = firstValueFrom(httpClient.getShadowModeConfig({ tenantId: customTenant })); const req = httpMock.expectOne(`${baseUrl}/policy/shadow/config`); @@ -734,6 +737,23 @@ describe('PolicySimulationHttpClient', () => { await promise; }); + + it('should map legacy default placeholder tenant to the active tenant context', async () => { + tenantServiceMock.activeTenantId.and.returnValue('demo-prod'); + + const promise = firstValueFrom( + httpClient.getSimulationHistory({ + tenantId: 'default', + page: 1, + pageSize: 20, + }), + ); + const req = httpMock.expectOne((request) => request.url === `${baseUrl}/policy/simulations/history`); + expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('demo-prod'); + req.flush({ items: [], total: 0, hasMore: false }); + + await promise; + }); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts index d1eb5fd7b..bc9ec95df 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts @@ -110,6 +110,8 @@ export interface PolicySimulationApi { export const POLICY_SIMULATION_API = new InjectionToken('POLICY_SIMULATION_API'); export const POLICY_SIMULATION_API_BASE_URL = new InjectionToken('POLICY_SIMULATION_API_BASE_URL'); +const LEGACY_PLACEHOLDER_TENANTS = new Set(['default']); + // ============================================================================ // HTTP Implementation // ============================================================================ @@ -426,7 +428,15 @@ export class PolicySimulationHttpClient implements PolicySimulationApi { } private resolveTenant(tenantId?: string): string { - const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId(); + const requestedTenant = tenantId?.trim() || null; + const activeTenant = + this.tenantService.activeTenantId?.() ?? + this.authSession.getActiveTenantId(); + const tenant = + !requestedTenant || (activeTenant && LEGACY_PLACEHOLDER_TENANTS.has(requestedTenant.toLowerCase())) + ? activeTenant ?? requestedTenant + : requestedTenant; + if (!tenant) { throw new Error('PolicySimulationHttpClient requires an active tenant identifier.'); }