mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 01:52:17 -05:00
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