Permission model
A permission is a(verb, resource_glob) pair. For example:
("view", "minion:*")— view any minion("accept", "key:*")— accept any key("run", "*")— run anything
viewer and operator roles, they have the combined permissions of both.
How matching works
The check function (rbac/engine.py) returns true if and only if:
- The user is active (
is_active = true), and - At least one of the user’s permissions matches both the requested verb and resource.
fnmatch.fnmatchcase — the entire string must match, not just a substring. The * wildcard matches any sequence of characters; ? matches any single character. The colon (:) is a namespace separator by convention (e.g. key:web-*, minion:db-*).
Inactive users always fail permission checks, even if their roles would otherwise allow access.
Built-in roles
Halite seeds three built-in roles on every boot. The seed is idempotent — new permissions added to a built-in role are picked up on the next restart without requiring a migration.admin
Full access to all features and administration.operator
Run salt commands and manage keys, but no user administration.viewer
Read-only access to everything except audit.The
viewer role does not include view audit:* — access to the audit log is intentionally excluded from viewer-level access.Custom roles
You can create roles with arbitrary(verb, resource_glob) pairs through the Users & Roles page. Custom roles work identically to built-in roles — Halite resolves permissions the same way regardless of the is_builtin flag.
Worked examples
The following table shows whether each built-in role passes a selection of example permission checks:Guarding endpoints
Routes are protected withrequire_perm(verb, resource), a FastAPI dependency factory defined in deps.py. It wraps current_user and calls rbac.engine.check. If the check fails, it raises HTTP 403.
Related pages
- Users & Roles — manage users and assign roles in the UI
- Audit Logging — every authorization decision is recorded