OutlabsAuth
Auth

Roles & Permissions

Permission catalog and role definitions.
Catalog and role definitions (not who has them).

Mental model

LayerWhat it isWhere
CatalogPermission names (resource:action)/v1/permissions
DefinitionsRoles + which permissions they carry/v1/roles
Direct assignUser ↔ role/v1/users/{id}/roles → 23
Entity assignUser ↔ entity + roles/v1/memberships → 54

This page is catalog + definitions only.

from outlabs_auth.routers import get_roles_router, get_permissions_router

app.include_router(get_roles_router(auth, prefix="/v1/roles"))
app.include_router(get_permissions_router(auth, prefix="/v1/permissions"))

Permissions catalog (/v1/permissions)

MethodPathPermissionNotes
GET/permission:readPaginated. Query: page, limit, resource
POST/permission:create + global actor (Enterprise)PermissionCreateRequest — name must be resource:action (409 on duplicate). is_system must be false (422 otherwise) — see System definitions
GET/{permission_id}permission:readOne permission
PATCH/{permission_id}permission:update + global actor (Enterprise)Display/status/tags — not rename. System permissions rejected
DELETE/{permission_id}permission:delete + global actor (Enterprise)System permissions rejected
GET/meAuthenticatedCurrent user’s permission names (list[str]); optional entity_id
GET/user/{user_id}permission:read + target in tenant scopeAnother user’s permission names; optional entity_id. Out-of-scope users → 404
POST/checkpermission:check + target in tenant scopeBatch check; optional entity_id for entity/tree context
GET/{permission_id}/historypermission:readAppend-only definition history

Schemas: PermissionResponse, PermissionCreateRequest, PermissionUpdateRequest, PermissionCheckRequest / PermissionCheckResponse (has_all_permissions, results map).

Without entity_id, /me and /user/{id} return the historical aggregate (the union across contexts, *:* for superusers). With entity_id they return the catalog permissions held at that entity — direct grants there plus _tree / _all grants inherited from ancestors — the same semantics as POST /check. Gate entity-level UI actions on the entity-context set.

For effective permissions with sources (role vs membership), use GET /v1/users/{id}/permissions on the users router.

ABAC condition groups/conditions also hang off /{permission_id}/… when you use ABAC — same pattern as roles below.

The catalog is shared by every tenant. On EnterpriseRBAC with enforce_user_scope (the default), creating, updating or deleting a permission, and every write to its conditions or condition groups, needs a global actor — a superuser, a holder of a direct system-wide role, a service token or an unanchored integration principal. A tenant-scoped holder of permission:* gets 403 (DD-061). Reads are unchanged.

System definitions

is_system (permissions) and is_system_role (roles) mark the library-owned catalog. System definitions are immutable once created — no update, re-tagging, condition edits, or delete — so the HTTP API refuses to mint them: POST /v1/permissions/ rejects is_system=true, and RoleCreateRequest has no is_system_role field at all. They come from two places only:

  • seed_system_records(...) (outlabs_auth.bootstrap), which seeds the library's permission catalog (SYSTEM_PERMISSION_CATALOG, overridable via its permission_catalog parameter) at startup, idempotently.
  • Host code calling the services directly (permission_service. create_permission(..., is_system=True) / role_service.create_role(..., is_system_role=True)) — creation-time tags, permission_names, and entity_type_permissions all work there; immutability begins after the row exists.

Rationale and history: DD-060 in docs/DESIGN_DECISIONS.md.


Roles (/v1/roles)

CRUD and permission sets

MethodPathPermissionNotes
GET/role:readPaginated. Query: page, limit, search, is_global, root_entity_id
POST/role:createRoleCreateRequest. Actor must already hold every permission they attach
GET/{role_id}role:readOne role
PATCH/{role_id}role:updateRoleUpdateRequest; permissions replaces the set when sent. Widening changes (activation, is_global, hierarchy scope, auto-assign, new or cleared assignable_at_types) re-run containment for the role's permissions
GET/{role_id}/historyrole:readAppend-only definition history (visibility rules apply)
DELETE/{role_id}role:delete204
POST/{role_id}/permissionsrole:updateBody: list[str] permission names to add
DELETE/{role_id}/permissionsrole:updateBody: list[str] to remove

Enterprise helper

MethodPathPermissionNotes
GET/entity/{entity_id}Tree role:readRoles available for that entity (pickers / memberships UI)

Role response fields worth knowing

RoleResponse includes: name, display_name, permissions[], is_system_role, is_global, status, optional root_entity_id / scope_entity_id, scope (entity_only | hierarchy), assignable_at_types[], is_auto_assigned.

Enterprise tips:

  • Non-global actors only manage roles in their trees; system-wide roles they cannot touch → 403; out-of-tree → 404. A personal API key carries its owner's tenant scope (narrowed to its anchor when it has one), never global scope on its own (DD-061)
  • Writes to an entity-local role (create with scope_entity_id, update, delete, permission and ABAC condition changes) also need its scope entity in the tree of a non-global actor's own root (403 otherwise, DD-061 decision 17): a role seen only through a membership left in another tenant's tree by an earlier cross-root move is read-only, and unrooted legacy administrators change no entity-local roles
  • In an entity-context check a role assigned directly to a user follows the same reach as a membership role: an org-scoped role only inside its own organization's tree, an entity-local role only inside its scope, system-wide roles everywhere (DD-054, DD-061)
  • SimpleRBAC treats list/create visibility as effectively global
  • assignable_at_types limits which entity types may receive the role; an empty list means assignable everywhere, so clearing it counts as widening
  • is_auto_assigned can auto-apply on membership create/update

ABAC on roles (when enabled)

Under /{role_id}/condition-groups and /{role_id}/conditions — CRUD with role:read / role:update. Walkthrough: ABAC.


Simple vs Enterprise

ConcernSimpleRBACEnterpriseRBAC
Role visibilitySystem-wideScoped to actor’s entity access
GET /roles/entity/{id}Rarely usefulPrimary picker for memberships
Tree permissionsUnusedresource:action_tree + /permissions/check?entity_id=
Assign pathUsers router direct rolesPrefer memberships; direct assign for org-wide roles

Suggested host flow

  1. Seed or create permissions (resource:action).
  2. Create roles and attach permission lists (delegation check applies).
  3. Simple: POST /v1/users/{id}/roles.
    Enterprise: POST /v1/memberships/ with role_ids; use GET /v1/roles/entity/{entity_id} in the UI.
  4. Gate product UI with POST /v1/permissions/check (and entity_id when needed).
  5. Optional attribute conditions: ABAC.