# Feature Specification: RBAC UI Enforcement Helper v2 (Suite-wide, Mixed Visibility, Record-Scoped) **Feature Branch**: `066-rbac-ui-enforcement-helper-v2` **Created**: 2026-01-30 **Status**: Draft **Input**: Make the RBAC UI enforcement helper suite-ready by supporting mixed visibility, record-scoped tenancy, and deterministic bulk preflight authorization (follow-up to Feature 066 v1). ## Clarifications ### Session 2026-01-28 (carried forward from Feature 066 v1) - Q: For Bulk Actions with mixed-permission records (some authorized, some not), what should the default behavior be? → A: All-or-nothing (if any selected record would be unauthorized, the bulk action is disabled for members; if execution is forced, it must fail with 403 for members / 404 for non-members). - Q: Should the helper render actions at all for non-members (in case a tenant page is reachable via misrouting), or always hide them? → A: Hide for non-members in UI, but still enforce 404 server-side for any execution attempt. - Q: How strict should the “no ad-hoc authorization patterns in app/Filament/**” guard be in v1? → A: CI-failing (new ad-hoc patterns fail tests/CI). ### Session 2026-01-30 - Q: For mixed visibility, should `UiEnforcement::preserveVisibility()` be restricted to tenant-scoped surfaces (tenant routing already denies non-members), and forbidden for record-scoped / cross-tenant lists? → A: Yes — tenant-scoped only; record-scoped must use `andVisibleWhen(...)` / `andHiddenWhen(...)` (or default RBAC visibility). - Q: For bulk actions, should v2’s default “all-or-nothing” preflight apply to authorization only, or to authorization + business eligibility? → A: Authorization only — disable if any selected record is unauthorized; business-ineligible records may be skipped with a clear notification. - Q: For record-scoped tenant pages/actions (e.g., TenantResource row actions / edit/view), should a non-member be denied as 404 or 403 on direct access/execution? → A: 404 (deny-as-not-found). - Q: What should the default `UiTooltips` message be for member without capability (disabled action tooltip)? → A: “Insufficient permission — ask a tenant Owner.” - Q: Which authorization plane(s) are in scope for `UiEnforcement` v2? → A: Tenant plane only (tenant users/memberships with `Capabilities::*`); platform plane (`/system`) is out of scope. ## Problem Statement Feature 066 v1 introduced a canonical RBAC UI enforcement helper (`UiEnforcement`) and a CI-failing file-scan guard (`NoAdHocFilamentAuthPatternsTest`) to prevent drift. This removed many ad-hoc patterns, but several high-value Filament surfaces remain difficult to migrate due to: 1. Mixed visibility (business visibility + RBAC visibility) 2. Record-scoped tenancy (resources that are not tenant-scoped; the record *is* the tenant) 3. Bulk actions needing predictable all-or-nothing semantics without N+1 or partial execution surprises 4. Filament execution semantics (hidden/disabled blocks execution as a no-op; 404/403 cannot always be asserted at action execution time) v2 makes the helper suite-ready by adding explicit primitives for these cases and migrating remaining high-risk surfaces. ## Goals ### G1 — Suite-wide RBAC UX consistency A tenant member sees a consistent UI pattern everywhere: - **Non-member**: cannot discover or act (deny-as-not-found for pages; actions hidden + cannot execute) - **Member without capability**: action visible but disabled with a standard tooltip; cannot execute - **Member with capability**: action enabled; executes normally ### G2 — Eliminate remaining ad-hoc RBAC in Filament Remove `Gate::...`, `abort_*`, policy ability string literals, and custom RBAC patterns from remaining targeted Filament surfaces, replacing them with `UiEnforcement` and canonical `Capabilities::*`. ### G3 — Support mixed visibility and record-scoped tenancy Provide first-class APIs so teams can migrate without special casing per resource. ### G4 — Keep enforcement testable and stable Guard remains stable (file scan), allowlist shrinks, and we add targeted integration tests for each migrated surface. ## Non-Goals - Redesign pages or change business flows beyond RBAC visibility/disable/confirm behavior. - Rebuild authentication (063/064) or expand RBAC model semantics (065) beyond capabilities usage. - Convert every remaining Filament file in one shot; v2 targets a defined set and continues shrinking allowlist incrementally. ## Terms - **Member**: user has `canAccessTenant($tenant)` / membership. - **Capability**: canonical string key defined in `app/Support/Auth/Capabilities.php` (single source of truth). - **Tenant-scoped page**: Filament tenant context is set (`Filament::getTenant()` non-null). - **Record-scoped tenant**: record itself defines tenant context (e.g., tenant list rows where each record is a Tenant). - **Business visibility**: visibility logic unrelated to RBAC (trashed state, filter state, record status, etc.). - **Authorization plane**: v2 applies to the tenant plane (`/admin/t/{tenant}`) only; the platform plane (`/system`) is out of scope. ## RBAC-UX Contract (v2) This feature follows the RBAC constitution rules and clarifies Filament execution reality. ### 5.1 Non-member - **Pages/routes**: MUST be deny-as-not-found (404) using route/middleware scoping (already handled by tenant routing + membership guard). - **Actions**: MUST be hidden and cannot execute. **Note**: Filament treats hidden as non-executable (silent no-op). This is acceptable and is the enforced behavior. ### 5.2 Member without capability - Action is visible but disabled. - Tooltip MUST be from `UiTooltips` (or a standard override). - Execution is blocked for normal UI interaction (Filament disabled no-op). - Server-side authorization remains required for any mutation action (Policy/Gate). Where an execution attempt is reachable, it MUST be forbidden (403) for members without the capability. - Default tooltip text: “Insufficient permission — ask a tenant Owner.” ### 5.3 Member with capability - Enabled and executes. - Any destructive action MUST require confirmation (`->requiresConfirmation()`). ### 5.4 Bulk actions (default) All-or-nothing (authorization): if any selected record fails authorization, the bulk action is disabled (members) and cannot execute; no partial execution. Business eligibility (e.g., inactive/archived) is handled separately: ineligible records may be skipped with deterministic user feedback. ## Requirements ### Functional Requirements - **FR-001 Mixed Visibility Support**: `UiEnforcement` MUST support combining RBAC visibility with existing business visibility patterns without overriding them. - A resource can keep business `->visible()` conditions (trashed/state/filter). - RBAC adds membership/capability constraints without losing business visibility behavior. - **FR-002 Record-Scoped Tenant Context**: `UiEnforcement` MUST support record-scoped tenant resolution, where `$record` determines the tenant context. - Table action closures receive `$record`, and enforcement evaluates membership/capability against that record-tenant. - No hard dependency on `Filament::getTenant()` for these surfaces. - **FR-003 Bulk All-or-Nothing Preflight**: `UiEnforcement` MUST support bulk actions with a preflight authorization check that is deterministic, avoids N+1 DB queries (set-based where possible), and disables actions for mixed authorization selections. - The default preflight decision is based on authorization only; business eligibility remains separate (records may be skipped with clear feedback). - **FR-004 Canonical Capability Enforcement**: All v2-migrated surfaces MUST reference capabilities only via `Capabilities::*` constants. - **FR-005 Stable Guardrails**: The file-scan guard MUST remain CI-failing and allowlist-driven. - Allowlist entries MUST be removed as surfaces are migrated. - New ad-hoc patterns in `app/Filament/**` MUST fail CI. - **FR-006 Suite-wide Migration Targets (v2)**: v2 MUST migrate the following surface categories: - **Tier 1 (high impact / high risk)**: - Backup/Restore surfaces that currently contain ad-hoc RBAC patterns: - BackupSet resources / relation managers (mixed visibility) - RestoreRun resources / actions (mixed visibility) - TenantResource table actions (record-scoped tenant) - **Tier 2 (remaining allowlist “easy wins”)**: - Inventory item / inventory sync run resources - Any remaining Entra group run list pages - ProviderConnection pages where mixed visibility exists (if not completed in v1) - v2 MUST produce a measurable allowlist reduction (at minimum: remove allowlist entries for all migrated files). - **FR-007 Tests for Each Migrated Surface**: Each migrated surface MUST have at least one focused integration test asserting: - Non-member: action hidden + cannot execute - Member no cap: visible disabled + tooltip - Member with cap: enabled executes ### Non-Functional Requirements - **NFR-001 DB-only Rendering**: Helper and migrations MUST not introduce outbound HTTP during rendering (no Graph calls; DB only). - **NFR-002 Performance / Query Discipline**: Membership/capability evaluation should be O(1) per request once membership is loaded (no repeated membership queries per action render). Bulk preflight MUST be set-based where feasible. ## Design ### 7.1 `UiEnforcement` API extensions (v2) #### 7.1.1 Preserve / Compose Visibility (mixed visibility) Add explicit composition methods: - `preserveVisibility()` - Means: do not write `->visible(...)` / `->hidden(...)` closures. - Only apply disabled/tooltip/confirmation, and always enforce server-side authorization for any mutation action (Policy/Gate), independent of UI visibility/disabled state. - Optional: add additional defensive guards (e.g., explicit deny-as-not-found behavior) where the action/page lifecycle makes it reachable. - Restriction: tenant-scoped surfaces only (record-scoped / cross-tenant lists MUST NOT use this). - `andVisibleWhen(callable $businessVisible)` - Wrap business-visible closure and AND it with RBAC membership/capability visibility checks. - `andHiddenWhen(callable $businessHidden)` - Combine business-hidden logic with RBAC hidden logic. **Rule**: Default `apply()` sets RBAC visibility itself. Mixed-visibility surfaces MUST explicitly opt in to preservation or composition. #### 7.1.2 Tenant context providers Introduce explicit tenant resolver configuration: - `tenantFromFilament()` (default) - `tenantFromRecord()` (for record == tenant) - `tenantFrom(callable $resolver)` (custom mapping for pages where record maps to tenant) This MUST work for: - header actions (no record parameter) - table actions (record available in closure) - bulk actions (records collection or selection IDs) #### 7.1.3 Bulk preflight For bulk actions add: - `preflightSelection(callable $preflight)` OR built-in modes: - `preflightByTenantMembership()` using IDs and tenant context - `preflightByCapability()` for all selected records **Default for v2**: disable bulk action if any unauthorized (all-or-nothing). ### 7.2 Execution semantics (documented) Spec and quickstart MUST explicitly state: - hidden/disabled actions are blocked by Filament as no-ops, which is acceptable security behavior - 404/403 semantics are asserted at routing/page access and by server-side checks where reachable ### 7.3 Migration policy - Migrate 1–2 files per batch. - Remove allowlist entry in the same batch. - Run Pint + targeted tests + guard test each batch. ## User Stories & Testing ### US1 — Mixed Visibility: Backup/Restore surfaces become UiEnforcement-compliant (Priority: P1) As a tenant admin, I want backup/restore actions to follow the same RBAC UX patterns while preserving business visibility logic (trashed, filter state). **Acceptance Criteria** - Existing visibility behavior remains unchanged. - RBAC behavior is consistent (hidden/disabled + tooltip). - No ad-hoc `Gate/abort` patterns remain on migrated surfaces. ### US2 — Record-Scoped Tenant: TenantResource actions enforced correctly (Priority: P1) As a tenant-plane user with multiple tenant memberships, I want tenant list row actions to be enforced per tenant record without leaking other tenants. **Acceptance Criteria** - Row actions use record-tenant context. - Non-member never sees row actions for that tenant (hidden). - Member lacking capability sees disabled + tooltip. ### US3 — Bulk action preflight prevents partial execution (Priority: P2) As an operator, bulk actions should be disabled if selection contains unauthorized records. **Acceptance Criteria** - All-or-nothing semantics. - No partial execution. - Deterministic feedback (standard tooltip message). ### US4 — Guard allowlist shrinks with each migration (Priority: P2) As a maintainer, I want CI to fail if new ad-hoc RBAC patterns are added in Filament. **Acceptance Criteria** - Allowlist reduction is measurable (count down). - New violations fail tests. ## Verification Checklist (manual + automated) ### Automated - `vendor/bin/sail bin pint --dirty` - `vendor/bin/sail artisan test tests/Feature/Guards --compact` - Focused tests per migrated surface - RBAC suites: `tests/Feature/Rbac` + `tests/Unit/Support/Rbac` ### Manual spot checks (UI) - Login as Readonly → verify disabled tooltips and no execution. - Remove membership → verify actions disappear and cannot execute. - Bulk action selection with mixed permissions → action disabled. ## Rollout / Migration Strategy - Keep the guard allowlist for legacy files. - Every migration PR reduces allowlist entries. - Any new Filament file MUST NOT introduce forbidden patterns. ## v2 Deliverables ### Code - `UiEnforcement` enhancements: mixed visibility + record-tenant context + bulk preflight support. - Migrated Tier 1 and Tier 2 target surfaces. - Expanded tests for v2 targets. ### Docs - Update the feature quickstart with mixed-visibility recipes and record-tenant examples. - Update spec/constitution references if needed (no new constitution rules unless discovered gaps). ### Quality - Guard remains CI-failing and stable. - Allowlist shrunk measurably. ## Success Criteria ### Measurable Outcomes - **SC-001**: For every migrated surface, tests assert the RBAC-UX contract: non-member hidden/no-exec, member-no-cap disabled + tooltip, member-with-cap enabled + executes. - **SC-002**: Guard allowlist removes entries for every migrated file, and CI continues to fail for newly introduced ad-hoc auth patterns in `app/Filament/**`. - **SC-003**: Bulk preflight authorization is deterministic and does not degrade into per-record membership queries (verified via query count assertions on representative bulk selection tests). - **SC-004**: Rendering/hydration tests for migrated pages/actions enforce DB-only rendering (stray HTTP requests are prevented during render).