Model permissions around actions
Enterprise RBAC works when permissions describe stable business actions and roles merely bundle those permissions for people. If code asks “is this user an admin?” everywhere, roles become hard-coded policy and every new customer-specific role requires code changes.
Roles are bundles, not policy logic
- Use permissions such as invoice.read, invoice.create, invoice.approve rather than broad role names in authorization checks.
- Scope role assignments by tenant and, when needed, by organization unit or resource.
- Separate maker and approver capabilities when a process requires segregation of duties.
- Super-admin or support access should be explicit, short-lived where possible, and fully audited.
- Default deny: an unknown permission or missing scope should fail closed.
The authorization decision path
A request is authorized by permission, tenant scope, resource scope, and process constraints before the mutation executes.
Authorization decision, not role-name branching
A request is authorized by permission, tenant scope, resource scope, and process constraints before the mutation executes.
Adding a finance approver safely
Authorize capabilities, not labels
The application asks for the business capability and lets the policy layer evaluate tenant and resource constraints.
await authorize(user, {
permission: 'refund.approve',
tenantId: refund.tenantId,
resource: refund
});RBAC anti-patterns
Authorization design checklist
- Create a permission catalog with clear verbs and resources.
- Keep role definitions configurable per tenant.
- Evaluate scope and process rules after permission lookup.
- Log privileged authorization decisions.
- Test denial paths, maker-checker rules, and role changes.
