Principals
The who of an authenticated Flux request — users, service accounts, workers, and execution principals.
A principal is the “who” behind an authenticated request. Every credential Flux accepts — OIDC bearer token, API key, worker session token, execution token — resolves to one row in the principals table. That row, plus the roles attached to it, is what require_permission(...) checks on every route.
There is one model and one storage backend. The flavour of principal is just the value of a type column.
The model
PrincipalModel in flux/security/principals.py is the single ORM class. The columns that matter:
| Column | Purpose |
|---|---|
id | UUID hex, the internal identifier used in principal_roles and api_keys |
type | "user" or "service_account" — the only two values the codebase writes |
subject | The natural identifier (OIDC sub, service-account name, worker name) |
external_issuer | Which provider vouched for that subject (OIDC issuer URL, or the literal "flux") |
enabled | Toggle for soft-disable; disabled principals fail auth without being deleted |
last_seen_at | Updated on every successful resolve |
A UniqueConstraint("subject", "external_issuer") means the same sub can exist twice if it came from two different issuers. The pair (subject, external_issuer) is the public identity; the UUID id is internal.
The four flavours, one type column
Flux does not subclass PrincipalModel. The difference between a human and a worker lives in how the row is created and which roles it carries.
- OIDC users —
type="user",external_issuer=<OIDC issuer URL>. Created lazily byOIDCProvider._resolve_principal(flux/security/providers/oidc.py) the first time a valid bearer token arrives; default roles come from[flux.security.auth.oidc] default_user_roles. These rows are persisted — not throwaway. - Service accounts —
type="service_account",external_issuer="flux". Created explicitly by an operator. They authenticate via theapi_keystable, which stores hashed key material keyed onprincipal_id. - Workers — same row shape as a service account:
type="service_account",external_issuer="flux",subject=<worker name>. Auto-created onPOST /workers/registerafter a valid bootstrap token, granted theworkerrole, and issued a fresh API key assession_token. Older keys are revoked first, so re-registering a worker rotates its credential. - Execution principals — no separate row. The execution token in
flux/security/execution_token.pycarries a(subject, principal_issuer)pair and resolves back to the principal that started the run. The worker acts on that principal’s behalf for the lifetime of the execution.
Roles and the registry
Roles live in a join table, principal_roles, with composite key (principal_id, role_name) and an assigned_by audit column. PrincipalRegistry.get_roles(principal_id) returns the names; AuthService then resolves those names to permission sets via RoleModel. Nothing about the principal row itself grants permissions — an enabled principal with no rows in principal_roles authenticates but has no permissions.
Operator surface
flux principals (defined in flux/cli_auth.py) wraps the /admin/principals/... routes:
flux principals list [--type user|service_account]
flux principals create <subject> --type service_account [--role <name>] [--issuer flux]
flux principals show <subject> [--issuer ...]
flux principals grant <subject> --role <name>
flux principals revoke <subject> --role <name>
flux principals enable|disable <subject>
flux principals delete <subject> [--force]
flux principals create-key <subject> --key-name <key-name> [--expires <iso8601>]
flux principals list-keys <subject>
flux principals revoke-key <subject> --key-name <key-name>
Only user and service_account are accepted by --type. There is no separate worker subcommand — workers are service accounts created by the registration handler.
What can go wrong
Authenticated but blocked. The principal resolves, but get_roles returns an empty list, so every route 403s. The principal row exists; the join table is empty. Fix with flux principals grant <subject> --role <role>.
Stale credentials after offboarding. Deactivating a user in your IdP stops new OIDC sign-ins, but any API key minted against that principal keeps working until you remove it. delete refuses to drop a principal with live keys unless you pass --force; the safer flow is revoke-key (or disable) first, then delete. Because OIDC principals are persisted, removing a user from the IdP does not garbage-collect their row — disable or delete it explicitly.