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

11 KiB
Raw Blame History

description feature version status
Tenant RBAC v1 — Capabilities-first authorization + Membership Management 065-tenant-rbac-v1 1.0.0 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
  • 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)