TenantAtlas/specs/066-rbac-ui-enforcement-helper/plan.md
ahmido 6a86c5901a 066-rbac-ui-enforcement-helper (#81)
Kontext / Ziel
Diese PR standardisiert Tenant‑RBAC Enforcement in der Filament‑UI: statt ad-hoc Gate::*, abort_if/abort_unless und kopierten ->visible()/->disabled()‑Closures gibt es jetzt eine zentrale, wiederverwendbare Implementierung für Actions (Header/Table/Bulk).

Links zur Spec:

spec.md
plan.md
quickstart.md
Was ist drin
Neue zentrale Helper-API: UiEnforcement (Tenant-plane RBAC‑UX “source of truth” für Filament Actions)
Standardisierte Tooltip-Texte und Context-DTO (UiTooltips, TenantAccessContext)
Migration vieler tenant‑scoped Filament Action-Surfaces auf das Standardpattern (ohne ad-hoc Auth-Patterns)
CI‑Guard (Test) gegen neue ad-hoc Patterns in app/Filament/**:
verbietet Gate::allows/denies/check/authorize, use Illuminate\Support\Facades\Gate, abort_if/abort_unless
Legacy-Allowlist ist aktuell leer (neue Verstöße failen sofort)
RBAC-UX Semantik (konsequent & testbar)
Non-member: UI Actions hidden (kein Tenant‑Leak); Execution wird blockiert (Filament hidden→disabled chain), Defense‑in‑depth enthält zusätzlich serverseitige Guards.
Member ohne Capability: Action visible aber disabled + Standard-Tooltip; Execution wird blockiert (keine Side Effects).
Member mit Capability: Action enabled und ausführbar.
Destructive actions: über ->destructive() immer mit ->requiresConfirmation() + klare Warntexte (Execution bleibt über ->action(...)).
Wichtig: In Filament v5 sind hidden/disabled Actions typischerweise “silently blocked” (200, keine Ausführung). Die Tests prüfen daher UI‑State + “no side effects”, nicht nur HTTP‑Statuscodes.

Sicherheit / Scope
Keine neuen DB-Tabellen, keine Migrations, keine Microsoft Graph Calls (DB‑only bei Render; kein outbound HTTP).
Tenant Isolation bleibt Isolation‑Boundary (deny-as-not-found auf Tenant‑Ebene, Capability erst nach Membership).
Kein Asset-Setup erforderlich; keine neuen Filament Assets.
Compliance Notes (Repo-Regeln)
Filament v5 / Livewire v4.0+ kompatibel.
Keine Änderungen an Provider‑Registrierung (Laravel 11+/12: providers.php bleibt der Ort; hier unverändert).
Global Search: keine gezielte Änderung am Global‑Search-Verhalten in dieser PR.
Tests / Qualität
Pest Feature/Unit Tests für Member/Non-member/Tooltip/Destructive/Regression‑Guard.
Guard-Test: “No ad-hoc Filament auth patterns”.
Full suite laut Tasks: vendor/bin/sail artisan test --compact → 837 passed, 5 skipped.
Checklist: requirements.md vollständig (16/16).
Review-Fokus
API‑Usage in neuen/angepassten Filament Actions: UiEnforcement::forAction/forTableAction/forBulkAction(...)->requireCapability(...)->apply()
Guard-Test soll “red” werden, sobald jemand neue ad-hoc Auth‑Patterns einführt (by design).

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #81
2026-01-30 16:58:02 +00:00

189 lines
8.0 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.

# Implementation Plan: RBAC UI Enforcement Helper v1
**Branch**: `066-rbac-ui-enforcement-helper` | **Date**: 2026-01-28 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/066-rbac-ui-enforcement-helper/spec.md`
## Summary
Provide a single, centrally maintained enforcement helper (`UiEnforcement`) that codifies the RBAC-UX constitution rules for tenant-scoped Filament actions:
- Non-member → 404 (deny-as-not-found), hidden in UI
- Member without capability → 403 on execution, visible-but-disabled in UI with standard tooltip
- Member with capability → enabled
- Destructive actions → `requiresConfirmation()` + clear warning
The helper wraps/augments Filament Actions (header, table row, bulk) to provide default UI + server-side enforcement, and ships with regression tests + a CI-failing guard against ad-hoc authorization patterns.
## Technical Context
**Language/Version**: PHP 8.4 (Laravel 12)
**Primary Dependencies**: Filament v5, Livewire v4
**Storage**: PostgreSQL (existing tables — no new tables)
**Testing**: Pest v4 (Feature + Unit tests)
**Target Platform**: Docker / Sail local, Dokploy VPS (Linux)
**Project Type**: Web / Monolith (backend + Filament admin)
**Performance Goals**: No additional DB queries beyond request-scope cached membership (FR-012)
**Constraints**: DB-only at render time (FR-013); no outbound HTTP
**Scale/Scope**: ~40+ tenant-scoped action surfaces; v1 migrates 36 exemplar surfaces
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| Inventory-first | N/A | No Inventory changes |
| Read/write separation | ✔ | Helper enforces existing gates; no new writes |
| Graph contract path | N/A | No Graph calls |
| Deterministic capabilities | ✔ | Uses existing `Capabilities` registry |
| RBAC-UX planes | ✔ | Tenant-plane only; cross-plane logic untouched |
| Tenant isolation | ✔ | 404 for non-members; capability check requires membership first |
| Run observability | N/A | No long-running work; helper is request-scope only |
| Data minimization | ✔ | No additional logging beyond existing deny logs |
| Badge semantics | N/A | No badge changes |
## Existing RBAC Primitives (Research)
| Component | Location | Purpose |
|-----------|----------|---------|
| `Capabilities` | `app/Support/Auth/Capabilities.php` | Canonical tenant capability registry (constants) |
| `PlatformCapabilities` | `app/Support/Auth/PlatformCapabilities.php` | Platform-plane capabilities |
| `RoleCapabilityMap` | `app/Services/Auth/RoleCapabilityMap.php` | Role → capabilities mapping |
| `CapabilityResolver` | `app/Services/Auth/CapabilityResolver.php` | Request-scope cached role/capability resolution |
| `User::canAccessTenant()` | `app/Models/User.php:123` | Membership check |
| `AuthServiceProvider` | `app/Providers/AuthServiceProvider.php` | Registers Gates for all capabilities |
| Existing ad-hoc patterns | `app/Filament/**` | 50+ `->visible(fn ...)` / `->disabled(fn ...)` calls — target for migration |
## Project Structure
### Documentation (this feature)
```text
specs/066-rbac-ui-enforcement-helper/
├── spec.md # Feature specification
├── plan.md # This file
├── research.md # (no separate file needed — inline above)
├── data-model.md # (no schema changes)
├── quickstart.md # Adoption guide
├── checklists/
│ └── requirements.md # Spec quality checklist
└── tasks.md # Phase 2 output (/speckit.tasks)
```
### Source Code (repository root)
```text
app/
├── Support/
│ └── Rbac/
│ ├── UiEnforcement.php # Central facade/builder
│ ├── TenantAccessContext.php # DTO: tenant, user, isMember, capabilityCheck
│ └── UiTooltips.php # Standardized tooltip strings
├── Services/Auth/
│ ├── CapabilityResolver.php # (existing, reused)
│ └── RoleCapabilityMap.php # (existing, reused)
├── Filament/
│ └── Resources/... # 36 exemplar migrations
tests/
├── Feature/
│ └── Rbac/
│ └── UiEnforcementTest.php # Integration tests
│ └── Guards/
│ └── NoAdHocFilamentAuthPatternsTest.php # CI-failing guard (file-scan)
├── Unit/
│ └── Support/Rbac/
│ └── UiEnforcementTest.php # Unit tests
```
**Structure Decision**: All new code lives in `app/Support/Rbac/` (helper) + tests; no new models/tables required.
## Key Design Decisions
### UiEnforcement API (FR-001)
```php
use App\Support\Rbac\UiEnforcement;
// Basic usage
UiEnforcement::forAction($action)
->requireMembership() // default: true
->requireCapability(Capabilities::PROVIDER_MANAGE)
->destructive() // optional: adds confirmation
->apply();
// Table/row action (receives record or record-accessor closure)
UiEnforcement::forTableAction(Action $action, Model|Closure $record)
->requireCapability(Capabilities::TENANT_DELETE)
->destructive()
->apply();
// Mixed visibility support (keep business visibility, add RBAC visibility)
UiEnforcement::forAction($action)
->preserveVisibility()
->requireCapability(Capabilities::TENANT_MANAGE)
->apply();
// Bulk action (all-or-nothing)
UiEnforcement::forBulkAction($action)
->requireCapability(Capabilities::TENANT_MANAGE)
->apply();
```
Internally:
1. Resolves current tenant + user via `Filament::getTenant()` + `auth()->user()`
2. Checks membership via `CapabilityResolver` (request-scope cached)
3. Sets `->hidden()` for non-members (FR-002a)
4. Sets `->disabled()` + `->tooltip()` for members without capability (FR-004)
5. Wraps handler with server-side guard (FR-005): `abort(404)` / `abort(403)`
### Tooltip Copy (FR-008)
```php
class UiTooltips
{
public const INSUFFICIENT_PERMISSION = 'You don\'t have permission to do this. Ask a tenant admin.';
public const DESTRUCTIVE_CONFIRM_TITLE = 'Are you sure?';
public const DESTRUCTIVE_CONFIRM_DESCRIPTION = 'This action cannot be undone.';
}
```
### Destructive Confirmation (FR-007)
`->destructive()` calls:
- `$action->requiresConfirmation()`
- `$action->modalHeading(UiTooltips::DESTRUCTIVE_CONFIRM_TITLE)`
- `$action->modalDescription(UiTooltips::DESTRUCTIVE_CONFIRM_DESCRIPTION)`
### All-or-nothing Bulk (FR-010a)
Before rendering, bulk action checks all selected records. If any record fails capability check for the member, action is disabled.
### Guardrail (FR-011)
`tests/Feature/Guards/NoAdHocFilamentAuthPatternsTest.php` scans `app/Filament/**` for forbidden patterns like:
- `Gate::allows(...)` / `Gate::denies(...)`
- `abort_if(...)` / `abort_unless(...)`
It uses a legacy allowlist so CI fails only for **new** violations, and the allowlist should shrink as resources are migrated.
## v1 Migration Targets (FR-009)
| Surface | File | Current Pattern | Notes |
|---------|------|-----------------|-------|
| TenantResource table actions | `TenantResource.php` | Multiple `->visible(fn ...)` + `->disabled(fn ...)` | High-traffic, high-value |
| ProviderConnectionResource actions | `EditProviderConnection.php` | Multiple `canAccessTenant` + capability checks inline | Complex, good test case |
| BackupSetResource table actions | `BackupSetResource.php` | Many `->disabled(fn ...)` closures | Destructive actions |
| PolicyResource ListPolicies sync | `ListPolicies.php` | Inline checks | Good example |
| EntraGroupResource sync | `ListEntraGroups.php` | Inline checks | Good example |
| FindingResource actions | `FindingResource.php` | `->authorize(fn ...)` inline | Good example |
## Complexity Tracking
> No constitution violations. Complexity is low (helper + tests + 36 migrations).
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| — | — | — |