Files
abrown38andClaude Opus 5.5 3c0808591f Steward 0.1.0 (in development): a DHCPv4 server with a web UI, API and CLI
Subnets served per interface, pools, reservations, options at three
levels, leases in SQLite saved before the ACK, ping and decline conflict
checks, validated changes with history and undo, users and tokens, an
audit log, a Kea importer, a sandboxed systemd unit and Debian packages.
Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:14:07 +01:00

138 lines
5.6 KiB
Markdown

# The REST API
Everything the web UI and the CLI do goes through this API, under
`/api/v1`. Requests and answers are JSON.
## Authentication
* **API tokens**: `Authorization: Bearer steward_...`. Make one in the web
UI (your account), with `steward token create NAME`, or with `stewardd
user token USER NAME` on the server.
* **Sessions**: `POST /auth/login` with `{"username", "password"}` sets a
cookie and returns `csrf_token`. Requests other than GET must send it as
`X-CSRF-Token`.
* **The local socket** (`/run/steward/steward.sock`): root and stewardd's
own user need nothing; others send a token.
Failed logins and token attempts are rate limited per address and per
user name. [permissions.md](permissions.md) has what each call needs.
## Errors
```json
{"error": {"code": "validation", "message": "subnet iot (10.0.20.0/24): it overlaps subnet office (10.0.10.0/24)",
"problems": [{"severity": "error", "code": "subnet_overlap", "message": "...", "object": "subnet iot (10.0.20.0/24)"}]}}
```
| Status | Code | |
|---|---|---|
| 400 | `bad_request` | the request can't be read (an unknown option name, a bad address) |
| 401 | `unauthenticated`, `session_expired`, `login_failed` | |
| 403 | `forbidden`, `csrf` | |
| 404 | `not_found` | |
| 409 | `conflict` | e.g. undoing a change after the objects changed again |
| 422 | `validation` | the change would make the configuration invalid; `problems` lists why |
| 429 | `rate_limited` | |
## Changes
Calls that change the configuration answer:
```json
{"applied": true, "history_id": 7,
"changes": [{"kind": "subnet", "id": 12, "label": "iot (10.0.20.0/24)", "before": {...}, "after": {...}}],
"warnings": [{"severity": "warning", "code": "interface_unknown", "message": "...", "object": "..."}],
"object": {...}}
```
* `?dry_run=1` checks the change and returns what it would do, with
`"applied": false, "dry_run": true`.
* `before` is missing for a create and `after` for a delete.
* `warnings` are the problems the change brings; older ones are in
`/status`.
* `object` is the created or changed object.
Creating answers 201, other changes 200.
## Endpoints
### Status
| | |
|---|---|
| `GET /healthz` | `{"ok": true, "version"}`, without authentication |
| `GET /status` | build, start time, `interfaces` (`listening`, `error`, `addresses`, `subnets`), `subnets` with usage, `problems`, `leases` (counts by state), `messages` (counts by type), `ping_problem` |
| `GET /interfaces` | the host's interfaces, their addresses and subnets |
| `GET /catalog` | the option catalog, value types, reserved codes, roles |
### Settings
`GET /settings`, `PATCH /settings` with any of:
```json
{"lease_time": 43200, "ping_check": true, "ping_timeout_ms": 500, "decline_hold": 86400,
"not_authoritative": false, "options": [{"option": "domain-name-servers", "value": "10.0.10.2"}]}
```
`options` replaces the list. Each option is `{"option": NAME or NUMBER,
"value": TEXT}`, with `"type"` for numbers the catalog doesn't know
([options.md](options.md)).
### Subnets
| | |
|---|---|
| `GET /subnets` | every subnet, with `usage` (`pool_size`, `leased`, `declined`, `reservations`, `reservations_leased`) |
| `POST /subnets` | create; needs `prefix` and `interface` |
| `GET /subnets/{ref}` | by ID, name, or prefix with `_` for `/` (`10.0.10.0_24`) |
| `PATCH /subnets/{ref}` | change the given fields |
| `DELETE /subnets/{ref}` | delete, with its reservations |
```json
{"name": "office", "interface": "eth0", "prefix": "10.0.10.0/24",
"pools": ["10.0.10.100-10.0.10.199", "10.0.10.224/28"], "lease_time": 0,
"options": [{"option": "routers", "value": "10.0.10.1"}], "disabled": false, "comment": ""}
```
`pools` and `options` replace the lists; `lease_time` 0 means the
settings'.
### Reservations
| | |
|---|---|
| `GET /reservations[?subnet=REF]` | each with `subnet_id`, `subnet` and its current `lease` |
| `POST /reservations` | `{"ip", "mac", "client_id", "hostname", "options", "comment"}`; `ip` and `mac` or `client_id` needed |
| `GET`, `PATCH`, `DELETE /reservations/{ref}` | by ID, address, or MAC address (when it has one reservation) |
### Leases and activity
| | |
|---|---|
| `GET /leases[?subnet=&state=&interface=&q=]` | every lease, with its state now (`active`, `expired`, `released`, `declined`), `subnet` and its reservation; `q` searches address, MAC and host name |
| `GET /leases/{ip}` | one |
| `DELETE /leases/{ip}` | forget it (audited) |
| `POST /leases/{ip}/reserve` | make it a reservation; optional `{"hostname", "comment"}` |
| `GET /events[?mac=&interface=&limit=]` | the latest DHCP messages, newest first (the last 1000 are kept in memory) |
### History and import
| | |
|---|---|
| `GET /history[?limit=&before=]` | changes, newest first |
| `GET /history/{id}` | one, with each object before and after |
| `POST /history/{id}/revert` | undo it; 409 if an object it touched has changed since, or it was undone already |
| `POST /import/kea` | `{"config": TEXT, "leases": [TEXT, ...], "interfaces": {"PREFIX": "IFACE"}, "replace": false}` ([kea.md](kea.md)); answers with `leases`, `notes`, `skipped` too |
### Users, tokens, audit
| | |
|---|---|
| `POST /auth/login`, `POST /auth/logout`, `GET /auth/whoami` | |
| `POST /me/password` | `{"current", "password"}` |
| `GET /me/tokens`, `POST /me/tokens` | `{"name", "role", "grants", "expires_in"}`; the answer's `secret` is shown once |
| `GET /tokens`, `DELETE /tokens/{id}` | all tokens (administrators), revoke |
| `GET`, `POST /users`; `PATCH`, `DELETE /users/{id}` | administrators; `{"username", "password", "admin", "role", "grants", "disabled", "comment"}` |
| `GET`, `POST /users/{id}/tokens` | |
| `GET /audit[?user=&action=&result=&q=&before=&limit=]` | |