Commit Graph

5 Commits

Author SHA1 Message Date
Claude
08febd56ca fix(dashboard): require X-API-Key on /dashboard (fail-closed, #25)
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).
2026-08-26 02:54:37 -05:00
reviewer
1523074cbe tests(#9): make the no-second-server guard survive #2 merging
The guard asserted gitea_saas/server.py was absent from the tree. That held
only until #2 (which owns the app factory) merged; afterwards it fired against
#2's own file rather than anything #9 shipped, and was the single failure when
issue-9 was merged onto main.

issue-9 adds exactly two files (gitea_saas/dashboard.py, tests/test_dashboard.py)
and never contained server.py, so the invariant the docstring describes was
always satisfied. It is now checked directly, via AST, so mount_dashboard's
docstring example (app = Flask(__name__)) is not mistaken for a real call.
2026-08-26 02:18:32 -05:00
Claude
f7d0bc5666 fix(#9): bounce 2 — constant-time API-key compare + single header constant
Review finding (2026-08-26): the dashboard was the only auth path in the
service NOT using hmac.compare_digest — plain str != short-circuits on the
first differing byte (prefix timing leak on the shared secret) — and the
header name was a hardcoded literal instead of a named constant.

- import hmac; compare via hmac.compare_digest (503 fail-closed branch
  kept exactly as reviewed)
- API_KEY_HEADER module constant is the single source of truth; when #24
  consolidates auth paths this line becomes an import of the shared
  config constant (config.py lands with #2/#6, both unmerged)
- new test: a wrong key of the SAME length as the configured key must be
  rejected (the length-differing fixture cannot distinguish != from
  compare_digest)
2026-08-25 21:00:34 -05:00
Claude
c1b3105257 fix(#9): bounce — #16 id-key API + mount dashboard onto app factory
Review findings (manual review of !22):
1. Main moved: #16 renamed create_client/get_client records to flat
   id-keyed shape. Tests read the old nested ['client']/['client_id']
   shape (10 collection errors). Fixed to the flat ['id'] record;
   parameter names stay unchanged per review.
2. server.py three-way conflict (#2/#6/#9): issue #9 must NOT define a
   second Flask app. gitea_saas/server.py is dropped from this branch;
   dashboard.py now exposes mount_dashboard(app, core, db_path, api_key)
   which #2's app factory wires in. Endpoint tests build a throwaway app
   the same way (X-API-Key discipline, fail-closed 503, 401) and add a
   mount-contract test asserting no server.py ships with this issue.

Rebased onto current main (bcf03bd). Full gate: 58 passed, diff clean.
2026-08-25 11:38:54 -05:00
Claude
3daac0717c gitea-saas-core#9: SaaS operator dashboard — clients/queue/throughput/review verdicts/ledger flash (HTML page + authed JSON summary) 2026-08-25 11:30:15 -05:00