Spec 078: Operations tenantless canonical detail #95
@ -15,7 +15,15 @@ ## 0. Executive Summary
|
|||||||
- Tenant-scoped detail: `/admin/t/{tenant}/operations/r/{record}`
|
- Tenant-scoped detail: `/admin/t/{tenant}/operations/r/{record}`
|
||||||
- Tenant-scoped list: `/admin/t/{tenant}/operations`
|
- 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}`
|
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`
|
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
|
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
|
### P-078-003 — Deny-as-not-found
|
||||||
Any user not entitled to view a run MUST receive **404**, not 403. (Constitution: RBAC-UX-002)
|
Any user not entitled to view a run MUST receive **404**, not 403. (Constitution: RBAC-UX-002)
|
||||||
|
|
||||||
### P-078-004 — No leakage via redirects
|
### P-078-004 — No leakage via legacy URLs
|
||||||
Legacy routes MUST NOT leak existence of runs or tenant association; redirects MUST be authorization-aware.
|
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
|
### P-078-005 — Filament-native
|
||||||
Use Filament pages/resources/infolists/actions and Livewire v4 only. No custom SPA routing.
|
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**:
|
**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}`.
|
1. **Given** any user (member or non-member), **When** user visits `/admin/t/{tenant}/operations/r/{record}`, **Then** 404.
|
||||||
2. **Given** non-member, **When** user visits legacy URL, **Then** 404 (no redirect, no existence leakage).
|
2. **Given** any user, **When** user visits `/admin/operations/r/{record}`, **Then** 404 (the `/r/` slug variant also does not exist).
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -153,8 +159,7 @@ ### Edge Cases
|
|||||||
- Run with `workspace_id = 0` or null → 404 (existing behavior in `OperationRunPolicy::view`)
|
- 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)
|
- 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)
|
- 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 tenant-scoped URLs (`/admin/t/{tenant}/operations/r/{record}`) → 404 (routes no longer registered after decommission)
|
||||||
- Legacy redirect for `/admin/operations/r/{record}` (the `/r/` slug variant) → redirect to `/admin/operations/{record}`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -164,7 +169,7 @@ ## Requirements
|
|||||||
- Authorization plane: Workspace-level (not tenant-level). Operations detail requires workspace membership only.
|
- 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.
|
- 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()`.
|
- 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.
|
- Global search: `OperationRunResource` has `$shouldRegisterNavigation = false` and no `$recordTitleAttribute` — not globally searchable.
|
||||||
|
|
||||||
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable — no auth handshakes involved.
|
**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-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.
|
- **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}`:
|
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.~~
|
||||||
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}`.
|
|
||||||
|
|
||||||
- **FR-078-006**: Add a legacy redirect handler for `GET /admin/operations/r/{record}`:
|
- **FR-078-012**: Optionally add a convenience redirect for `GET /admin/t/{tenant}/operations`:
|
||||||
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`:
|
|
||||||
1. Redirect **302** to `/admin/operations`.
|
1. Redirect **302** to `/admin/operations`.
|
||||||
|
|
||||||
Note: No auth check needed since `/admin/operations` already enforces workspace membership.
|
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.
|
||||||
|
|
||||||
- All redirects use **302** (not 301) during rollout phase. 301 may be adopted once stable.
|
|
||||||
|
|
||||||
#### 5.4 KPI Header Handling (Phase 1 Simplification)
|
#### 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-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-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-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-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).
|
- **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).
|
- Context JSON shown in UI uses allowlist-based redaction (existing behavior).
|
||||||
- The verification report section already sanitizes sensitive data.
|
- 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
|
## 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
|
- Create run with `tenant_id = null` and with `tenant_id` set
|
||||||
- Assert `/admin/operations/{run}` renders summary sections without crash
|
- Assert `/admin/operations/{run}` renders summary sections without crash
|
||||||
|
|
||||||
### T-078-002 — Tenant-scoped legacy detail redirects securely
|
### T-078-002 — Legacy tenant-scoped detail URLs return 404
|
||||||
- Authorized member + matching tenant → 302 to `/admin/operations/{run}`
|
- Any user visiting `/admin/t/{tenant}/operations/r/{record}` → 404 (route not registered)
|
||||||
- Non-member → 404 (no redirect)
|
- Any user visiting `/admin/operations/r/{record}` → 404 (route not registered)
|
||||||
- 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-004 — No auto-generated tenant-scoped routes exist
|
### 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)
|
- 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
|
## Acceptance Criteria
|
||||||
|
|
||||||
- ✅ Exactly one canonical run detail URL: `/admin/operations/{run}`
|
- ✅ 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
|
- ✅ No "Admin details" / tenant-scoped back-links from canonical pages
|
||||||
- ✅ Operations list works in workspace mode; tenant context only adds default filter
|
- ✅ Operations list works in workspace mode; tenant context only adds default filter
|
||||||
- ✅ Runs with `tenant_id = null` display safely
|
- ✅ Runs with `tenant_id = null` display safely
|
||||||
- ✅ Verification report renders correctly without tenant context
|
- ✅ Verification report renders correctly without tenant context
|
||||||
- ✅ Related contextual links (from `OperationRunLinks::related()`) replace the removed "Admin details" button
|
- ✅ Related contextual links (from `OperationRunLinks::related()`) replace the removed "Admin details" button
|
||||||
- ✅ Dead code (`OperationsDetail.php` + Blade view) removed
|
- ✅ 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
|
- ✅ Implementation is Filament v5 + Livewire v4 native only
|
||||||
- ✅ All redirects use 302 during rollout
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -349,5 +333,4 @@ ### KPI Header Deferral
|
|||||||
|
|
||||||
## Open Decisions
|
## 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.
|
- **"Copy JSON" capability-gating**: Recommended for enterprise environments but not in scope for this spec.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user