Skip to content

Synod and Purview

Two operator surfaces layered on top of the base “operator → roles → permissions” model:

  • Synod — a group of operators that simplifies granting the same set of roles to a team.
  • Purview — scoped souls visibility: which hosts and incarnations an operator sees in listings, based on their scope.

Without groups, granting the same set of roles to several operators would mean binding each role to each AID by hand — and changing the “team’s standard permission set” by walking every member. Synod adds a named intermediate level: the model becomes three-tier — Archon → Synod → roles.

  • Synod bundles roles. A group carries a set of roles and nothing more. Add an operator to the group and it automatically gets the group’s entire role bundle.
  • Synod carries no scope of its own. Scope (the on coven=… / on service=… / regex / soulprint selectors) lives on the roles themselves (Roles and access); the group only collects already-scoped roles together. A group member gets the roles with exactly the scope set on them.
  • Groups are flat. A Synod contains operators and roles, but not other groups — there’s no nesting.
  • An operator can belong to several groups. Its effective roles = directly bound ∪ obtained via all its Synods. Duplicates are idempotent — it’s a set union.

Synods are managed through the Operator API by the synod.* permission family:

  • synod.create — create a group (with a description). An empty group grants nothing — roles are added later.
  • synod.update — change only the description of a group. A group’s name is immutable (like role and incarnation names) — renaming is not supported.
  • synod.delete — delete a group (its memberships and role bundle are removed by cascade).
  • synod.list — list groups with their roles and members.
  • synod.add-operator / synod.remove-operator — add/remove an operator in the group.
  • synod.grant-role / synod.revoke-role — add/remove a role in the group’s bundle.

The synod.* family takes no scope selector: managing groups is a cluster-level operation, like role.* and operator.*.

Since a group member gets the group’s entire role bundle, both RBAC safeguards (see Default-deny) account for roles obtained via a Synod:

  • Least-privilege — you can add an operator to a group or add a role to a bundle only if the initiator itself holds all the effective permissions of that bundle/role; otherwise 403 forbidden.
  • Self-lockout — if full access (*) reaches an operator only via a group, then removing it from the group, deleting the group, or removing the *-granting role from the bundle is rejected with 409 would-lock-out-cluster when that would leave the cluster without an administrator.

A group is a convenient unit of revocation: a single synod.remove-operator operation strips the operator of the group’s entire role set at once, and the effect propagates across the cluster instantly.

A role’s scope limits not only what an operator can touch but also what it sees. Purview is an operator’s permitted visibility boundary, assembled from the scope of all its roles (direct and via groups).

The principle: for an operator with scope restrictions, a “show all souls” request means “show everything available to me”, not literally all souls.

  • Listings are narrowed by Purview. GET /v1/souls, GET /v1/incarnations, and the like return only nodes within the operator’s boundary. A host or incarnation outside the scope doesn’t appear in the list, and a request for a specific node outside the scope answers 404 — the existence of others’ nodes isn’t disclosed.
  • Purview’s dimensions are the same as the role scope selector’s: covens, SID regex, soulprint predicates (over host facts), and predicates over incarnation state. Multiple values within a dimension combine with OR.
  • An unrestricted operator (a role with full access * or with no explicit scope) sees all available souls — for it, Purview narrows nothing.

Run targeting is bounded by the same boundary

Section titled “Run targeting is bounded by the same boundary”

Purview is the upper bound not just of visibility but of targeting. When an operator launches a run by a broad filter (coven=…, a predicate over facts), the target host set is intersected with its Purview — the run won’t step outside the operator’s boundary.

The behavior when trying to step outside the scope depends on the form of the target:

  • a broad filter (coven=… or a predicate) — the target is silently narrowed to the intersection with Purview, just like listings;
  • explicitly listed foreign hosts (the operator named specific hosts outside its scope) — a 403 refusal: naming another’s node explicitly is treated as an escalation attempt, not a filter;
  • an empty intersection (no hosts left after narrowing) — 422: the request is valid, but there’s nothing to execute.

Purview visibility is fail-closed: under any scope uncertainty the result is an empty list, not all souls. If the scope couldn’t be computed (a predicate error, an unconfigured resolver, missing permissions) — the operator sees nothing rather than getting too much. Erring toward “hide the extra” is always preferred here over “show what belongs to others”.