# `Ankusa.Admin.Router`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/admin/router.ex#L1)

The operator HTTP API: health, Prometheus metrics, the redacted config, the
AsyncAPI document of the channels this instance publishes to, the
dead-letter queue, replay jobs, and this node's quarantine pen (list and
purge).

It exists so operating Ankusa does not require an Elixir shell. Everything
here had an IEx-only affordance before (`Ankusa.Dispatch.replay/2`,
`Ankusa.Edge.Quarantine.recent/2`, `:sys.get_state/1`-style poking); this is
the same surface over HTTP.

## Authentication and scopes

Every route but `GET /health` and `GET /ready` needs a credential
(`admin.auth`, see `Ankusa.Admin.Auth`): `401` without one, `403` when the
caller's scopes (`Ankusa.Principal`) do not cover the route. Scopes are
checked before anything else, so a caller without them learns nothing about
this node's roles or data.

| Route | Scope |
| --- | --- |
| `GET /metrics`, `GET /v1/wal` | `metrics:read` |
| `GET /v1/config`, `GET /asyncapi.json` | `config:read` |
| `PUT /v1/log-level` | `config:write` + `tenant:*` |
| `POST /v1/config/reload` | `config:write` + `tenant:*` |
| `GET /v1/dlq`, `DELETE /v1/dlq` | `dlq:read` / `dlq:write` + a `tenant` (filtered) |
| `POST /v1/replays` | `replays:write` + a `tenant` (filtered), and `dlq:write` / `quarantine:write` for those kinds |
| `GET /v1/replays[/:id]` | `replays:read` + a `tenant` (filtered) |
| `PATCH /v1/replays/:id` | `replays:write` + a `tenant` (filtered) |
| `GET /v1/quarantine`, `DELETE /v1/quarantine` | `quarantine:read` / `quarantine:write` + a `tenant` (filtered) |
| `GET /v1/tenants/:tenant/sources[/:name]` | `sources:read` + `tenant:<tenant>` |
| `POST/PUT/DELETE /v1/tenants/:tenant/sources[/:name]` | `sources:write` + `tenant:<tenant>` |
| `GET /v1/rate-limits` | `rate_limits:read` + a `tenant` (filtered) |
| `GET/PUT/DELETE /v1/tenants/:tenant/rate-limit` | `rate_limits:read` / `rate_limits:write` + `tenant:<tenant>` |
| `DELETE /v1/tenants/:tenant?confirm=<tenant>`, `GET /v1/tenants/:tenant/erase` | `tenants:write` + `tenant:*` |

The DLQ, the quarantine pen, replay jobs and the rate-limit listing hold every
tenant's rows, so they are filtered by tenant (`Auth.authorize_tenants/4`).
A caller limited to some `tenant:<id>` scopes gets only those tenants' rows,
whatever it asks: the filter is forced server-side. `?tenant=<id>` (the body
key `tenant` of `POST /v1/replays`) narrows the answer to one tenant, and
naming a tenant the caller does not hold is `403`. Only `tenant:*` reads or
changes them unfiltered. A quarantine purge deletes, and a replay touches,
only the caller's tenants' rows; a replay job is visible to (and steerable
by) a limited caller only when it is scoped to tenants that caller holds.
`/v1/config` is still redacted, because many people may hold `config:read`.

## Node-local by design

A route that needs a role this node does not run returns
`409 role_not_enabled` rather than an empty success: the DLQ is the dispatch
node's store, the quarantine list is the edge node's, so a wrong-node
answer must be distinguishable from "nothing there". `GET /v1/wal` is the
same kind of answer: it describes this node's own store, and under
`wal.type: none` there is no queue to describe (`409 wal_disabled`).
Aggregating across nodes is the operator's job (scrape every admin port).

# `call`

# `init`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
