# 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).
