Role-based access control
cadence:* permissions, BuiltInRBACProvider, Redis cache, and route dependencies.
Intended audience: Stakeholders, Business analysts, Solution architects, Developers, Testers
Learning outcomes by role
Stakeholders
- Relate roles and permissions to auditability and separation of duties for platform versus org admins.
Business analysts
- Map personas to roles and expected 403 outcomes when permissions are missing.
Solution architects
- Explain Postgres plus Redis caching for RBAC and implications for stale permission windows.
Developers
- Use permission constants, BuiltInRBACProvider, and route dependencies (roles_allowed) consistently.
Testers
- Build matrices of roles versus endpoints using cadence:* strings and wildcard behavior.
Every protected route in Cadence answers one question: does the caller hold the required permission string? Permissions are attached to roles, roles are assigned to users, and the combined effective permission set is cached in Redis for 300 seconds. When a permission check fails, the route returns 403.
Summary for stakeholders
Section titled “Summary for stakeholders”- Explicit permissions — Every capability in Cadence is guarded by a
cadence:*string. Granting a role grants its full permission set. Revoking a role revokes those capabilities when the cache expires or is invalidated. - Three built-in roles —
sys_admin(platform superuser),org_admin(full control over one org), andorg_member(read and chat in one org) cover most use cases. Custom roles can be created via the admin API.
Business analysis
Section titled “Business analysis”Map product personas to roles and their expected 403 outcomes when writing acceptance criteria. A user with org_member role cannot read org settings — that requires cadence:org:settings:read, which is only in org_admin and above. When a defect says “user got 403 but shouldn’t”, the cause is almost always a missing role assignment or a stale permission cache.
- Personas — Map product roles (org member, org admin, sys admin) to
cadence:*sets when writing 403 scenarios. - Cache staleness — Permission changes take up to 5 minutes to propagate unless
invalidate_cacheis explicitly called on the role-change code path.
Permission strings
Section titled “Permission strings”All constants live in cadence.core.authorization.permissions. Every permission follows the pattern cadence:{namespace}:{resource}:{action}.
System-scoped (cadence:system:*) — platform-wide operations:
| Constant | String |
|---|---|
SYSTEM_ADMIN | cadence:system:admin |
SYSTEM_SETTINGS_READ / _WRITE | cadence:system:settings:read/write |
SYSTEM_TIERS_READ / _WRITE | cadence:system:tiers:read/write |
SYSTEM_PLUGINS_READ / _WRITE | cadence:system:plugins:read/write |
SYSTEM_USERS_READ / _WRITE | cadence:system:users:read/write |
SYSTEM_PROVIDERS_READ / _WRITE | cadence:system:providers:read/write |
SYSTEM_TELEMETRY_READ / _WRITE | cadence:system:telemetry:read/write |
SYSTEM_HEALTH_READ | cadence:system:health:read |
SYSTEM_ORGS_CREATE | cadence:system:orgs:create |
SYSTEM_OAUTH_CLIENTS_READ / _WRITE | cadence:system:oauth_clients:read/write |
SYSTEM_ROLES_READ / _WRITE | cadence:system:roles:read/write |
SYSTEM_API_KEYS_READ / _WRITE | cadence:system:api_keys:read/write |
Org-scoped (cadence:org:*) — operations on a specific org:
| Constant | String |
|---|---|
ORG_READ / _WRITE | cadence:org:read/write |
ORG_MEMBERS_READ / _WRITE | cadence:org:members:read/write |
ORG_ORCHESTRATORS_READ / _WRITE | cadence:org:orchestrators:read/write |
ORG_ORCHESTRATORS_LIFECYCLE | cadence:org:orchestrators:lifecycle |
ORG_PLUGINS_READ / _WRITE | cadence:org:plugins:read/write |
ORG_LLM_CONFIGS_READ / _WRITE | cadence:org:llm-configs:read/write |
ORG_CENTRAL_POINTS_READ / _WRITE | cadence:org:central-points:read/write |
ORG_SETTINGS_READ / _WRITE | cadence:org:settings:read/write |
ORG_STATS_READ | cadence:org:stats:read |
User and chat (cadence:chat:*, cadence:profile:*):
| Constant | String |
|---|---|
CHAT_USE | cadence:chat:use |
CHAT_HISTORY_READ | cadence:chat:history:read |
PROFILE_READ / _WRITE | cadence:profile:read/write |
Built-in roles
Section titled “Built-in roles”Three roles are seeded at platform initialization via role_permissions_map():
| Role | Permissions |
|---|---|
sys_admin | SYSTEM_ADMIN + all system + all org + all user permissions |
org_admin | All org permissions + all user permissions |
org_member | cadence:org:read, cadence:org:orchestrators:read, cadence:chat:use, cadence:chat:history:read, cadence:profile:read/write |
SYSTEM_ADMIN (cadence:system:admin) is a special sentinel: when it appears in a user’s effective permissions, expand_wildcard_permissions replaces the entire set with every permission in the platform. This is how sys_admin bypasses all checks — not through a special flag, but through permission expansion.
BuiltInRBACProvider
Section titled “BuiltInRBACProvider”BuiltInRBACProvider in cadence.core.authorization.builtin_rbac loads and caches permissions:
-
Load —
effective_permissions(user_id, org_id)first checks Redis. On a cache miss, it callsUserRoleRepository.list_for_userto get all role assignments, thenRoleRepository.list_permissions_for_rolefor each role. Global assignments (org_id is None) contribute to the global set; org-scoped assignments only contribute whenorg_idmatches. -
Merge — Global and org-scoped permission sets are unioned, then passed through
expand_wildcard_permissions. IfSYSTEM_ADMINis present, the result is the fullPERMISSIONSfrozenset. -
Cache — The merged set is written to Redis under
authz:perms:{user_id}:{org_id or '_'}with a TTL of 300 seconds (5 minutes). The_placeholder is used when no org is in scope. -
Invalidate —
invalidate_cache(user_id)scans and deletes allauthz:perms:{user_id}:*keys. It is called automatically afterassign_roleandremove_role.
Route enforcement
Section titled “Route enforcement”Route handlers declare permission requirements as FastAPI dependencies:
Depends(roles_allowed(PERM))— The caller must hold the named permission (checked viasession.has_permission). No matching permission → 403.Depends(authenticated)— The caller must have a valid session, but no permission is checked.require_platform_sys_admin/require_admin— Narrow guards for platform-level operations.
Most org-scoped handlers also call org_context(security, org_id) to verify org membership, in addition to the permission check.
API keys vs interactive sessions
Section titled “API keys vs interactive sessions”When an API key is used, sanitize_api_key_flat_scopes strips three permissions before building the session:
cadence:system:admincadence:system:api_keys:readcadence:system:api_keys:write
This means no API key can ever act as a platform superuser or manage other API keys, regardless of what scopes were granted to it.
Verification and quality
Section titled “Verification and quality”Cache staleness — After a role change in the database, BuiltInRBACProvider.invalidate_cache must be called for the change to take effect immediately. Without invalidation, the old permission set lives in Redis for up to 5 minutes. Design role-change tests to account for this: change the role, call the cache invalidation path, then assert the permission boundary.
Session snapshot — Interactive JWT sessions store permissions in Redis at login time (BearerTokenSession.global_permissions and org_permissions). A role change does not update those session fields until the user refreshes or re-authenticates. Test permission changes with freshly issued tokens.