#11 Manual review/removal path for maintainers #39

Merged
JMR-dev merged 1 commits from ticket-11-manual-removal into main 2026-07-02 20:58:56 +00:00
JMR-dev commented 2026-07-02 20:54:45 +00:00 (Migrated from github.com)

What

An authenticated admin path for the single maintainer to review the pending
queue and pull a report before Friday's publish run. Built on the lifecycle
Manager from #10.

Endpoints (added to the existing internal/handler)

Method + path Result
GET /v1/admin/reports 200 {"status":"ok","reports":[<id>...]} — pending report ids
POST /v1/admin/reports/{id}/remove 200 {"status":"removed","id":<id>}
DELETE /v1/admin/reports/{id} 200 — REST alias of the remove above

remove calls lifecycle.Manager.MarkRemoved (#10), transitioning the report
pending -> removed, so #13's ListPending no longer returns it and it is
excluded from the next weekly publish
. Remove is idempotent.

Status codes: 200 list/remove, 404 unknown/never-pending id, 401
missing/malformed/wrong token and unset server secret (fail-closed), 405
wrong method (with Allow).

Auth choice — shared-secret Bearer token

Chosen over Cloudflare Access for a single-maintainer, low-volume tool: no Zero
Trust org/policy to provision, trivially callable from curl/scripts/CI, and
injects cleanly for tests + dev server. Full rationale + alternatives in
docs/decisions/admin-auth.md (ADR 0003).

  • Authorization: Bearer <token>, compared with crypto/subtle.ConstantTimeCompare.
  • Fail-closed: if the server secret is unset/empty, every request is rejected
    (401) regardless of what the client sends — a missing binding can never silently
    disable auth.
  • Injected via a new AdminBackend param on handler.New:
    • dev server + tests: handler.NewManagerBackend(lifecycle.New(store), token)
      over an in-memory store; token from the ADMIN_TOKEN env var.
    • Worker: workerAdminBackend reads ADMIN_TOKEN from Secrets Store and
      builds an R2-backed Manager per request (both are only available in-request,
      mirroring how WorkerSink loads its keyring).

Tests

Go unit tests (internal/handler, httptest) — all pass

--- PASS: TestAdminListReturnsPendingIDs
--- PASS: TestAdminListEmptyIsArray
--- PASS: TestAdminRemoveExcludesFromPending
--- PASS: TestAdminRemoveViaDelete
--- PASS: TestAdminRemoveIdempotent
--- PASS: TestAdminRemoveUnknownIs404
--- PASS: TestAdminMissingTokenIs401
--- PASS: TestAdminInvalidTokenIs401
--- PASS: TestAdminMalformedAuthHeaderIs401
--- PASS: TestAdminUnsetSecretFailsClosed
--- PASS: TestAdminNilBackendFailsClosed
--- PASS: TestAdminWrongMethodIs405
ok  github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/handler

go build ./..., go vet ./..., and full go test ./... are green (host, no
TinyGo). The Worker package also type-checks under GOOS=js GOARCH=wasm go build.

Bruno API tests (api-tests/, OpenCollection YAML) — 19/19 pass

Run against ADMIN_TOKEN=local-dev-admin-token go run ./cmd/devserver, then
cd api-tests && bru run --env local:

admin-seed-report (202 Accepted)              ✓ seed report is accepted (202)
admin-list-authed (200 OK)                    ✓ authenticated list returns 200
                                              ✓ body has status ok and a reports array
                                              ✓ at least one pending report is listed (the seed)
admin-remove-authed (200 OK)                  ✓ authenticated remove returns 200
                                              ✓ body confirms the removed id
admin-list-excludes-removed (200 OK)          ✓ list returns 200
                                              ✓ the removed id is no longer pending
admin-unauthorized-no-token (401)             ✓ a request with no bearer token returns 401
                                              ✓ 401 advertises the Bearer challenge
admin-unauthorized-bad-token (401)            ✓ a request with the wrong bearer token returns 401

📊 Requests 11 (11 Passed) · Tests 19/19 · Status ✓ PASS

(The 6 pre-existing ingest-contract tests also still pass in the same run.)

Worker / wrangler (conflict-awareness with #13)

  • worker/main.go: one-line change — added the workerAdminBackend{} arg to the
    existing handler.New(...) call (plus a comment). New logic lives in the new
    worker/admin.go. Triggers/cron untouched (owned by #13).
  • wrangler.jsonc: added only the ADMIN_TOKEN secrets_store_secrets
    entry. Did not touch triggers.
  • #13 also edits worker/main.go; the overlap is limited to the single
    handler.New(...) line, so any conflict is trivial to resolve.

Out-of-scope notes

  • The Worker admin backend is unverified locally (no TinyGo here) but compiles
    under the standard GOOS=js GOARCH=wasm toolchain and uses only existing
    exported helpers; CI's pnpm run build is the authoritative check.

Closes #11

## What An authenticated admin path for the single maintainer to review the pending queue and pull a report before Friday's publish run. Built on the lifecycle Manager from #10. ### Endpoints (added to the existing `internal/handler`) | Method + path | Result | | --- | --- | | `GET /v1/admin/reports` | `200 {"status":"ok","reports":[<id>...]}` — pending report ids | | `POST /v1/admin/reports/{id}/remove` | `200 {"status":"removed","id":<id>}` | | `DELETE /v1/admin/reports/{id}` | `200` — REST alias of the remove above | `remove` calls `lifecycle.Manager.MarkRemoved` (#10), transitioning the report `pending -> removed`, so **#13's `ListPending` no longer returns it and it is excluded from the next weekly publish**. Remove is idempotent. Status codes: **200** list/remove, **404** unknown/never-pending id, **401** missing/malformed/wrong token *and* unset server secret (fail-closed), **405** wrong method (with `Allow`). ## Auth choice — shared-secret Bearer token Chosen over Cloudflare Access for a single-maintainer, low-volume tool: no Zero Trust org/policy to provision, trivially callable from curl/scripts/CI, and injects cleanly for tests + dev server. Full rationale + alternatives in `docs/decisions/admin-auth.md` (ADR 0003). - `Authorization: Bearer <token>`, compared with `crypto/subtle.ConstantTimeCompare`. - **Fail-closed:** if the server secret is unset/empty, every request is rejected (401) regardless of what the client sends — a missing binding can never silently disable auth. - Injected via a new `AdminBackend` param on `handler.New`: - **dev server + tests:** `handler.NewManagerBackend(lifecycle.New(store), token)` over an in-memory store; token from the `ADMIN_TOKEN` env var. - **Worker:** `workerAdminBackend` reads `ADMIN_TOKEN` from Secrets Store and builds an R2-backed Manager per request (both are only available in-request, mirroring how `WorkerSink` loads its keyring). ## Tests ### Go unit tests (`internal/handler`, httptest) — all pass ``` --- PASS: TestAdminListReturnsPendingIDs --- PASS: TestAdminListEmptyIsArray --- PASS: TestAdminRemoveExcludesFromPending --- PASS: TestAdminRemoveViaDelete --- PASS: TestAdminRemoveIdempotent --- PASS: TestAdminRemoveUnknownIs404 --- PASS: TestAdminMissingTokenIs401 --- PASS: TestAdminInvalidTokenIs401 --- PASS: TestAdminMalformedAuthHeaderIs401 --- PASS: TestAdminUnsetSecretFailsClosed --- PASS: TestAdminNilBackendFailsClosed --- PASS: TestAdminWrongMethodIs405 ok github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/handler ``` `go build ./...`, `go vet ./...`, and full `go test ./...` are green (host, no TinyGo). The Worker package also type-checks under `GOOS=js GOARCH=wasm go build`. ### Bruno API tests (`api-tests/`, OpenCollection YAML) — 19/19 pass Run against `ADMIN_TOKEN=local-dev-admin-token go run ./cmd/devserver`, then `cd api-tests && bru run --env local`: ``` admin-seed-report (202 Accepted) ✓ seed report is accepted (202) admin-list-authed (200 OK) ✓ authenticated list returns 200 ✓ body has status ok and a reports array ✓ at least one pending report is listed (the seed) admin-remove-authed (200 OK) ✓ authenticated remove returns 200 ✓ body confirms the removed id admin-list-excludes-removed (200 OK) ✓ list returns 200 ✓ the removed id is no longer pending admin-unauthorized-no-token (401) ✓ a request with no bearer token returns 401 ✓ 401 advertises the Bearer challenge admin-unauthorized-bad-token (401) ✓ a request with the wrong bearer token returns 401 📊 Requests 11 (11 Passed) · Tests 19/19 · Status ✓ PASS ``` (The 6 pre-existing ingest-contract tests also still pass in the same run.) ## Worker / wrangler (conflict-awareness with #13) - `worker/main.go`: one-line change — added the `workerAdminBackend{}` arg to the existing `handler.New(...)` call (plus a comment). New logic lives in the new `worker/admin.go`. **Triggers/cron untouched** (owned by #13). - `wrangler.jsonc`: added **only** the `ADMIN_TOKEN` `secrets_store_secrets` entry. Did not touch triggers. - `#13` also edits `worker/main.go`; the overlap is limited to the single `handler.New(...)` line, so any conflict is trivial to resolve. ## Out-of-scope notes - The Worker admin backend is unverified locally (no TinyGo here) but compiles under the standard `GOOS=js GOARCH=wasm` toolchain and uses only existing exported helpers; CI's `pnpm run build` is the authoritative check. Closes #11
Sign in to join this conversation.