Spec 078: Operations tenantless canonical detail #95

Merged
ahmido merged 5 commits from 078-operations-tenantless-canonical into dev 2026-02-07 09:07:29 +00:00
Showing only changes of commit 3aa8f27213 - Show all commits

View File

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