Files
terdut-server/SERVICE-ACCOUNTS.md
T
Niklas Ye 44b2eb2cc3 Rewrite the README as highlights with screenshots; move the detail into docs/
The README was 1,240 lines of reference material and still described a
SQLite quick start. It is now a short tour (highlights, screenshots of the
web UI, an accurate quick start against Postgres), and each topic has its
own page under docs/ with an index: deployment, configuration, Alertmanager,
incidents, notifications, escalation, dead man's switches, single sign-on,
web UI, API and development. SERVICE-ACCOUNTS.md is rewritten from a
proposal into a reference, and TEAM-LOOKUP.md is gone with the endpoint it
described. The "Upgrading to ..." sections for an unreleased product are
dropped.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-09 14:56:13 +02:00

61 lines
3.0 KiB
Markdown

# Service accounts
A non-human credential for automation (terdut-operator, CI, scripts). It is not a
`users` row: no password, no `is_admin`, no OIDC identity, so it can never be
pulled into login or group sync, and it is never mistaken for a person in an
audit trail. The bearer token has the same shape as an API key (SHA-256 hash
stored, raw value shown once), prefixed `tdsa_`.
## Scopes
- **instance** — acts as owner of every team's *configuration* (rename, OIDC
groups, escalation, dead man's switches, integrations, members, delete) and may
create teams. It is not a member of any team, so it reads no incidents or
queue. It is never an administrator: user management and
`/api/admin/settings` stay human-only.
- **team** — acts as owner of exactly one team, through a single synthetic
membership. It may also mint another service account for its own team.
An account has many keys, so rotating is "mint a new key, revoke the old one"
without losing the account's identity or history.
## Endpoints
- `POST /api/service-accounts` `{name, scope, team_id}` — returns the account and
its first key. An instance-scoped account is granted by a human administrator;
a team-scoped one by an administrator, that team's owner, or an instance-scoped
account.
- `GET /api/service-accounts?name=` — look one up by name.
- `POST /api/service-accounts/{id}/keys`, `DELETE .../keys/{keyID}` — mint or
revoke a key. An instance-scoped account may manage any team-scoped account's
keys, and any account may manage its own.
## Seeding the operator's account
`TERDUT_OPERATOR_KEY` (at least 32 characters) creates the instance-scoped account
`terdut-operator` if missing and replaces its `seed` key with this value at every
start (`internal/api/operator_key.go`). The deployer generates the key and
nothing has to call `/api/bootstrap` for it; rotating is a restart with a new
value. With `TERDUT_OPERATOR_MODE` on, configuration writes by humans are refused
and a service account of either scope passes.
## How it is enforced
Every request resolves to one `Caller` (`internal/api/caller.go`): a human
(session or API key) or a service account.
- `Caller.IsAdmin()` is true only for a human administrator. `AdminOnly` and
`requireSelfOrAdmin` key on it alone; do not widen them — each time a gap came up
the fix was a narrower purpose-built capability instead.
- `Caller.IsInstanceServiceAccount()` is true only for an instance-scoped account,
never for a human. `requireTeamOwner` and `callerOwnsTeam` admit it for any team.
- `Caller.Role(teamID)`/`TeamIDs()` are a human's memberships or a team-scoped
account's single owner membership; instance scope has none.
- `Caller.AsHuman()` is what a handler must call when it needs a real `user_id`;
handlers meant for people answer 403 to a service account instead of writing a
zero id.
Where a service account acts on an incident (acknowledge, resolve), the
timeline and `acknowledged_by` record it through parallel `*_service_account_id`
columns, never as a user.