Skip to content

fix(security): require auth on printer detail GET and job retry - #145

Merged
strausmann merged 2 commits into
mainfrom
fix/printer-detail-and-job-retry-auth-gap
Aug 14, 2026
Merged

strausmann merged 2 commits into
mainfrom
fix/printer-detail-and-job-retry-auth-gap

Conversation

@strausmann

@strausmann strausmann commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

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_printer and clear_printer_queue all require 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, with zero credentials.
  • POST /api/jobs/{id}/retry (jobs.py::retry_job) — clones a terminal job into a fresh QUEUED job that the worker actually dispatches to the physical printer. list_jobs, get_job and cancel_job in the same file all require read/print scope; retry_job did not — an unauthenticated caller could trigger a real reprint. 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, whose own module docstring states "Auth: none beyond the Pangolin proxy SSO at the network layer", or the qr.py landing pages, documented as intentionally public QR-scan URLs. Neither get_printer nor retry_job carried any such note; they diverged from every sibling in their own file with no explanation.

Update: review caught that the initial get_printer fix 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 got 200 (including the connection dict) for GET /api/printers/{B}. Fixed in a follow-up commit on this branch; see the added bullet under Fix.

Fix

  • Added _auth: ReadAuthDep to get_printer.
  • Added _auth: PrintAuthDep to retry_job, pause_job, resume_job.
  • Added a structural guardrail test (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_DESIGN allowlist. Verified it actually catches this regression by reverting the fix locally and re-running it (it correctly flagged all four previously-unguarded routes).
  • Added concrete 401/200 regression tests to 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.
  • Fixed one existing direct-call unit test (test_retry_job_direct_returns_new_queued_job) that called retry_job() without the new required argument.
  • Follow-up commit: added check_printer_access(_auth, printer_id) to get_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 on list_printers/get_printer_tape/get_printer_queue is 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

  • Bug fix (non-breaking)
  • Test only (guardrail + regression coverage)

Hardware tested on

  • No hardware impact (auth-layer only; no printer I/O touched)

Test coverage

  • Added/updated unit tests
  • Added/updated integration tests
  • Existing tests still pass

Full backend suite: 1043 passed, 19 skipped (uv run pytest). ruff check/ruff format --check clean on all changed files. mypy app/ (CI scope) unaffected — 3 pre-existing, unrelated errors outside the touched files.

Checklist

  • PR title follows Conventional Commits (fix(security): ...)
  • CHANGELOG entry isn't needed (semantic-release generates it)
  • No private IPs, hostnames, domains, real tokens, or PII in commits
  • CI is green (will be checked again after CI runs)

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.
Copilot AI lite review requested due to automatic review settings August 14, 2026 09:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@strausmann
strausmann requested a lite review from Copilot August 14, 2026 09:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@strausmann

Copy link
Copy Markdown
Owner Author

Note on the two failing CI checks (Privacy / secret scan, Python — lint, type, test → Ruff format check on app/integrations/README.md): both are pre-existing on main at the commit this branch is based on (aec51f0) and untouched by this PR — verified with `git diff aec51f0 -- app/integrations/README.md` (no diff) and by checking that the flagged `strausmann.cloud` occurrences live only in `docs/policies/privacy.md` and `docs/decisions/0014-...md`, neither of which this PR touches. `main`'s own recent CI runs show the same "CI: failure" pattern (e.g. the last three pushes to `main`). Left out of scope here deliberately — mixing an unrelated formatting/doc fix into a security-auth PR would make the diff harder to review; happy to open a separate PR for those if wanted.

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.
@strausmann
strausmann merged commit 23f4c70 into main Aug 14, 2026
17 of 19 checks passed
@strausmann
strausmann deleted the fix/printer-detail-and-job-retry-auth-gap branch August 14, 2026 09:29
github-actions Bot pushed a commit that referenced this pull request Aug 15, 2026
## <small>0.11.1 (2026-08-15)</small>

* fix(security): require auth on printer detail GET and job retry (#145) ([23f4c70](23f4c70)), closes [#145](#145)

[skip ci]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants