Add a system administrator role, and gate account management behind it
CI / chart (pull_request) Successful in 2s
CI / security (pull_request) Successful in 16s
CI / test (pull_request) Successful in 1m37s

Until now every authenticated caller could create and delete users, set
anybody's password and mint anybody's API keys -- auth.go said so in a
comment. Defensible with one operator and a hand-made account; not once
people sign themselves up (#7), and not in a multi-tenant install (#4),
where the user list is no longer everybody who works here.

users.is_admin is the flag. AdminOnly gates creating and deleting users
and granting the flag itself. The endpoints that are self-service for
your own account and administration for somebody else's -- password,
ntfy topic, API keys -- go through requireSelfOrAdmin instead, because
which rule applies depends on the {id} in the path rather than on the
route.

Minting your own API key stays self-service. A key carries exactly the
rights of the user it belongs to, so issuing one is no more than signing
in again; requiring an admin for it would mean a responder cannot set up
the TUI without somebody else in the room.

/api/users stays readable by everybody. The queue's assignment control
and the on-call schedule both have to name people, and hiding the roster
from the people on it buys nothing.

THE MIGRATION MAKES EVERY EXISTING USER AN ADMINISTRATOR. They already
hold these powers, so nobody's access changes on upgrade: it names what
is already true and leaves demotion as a deliberate act. Promoting only
user 1 would silently strip the others, and could leave an install whose
only administrator is an account nobody has a password for.

Two guards keep an install administrable: the last administrator can be
neither deleted nor demoted, and nobody can delete or demote themselves
-- the likelier accident, where the only admin clears their own flag
while tidying up and locks the door behind them.

No UI changes: there are no account-management screens yet. models.User
carries is_admin (not omitempty, so a client can tell false from an old
server), which is what #5's admin page will render from.
This commit is contained in:
Niklas Ye
2026-09-20 13:20:04 +02:00
parent e3ad19c110
commit 1377d9005b
8 changed files with 491 additions and 19 deletions
+51 -8
View File
@@ -445,6 +445,20 @@ Authorization: Bearer <api-key>
or the web UI's session cookie. A request that carries an `Authorization` header
is judged on that header alone.
Two kinds of user exist. An **administrator** manages accounts: creating and
deleting users, setting anybody's password, minting keys for anybody, and
granting the flag itself. Everybody else works incidents — acknowledging,
assigning, snoozing, resolving, noting — and manages their own account and
nobody else's. An API key carries exactly the rights of the user it belongs to.
The first user, from `/api/bootstrap`, is an administrator. Users created
afterwards are not, until an administrator says so. An install always keeps at
least one: the last administrator can be neither deleted nor demoted, and
nobody can delete or demote themselves.
Endpoints that require the flag answer `403` with
`{"error":"administrator access required"}`.
| Method | Path | Description |
|---|---|---|
| `POST` | `/api/login` | `{"username","password"}` → sets the session cookie, returns `{user, has_password}`. `429` after too many failures |
@@ -455,14 +469,21 @@ is judged on that header alone.
| Method | Path | Description |
|---|---|---|
| `POST` | `/api/bootstrap` | Create first user + API key `{"username","email","password"?}` (only works on empty DB) |
| `GET` | `/api/users` | List users |
| `POST` | `/api/users` | Create user `{"username","email"}` |
| `DELETE` | `/api/users/{id}` | Delete user (cascades to keys) |
| `PUT` | `/api/users/{id}/notify` | Set push notification target `{"ntfy_topic"}` — empty string clears it |
| `PUT` | `/api/users/{id}/password` | Set web UI password `{"password","current_password"}`. `current_password` is required only when changing your own existing password. Ends the user's other sessions |
| `POST` | `/api/users/{id}/api-keys` | Issue API key `{"name"}` — key shown once |
| `DELETE` | `/api/users/{id}/api-keys/{keyID}` | Revoke API key |
**admin** marks an endpoint that requires the administrator flag; **self or
admin** marks one you may use on your own account and an administrator may use
on anybody's.
| Method | Path | Who | Description |
|---|---|---|---|
| `POST` | `/api/bootstrap` | — | Create first user + API key `{"username","email","password"?}` (only works on empty DB). The user is an administrator |
| `GET` | `/api/users` | any | List users. Open to everybody: the queue's assignment control and the schedule both have to name people |
| `POST` | `/api/users` | **admin** | Create user `{"username","email"}`. Not an administrator |
| `DELETE` | `/api/users/{id}` | **admin** | Delete user (cascades to keys). `409` for yourself or the last administrator |
| `PUT` | `/api/users/{id}/admin` | **admin** | Grant or revoke the administrator flag `{"is_admin"}`. `409` for yourself or the last administrator |
| `PUT` | `/api/users/{id}/notify` | self or admin | Set push notification target `{"ntfy_topic"}` — empty string clears it |
| `PUT` | `/api/users/{id}/password` | self or admin | Set web UI password `{"password","current_password"}`. `current_password` is required only when changing your own existing password. Ends the user's other sessions |
| `POST` | `/api/users/{id}/api-keys` | self or admin | Issue API key `{"name"}` — key shown once |
| `DELETE` | `/api/users/{id}/api-keys/{keyID}` | self or admin | Revoke API key |
### Alert ingestion
@@ -678,6 +699,28 @@ averages over incidents that have actually been acknowledged or resolved, and ar
---
## Upgrading to roles
Before this release every authenticated caller could create and delete users,
set anybody's password and mint anybody's API keys. That is now the
administrator flag, and the migration **makes every existing user an
administrator** — they already held those powers, so nobody's access changes on
upgrade and demotion is a deliberate act afterwards. Promoting only the first
user would have silently stripped the rest, and could leave an install whose
only administrator is an account nobody has a password for.
Users created after the upgrade are not administrators. Hand the flag out with:
```bash
curl -X PUT https://terdut.example.com/api/users/7/admin \
-H "Authorization: Bearer $TERDUT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"is_admin": true}'
```
Nothing in the API changed shape, so terdut-tui needs no new version — but a
non-administrator now gets `403` where a `200` used to come back.
## Upgrading from SQLite
Versions up to v0.10.2 stored everything in a SQLite file. From the Postgres release onwards