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