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:

ColumnPurpose
idUUID hex, the internal identifier used in principal_roles and api_keys
type"user" or "service_account" — the only two values the codebase writes
subjectThe natural identifier (OIDC sub, service-account name, worker name)
external_issuerWhich provider vouched for that subject (OIDC issuer URL, or the literal "flux")
enabledToggle for soft-disable; disabled principals fail auth without being deleted
last_seen_atUpdated 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.

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.