Documentation for v2.11.0. Read the latest, v2.12.0.
Operator
Roles and permissions
Authorisation is expressed in permissions, the unit every check tests. Roles are fixed bundles of permissions defined in the service, not composed by an operator, so what a role confers cannot drift from what the code enforces. This chapter is the complete table of both, and the two things that sit beside them: root and maintainers.
What changed in 2.6.0
Maintainers also manage the webhooks of their Producers and of the Consumers of the same name.
Permissions#
| Permission | What it allows | Held by |
|---|---|---|
manage_users |
List, approve and delete user accounts; create service accounts and issue, see and revoke their API tokens. | admin, user_manager |
manage_roles |
Grant and revoke roles, manage groups and their roles and members, assign maintainers. | admin, user_manager |
manage_producers |
Administer Producers and their version lines: delete a version (the escape hatch from GA immutability), delete a Producer, edit its metadata, manage its webhooks, rename and delete sanshain-branches. | admin; maintainer within its Producers |
manage_consumers |
Delete a Consumer and its recorded dependencies. | admin |
view_audit |
Read the audit log and timeline, export it as CSV. | admin, viewer |
manage_settings |
Cleanup ages, the cache, registration and auto-approve, the webhook allow-list and global webhooks, the debug flags. | admin |
manage_auth_config |
Read and change the authentication mode and the LDAP and OIDC configuration. The only accounts that sign in while the mode is Off or Dev. | admin |
view_observability |
Read logs, stats and the debug configuration. Defined and reported in a user's effective permissions; in 2.6.0 no route checks it, the observability routes admit every signed-in user. | admin, viewer |
run_destructive_operations |
The bulk deletions of the Danger Zone and the whole-database snapshot download. | admin |
release_ga |
Publish with stability: ga (a fresh GA, a promotion, an idempotent GA
re-provide), cut a sanshain-branch.
|
admin, releaser |
Root holds all ten and any permission a later release adds. GET /admin/roles returns
this catalogue; GET /auth/me returns the caller's effective permissions, which is
what the web UI gates on.
Roles#
| Role | Confers | Granted how |
|---|---|---|
admin |
Every permission. | Instance-wide; admin-guarded. |
user_manager |
manage_users, manage_roles. |
Instance-wide. |
viewer |
view_audit, view_observability. |
Instance-wide. |
maintainer |
manage_producers, within a set of Producers. |
Never instance-wide: assigned to a Producer. Granting it as a role answers 400
Role 'maintainer' is scoped to specific Producers and cannot be granted
instance-wide.
|
releaser |
release_ga. |
Instance-wide; admin-guarded. |
admin and releaser are
admin-guarded: only an admin or root may grant or revoke them, or put them on or
take them off a group; a user_manager who tries gets a bare 403. For
admin that closes self-escalation; for releaser it keeps
manage_roles from being a side door to release rights.
Publishing GA without the permission answers 403 with the remedy (publishing GA for 'orders' requires the 'releaser' role — publish as a snapshot, or ask an
administrator to grant the role) and writes a VERSION_REJECTED audit entry with reason
ga_requires_releaser. Grant releaser to whatever performs releases,
typically the CI user whose token sets the ga switch. Holders also get a
Promote to GA button on snapshot entries; the promoting account is named in the
VERSION_PROMOTED entry while the original provider stays credited.
How routes are guarded#
Every /admin/* route declares one of three guards, and a build-time test keeps the
router and maintenance.yaml in step (the generated
Maintenance API shows the guard of every route):
- Any signed-in caller: the read-only listings the dashboard needs before it knows who is looking.
-
A global permission: the caller must hold it; otherwise a bare
403. - A Producer-scoped permission: the caller holds it globally, or maintains the Producer named in the path.
Admin routes take a session token only. An API token answers 401, a
read_only one on the backup route (the only admin route that admits API tokens)
answers 403 with
this API token has scope 'read_only' and may not call administrative routes; use a token with
scope 'full' of an account holding the permission.
Maintainers#
A maintainer is responsible for a set of Producers. It is an assignment, not a role, because it means nothing without the Producers it is over. A user or a group can be assigned; group assignment is what scales, so responsibility follows membership. A Producer-scoped action admits either the global permission or maintainership of that Producer, so an administrator acts on any Producer and a maintainer on theirs, and is refused on everybody else's.
In the Maintainers section of the admin page, pick a Producer, then tick the users and groups responsible for it; each tick or untick is saved at once. The same kind of picker ticks members into a Sanshain group, picks the Focus of the graph, and picks the account a token is issued For: type to narrow the list, no accept button.
What maintaining a Producer covers in 2.6.0:
- Deleting one of its versions, and the Producer itself.
- Reading its maintainers.
- Managing its webhooks, and the webhooks of the Consumer of the same name (the "same system" rule).
-
Retiring one of its API families, which otherwise needs
releaser: a refusal readsretiring an API family of 'orders' requires the 'releaser' role or a maintainer grant on that Producer — ask an administrator for either.
Maintainers do not release: a GA provide stays the pipeline's job.
| Route | Guard | Effect |
|---|---|---|
GET /admin/maintainers |
manage_roles |
Every Producer with its maintainers. |
GET /admin/producers/{name}/maintainers |
manage_producers, Producer-scoped |
Users and groups maintaining it; a maintainer may read their own. |
POST /admin/producers/{name}/maintainers |
manage_roles |
Body {"user_id": 3} or {"group_id": 1}, exactly one. |
DELETE /admin/producers/{name}/maintainers/users/{user_id},
…/groups/{group_id}
|
manage_roles |
Unassign. |
GET /admin/users/{id}/maintains |
manage_roles |
The Producers a user is responsible for. |
Groups#
A group is a set of users that roles attach to. Groups carry a source:
-
native: Sanshain's own. Membership is yours to edit (
POST/DELETE /admin/groups/{id}/members…). -
ldap: mirrors a directory group by name. Membership belongs to the directory
and is not stored; only the roles you attach are. It is re-read through the service account on
every check and cached (Directory group caching), so a change in the directory takes effect without a new login. Renaming one or editing its
members answers
400(Only Sanshain's own groups can be renamed; a directory group's name is the directory's.,Group '…' comes from the directory, so its membership is the directory's to change.).
A directory group is added by searching for it: type a part of its name under
Add a directory group in the Groups section, or in the Maintainers picker, and
Sanshain searches the directory through the service account (group,
groupOfNames, groupOfUniqueNames or posixGroup entries whose
cn contains the text, at most 20). Picking a hit stores the group under its DN, the
form memberOf answers. So a team cn=carla,ou=teams,… becomes the
maintainer of penguin with one tick, and whoever joins or leaves the team in the directory
gains or loses that without a change in Sanshain.
A native and a directory group may share a name; the UI shows the origin. The LDAP form's
Bootstrap Admin Group is the usual first directory group: saving it creates the
group with admin attached. OIDC groups are not mapped here; only the OIDC
admin_group grants a role, see
Authentication.
| Route | Effect |
|---|---|
GET /admin/roles |
The role catalogue and what each confers. |
GET/POST /admin/users/{id}/roles,
DELETE …/roles/{role}
|
Roles granted directly to a user; grant with {"role": "user_manager"}. |
GET/POST /admin/groups |
All groups with origin, roles and members; create a native one with
{"name": "…"}.
|
GET /admin/directory-groups?q=carla |
Directory groups whose name contains the text, each with its dn,
name and the group_id it has in Sanshain, if added.
|
POST /admin/groups/directory |
Add a directory group by its DN: {"dn": "cn=carla,ou=teams,dc=corp,dc=com"}. |
PUT/DELETE /admin/groups/{id} |
Rename, replace its roles, or both; delete a group and its grants. |
POST /admin/groups/{id}/members, DELETE …/members/{user_id} |
Membership of a native group; body {"user_id": 3}. |
All of them need manage_roles and are audited (GRANT_ROLE,
REVOKE_ROLE, CREATE_GROUP, UPDATE_GROUP,
DELETE_GROUP, ASSIGN_MAINTAINER, UNASSIGN_MAINTAINER).
Token scopes, from the operator's side#
Users mint their own API tokens on /account.html; an operator cannot mint one for
someone else. A token's scope is fixed at creation:
full does everything its owner may, read_only reads reports, versions,
the update streams and the MCP endpoint and is refused on every route that changes the catalog
with 403
this API token has scope 'read_only' and may not change the catalog; create a token with scope
'full' for provide, require and catalog pushes. A token never exceeds its owner: a full token of a user without
releaser cannot publish GA. Expired tokens stop validating at once and are removed by
the cleanup pass. Deleting a user revokes their tokens. The 2.6.0 UI has no administrative view of
other users' tokens.
Checked against v2.6.0.