---
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 `<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)