TenantAtlas/specs/106-required-permissions-sidebar-context/research.md

72 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Research: 106 — Required Permissions Sidebar Context Fix
**Date**: 2026-02-22 | **Branch**: `106-required-permissions-sidebar-context`
## RQ-001: How does `EnsureFilamentTenantSelected` currently handle workspace-scoped pages with `{tenant}` params?
**Finding**: The middleware at `app/Support/Middleware/EnsureFilamentTenantSelected.php` follows this flow:
1. **Livewire `/livewire/update`** — checks referer for known workspace-scoped patterns (currently only `/admin/operations/{run}`). If matched, calls `configureNavigationForRequest()` without setting tenant and returns early.
2. **`/admin/operations/{run}`** — early return with workspace nav (no tenant set).
3. **`/admin/operations`** — early return with workspace nav (no tenant set).
4. **Route has `{tenant}` param** (line ~74131) — resolves tenant, validates workspace membership + tenant access, then **always** calls `Filament::setTenant($tenant, true)` and `configureNavigationForRequest()`. This is the root cause: `required-permissions` passes through here and gets tenant sidebar.
5. **Allowlisted workspace-scoped paths** (line ~133141) — string prefix checks for `/admin/w/`, `/admin/workspaces`, `/admin/operations`, plus an exact-match array. These get workspace nav without tenant.
**Root cause**: Step 4 has no exclusion mechanism. Any page with a `{tenant}` route parameter — including `TenantRequiredPermissions` — gets `Filament::setTenant()` called, triggering tenant sidebar.
**Decision**: Add a path-based allowlist check **inside** step 4, before `Filament::setTenant()` is called. When the path matches a workspace-scoped page that uses `{tenant}` for data scoping (not sidebar scoping), skip `setTenant()` but still perform all authorization checks.
**Alternatives considered**:
- *Page-level override (e.g., `$isWorkspaceScoped` property)*: Would require Filament page resolution in middleware (too expensive / wrong layer).
- *Separate middleware for Required Permissions route*: Duplicates authorization logic; fragile.
- *Clear tenant after middleware runs*: Race condition with `configureNavigationForRequest()` which already ran.
## RQ-002: What pages currently use the allowlist pattern?
**Finding**: The middleware already has two precedent patterns for workspace-scoped exemptions:
| Pattern | Type | Location in middleware |
|---|---|---|
| `/admin/operations` | Exact match | Line ~67 |
| `/admin/operations/{run}` | Regex | Line ~61 |
| `/livewire/update` + referer `/admin/operations/{run}` | Referer check | Line ~4956 |
| `/admin/w/`, `/admin/workspaces`, etc. | Prefix/exact | Line ~133141 |
**Decision**: Follow the same pattern (regex match + referer check for Livewire) for `/admin/tenants/{tenant}/required-permissions`.
## RQ-003: How does `configureNavigationForRequest` determine sidebar?
**Finding** (lines 152247):
- If `Filament::getTenant()` is filled → `$panel->navigation(true)` (default tenant nav).
- If tenant is null → builds a custom `NavigationBuilder` with workspace-scoped items only (Manage workspaces, Operations, Alert targets/rules/deliveries, Alerts, Audit Log).
**Decision**: No changes needed to `configureNavigationForRequest`. The fix is upstream: don't call `setTenant()` for workspace-scoped pages, so the method naturally shows workspace nav.
## RQ-004: What authorization must still run for exempted pages?
**Finding**: The `{tenant}` param resolution block (lines 74131) performs:
1. Null user check → early return
2. `HasTenants` interface check → abort 404
3. Tenant resolution by `external_id` (including soft-deleted) → abort 404
4. Workspace ID null check → abort 404
5. Tenant workspace ownership check → abort 404
6. Workspace existence check → abort 404
7. User workspace membership check → abort 404
8. `canAccessTenant()` → abort 404
**Decision**: All 8 checks must still run. Only `Filament::setTenant()` and `app(WorkspaceContext::class)->rememberLastTenantId()` are skipped for exempted workspace-scoped pages. The tenant is still resolved and authorized — it just isn't set as Filament's active tenant.
## RQ-005: Livewire update handling
**Finding**: For Livewire `/livewire/update` requests, the middleware inspects the `Referer` header to determine context. Currently only `/admin/operations/{run}` is checked.
**Decision**: Add `/admin/tenants/{tenant}/required-permissions` to the referer pattern check. The regex `#^/admin/tenants/[^/]+/required-permissions$#` will match referers from the Required Permissions page, ensuring Livewire re-renders (filter changes) also get workspace sidebar.
## RQ-006: Impact on `rememberLastTenantId()`
**Finding**: When `Filament::setTenant()` is called in step 4, `rememberLastTenantId()` stores the tenant in session. This enables "return to last tenant" convenience.
**Decision**: For workspace-scoped exempted pages, `rememberLastTenantId()` should NOT be called. Visiting Required Permissions should not change the user's "last active tenant" context. The tenant is used only for data display on that page.
**Rationale**: If we called `rememberLastTenantId()`, navigating to Required Permissions would change the user's default tenant context globally — unexpected side effect for a workspace-level page.