feat(api): add API keys and the integration API #8

Merged
ryan.mcgee merged 28 commits from feat/api-keys into main 2026-10-15 18:07:19 +00:00
Owner

Summary

  • Organization API keys and an integration API (/api/v1) for nodes, resources, targets and headers, described by a public OpenAPI 3.1 document.
  • Stacked on #7 (feat/backend-tls); retarget to main once #2 to #7 are merged.

Changes

  • api_keys table (reversible, portable): name, read or write access, optional expiry, last use, creator (kept as null if the user is deleted). Keys are random 256-bit secrets (tck_...) stored only as SHA-256 and shown once, like node tokens.
  • ApiKeys context and a key-backed Scope (no user; write acts as admin, read as member), so the existing context authorization applies unchanged. Keys can never create, list or revoke keys, so a leaked key cannot mint a successor.
  • Plugs, in order: ApiKeyAuth (one uniform 401 for every failure; no session or cookie is ever consulted), ApiWriteAccess (read keys get 403 on anything but GET/HEAD/OPTIONS before a controller runs, as a second line of defense), ApiPathIds (malformed ids are 404).
  • Endpoints: organization, nodes (never their tokens), resources, targets, headers. Responses are {"data": ...}; every error is {"errors": ...}. Other organizations' records are 404. Controllers answer 404 themselves so the shape does not depend on the environment (in development Phoenix would otherwise render a debug page with the failing query).
  • Api.Spec is the OpenAPI document as code, served publicly at /api/v1/openapi.json.
  • UI: an API keys page for admins (create, one-time reveal, revoke), linked from the organization page; members are sent away and do not see the link.
  • README section and status update.

Verification

  • 693 tests pass on SQLite and PostgreSQL; format, compile --warnings-as-errors, credo --strict clean; mix dialyzer 0 errors on both backends.
  • Route-driven tests: every /api/v1 route refuses missing, malformed, wrong-kind, expired and revoked keys with the same 401, and every write route refuses read keys with 403, including routes added later. Tenant isolation, mass assignment, a secrets sweep of every response, and a contract test tying the OpenAPI document to the router, the serializer, the Ecto enums and every documented input.
  • Migration up, down, up on populated databases on both backends: all other tables unchanged, SQLite integrity and foreign-key checks clean; also applied to the existing populated dev database.
  • Live (compose stack, real browser): keys created in the UI; with the write key a resource, a routed target and a header were created through the API and Traefik served them (path stripped, header added) on its next poll; the read key got 200 on GET and 403 on every write; unknown and malformed ids, an invalid body, a missing key and a wrong key all gave the documented JSON; after revoking in the UI both keys gave 401. Dev data restored.
  • Observed: in development a malformed JSON body gets Phoenix's debug page rather than the JSON error (the production shape is asserted by a test).

Not included

  • Access control, node writes and members through the API; managing API keys through the API (deliberate); rate limiting of failed authentication (keys are 256-bit; node tokens have none either).
## Summary - Organization API keys and an integration API (`/api/v1`) for nodes, resources, targets and headers, described by a public OpenAPI 3.1 document. - Stacked on #7 (`feat/backend-tls`); retarget to `main` once #2 to #7 are merged. ## Changes - `api_keys` table (reversible, portable): name, read or write access, optional expiry, last use, creator (kept as null if the user is deleted). Keys are random 256-bit secrets (`tck_...`) stored only as SHA-256 and shown once, like node tokens. - `ApiKeys` context and a key-backed `Scope` (no user; write acts as admin, read as member), so the existing context authorization applies unchanged. Keys can never create, list or revoke keys, so a leaked key cannot mint a successor. - Plugs, in order: `ApiKeyAuth` (one uniform 401 for every failure; no session or cookie is ever consulted), `ApiWriteAccess` (read keys get 403 on anything but GET/HEAD/OPTIONS before a controller runs, as a second line of defense), `ApiPathIds` (malformed ids are 404). - Endpoints: organization, nodes (never their tokens), resources, targets, headers. Responses are `{"data": ...}`; every error is `{"errors": ...}`. Other organizations' records are 404. Controllers answer 404 themselves so the shape does not depend on the environment (in development Phoenix would otherwise render a debug page with the failing query). - `Api.Spec` is the OpenAPI document as code, served publicly at `/api/v1/openapi.json`. - UI: an API keys page for admins (create, one-time reveal, revoke), linked from the organization page; members are sent away and do not see the link. - README section and status update. ## Verification - 693 tests pass on SQLite and PostgreSQL; format, `compile --warnings-as-errors`, `credo --strict` clean; `mix dialyzer` 0 errors on both backends. - Route-driven tests: every `/api/v1` route refuses missing, malformed, wrong-kind, expired and revoked keys with the same 401, and every write route refuses read keys with 403, including routes added later. Tenant isolation, mass assignment, a secrets sweep of every response, and a contract test tying the OpenAPI document to the router, the serializer, the Ecto enums and every documented input. - Migration up, down, up on populated databases on both backends: all other tables unchanged, SQLite integrity and foreign-key checks clean; also applied to the existing populated dev database. - Live (compose stack, real browser): keys created in the UI; with the write key a resource, a routed target and a header were created through the API and Traefik served them (path stripped, header added) on its next poll; the read key got 200 on GET and 403 on every write; unknown and malformed ids, an invalid body, a missing key and a wrong key all gave the documented JSON; after revoking in the UI both keys gave 401. Dev data restored. - Observed: in development a malformed JSON body gets Phoenix's debug page rather than the JSON error (the production shape is asserted by a test). ## Not included - Access control, node writes and members through the API; managing API keys through the API (deliberate); rate limiting of failed authentication (keys are 256-bit; node tokens have none either).
- organization admins create read or write API keys with an optional
  expiry; keys are random 256-bit secrets stored only as SHA-256 and
  shown once, and revoking one takes effect immediately
- a key authenticates /api/v1 requests with a bearer header and runs
  with the organization's scope (write acts as admin, read as member),
  so the existing context authorization applies; keys can never
  create, list or revoke keys
- /api/v1 covers the organization, nodes, resources, targets and
  headers; every error is {"errors": ...}, other organizations'
  records are 404, and secrets are never serialized
- the OpenAPI 3.1 description is code, served publicly, and held to
  the router, the serializer and the accepted inputs by tests
- an API keys page for admins, linked from the organization page
ryan.mcgee changed target branch from feat/backend-tls to main 2026-10-15 18:07:09 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
White_Ghost_Cyber_Systems/traefik-control!8
No description provided.