TenantAtlas/specs/067-rbac-troubleshooting/plan.md
ahmido 3490fb9e2c feat: RBAC troubleshooting & tenant UI bugfix pack (spec 067) (#84)
Summary
Implements Spec 067 “RBAC Troubleshooting & Tenant UI Bugfix Pack v1” for the tenant admin plane (/admin) with strict RBAC UX semantics:

Non-member tenant scope ⇒ 404 (deny-as-not-found)
Member lacking capability ⇒ 403 server-side, while the UI stays visible-but-disabled with standardized tooltips
What changed
Tenant view header actions now use centralized UI enforcement (no “normal click → error page” for readonly members).
Archived tenants remain resolvable in tenant-scoped routes for entitled members; an “Archived” banner is shown.
Adds tenant-scoped diagnostics page (/admin/t/{tenant}/diagnostics) with safe repair actions (confirmation + authorization + audit log).
Adds/updates targeted Pest tests to lock the 404 vs 403 semantics and action UX.
Implementation notes
Livewire v4.0+ compliance: Uses Filament v5 + Livewire v4 conventions; widget Blade views render a single root element.
Provider registration: Laravel 11+ providers stay in providers.php (no changes required).
Global search: No global search behavior/resources changed in this PR.
Destructive actions:
Tenant archive/restore/force delete and diagnostics repairs execute via ->action(...) and include ->requiresConfirmation().
Server-side authorization is enforced (non-members 404, insufficient capability 403).
Assets: No new assets. No change to php artisan filament:assets expectations.
Tests
Ran:

vendor/bin/sail bin pint --dirty
vendor/bin/sail artisan test --compact (focused files for Spec 067)

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #84
2026-01-31 20:09:25 +00:00

159 lines
8.3 KiB
Markdown

# Implementation Plan: RBAC Troubleshooting & Tenant UI Bugfix Pack v1
**Branch**: `067-rbac-troubleshooting` | **Date**: 2026-01-31 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from [specs/067-rbac-troubleshooting/spec.md](spec.md)
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
## Summary
Tighten tenant-plane (`/admin`) RBAC UX and eliminate sharp edges by:
- Applying the existing `UiEnforcement` pattern consistently on Tenant admin surfaces (especially Tenant view header actions).
- Ensuring archived (soft-deleted) tenants can still resolve in tenant-scoped routes for entitled members (404 only for non-members).
- Adding a tenant-scoped diagnostics surface to detect and repair membership invariants (missing owner) and prevent GUID-vs-bigint mistakes.
- Adding targeted Pest tests to lock in 404 vs 403 semantics and action disable/tooltip UX, and re-running existing last-owner invariant tests as regression coverage.
## Technical Context
**Language/Version**: PHP 8.4 (per repo guidelines)
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4
**Storage**: PostgreSQL (via Laravel Sail)
**Testing**: Pest v4 (PHPUnit v12 runner)
**Target Platform**: Laravel Sail (Docker) on macOS/Linux
**Project Type**: Web application (Laravel monolith)
**Performance Goals**: N/A (admin UX + correctness)
**Constraints**: Tenant-plane only (`/admin`); no `/system` expansion; no new outbound integration calls for diagnostics; must preserve RBAC-UX semantics (404 vs 403)
**Scale/Scope**: Touches Tenant admin UI, tenancy binding for archived tenants, membership invariants + tests
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: clarify what is “last observed” vs snapshots/backups
- Read/write separation: any writes require preview + confirmation + audit + tests
- Graph contract path: Graph calls only via `GraphClientInterface` + `config/graph_contracts.php`
- Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
- RBAC-UX: two planes (/admin vs /system) remain separated; cross-plane is 404; non-member tenant access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks)
- RBAC-UX: destructive-like actions require `->requiresConfirmation()` and clear warning text
- RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on `/auth/*` without `OperationRun`
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
- Badge semantics (BADGE-001): status-like badges use `BadgeCatalog` / `BadgeRenderer`; no ad-hoc mappings; new values include tests
**Gate evaluation**: PASS (no constitution violations intended).
- No new Microsoft Graph call paths are introduced.
- Mutations involved (archive/restore, membership repairs) keep explicit confirmation and server-side enforcement.
- Auditing: membership repairs use existing audit logger patterns; tenant lifecycle actions already log.
**Post-design re-check**: PASS (design artifacts: [research.md](research.md), [data-model.md](data-model.md), [contracts/](contracts/)).
## Project Structure
### Documentation (this feature)
```text
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```
### Source Code (repository root)
```text
app/
├── Filament/
│ ├── Resources/
│ ├── Pages/
│ └── Widgets/
├── Models/
├── Policies/
├── Services/
└── Support/
database/
├── migrations/
└── factories/
resources/
├── views/
└── css/
routes/
└── web.php
tests/
├── Feature/
└── Unit/
```
**Structure Decision**: Laravel monolith. Changes will land in `app/Filament/**` (tenant UI), `app/Models/**` (tenant binding/invariants), `app/Services/**` (membership repairs), and `tests/**` (Pest feature tests).
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
N/A.
## Phase 0 — Research (repo-backed)
### Key findings (evidence)
- Tenancy config: [app/Providers/Filament/AdminPanelProvider.php](../../app/Providers/Filament/AdminPanelProvider.php#L32-L45) uses `->tenant(Tenant::class, slugAttribute: 'external_id')` and `->tenantRoutePrefix('t')`.
- Tenant archiving is soft delete + status update: [app/Models/Tenant.php](../../app/Models/Tenant.php#L63-L92) sets `status='archived'` during soft delete and restores back to `active`.
- Tenant view header actions currently bypass UI enforcement:
- `EditAction::make()` is unconditional (can lead to 403-on-click UX): [app/Filament/Resources/TenantResource/Pages/ViewTenant.php](../../app/Filament/Resources/TenantResource/Pages/ViewTenant.php#L19-L24)
- `Deactivate` currently executes without capability checks (security issue): [app/Filament/Resources/TenantResource/Pages/ViewTenant.php](../../app/Filament/Resources/TenantResource/Pages/ViewTenant.php#L51-L74)
- Tenant list Restore action lacks icon: [app/Filament/Resources/TenantResource.php](../../app/Filament/Resources/TenantResource.php#L406-L454)
- Membership invariant last owner is already enforced server-side: [app/Services/Auth/TenantMembershipManager.php](../../app/Services/Auth/TenantMembershipManager.php#L253-L287)
- Membership uniqueness is already enforced at DB level: [database/migrations/2026_01_25_022729_create_tenant_memberships_table.php](../../database/migrations/2026_01_25_022729_create_tenant_memberships_table.php#L12-L26)
### Decisions
- Archived tenant 404s for members: implement tenant route binding that includes soft-deleted tenants, then rely on membership middleware to enforce deny-as-not-found for non-members.
- Replace ad-hoc tooltips on tenant actions with centralized `UiTooltips` constants (add a tenant-archived tooltip string).
- Standardize tenant page header actions using `UiEnforcement` so member-without-capability sees disabled actions with tooltip (no normal-click 403).
**Output**: [research.md](research.md)
## Phase 1 — Design (data model + contracts)
### Design outline
- Tenant status UX
- Archived tenants remain viewable for entitled members.
- Actions become status-aware (e.g., Deactivate disabled when already archived).
- Diagnostics
- Tenant-scoped, DB-only page that reports:
- Missing owner (0 owners)
- Identifier misuse risk (guardrails against using external GUID where an internal FK is expected)
- Repair actions are capability-gated and audit-logged.
### Artifacts
- [data-model.md](data-model.md)
- [contracts/](contracts/)
- [quickstart.md](quickstart.md)
## Phase 2 — Planning (for tasks.md)
Dependency-ordered implementation outline (will be expanded in `tasks.md`):
1. Fix tenant view header actions (Edit/Deactivate) to use UI enforcement + tooltips.
2. Add Restore icon consistency on tenant list row menu.
3. Ensure archived tenant is resolvable in tenant-scoped routes for members (avoid false 404).
4. Add tenant status banner on view page.
5. Add diagnostics page + safe repair actions for missing owner”.
6. Add regression tests for:
- Readonly UX (disabled + tooltip)
- Readonly cannot deactivate
- Archived tenant view access (member OK, non-member 404)
- GUID vs bigint guard test
7. Keep the no-ad-hoc authorization guard green.