Resolves PR #27's bounce finding. Rebased onto !26's head (the #25
/fdashboard guard, verified green) and resolved the dashboard.py +
test_dashboard.py conflict deliberately:
- /dashboard stays GUARDED (never unguarded) but now flows directly
through gitea_saas.auth.key_is_valid + unauthorized_response — the
local _guard closure is gone, so there is exactly one auth path.
- Unset configured key now refuses BOTH dashboard endpoints with the
canonical 401 (was: 503 on the HTML page, per #25's local guard; the
consolidation normalizes every refusal to one status/body).
- test_dashboard_page_guard_blocks_anonymous_access (!26's self-checking
test) is kept: removing the guard from /dashboard fails it.
- /dashboard is NOT in auth.PUBLIC_PATHS.
- Bare-mount test updated: /dashboard 401 without key, 200 with key.
Four front-ends (server, delivery, intake_factory, dashboard) hand-rolled
the same X-API-Key check with one hardcoded literal each and four distinct
401 bodies. Add the single auth path:
- gitea_saas/auth.py owns API_KEY_HEADER (the only "X-API-Key" literal),
PUBLIC_PATHS (liveness exemptions: /health, /healthz,
/api/factory/health), key_is_valid() (the one compare_digest call on the
API-key path, fail-closed on unset/non-str keys), and UNAUTHORIZED_BODY +
unauthorized_response() (canonical 401, byte-identical on every wire).
- config.py re-exports API_KEY_HEADER from auth.py for existing importers;
intake_factory.py drops its dead try/except ImportError fallback (config
is long merged; a failed import must be loud); dashboard.py replaces its
stale "lands with #2/#6" comment/literal with the real import.
- server before_request, delivery.authorized, intake _require_auth and
dashboard _guard all delegate to the shared helper; /healthz is now
open on the composed app (was 401 for a liveness route).
- Unset-key refusal is now the same canonical 401 as a wrong key
(dashboard's 503 API_KEY_NOT_CONFIGURED removed — one status for every
refusal). stripe_client.py's webhook-signature check is untouched
(explicitly out of scope).
Tests: new tests/test_auth.py pins the acceptance criteria (one
compare_digest on the API-key path + one exempt stripe site, one literal,
same-length wrong key rejected, unset key refuses every front-end,
byte-identical 401 body across all front-ends, composed-app route
exposure). Existing suites updated to the canonical body.
Suite: 158 passed -> 171 passed (1 skipped).
Closes#24
The HTML operator page served the same clients/ledger/queue/review data as
the authenticated JSON summary, but with no auth at all. The loopback
requirement it relied on existed only as prose (page note + docstring); it
was never enforced by mount_dashboard, which does not own the app and
cannot constrain the bind address.
Put /dashboard behind the same _guard as /api/dashboard/summary:
- no X-API-Key -> 401, no billing data
- no configured key -> 503 (fail-closed)
- valid key -> 200 HTML (unchanged rendering)
Also drop the now-false 'loopback-only' security claims from the page note
and the mount_dashboard docstring, and update the tests:
- test_dashboard_page_open_and_html now sends a valid key
- new test_dashboard_page_guard_blocks_anonymous_access: no key -> 401,
body contains no client data (mutation check: fails if guard removed)
- new test_dashboard_wrong_key_same_length_rejected: wrong same-length
key -> 401 on HTML page too
- test_summary_fail_closed_without_configured_key now asserts the HTML
page also fails closed (503), not open
- test_dashboard_mounts_onto_existing_app sends a valid key
/api/dashboard/summary behaviour unchanged. Suite: 160 passed, 1 skipped
(before: 158 passed, 1 skipped).