Files
Spoolman/spoolman/api
Donkie ed58a6e3c4 auth: user administration over the API, not just the command line
Phase 1 could only create the owner. Every other account had to be made with
python -m spoolman.cli, which meant shell access on the server was a
prerequisite for having a second user at all. require_admin() has existed since
phase 1 with no callers; these are its first.

Two accounts are shielded from demotion, disabling and deletion, and both
protections exist to stop an instance being administered into a state nobody
can recover from through the interface:

The owner is untouchable by anyone, themselves included. It is the account that
can always fix the others, so it must not be possible to remove the last thing
that can grant rights back.

An administrator cannot demote, disable or delete themselves. Somebody else
with the rights has to do it, which guarantees a second pair of hands still
exists afterwards.

Neither is a security boundary -- both are recoverable with the CLI, which
remains the escape hatch when no working account is left. They are there to
make the obvious mistake impossible, not to withstand an attacker who is
already an administrator.

The protections are consulted only on the changes that actually take something
away. Granting admin is unrestricted, and re-sending an unchanged account from
a form is not refused just because the form included the owner's own level --
_collect_changes compares against the stored row first, so a no-op is a no-op.

Passwords may be supplied or generated. A generated one is returned exactly
once, in the response that created it, which mirrors what the CLI already does
and avoids an administrator inventing "Password123" for somebody. Any reset
ends every session that account holds: whoever knew the old password may be
precisely who the reset is defending against, and their cookie would otherwise
outlive the credential it was obtained with. Disabling an account does the
same, so the state in the browser agrees with the state on disk.

Deleting a user takes its sessions and API keys with it and detaches its audit
entries. The update path goes through a field whitelist in the data layer, so a
mistake in the API layer cannot reach password_hash, is_owner, or the lockout
counters.

Claude-Session: https://claude.ai/code/session_01J3YgMAnSapoXqS9iU8Xnmk
2026-07-28 13:57:00 +02:00
..
2023-04-02 19:19:15 +02:00