Skip to main content
Halite uses role-based access control (RBAC). Permissions are attached to roles, users are assigned roles, and each request is checked against the union of the current user’s accumulated permissions.

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
A user accumulates the union of permissions across all their roles. If a user has both the 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:
  1. The user is active (is_active = true), and
  2. At least one of the user’s permissions matches both the requested verb and resource.
Matching uses anchored shell-style globs via 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 with require_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.