Repository navigation
fix(security): require auth on printer detail GET and job retry - #145
Conversation
GET /api/printers/{id} and POST /api/jobs/{id}/retry shipped with no
auth dependency at all, while every sibling endpoint in the same
router file required one:
- printers.py: list_printers, get_printer_status, get_printer_tape,
get_printer_queue, pause_printer, resume_printer and
clear_printer_queue all required ReadAuthDep/PrintAuthDep — the
single-printer detail GET did not. It returned full printer
metadata (including the raw `connection` dict: host/port/SNMP
config) to any caller that could reach the backend, no credentials
required.
- jobs.py: list_jobs, get_job and cancel_job all required
read/print scope — retry_job (which clones a terminal job into a
fresh QUEUED job the worker actually dispatches to the physical
printer) did not. The pause/resume 501 stubs had the same gap,
fixed for consistency even though they currently have no side
effect beyond a 404/501 existence check.
Both were silent omissions, not documented decisions (compare to
GET /api/events, which documents its network-layer-only auth model
right in its own module docstring).
Adds a structural guardrail test
(test_route_auth_coverage_guardrail.py) that enumerates every route
the real app mounts and fails the build the moment a new route ships
without a recognised auth dependency and without a conscious,
commented addition to an explicit public-by-design allowlist —
verified to catch this exact regression by reverting the fix locally
and re-running it. Adds concrete 401/200 regression coverage for both
endpoints to test_auth_wiring.py, the existing house-style auth-wiring
suite.
Full backend suite (1042 tests), ruff and mypy (CI scope: app/) pass.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Note on the two failing CI checks (Privacy / secret scan, Python — lint, type, test → Ruff format check on All checks relevant to this change (Python lint/type/test would otherwise be green — only the pre-existing README formatting line fails it; Go, both backend/frontend builds, oapi-codegen drift check, Docker smoke test, CodeQL, GitGuardian, license/CLA, PR-title lint) pass. |
Review finding on PR #145: get_printer required auth (fixed in the previous commit) but did not call check_printer_access(), unlike its siblings get_printer_status/pause_printer/resume_printer/ clear_printer_queue. A read-scoped key restricted via allowed_printer_ids to printer A still got 200 (including the raw connection dict — host/port/SNMP config) for GET /api/printers/{B}. This meant the endpoint's own declared leak was only half closed. Adds check_printer_access(_auth, printer_id) after the 404 check, matching the exact sibling pattern (guarded by `if _auth is not None` for consistency with the rest of the file). Adds a regression test seeding its own printer (not relying on the frequently-empty shared fixture data other ACL tests in this file skip on) so it actually runs rather than skipping — verified to fail (200 instead of 403) with the fix reverted and pass with it restored. The identical pre-existing gap on list_printers/get_printer_tape/ get_printer_queue is deliberately out of scope here — tracked as follow-up issue #146. Full backend suite (1043 tests) still green; ruff and mypy clean on touched files.
Summary
Security audit found two REST endpoints with no auth dependency at all, while every sibling endpoint in the same router file required one:
GET /api/printers/{id}(printers.py::get_printer) —list_printers,get_printer_status,get_printer_tape,get_printer_queue,pause_printer,resume_printerandclear_printer_queueall requireReadAuthDep/PrintAuthDep; the single-printer detail GET did not. It returned full printer metadata — including the rawconnectiondict (host/port/SNMP config) — to any caller that could reach the backend, with zero credentials.POST /api/jobs/{id}/retry(jobs.py::retry_job) — clones a terminal job into a freshQUEUEDjob that the worker actually dispatches to the physical printer.list_jobs,get_jobandcancel_jobin the same file all requireread/printscope;retry_jobdid not — an unauthenticated caller could trigger a real reprint. Thepause/resume501 stubs had the same gap; fixed for consistency even though they currently have no side effect beyond a 404/501 existence check.Both were silent omissions, not documented decisions — compare to
GET /api/events, whose own module docstring states "Auth: none beyond the Pangolin proxy SSO at the network layer", or theqr.pylanding pages, documented as intentionally public QR-scan URLs. Neitherget_printernorretry_jobcarried any such note; they diverged from every sibling in their own file with no explanation.Update: review caught that the initial
get_printerfix required auth but did not enforce the per-printer ACL (check_printer_access) its siblings (get_printer_status/pause_printer/resume_printer/clear_printer_queue) all call — a read-key scoped to printer A still got200(including theconnectiondict) forGET /api/printers/{B}. Fixed in a follow-up commit on this branch; see the added bullet under Fix.Fix
_auth: ReadAuthDeptoget_printer._auth: PrintAuthDeptoretry_job,pause_job,resume_job.tests/unit/api/test_route_auth_coverage_guardrail.py) that enumerates every route the real app (app.main.create_app) mounts and fails the build the moment a new route ships without a recognised auth dependency and without a conscious, commented addition to an explicit_PUBLIC_BY_DESIGNallowlist. Verified it actually catches this regression by reverting the fix locally and re-running it (it correctly flagged all four previously-unguarded routes).tests/integration/api/test_auth_wiring.py(the existing house-style auth-wiring suite) for both endpoints, seeding a real printer/job through the DB and hitting the real ASGI app.test_retry_job_direct_returns_new_queued_job) that calledretry_job()without the new required argument.check_printer_access(_auth, printer_id)toget_printer(after the 404 check, matching the exact sibling pattern) so a read-key restricted to printer A can no longer read printer B's metadata. Regression test seeds its own printer (not the frequently-empty shared fixture data other ACL tests in this file skip on), so it actually runs; verified to fail without the fix and pass with it. The identical pre-existing gap onlist_printers/get_printer_tape/get_printer_queueis deliberately out of scope here — tracked as security: enforce per-printer ACL scope on printer-detail read endpoints (list/tape/queue) #146.Contributor License Agreement (CLA)
Linked issue
No pre-existing issue for the original two findings — self-discovered during a security audit. The ACL follow-up finding is tracked for the remaining sibling endpoints as #146 (out of scope for this PR).
Type of change
Hardware tested on
Test coverage
Full backend suite:
1043 passed, 19 skipped(uv run pytest).ruff check/ruff format --checkclean on all changed files.mypy app/(CI scope) unaffected — 3 pre-existing, unrelated errors outside the touched files.Checklist
fix(security): ...)