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

5.2 KiB
Raw Blame History

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.