Files
Spoolman/spoolman/auth
Donkie 07be532f00 auth: API keys, machine credentials that cannot outrank their owner
Phase 1 created auth_api_key and left PrincipalKind.APIKEY unproduced. This
fills both in, so a printer or a script has a credential that is not somebody's
password.

A key is presented as spoolman_<prefix>.<secret>. The marker exists so a leaked
key is recognisable in a log or a paste, and the separated prefix is what makes
authentication one indexed lookup instead of a scan that verifies every stored
hash in turn. The separator is a full stop because both halves are url-safe
base64, whose alphabet already contains '-' and '_'. Secrets are hashed with
the same plain SHA-256 as session tokens and for the same reason: 256 bits from
a CSPRNG has no dictionary to attack, so scrypt on the hot path of every
printer poll would cost 50 ms per request to buy nothing.

Three restrictions are the substance of this, and each has a failure mode it
exists to prevent:

A key's level is capped against its owner's *current* level on every request
rather than baked in at creation, so demoting a user weakens their outstanding
keys immediately instead of leaving credentials in the wild that outrank the
account they belong to. Verified against a running server: a key issued at
manage drops to read the moment its owner is demoted, both in enforcement and
in what the listing reports.

A key never carries administrative rights, however senior its owner, so
everything behind require_admin needs a session.

require_user now refuses keys outright, which is what gates the key endpoints
themselves. A credential that can mint another credential can never really be
revoked -- whoever held a leaked key would issue themselves a fresh one ahead
of the withdrawal.

Keys skip CSRF, as cookies.py anticipated in phase 1. The check in
require_level was already conditioned on PrincipalKind.USER, so this needed no
code change; what it needed was the comment saying why a header that a
cross-site page cannot set is not an ambient credential and has nothing to
defend against.

/metrics accepts a key in addition to SPOOLMAN_METRICS_TOKEN rather than
instead of it. The shared token needs no database and no account, which is what
an operator scraping their own instance wants and what keeps working when the
database is down; a key is attributable and revocable through the interface,
which is what you want when the scraper belongs to somebody else. Only keys are
honoured there, never session cookies, so a signed-in browser tab does not
quietly make the endpoint readable.

Claude-Session: https://claude.ai/code/session_01J3YgMAnSapoXqS9iU8Xnmk
2026-07-28 13:40:28 +02:00
..
2026-07-28 08:50:24 +02:00
2026-07-28 08:50:24 +02:00
2026-07-28 08:50:24 +02:00