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
11 KiB
| 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
/systemplatform panel vs/admintenant panel, cross-scope 404)
Out of scope (v1):
/systemplatform 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
- Enterprise-grade tenant authorization: predictable, auditable, least privilege.
- Capabilities-first: feature code checks only capabilities (Gates/Policies), never raw roles.
- Membership management in tenant panel: tenant Owners manage members & roles.
- No regressions: existing tenant features remain usable; RBAC enforcement becomes consistent.
- 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 fromusers. - Platform plane:
/systemuses platform operators fromplatform_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, defaultmanual)source_ref(nullable string)created_by_user_id(nullable FK tousers)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.viewtenant.managetenant.deletetenant.sync
Membership:
tenant_membership.viewtenant_membership.manage
Tenant role mappings (optional in v1; no Graph resolution at render time):
tenant_role_mapping.viewtenant_role_mapping.manage
Providers:
provider.viewprovider.manageprovider.run
Audit:
audit.view
Backup schedules:
tenant_backup_schedules.managetenant_backup_schedules.run
6.3 Role → capability mapping (v1)
Rules:
Readonly:
tenant.viewtenant_membership.viewtenant_role_mapping.viewprovider.viewaudit.view
Operator:
- Readonly +
tenant.syncprovider.run
Manager:
- Operator +
tenant.manageprovider.manage
- NOT:
tenant_membership.manage(Owner-only) - NOT:
tenant_role_mapping.manage(Owner-only in v1) - Optional:
tenant.deleteif you explicitly decide managers can delete tenants (default: Owner-only)
Owner:
- Manager +
tenant_membership.managetenant_role_mapping.managetenant.delete
7) Authorization Architecture
7.1 Membership resolution
Given current user + current tenant (Filament tenant):
- Load membership:
tenant_membershipsrow 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(orTenants → View Tenant → Membersrelation 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.addtenant_membership.role_changetenant_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/**andapp/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)