RBAC and permissions
Flux's permission model — wildcard syntax, built-in roles, custom roles, and how permissions attach to principals.
Authentication answers who is calling. Authorization answers what they may do. Flux’s authorization layer is a small RBAC system: a principal holds one or more roles, a role holds a list of permission strings, and a permission string is a colon-delimited path with wildcards. The engine lives in auth_service.py (where the four built-in roles are defined) and identity.py (where the wildcard match runs). permissions.py only generates the permission tree for introspection.
The permission shape
A permission is a string of colon-delimited segments. The shapes that appear in the codebase fall into a few families:
| Family | Example | What it gates |
|---|---|---|
workflow:{ns}:{name}:{verb} | workflow:billing:invoice:run | Run / read / register a specific workflow |
workflow:{ns}:{name}:task:{task}:execute | workflow:billing:invoice:task:load:execute | Execute one task inside a workflow |
schedule:{*}:{verb} | schedule:*:manage | Create, pause, delete schedules |
execution:{*}:read | execution:*:read | Read execution history and events |
agent:{*}:{verb} | agent:*:create | Create / read / update / delete AI agents |
config:{*}:{verb} | config:*:read | Read or manage the config store |
service:{*}:{verb} | service:*:manage | Manage workflow-as-service exposure |
worker:{*}:{*} | worker:*:* | Internal — worker callbacks (claim, checkpoint, heartbeat) |
admin:{area}:{verb} | admin:roles:manage | Roles, principals, secrets, metrics |
Workflow permissions are the only ones that carry the namespace and workflow name as path segments. Everything else uses the family-plus-verb pattern with * filling the middle.
Built-in roles
Four roles are seeded on first boot (flux/security/auth_service.py::BUILT_IN_ROLES). They are persisted with built_in=True and the API blocks update and delete on them — you must clone, not edit:
admin— single permission:*. Matches everything.operator— read and run any workflow in any namespace, manage schedules and executions, manage agents, read/manage config, read/manage services. Cannot manage roles, principals, or secrets.viewer— read-only across workflows, executions, schedules, agents, config, services.worker—worker:*:*plusconfig:*:readandexecution:*:read. This is what the auto-provisioned worker principal carries.
Custom roles
Custom roles are first-class. Create them through the CLI, the REST API, or the SDK — they are stored as RoleModel rows alongside the built-ins:
flux roles create namespace-owner-billing \
--permissions "workflow:billing:*:run" \
--permissions "workflow:billing:*:read" \
--permissions "execution:*:read"
flux roles clone operator --name audit-operator
flux roles update audit-operator --add-permissions "admin:metrics:read"
flux roles delete audit-operator
The same calls are wired into POST /admin/roles, PATCH /admin/roles/{name}, etc. Permission strings are validated against PERMISSION_PATTERN before insert; built-in roles reject both update and delete.
Wildcard semantics
The matcher is _wildcard_match in flux/security/identity.py. The rules are not the same as glob:
- A terminal
*(the last segment of the pattern) matches any number of remaining target segments.workflow:billing:*matchesworkflow:billing:invoice:runandworkflow:billing:invoice:task:load:execute. - A non-terminal
*(in the middle) matches exactly one segment.workflow:*:*:readmatchesworkflow:billing:invoice:readbut notworkflow:billing:invoice:task:load:execute. - A bare
*in the permission set is a shortcut for “everything” — that is what theadminrole carries.
Worked examples:
workflow:*:*:run— run any workflow in any namespace.workflow:tenant-a:*:run— run any workflow insidetenant-a.workflow:tenant-a:report:run— run onlytenant-a/report.
How permissions reach the request
Roles attach to principals through PrincipalRoleModel (flux/security/principals.py) — a join table keyed on (principal_id, role_name). On every request AuthService.resolve_permissions walks the principal’s roles, unions their permission lists, and FluxIdentity.has_permission checks the route’s required string against the set. Routes declare what they need with Depends(require_permission("...")) from flux/security/dependencies.py; a miss returns 403 Permission denied: requires '<perm>'.
Provider errors during authentication are logged at error; a 403 itself is returned via FastAPI’s exception flow and shows up in the HTTP access log with the path and the missing permission. There is no separate audit table — the access log is the audit trail.
When it goes wrong
Three failure modes cover most 403s in practice:
- Wrong role assignment. Triage with
flux principals show <subject>to list the principal’s roles, thenflux roles show <role>to list its permissions. If the role is right but the user is in the wrong principal, fix the assignment withflux principals grant. - Wildcard too permissive. A role granted
workflow:*:*:runlets the principal run anything. Narrow it on the same principal by replacing the role assignment with a tenant-scoped role (workflow:tenant-a:*:run). Revoke the broad role withflux principals revoke. - Permission missing entirely. The resource family is not in any of the principal’s roles — for example a viewer trying to call
agent:*:create. The required permission appears verbatim in the 403 detail; add it explicitly withflux roles update <role> --add-permissions <perm>(custom roles only — clone first if the role is built-in).