# Quickstart: UiEnforcement Helper > Adoption guide for developers adding RBAC enforcement to Filament actions. ## TL;DR Replace ad-hoc `->visible(fn ...)` / `->disabled(fn ...)` closures with `UiEnforcement`. ```php // ❌ Before (ad-hoc) Action::make('sync') ->visible(fn () => auth()->user()->can('provider:manage', Filament::getTenant())) ->disabled(fn () => ! auth()->user()->can('provider:manage', Filament::getTenant())) ->action(function () { // no server-side guard }); // ✅ After (UiEnforcement) UiEnforcement::forAction( Action::make('sync') ->action(fn () => $this->sync()) ) ->requireCapability(Capabilities::PROVIDER_MANAGE) ->apply(); ``` ## When to Use | Scenario | Use UiEnforcement? | |----------|-------------------| | Tenant-scoped action (header, table, bulk) | ✅ Yes | | Platform-scoped action (/system panel) | ❌ No (use Gate directly) | | Read-only navigation link | ❌ No (use `->visible()` for nav items) | | Destructive action (delete, detach, restore) | ✅ Yes, with `->destructive()` | ## API Reference ### Header / Page Actions ```php use App\Support\Rbac\UiEnforcement; use App\Support\Auth\Capabilities; protected function getHeaderActions(): array { return [ UiEnforcement::forAction( Action::make('createBackup') ->action(fn () => $this->createBackup()) ) ->requireCapability(Capabilities::BACKUP_CREATE) ->apply(), UiEnforcement::forAction( Action::make('deleteAllBackups') ->action(fn () => $this->deleteAll()) ) ->requireCapability(Capabilities::BACKUP_MANAGE) ->destructive() ->apply(), ]; } ``` ### Table Row Actions ```php public static function table(Table $table): Table { return $table ->columns([...]) ->actions([ UiEnforcement::forTableAction( Action::make('restore') ->action(fn (Policy $record) => $record->restore()), fn () => $this->getRecord() // record accessor ) ->requireCapability(Capabilities::RESTORE_EXECUTE) ->destructive() ->apply(), ]); } ``` ### Bulk Actions ```php ->bulkActions([ UiEnforcement::forBulkAction( BulkAction::make('deleteSelected') ->action(fn (Collection $records) => $records->each->delete()) ) ->requireCapability(Capabilities::TENANT_DELETE) ->destructive() ->apply(), ]) ``` ## Behavior Matrix | User Status | UI State | Server Response | |-------------|----------|-----------------| | Non-member | Hidden | Blocked (no execution, 200) | | Member, no capability | Visible, disabled + tooltip | Blocked (no execution, 200) | | Member, has capability | Enabled | Executes | | Member, destructive action | Confirmation modal | Executes after confirm | > **Note on 404/403 Responses:** In Filament v5, hidden actions are automatically > treated as disabled, so execution is blocked silently (returns 200 with no side > effects). True 404 enforcement happens at the page/routing level via tenant > middleware. The UiEnforcement helper includes defense-in-depth server-side > guards that abort(404/403) if somehow reached, but the primary protection is > Filament's isHidden/isDisabled chain. ## Tooltip Customization Default tooltip: *"You don't have permission to do this. Ask a tenant admin."* Override per-action: ```php UiEnforcement::forAction($action) ->requireCapability(Capabilities::PROVIDER_MANAGE) ->tooltip('Contact your organization owner to enable this feature.') ->apply(); ``` ## Testing Test both UI state and execution blocking: ```php it('hides sync action for non-members', function () { $user = User::factory()->create(); $tenant = Tenant::factory()->create(); // user is NOT a member actingAs($user); Filament::setTenant($tenant, true); Livewire::test(ListPolicies::class) ->assertActionHidden('sync'); }); it('blocks action execution for non-members (no side effects)', function () { $user = User::factory()->create(); $tenant = Tenant::factory()->create(); Queue::fake(); actingAs($user); Filament::setTenant($tenant, true); // Hidden actions are blocked silently (200 but no execution) Livewire::test(ListPolicies::class) ->mountAction('sync') ->callMountedAction() ->assertSuccessful(); // Verify no side effects occurred Queue::assertNothingPushed(); }); it('disables sync action for readonly members', function () { [$user, $tenant] = createUserWithTenant(role: 'readonly'); actingAs($user); Filament::setTenant($tenant, true); Livewire::test(ListPolicies::class) ->assertActionVisible('sync') ->assertActionDisabled('sync'); }); it('shows disabled tooltip for readonly members', function () { [$user, $tenant] = createUserWithTenant(role: 'readonly'); actingAs($user); Filament::setTenant($tenant, true); Livewire::test(ListPolicies::class) ->assertActionHasTooltip('sync', UiTooltips::INSUFFICIENT_PERMISSION); }); ``` ## Migration Checklist When migrating an existing action: - [ ] Remove `->visible(fn ...)` closure (UiEnforcement handles this) - [ ] Remove `->disabled(fn ...)` closure (UiEnforcement handles this) - [ ] Remove inline `Gate::check()` / `abort_unless()` from action handler - [ ] Wrap action with `UiEnforcement::forAction(...)->requireCapability(...)->apply()` - [ ] Add `->destructive()` if action modifies/deletes data - [ ] Add test for non-member (hidden + no execution) - [ ] Add test for member without capability (disabled + tooltip) - [ ] Add test for member with capability (enabled + executes) ### Real Example: ListPolicies Sync Action ```php // Before (ad-hoc) Action::make('sync') ->icon('heroicon-o-arrow-path') ->action(function (): void { $tenant = Tenant::current(); if (! $tenant) { return; } // ... sync logic }) ->disabled(fn (): bool => ! Gate::allows(Capabilities::TENANT_SYNC, Tenant::current())) // After (UiEnforcement) UiEnforcement::forAction( Action::make('sync') ->icon('heroicon-o-arrow-path') ->action(function (): void { $tenant = Tenant::current(); if (! $tenant) { return; } // ... sync logic }) ) ->requireCapability(Capabilities::TENANT_SYNC) ->destructive() ->apply() ``` ## Common Mistakes ### ❌ Forgetting `->apply()` ```php // This does nothing! UiEnforcement::forAction($action) ->requireCapability(Capabilities::PROVIDER_MANAGE); // missing ->apply() ``` ### ❌ Using with non-tenant panels ```php // UiEnforcement is tenant-scoped only! // For /system panel, use Gate::check() directly ``` ### ❌ Mixing old and new patterns ```php // Don't mix - pick one UiEnforcement::forAction( Action::make('sync') ->visible(fn () => someOtherCheck()) // ❌ conflict ) ->requireCapability(Capabilities::PROVIDER_MANAGE) ->apply(); ``` ## Questions? See [spec.md](./spec.md) for full requirements or [plan.md](./plan.md) for implementation details.