--- description: "Tenant RBAC v1 — Capabilities-first authorization + Membership Management" feature: "065-tenant-rbac-v1" version: "1.0.0" status: "draft" --- # Spec 065 — Tenant RBAC v1 (Capabilities-first) + Membership Management **Scope**: `/admin` Tenant Panel (Entra users) **Depends on**: - **063 Entra sign-in v1** (tenant users authenticate via Entra / OIDC) - **064 Auth structure v1** (separate `/system` platform panel vs `/admin` tenant panel, cross-scope 404) **Out of scope (v1)**: - `/system` platform RBAC expansion (system console / global views) - Entra group-to-role mapping (v2) - SCIM provisioning (v2) - Impersonation (v2) - Custom per-feature roles UI (v2) - “Invite token” email onboarding flows (optional v2, depending on your Entra setup) --- ## 0) Goals 1. **Enterprise-grade tenant authorization**: predictable, auditable, least privilege. 2. **Capabilities-first**: feature code checks only capabilities (Gates/Policies), never raw roles. 3. **Membership management** in tenant panel: tenant Owners manage members & roles. 4. **No regressions**: existing tenant features remain usable; RBAC enforcement becomes consistent. 5. **Testable**: every sensitive permission boundary has regression tests. --- ## 1) Non-goals - This spec does **not** create a global “MSP console” across tenants. - This spec does **not** implement Entra group claims ingestion or Graph-based membership resolution. - This spec does **not** change provider connection credentials strategy (that stays in Provider foundation specs). - This spec does **not** redesign UI pages; it adds management and enforcement. --- ## 2) Terms & Principles ### 2.1 Two planes (already established by 064) - **Tenant plane**: `/admin/t/{tenant}` uses Entra users from `users`. - **Platform plane**: `/system` uses platform operators from `platform_users`. - Cross-plane access is deny-as-not-found (404). This spec does not change that. ### 2.2 Capabilities-first - Roles exist for UX, but **code checks capabilities**. - Capabilities are registered in a **central registry**. - A **role → capability mapping** is the only place that references role names. ### 2.3 Least privilege - Readonly is view-only. - Operator can run operations but cannot manage configuration/members/credentials or delete. - Manager can manage tenant configuration and run operations; cannot manage memberships. - Owner can manage memberships and “danger zone”. --- ## 3) Requirements (Functional) ### FR-001 Membership source of truth Authorization MUST be derived from a tenant membership record for the current (user_id, tenant_id). ### FR-002 Tenant membership management UI Tenant Owners MUST be able to: - add members - change roles - remove members ### FR-003 Last owner protection The system MUST prevent removing or demoting the last remaining Owner for a tenant. ### FR-004 Capability registry A canonical tenant capability registry MUST exist (single source of truth). ### FR-005 Role to capability mapping Tenant roles MUST map to capability sets via a central mapper (no distributed role checks). ### FR-006 Enforcement in server-side authorization All mutations MUST be protected by Policies/Gates. UI hiding is insufficient. ### FR-007 Operator constraints Operators MUST NOT be able to: - manage members - manage provider connections/credentials - change tenant settings - perform destructive actions (delete/force delete) ### FR-008 Readonly constraints Readonly MUST NOT be able to mutate data OR start tenant operations. ### FR-009 Operations start permissions Starting a tenant operation (enqueue-only actions) MUST require the relevant capability. ### FR-010 Audit logging for access-control changes Membership add/remove/role_change MUST write AuditLog entries with stable action IDs and redacted data. ### FR-011 Tenant switcher and route scoping Only tenants where the user has membership MUST be listable/selectable; non-member tenant routes MUST 404. Additionally, tenant-plane global search MUST be tenant-scoped (non-members MUST see no results, and any result URLs must still be deny-as-not-found when accessed directly). ### FR-012 Regression tests RBAC boundaries MUST be covered by tests (positive + negative cases). --- ## 4) Requirements (Non-functional) ### NFR-001 Performance Membership/capability evaluation MUST be O(1) per request after initial load (no N+1). ### NFR-002 Data minimization No user secrets are stored; only Entra identifiers and minimal profile fields. ### NFR-003 DB-only render guarantee RBAC UI surfaces (members listing) MUST be DB-only at render time (no outbound HTTP, no Graph). ### NFR-004 Observability AuditLog and denied actions MUST be diagnosable without leaking secrets. --- ## 5) Data Model ### 5.1 Table: tenant_memberships Note: The `tenant_memberships` table is already present in the repository (introduced by an earlier migration). This feature verifies the schema and treats it as the source of truth for tenant-plane authorization. Columns: - `id` (uuid, primary key) - `tenant_id` (FK to tenants) - `user_id` (FK to users) - `role` (enum: `owner|manager|operator|readonly`) - `source` (enum: `manual|entra_group|entra_app_role|break_glass`, default `manual`) - `source_ref` (nullable string) - `created_by_user_id` (nullable FK to `users`) - `created_at`, `updated_at` Constraints: - unique `(tenant_id, user_id)` - index `(tenant_id, role)` - FK constraints (tenant_id/user_id/created_by_user_id) ### 5.2 Optional (deferred): tenant_invites Not required for v1 unless you want email-based invites without the user existing in DB. --- ## 6) Capability Registry & Role Mapping ### 6.1 Naming convention Capabilities are strings, and this repository’s canonical registry is `App\Support\Auth\Capabilities`. Tenant-scoped capabilities are defined as `.`, with some namespaces using underscores (e.g. `tenant_membership.manage`). ### 6.2 Canonical capabilities (v1 baseline) Minimum set (extendable, but these are the baseline contracts as of 2026-01-28): Tenant: - `tenant.view` - `tenant.manage` - `tenant.delete` - `tenant.sync` Membership: - `tenant_membership.view` - `tenant_membership.manage` Tenant role mappings (optional in v1; no Graph resolution at render time): - `tenant_role_mapping.view` - `tenant_role_mapping.manage` Providers: - `provider.view` - `provider.manage` - `provider.run` Audit: - `audit.view` Backup schedules: - `tenant_backup_schedules.manage` - `tenant_backup_schedules.run` ### 6.3 Role → capability mapping (v1) Rules: Readonly: - `tenant.view` - `tenant_membership.view` - `tenant_role_mapping.view` - `provider.view` - `audit.view` Operator: - Readonly + - `tenant.sync` - `provider.run` Manager: - Operator + - `tenant.manage` - `provider.manage` - NOT: `tenant_membership.manage` (Owner-only) - NOT: `tenant_role_mapping.manage` (Owner-only in v1) - Optional: `tenant.delete` if you explicitly decide managers can delete tenants (default: Owner-only) Owner: - Manager + - `tenant_membership.manage` - `tenant_role_mapping.manage` - `tenant.delete` --- ## 7) Authorization Architecture ### 7.1 Membership resolution Given current user + current tenant (Filament tenant): - Load membership: `tenant_memberships` row for (user_id, tenant_id) - If missing: tenant access is deny-as-not-found (404) (membership scoping rule). ### 7.2 Capability resolution - Resolve role from membership - Map role → capability set - Cache in-request. Optional: short-lived cache keyed `(user_id, tenant_id)` max 60s (DB-only). ### 7.3 Gates and Policies - Define per-capability Gates for all entries in `App\Support\Auth\Capabilities`. - Resources MUST call Gate/Policies for: - pages - table actions - bulk actions - form actions - relation manager actions No feature code checks role strings directly (or uses role helper methods) outside the central mapping/resolver. --- ## 8) UI: Tenant Members Management (Admin Panel) ### 8.1 Location Tenant-scoped settings section: - `Settings → Members` (or `Tenants → View Tenant → Members` relation manager), consistent with your existing navigation style. ### 8.2 List view Columns: - user name - user email - role (badge) - added_at Actions: - Add member (Owner only) - Change role (Owner only) - Remove member (Owner only) ### 8.3 Add member flow (v1 minimal, enterprise-safe) Input: - Entra email (UPN) or existing user picker Behavior: - If matching user exists (email match): create membership row. - If not found: - v1: Require the user to sign in first (cleaner), to avoid user identity conflicts. ### 8.4 Last owner protection - If membership is last owner: remove/demote blocked with clear message. - Emit AuditLog `tenant_membership.last_owner_blocked` (optional). --- ## 9) Audit Logging ### 9.1 Canonical action_ids - `tenant_membership.add` - `tenant_membership.role_change` - `tenant_membership.remove` - Optional: `tenant_membership.last_owner_blocked` ### 9.2 Minimal log payload - actor_user_id - tenant_id - target_user_id - before_role/after_role where relevant - timestamp - ip (optional) No secrets, no tokens. --- ## 10) Repo-wide Enforcement Sweep (must-do) ### 10.1 Destructive actions policy Any destructive action MUST: - have server-side authorization (Policy/Gate) - have `requiresConfirmation()` - have at least one negative test (operator/readonly cannot) ### 10.2 “Operator cannot manage” Specifically enforce: - Provider connection delete/disable/credential rotate - Tenant settings mutations - Membership changes - Force delete actions --- ## 11) Tests (Pest) ### 11.1 Unit tests - Role → capability mapping invariants: - readonly has no start/manage - operator cannot manage members/settings/providers/credentials - owner has members.manage - Last owner guard logic ### 11.2 Feature tests - Membership scoping: - tenant list/switcher shows only memberships - non-member route returns 404 - Membership management: - owner can add/change/remove - manager/operator/readonly cannot - last owner cannot be removed/demoted - Provider constraints: - operator cannot delete provider connection or rotate credentials - Operations starts: - operator can start allowed operation (creates OperationRun) if capability exists - readonly cannot start operations ### 11.3 Regression guard (optional but recommended) - Architecture/grep test to flag role-string checks in `app/Filament/**` and `app/Jobs/**` (except in the central role→capability mapper). --- ## 12) Acceptance Criteria (Definition of Done) - Tenant members management works; last owner rule enforced. - Operator cannot manage/delete sensitive resources (tested). - Readonly is view-only across tenant plane (tested). - All new mutations are Policy/Gate enforced and audit logged. - No outbound HTTP during render/hydration for tenant RBAC UI. - No role-string checks exist outside the central mapper/registry. --- ## 13) v2 Roadmap (Explicit) - Entra group-to-role mapping (scheduled sync, no render-time Graph calls) - Invite tokens (email-based) if needed - Custom roles per tenant - Impersonation (audited, time-limited) - System console global views (cross-tenant dashboards)