DocsGovernanceRBAC

RBAC

Role-based access control decides what each principal is allowed to do. Permission keys define the actions that exist; roles group keys together; assignments map a principal to roles. The same model governs humans, AI agents, and service accounts.

RBAC is the authority layer. It answers "is this principal allowed?" The enforcement layer is what makes the answer impossible to bypass.


How it works

RBAC in RootCX is global. A single role can span multiple apps, tools, and integrations. 4 concepts:

  1. Permission keys define what actions exist. Declared in manifests, auto-generated on deploy.
  2. Roles group permission keys together. Created at runtime via the API. Support inheritance.
  3. Assignments map a principal to one or more roles.
  4. Resolution computes a principal's effective permissions: the union of every key from every assigned role, including inherited roles.

The same resolution logic runs in two places (as Rust in the Core and as PL/pgSQL inside the RLS policies), so an HTTP check and a row-level check always agree. See Enforcement.


Permission key format

Keys use a namespace:scope:action convention:

Namespace Example Source
app: app:crm:contacts.read App entity CRUD, auto-generated on deploy
app: app:crm:action:pipeline Auto-created for declared app actions
app: app:crm:invoke Auto-created to gate invoking the app's agent
app: app:crm:cron.trigger Auto-created for the app's scheduled jobs
tool: tool:query_data Built-in agent tool permissions
integration: integration:gmail:send_email Auto-created for integration actions on deploy
storage: storage:read, storage:invoices:read Platform storage (global or per-bucket)
platform: platform:apps.create Self-service: create and own apps
admin: admin:apps.deploy, admin:secrets.manage Platform administration
* (global admin) Matches everything

App permission keys are declared as entity.action in the manifest and auto-prefixed with app:{appId}: on deploy. The Core always generates the 4 CRUD keys for every entity; any permissions you declare explicitly are added on top, never a replacement.

The admin: namespace gates platform operations such as deploying apps, managing secrets, reading the audit log, querying the database, and managing agents, integrations, and service accounts. Global admins (*) satisfy these implicitly.


Wildcard matching

Permission keys support wildcards with the :* suffix. The matching algorithm:

  1. * matches everything (global admin).
  2. prefix:* matches any key that starts with prefix:, with the colon boundary enforced.
  3. Otherwise, an exact string match.
Pattern Matches Does not match
* Everything
app:crm:* app:crm:contacts.read, app:crm:deals.create app:support:tickets.read
tool:* tool:query_data, tool:mutate_data app:crm:contacts.read
integration:gmail:* integration:gmail:send_email integration:slack:send

The colon boundary is enforced: app:crm:* does not match app:crm_extended:something.


Actions and HTTP methods

Each entity permission key maps to one operation on the Data API:

Action suffix HTTP Description
.create POST Create a new record
.read GET List or get records
.update PATCH Modify an existing record
.delete DELETE Remove a record

For declared actions (in the actions array of the manifest), the Core auto-creates keys in the format app:{appId}:action:{actionId}. You do not declare these in the permissions block.


Built-in roles

2 roles are seeded on every Core:

  • admin: permissions ["*"] (global admin). Cannot be deleted, and its permissions and inherits cannot be modified. The first registered user is automatically assigned this role.
  • base: no permissions. The deny-by-default role for federated and invited users. It is the safe default_role for OIDC; real access comes from explicitly granted roles.

2 more roles are generated automatically, not seeded:

  • app:{appId}:admin: carries app:{appId}:*. Assigned to whoever installs an app, giving them full control of that app without platform-level authority.
  • app:{appId}:agent: a least-privilege role derived from the app's data contract. Assigned to the app's agent on first deploy. See AI Agent Governance.

Role inheritance

Roles can declare an inherits array pointing to other roles. The Core walks the full inheritance chain, merging all permission keys transitively. Maximum depth: 64 levels.

Cycles are detected and rejected on create and update (depth-first cycle detection). Diamond inheritance is supported: a role can inherit two roles that both inherit a common parent, and the common parent is resolved once.

Example: editor inherits from viewer. A user with the editor role gets the permissions of both editor and viewer.


Managing roles

Create a role

POST /api/v1/roles

Admin only.

{
  "name": "editor",
  "description": "Can read and write contacts",
  "inherits": ["viewer"],
  "permissions": ["app:crm:contacts.create", "app:crm:contacts.update"]
}

The name must not be empty and cannot be admin (reserved). Every permission key is validated against the [a-z0-9_:.*] charset. If inherits would create a cycle, the request is rejected with 400.

Update a role

PATCH /api/v1/roles/{roleName}

Admin only. All fields optional:

{
  "description": "Updated description",
  "inherits": ["viewer", "commenter"],
  "permissions": ["app:crm:contacts.create", "app:crm:contacts.update", "app:crm:contacts.delete"]
}

You cannot modify the permissions or inherits of the admin role. Changing a role's permissions or inherits invalidates the affected principals' running workers, so the new authority takes effect on the next action.

Delete a role

DELETE /api/v1/roles/{roleName}

Admin only. Cannot delete the admin role. Deleting a role also removes every assignment for it.

List roles

GET /api/v1/roles

Returns all roles with their description, inherits, and permissions arrays.

[
  {
    "name": "editor",
    "description": "Can read and write contacts",
    "inherits": ["viewer"],
    "permissions": ["app:crm:contacts.create", "app:crm:contacts.update"]
  }
]

Assignments

Assign a role

POST /api/v1/roles/assign

Admin only.

{ "userId": "3f7a1b2c-...", "role": "editor" }

The role must exist. Duplicate assignments are silently ignored. The target can be any principal: a human, an agent, or a service account.

Revoke a role

POST /api/v1/roles/revoke

Admin only.

{ "userId": "3f7a1b2c-...", "role": "editor" }

Cannot revoke the last admin assignment (prevents lockout); returns 400. Returns 404 if the assignment does not exist.

List assignments

GET /api/v1/roles/assignments

Admin only. Returns every principal-to-role mapping:

[
  { "userId": "3f7a1b2c-...", "role": "editor", "assignedAt": "2024-12-01T10:00:00Z" }
]

Checking permissions

Current principal

GET /api/v1/permissions

Authenticated. Returns the caller's effective permissions, with every role expanded and all inherited keys merged:

{
  "roles": ["editor", "viewer"],
  "permissions": ["app:crm:contacts.create", "app:crm:contacts.read", "app:crm:contacts.update"]
}

Specific principal

GET /api/v1/permissions/{userId}

Returns another principal's effective permissions. If the caller is not the target, admin is required.

Available permissions

GET /api/v1/permissions/available

Authenticated. Returns the declared permission keys the caller is allowed to grant. The catalog is filtered to keys the caller already holds, so a non-admin cannot hand out authority it lacks.

[
  { "key": "app:crm:contacts.create", "description": "create contacts" },
  { "key": "integration:gmail:send_email", "description": "Send Email via gmail" }
]

API endpoints summary

Method Path Auth Description
GET /api/v1/roles No List all roles
POST /api/v1/roles Admin Create a role
PATCH /api/v1/roles/{roleName} Admin Update a role
DELETE /api/v1/roles/{roleName} Admin Delete a role
GET /api/v1/roles/assignments Admin List all assignments
POST /api/v1/roles/assign Admin Assign a role to a principal
POST /api/v1/roles/revoke Admin Revoke a role from a principal
GET /api/v1/permissions Yes Caller's effective permissions
GET /api/v1/permissions/{userId} Yes (admin for others) A principal's effective permissions
GET /api/v1/permissions/available Yes Grantable permission keys, filtered to the caller's authority