diff --git a/docs/plans/2026-03-01-config-download-token-design.md b/docs/plans/2026-03-01-config-download-token-design.md new file mode 100644 index 0000000..4ea0217 --- /dev/null +++ b/docs/plans/2026-03-01-config-download-token-design.md @@ -0,0 +1,77 @@ +# Config Download Token Design + +**Date:** 2026-03-01 +**Status:** Approved + +## Problem + +Downloading a generated config ZIP from the command line requires extracting an httpOnly OIDC +session cookie from the browser, which is fragile and not scriptable. Users need a stable, +per-config token they can embed in automation scripts. + +## Solution + +Add a `download_token` field to each `Config`. The existing generate endpoint accepts this token +in the POST body as an alternative to OIDC cookie auth, allowing unauthenticated-but-authorized +downloads. + +## Data Model + +- Add `download_token: str` column to `configs` table. +- Value: `secrets.token_urlsafe(32)` — 32 bytes of URL-safe random data (43 characters). +- Generated automatically on config creation. +- Stored as plaintext (the token is low-value; it only grants read access to a single config's + generated output). +- Alembic migration backfills existing configs with auto-generated tokens. + +## API Changes + +### Modified: `POST /api/configs/{id}/generate` + +Accepts an optional JSON body: + +```json +{ "token": "..." } +``` + +Auth logic (either is sufficient): +1. Valid OIDC `access_token` cookie + `owner_id` match +2. `token` in body matches `config.download_token` (no owner filter needed) + +Error responses: +- No cookie and no/wrong token → 401 +- Valid token but wrong config ID → 404 + +Example curl usage: + +```bash +curl -X POST "https://host/api/configs/1/generate?format=zip" \ + -H 'Content-Type: application/json' \ + -d '{"token": "abc..."}' -o shorewall.zip +``` + +### New: `POST /api/configs/{id}/regenerate-token` + +- OIDC-protected, owner-only. +- Generates a new `secrets.token_urlsafe(32)`, saves it to the config, returns it. +- Response: `{ "download_token": "..." }` +- Non-owner → 403. + +## Schema Changes + +- `ConfigOut` gains `download_token: str`. +- New `GenerateRequest` Pydantic model: `token: Optional[str] = None`. + +## Frontend Changes + +On the Config Detail page header area (above the tabs): + +- Read-only text field showing `download_token`. +- Copy-to-clipboard icon button. +- "Regenerate" button that calls the new endpoint and updates the displayed value. + +## Migration + +New Alembic migration `0012_config_add_download_token.py`: +- Add `download_token` column with `server_default=''`. +- Backfill with `secrets.token_urlsafe(32)` for all existing rows via a data migration step.