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>
138 lines
5.6 KiB
Markdown
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=]` | |
|