Skip to content

Tenant RBAC — Developer Guide

Workspace (tenant) authorization is isolated per workspace: users, roles, and role↔permission assignments never cross tenant boundaries. Permissions are a shared vocabulary on the tenant-api guard; roles are scoped with roles.tenant_id.

Model

text
Workspace
  → Users (tenant DB / tenancy-scoped users table)
  → Roles (central roles table, tenant_id = workspace UUID)
  → Permissions (global names on guard tenant-api)
  → Application access (policies + middleware)
LayerConcernEnforcement
TenancyWorkspace isolationStancl tenancy + X-Tenant-Domain / host
Module licensingWorkspace owns the domainmodule:{slug} middleware
RBACUser may act inside that domainSpatie can:{permission} / policies

Both module ownership and permission checks are required for module features:

text
Workspace owns module? → User has permission? → Allow; otherwise deny

Core Administration (Users, Roles, Settings) does not require a module subscription; it uses Spatie permissions only.

Isolation approach

Spatie teams are not enabled. Isolation uses:

  1. roles.tenant_id set to the workspace UUID when roles are created (provisioning + CRUD).
  2. Unique index (tenant_id, name, guard_name).
  3. Role assignment by Role model instance (not bare name), so Spatie attaches the correct row.
  4. Permission validation: exists:roles,name filtered by current tenant_id + guard_name = tenant-api.
  5. RolePolicy / TenantUserPolicy ensure actors only see/mutate resources in the current workspace.

Permissions remain a shared vocabulary defined in config/tenant-permissions.php (e.g. users.list, leads.view, communication-templates.use). They are not copied per tenant.

How permissions reach production:

MomentMechanism
New workspaceTenantAuthorizationProvisioningService::provisionDefaults() during TenantProvisioningService
Existing workspace + new permissionsAdditive data migration via TenantPermissionSynchronizer::grantMissingDefaultRolePermissions([...])
Login / dashboard / role listingNever mutates roles or permissions

Do not run permission seeders in production. Do not syncPermissions() on customized roles during deploy.

Legacy shared roles

Workspaces created before isolation may still have users on tenant_id = null roles. Repair is an explicit maintenance operation; authenticated requests never mutate authorization state. Operators run:

bash
php artisan tenants:isolate-roles

Owner

  • Role name: superadmin (TenantAuthorizationProvisioningService::OWNER_ROLE)
  • Created during workspace registration
  • Receives all tenant-api permissions
  • Protected from deletion (config/tenant-protected-roles.php)
  • Cannot access the Central Application (separate guard / user model)

Default undeletable roles: admin, manager, staff (editable permissions; not deletable).

API (tenant)

Prefix: /api/tenant/v1 · Guard: auth:tenant-api

ResourceRoutes
UsersCRUD + suspend / unsuspend / restore / force-delete / change-password / verify-email / resend-verification
RolesCRUD + clone
Permissions matrixGET roles/permissions-matrix

Services: TenantUserService, TenantRoleService. Policies gate every mutating action.

Frontend

Shared Central pages are reused with getAuthContext():

PathPage
/usersUsersPage (no Invite; workspace copy)
/rolesRolesPage
/roles/matrixRolesMatrixPage

Axios uses the tenant API base when the SPA is on a tenant path. Navigation: Administration → Users / Roles (PERMISSIONS.users.list / roles.list).

Tests

  • Pest: tests/Feature/Tenant/User/, tests/Feature/Tenant/Role/ (CRUD, assignment, isolation, authorization)
  • Playwright: npm run test:e2e:tenant-rbac

Official documentation for the SaleOS SaaS Platform.