TenantAtlas/specs/065-tenant-rbac-v1/spec.md
ahmido d90fb0f963 065-tenant-rbac-v1 (#79)
PR Body
Implements Spec 065 “Tenant RBAC v1” with capabilities-first RBAC, tenant membership scoping (Option 3), and consistent Filament action semantics.

Key decisions / rules

Tenancy Option 3: tenant switching is tenantless (ChooseTenant), tenant-scoped routes stay scoped, non-members get 404 (not 403).
RBAC model: canonical capability registry + role→capability map + Gates for each capability (no role-string checks in UI logic).
UX policy: for tenant members lacking permission → actions are visible but disabled + tooltip (avoid click→403).
Security still enforced server-side.
What’s included

Capabilities foundation:
Central capability registry (Capabilities::*)
Role→capability mapping (RoleCapabilityMap)
Gate registration + resolver/manager updates to support tenant-scoped authorization
Filament enforcement hardening across the app:
Tenant registration & tenant CRUD properly gated
Backup/restore/policy flows aligned to “visible-but-disabled” where applicable
Provider operations (health check / inventory sync / compliance snapshot) guarded and normalized
Directory groups + inventory sync start surfaces normalized
Policy version maintenance actions (archive/restore/prune/force delete) gated
SpecKit artifacts for 065:
spec.md, plan/tasks updates, checklists, enforcement hitlist
Security guarantees

Non-member → 404 via tenant scoping/membership guards.
Member without capability → 403 on execution, even if UI is disabled.
No destructive actions execute without proper authorization checks.
Tests

Adds/updates Pest coverage for:
Tenant scoping & membership denial behavior
Role matrix expectations (owner/manager/operator/readonly)
Filament surface checks (visible/disabled actions, no side effects)
Provider/Inventory/Groups run-start authorization
Verified locally with targeted vendor/bin/sail artisan test --compact …
Deployment / ops notes

No new services required.
Safe change: behavior is authorization + UI semantics; no breaking route changes intended.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #79
2026-01-28 21:09:47 +00:00

400 lines
11 KiB
Markdown
Raw Permalink 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.

---
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 repositorys canonical registry is `App\Support\Auth\Capabilities`.
Tenant-scoped capabilities are defined as `<namespace>.<action>`, 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)