Skip to main content

Documentation Index

Fetch the complete documentation index at: https://www.halite-app.com/llms.txt

Use this file to discover all available pages before exploring further.

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-*).
# From rbac/engine.py
def matches_glob(pattern: str, value: str) -> bool:
    return fnmatch.fnmatchcase(value, pattern)

def check(user, verb, resource) -> bool:
    if not user.is_active:
        return False
    for p_verb, p_resource in user.permissions_cache:
        if matches_glob(p_verb, verb) and matches_glob(p_resource, resource):
            return True
    return False
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.
VerbResource glob
**

operator

Run salt commands and manage keys, but no user administration.
VerbResource glob
view*
run*
acceptkey:*
deletekey:*
executesalt:*
killjob:*
rejectkey:*

viewer

Read-only access to everything except audit.
VerbResource glob
viewminion:*
viewkey:*
viewjob:*
viewgrain:*
viewpillar:*
viewevent:*
viewsetting:*
viewinventory:*
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:
Requestadminoperatorviewer
view, minion:web-01Pass (*/*)Pass (view/*)Pass (view/minion:*)
run, salt:test.pingPassPass (run/*)Fail
accept, key:db-01PassPass (accept/key:*)Fail
kill, job:20240101000000000000PassPass (kill/job:*)Fail
view, audit:*PassPass (view/*)Fail (not in viewer perms)
delete, user:alicePassFailFail

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.
# Example: protect a route that views minions
@router.get("/minions", dependencies=[require_perm("view", "minion:*")])
async def list_minions(...):
    ...