44b2eb2cc3
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
61 lines
3.0 KiB
Markdown
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.
|