diff --git a/specs/078-operations-tenantless-canonical/spec.md b/specs/078-operations-tenantless-canonical/spec.md index fb409f7..9b6f48e 100644 --- a/specs/078-operations-tenantless-canonical/spec.md +++ b/specs/078-operations-tenantless-canonical/spec.md @@ -15,7 +15,15 @@ ## 0. Executive Summary - Tenant-scoped detail: `/admin/t/{tenant}/operations/r/{record}` - Tenant-scoped list: `/admin/t/{tenant}/operations` -This spec makes the tenantless detail page **the only run detail view**, removes the auto-generated tenant-scoped pages, and provides **secure, deny-as-not-found redirects** for legacy URLs—without introducing non-native UI frameworks. +This spec makes the tenantless detail page **the only run detail view**, removes the auto-generated tenant-scoped pages (legacy detail URLs naturally 404), and ensures **deny-as-not-found** security—without introducing non-native UI frameworks. + +--- + +## Clarifications + +### Session 2026-02-06 + +- Q: Should a tenant-scoped legacy URL (`/admin/t/{tenant}/operations/r/{record}`) for a run with `tenant_id = null` be allowed (redirect) or blocked (404)? → A: ~~Allow redirect~~ Superseded — FR-078-005/006 removed; legacy detail URLs naturally 404 after route decommission. --- @@ -23,9 +31,9 @@ ## 1. Goals 1. **Single canonical run detail view** — `/admin/operations/{run}` 2. **Remove auto-generated tenant-scoped pages** — decommission both `/admin/t/{tenant}/operations/r/{record}` and `/admin/t/{tenant}/operations` -3. **Secure legacy compatibility** — redirect legacy URLs with membership + tenant-match checks (no leakage) +3. **Clean decommission** — legacy tenant-scoped detail URLs naturally 404 after route removal (no redirect handlers needed) 4. **No duplication of UI logic** — reuse `OperationRunResource::infolist()` in the tenantless viewer -5. **Enterprise security semantics** — non-member → 404, no existence leakage via redirects +5. **Enterprise security semantics** — non-member → 404, no existence leakage --- @@ -73,8 +81,8 @@ ### P-078-002 — No tenant context dependency ### P-078-003 — Deny-as-not-found Any user not entitled to view a run MUST receive **404**, not 403. (Constitution: RBAC-UX-002) -### P-078-004 — No leakage via redirects -Legacy routes MUST NOT leak existence of runs or tenant association; redirects MUST be authorization-aware. +### P-078-004 — No leakage via legacy URLs +Decommissioned tenant-scoped routes MUST NOT exist after migration. Removed routes naturally return 404 — no redirect handlers, no existence leakage. ### P-078-005 — Filament-native Use Filament pages/resources/infolists/actions and Livewire v4 only. No custom SPA routing. @@ -100,20 +108,18 @@ ### User Story 1 — View operation run via canonical URL (Priority: P1) --- -### User Story 2 — Legacy tenant-scoped URL redirects safely (Priority: P2) +### User Story 2 — Legacy tenant-scoped detail URLs return 404 (Priority: P2) -A user has a bookmarked or old notification link pointing to `/admin/t/{tenant}/operations/r/{record}`. The system redirects to the canonical URL if authorized, or returns 404 if not. +A user has a bookmarked or old notification link pointing to `/admin/t/{tenant}/operations/r/{record}`. After route decommission, the system returns 404 — no redirect, no existence leakage. -**Why this priority**: Prevents broken links from historical notifications/bookmarks without leaking information. +**Why this priority**: Ensures decommissioned routes don't silently serve stale pages or leak information. -**Independent Test**: Hit legacy URL with authorized/unauthorized users and matching/mismatching tenant IDs; assert correct redirect or 404. +**Independent Test**: Hit legacy detail URLs; assert 404 for all users regardless of membership. **Acceptance Scenarios**: -1. **Given** authorized member + matching `tenant_id`, **When** user visits `/admin/t/{tenant}/operations/r/{record}`, **Then** 302 redirect to `/admin/operations/{record}`. -2. **Given** non-member, **When** user visits legacy URL, **Then** 404 (no redirect, no existence leakage). -3. **Given** member but URL has wrong `{tenant}` (doesn't match `run.tenant_id`), **When** user visits legacy URL, **Then** 404. -4. **Given** run doesn't exist, **When** user visits legacy URL, **Then** 404. +1. **Given** any user (member or non-member), **When** user visits `/admin/t/{tenant}/operations/r/{record}`, **Then** 404. +2. **Given** any user, **When** user visits `/admin/operations/r/{record}`, **Then** 404 (the `/r/` slug variant also does not exist). --- @@ -153,8 +159,7 @@ ### Edge Cases - Run with `workspace_id = 0` or null → 404 (existing behavior in `OperationRunPolicy::view`) - Run exists but requesting user has no workspace membership → 404 (deny-as-not-found) - Verification report section with `Filament::getTenant()` returning null → falls back to `OperationRunLinks::tenantlessView()` for previous-run URLs (already handled in infolist) -- Legacy redirect with non-numeric `{record}` → Laravel model binding returns 404 naturally -- Legacy redirect for `/admin/operations/r/{record}` (the `/r/` slug variant) → redirect to `/admin/operations/{record}` +- Legacy tenant-scoped URLs (`/admin/t/{tenant}/operations/r/{record}`) → 404 (routes no longer registered after decommission) --- @@ -164,7 +169,7 @@ ## Requirements - Authorization plane: Workspace-level (not tenant-level). Operations detail requires workspace membership only. - 404 vs 403: Non-member → 404 (RBAC-UX-002). No capability-gating for view-only operations access in this spec. - Server-side enforcement: `OperationRunPolicy::view()` + `WorkspaceMembership` check in `TenantlessOperationRunViewer::mount()`. -- Legacy redirects enforce authorization before redirecting (P-078-004). +- Legacy tenant-scoped routes are decommissioned (naturally 404); no redirect handlers needed (P-078-004). - Global search: `OperationRunResource` has `$shouldRegisterNavigation = false` and no `$recordTitleAttribute` — not globally searchable. **Constitution alignment (OPS-EX-AUTH-001):** Not applicable — no auth handshakes involved. @@ -184,24 +189,14 @@ #### 5.2 Decommission Auto-Generated Tenant-Scoped Pages - **FR-078-004**: Remove the `'view'` page entry from `OperationRunResource::getPages()` and delete `ViewOperationRun.php`. This eliminates the auto-generated `/admin/t/{tenant}/operations/r/{record}` route. - **FR-078-011**: Remove the `'index'` page entry from `OperationRunResource::getPages()` and delete `ListOperationRuns.php`. This eliminates the auto-generated `/admin/t/{tenant}/operations` route. The resource retains only its `table()` and `infolist()` schema builders for reuse by the custom pages. -#### 5.3 Legacy Redirect Compatibility (Secure) +#### 5.3 Legacy URL Handling -- **FR-078-005**: Add a legacy redirect handler for `GET /admin/t/{tenant}/operations/r/{record}`: - 1. Resolve the run by `{record}`. - 2. If user is not a member of `run.workspace_id` → **404**. - 3. If `run.tenant_id` is not null and does not match `{tenant}` → **404**. - 4. Redirect **302** to `/admin/operations/{record}`. +Legacy detail URLs (`/admin/t/{tenant}/operations/r/{record}` and `/admin/operations/r/{record}`) naturally return 404 after route decommission — no redirect handlers are needed. ~~FR-078-005 and FR-078-006 removed.~~ -- **FR-078-006**: Add a legacy redirect handler for `GET /admin/operations/r/{record}`: - 1. If user cannot view the run (policy check) → **404**. - 2. Redirect **302** to `/admin/operations/{record}`. - -- **FR-078-012**: Add a legacy redirect handler for `GET /admin/t/{tenant}/operations`: +- **FR-078-012**: Optionally add a convenience redirect for `GET /admin/t/{tenant}/operations`: 1. Redirect **302** to `/admin/operations`. - Note: No auth check needed since `/admin/operations` already enforces workspace membership. - -- All redirects use **302** (not 301) during rollout phase. 301 may be adopted once stable. + Note: No auth check needed since `/admin/operations` already enforces workspace membership. This redirect is a convenience only; the route could also be left to 404 naturally. #### 5.4 KPI Header Handling (Phase 1 Simplification) @@ -227,7 +222,7 @@ ### Measurable Outcomes - **SC-001**: Exactly one canonical run detail URL exists: `/admin/operations/{run}`. The route `filament.admin.resources.operations.view` is no longer registered. - **SC-002**: All "View run" links across notifications, widgets, tables, and jobs resolve to `/admin/operations/{run}` (no tenant-scoped detail links remain). -- **SC-003**: Legacy tenant-scoped URLs redirect securely (authorized → 302 to canonical; unauthorized → 404) with zero information leakage. +- **SC-003**: Legacy tenant-scoped detail URLs return 404 after route decommission (no redirect handlers, no information leakage). - **SC-004**: Runs with `tenant_id = null` render on canonical detail without errors. - **SC-005**: Verification report section renders correctly on canonical detail when `Filament::getTenant()` returns null. - **SC-006**: All existing Pest tests pass after migration (no regressions). @@ -245,10 +240,6 @@ ### SR-078-002 — No sensitive leakage in context JSON - Context JSON shown in UI uses allowlist-based redaction (existing behavior). - The verification report section already sanitizes sensitive data. -### SR-078-003 — Legacy redirect authorization -- Legacy redirects MUST verify workspace membership before issuing any redirect. -- A redirect to `/admin/operations/{run}` MUST NOT be issued if the user would receive 404 on the target page. - --- ## UX Requirements @@ -271,15 +262,9 @@ ### T-078-001 — Canonical run detail renders without tenant context - Create run with `tenant_id = null` and with `tenant_id` set - Assert `/admin/operations/{run}` renders summary sections without crash -### T-078-002 — Tenant-scoped legacy detail redirects securely -- Authorized member + matching tenant → 302 to `/admin/operations/{run}` -- Non-member → 404 (no redirect) -- Mismatched `tenant_id` in URL → 404 -- Non-existent run → 404 - -### T-078-003 — Legacy tenantless /r/ redirect -- `/admin/operations/r/{run}` redirects to `/admin/operations/{run}` for authorized user -- Non-member → 404 +### T-078-002 — Legacy tenant-scoped detail URLs return 404 +- Any user visiting `/admin/t/{tenant}/operations/r/{record}` → 404 (route not registered) +- Any user visiting `/admin/operations/r/{record}` → 404 (route not registered) ### T-078-004 — No auto-generated tenant-scoped routes exist - Assert route names `filament.admin.resources.operations.view` and `filament.admin.resources.operations.index` are not registered (or return 404) @@ -310,16 +295,15 @@ ### T-078-010 — Related links appear on canonical detail ## Acceptance Criteria - ✅ Exactly one canonical run detail URL: `/admin/operations/{run}` -- ✅ Auto-generated tenant-scoped routes (list + view) are removed; legacy URLs redirect securely or 404 without leakage +- ✅ Auto-generated tenant-scoped routes (list + view) are removed; legacy detail URLs return 404 - ✅ No "Admin details" / tenant-scoped back-links from canonical pages - ✅ Operations list works in workspace mode; tenant context only adds default filter - ✅ Runs with `tenant_id = null` display safely - ✅ Verification report renders correctly without tenant context - ✅ Related contextual links (from `OperationRunLinks::related()`) replace the removed "Admin details" button - ✅ Dead code (`OperationsDetail.php` + Blade view) removed -- ✅ Tests cover redirects, 404 semantics, infolist rendering, and canonical link rules +- ✅ Tests cover 404 semantics, infolist rendering, and canonical link rules - ✅ Implementation is Filament v5 + Livewire v4 native only -- ✅ All redirects use 302 during rollout --- @@ -349,5 +333,4 @@ ### KPI Header Deferral ## Open Decisions -- **301 vs 302**: Using 302 during rollout. Can be promoted to 301 once no new legacy links are being created. - **"Copy JSON" capability-gating**: Recommended for enterprise environments but not in scope for this spec.