Manage the password in the app; close remaining audit findings
Password management moves out of the CLI entirely. The credential is now a salted scrypt hash in the database (so it survives rebuilds, living in the /data volume) rather than an environment variable; PARTS_PASSWORD is demoted to a bootstrap value that stops working the moment a password is set in the UI. Every token carries a session epoch, so changing the password — or "sign out other devices" — invalidates outstanding cookies while keeping the browser that made the change signed in. A banner nags until the handed-over password is replaced. app/admin.py remains for the one case the UI cannot cover, a forgotten password. Audit findings: - Taxonomy update and delete scanned affected parts before taking the write lock, so a concurrent rename could leave the search index matching a name the UI no longer showed. All four routes now lock first; removing the lock again makes the new test fail exactly that way. - History of a missing part returned 200 with an empty list; now 404. - Infinity and NaN passed ge=0 and failed at the database. They are rejected as 422 now, and the validation error handler no longer chokes trying to echo a non-finite value back. Checks go from 134 to 181. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""Recovery CLI.
|
||||
|
||||
Everyday password changes happen in the UI. This exists for the one case the UI
|
||||
cannot help with — a forgotten password — and for the initial handover.
|
||||
|
||||
docker exec -it parts python -m app.admin set-password
|
||||
docker exec parts python -m app.admin show-status
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import sys
|
||||
|
||||
from . import auth, db
|
||||
|
||||
|
||||
def set_password(argv):
|
||||
password = argv[0] if argv else None
|
||||
if password is None:
|
||||
password = getpass.getpass("New password: ")
|
||||
if password != getpass.getpass("Repeat: "):
|
||||
print("Passwords did not match.", file=sys.stderr)
|
||||
return 1
|
||||
problem = auth.password_problem(password)
|
||||
if problem:
|
||||
print(problem, file=sys.stderr)
|
||||
return 1
|
||||
with db.session() as conn:
|
||||
auth.set_password(conn, password)
|
||||
print("Password set. All existing sessions have been signed out.")
|
||||
return 0
|
||||
|
||||
|
||||
def clear_password(_argv):
|
||||
"""Fall back to the PARTS_PASSWORD environment variable again."""
|
||||
with db.session() as conn:
|
||||
conn.execute("DELETE FROM settings WHERE key = ?", (auth.PASSWORD_KEY,))
|
||||
auth.bump_epoch(conn)
|
||||
print("Stored password cleared; PARTS_PASSWORD from the environment is live again.")
|
||||
print("All existing sessions have been signed out.")
|
||||
return 0
|
||||
|
||||
|
||||
def show_status(_argv):
|
||||
with db.session() as conn:
|
||||
stored = auth.stored_hash(conn)
|
||||
print("password source :", "database (set in the app)" if stored else "PARTS_PASSWORD env var")
|
||||
print("session epoch :", auth.current_epoch(conn))
|
||||
print("auth enabled :", auth.auth_enabled())
|
||||
return 0
|
||||
|
||||
|
||||
COMMANDS = {"set-password": set_password, "clear-password": clear_password, "show-status": show_status}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
if not argv or argv[0] not in COMMANDS:
|
||||
print("usage: python -m app.admin {" + "|".join(COMMANDS) + "}", file=sys.stderr)
|
||||
return 2
|
||||
db.init()
|
||||
return COMMANDS[argv[0]](argv[1:])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user