# Implementation Plan: Tenant RBAC v1 **Branch**: `065-tenant-rbac-v1` | **Date**: 2026-01-27 | **Spec**: [spec.md](spec.md) **Input**: Feature specification from `/specs/[###-feature-name]/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts. ## Summary This plan outlines the implementation of a capabilities-first Tenant RBAC system within the existing Laravel and Filament application. The primary requirement is to introduce granular, server-side enforced permissions for tenant users, managed through a new "Members" UI. The technical approach involves: 1. Verifying the existing `tenant_memberships` table matches the spec (role + minimal provenance fields). 2. Using the existing central capability registry (`App\Support\Auth\Capabilities`) and role → capability mapping to enforce least privilege. 3. Ensuring Laravel Gates are defined per capability for all registry entries (no hand-maintained capability lists). 4. Auditing / completing the Filament Relation Manager (`TenantMembershipsRelationManager`) so only Owners can manage memberships. 5. Adding/expanding unit + feature tests with Pest to ensure the RBAC system is secure and correct. ## Phases & Checkpoints Phase 1 — Setup & Database - Done when: `tenant_memberships` schema + relationships are verified and documented, and all related tests pass. Phase 2 — Foundational RBAC Core - Done when: capability registry + role mapping are aligned with least-privilege semantics and Gates are registered from `Capabilities::all()`. - Done when: request-scope caching prevents repeated membership queries (query-count test). Phase 3 — Membership Management UI - Done when: Owners can add/change/remove members via Filament, last-owner rules are enforced, and all membership mutations require confirmation. - Done when: members UI render/hydration is DB-only (no outbound HTTP and no jobs dispatched). Phase 4 — Authorization Enforcement - Done when: tenant route scoping is deny-as-not-found for non-members and global search is tenant-scoped. - Done when: enforcement sweep removes role-ish authorization checks from tenant-plane feature code and replaces them with capability Gates/Policies. Phase 5 — Polish & Finalization - Done when: Pint passes for all changed files and the full test suite passes. ## Technical Context **Language/Version**: PHP 8.4+ **Primary Dependencies**: Laravel 12, Filament 5, Livewire 4, Pest 4 **Storage**: PostgreSQL **Testing**: Pest **Target Platform**: Web (Laravel application) **Project Type**: Web application **Performance Goals**: Membership/capability evaluation MUST be O(1) per request after initial load (NFR-001). **Constraints**: RBAC UI surfaces MUST be DB-only at render time (NFR-003). **Scale/Scope**: The system should be designed to handle a moderate number of tenants and users, with the potential to scale. Initial design should not be a bottleneck for future growth. ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - [X] **Inventory-first**: N/A for this feature. - [X] **Read/write separation**: All membership changes (write operations) are specified to require Owner-level privileges and will be implemented with confirmations and audit logs. - [X] **Graph contract path**: N/A. The spec explicitly states no Graph calls for RBAC UI. - [X] **Deterministic capabilities**: The role-to-capability mapping is deterministic and defined in a central place. - [X] **RBAC Standard**: This feature implements the RBAC standard. It establishes the two-plane separation, capabilities-first authorization, and least-privilege roles. - [X] **Tenant isolation**: All queries and actions are tenant-scoped through the `tenant_memberships` table. - [X] **Run observability**: N/A for the core RBAC logic, as it's synchronous. Any long-running operations triggered by authorized users will follow this principle. - [X] **Automation**: N/A for this feature. - [X] **Data minimization**: The `tenant_memberships` table stores only essential information (IDs, role, and minimal provenance fields like `source` and `created_by_user_id`). - [X] **Badge semantics (BADGE-001)**: The role badge in the members list will use the `BadgeCatalog`. ## Project Structure ### Documentation (this feature) ```text specs/065-tenant-rbac-v1/ ├── plan.md # This file ├── research.md # Phase 0 output ├── data-model.md # Phase 1 output ├── quickstart.md # Phase 1 output ├── contracts/ │ └── capabilities.md # Phase 1 output └── tasks.md # Phase 2 output (/speckit.tasks command) ``` ### Source Code (repository root) The project follows a standard Laravel structure. Key files for this feature will be located in: ```text app/ ├── Models/ │ ├── TenantMembership.php # Existing pivot model │ └── User.php # Existing relationship to tenants/memberships │ └── Tenant.php # Existing relationship to users/memberships ├── Policies/ │ └── (optional) TenantMembershipPolicy.php # Optional policy for membership mutations (currently Gate-driven) ├── Providers/ │ └── AuthServiceProvider.php # Register per-capability Gates └── Filament/ └── Resources/ └── TenantResource/ └── RelationManagers/ └── TenantMembershipsRelationManager.php # New Filament relation manager database/ └── migrations/ └── 2026_01_25_022729_create_tenant_memberships_table.php # Existing migration tests/ ├── Feature/ │ └── Filament/ │ └── TenantMembersTest.php # New feature test for RBAC UI └── Unit/ └── Auth/ # Existing unit tests for capability registry + resolver ``` **Structure Decision**: The implementation will use the existing Laravel project structure. New classes and files will be created in their conventional locations. The primary UI for membership management will be a Filament Relation Manager on the `TenantResource`. ## Complexity Tracking > **Fill ONLY if Constitution Check has violations that must be justified** | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | [e.g., 4th project] | [current need] | [why 3 projects insufficient] | | [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |