Add a guarded way to retire the plaintext shared password

settings.json can still hold the old plaintext login_password after
accounts exist, where it's ignored but sitting in cleartext. Removing it
by hand has a sharp edge: with no accounts AND no login_password, the
login route reads "nothing configured" as dev mode and lets anyone in
without a password, so a tidy-up can quietly open the dashboard.

`manage_users.py retire-shared-password` removes the key only when at
least one account exists, and says so plainly when it won't. It also
warns if LOGIN_PASSWORD is still set in the environment, since that's a
second copy people forget.

Needed delete_setting() in app.py — save_settings() only ever merges, so
it had no way to take a key away.

Verified: refuses with exit 1 when no accounts exist and leaves the file
untouched; removes the key while preserving timezone and users; is
idempotent; warns about the env var; and after removal only the account
password signs in, with the retired shared password and the env value
both rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Barely Removable 2026-08-27 13:06:31 -07:00
parent 63ad19eb12
commit 2f73367c6b
2 changed files with 66 additions and 1 deletions

13
app.py
View file

@ -45,6 +45,19 @@ def save_settings(data):
with open(SETTINGS_FILE, "w") as f:
json.dump(existing, f, indent=2)
def delete_setting(key):
"""Remove a key outright. save_settings() only ever merges, so it can't
take something away — used to retire the plaintext shared password once
real accounts exist. Returns True if the key was there."""
settings = load_settings()
if key not in settings:
return False
del settings[key]
os.makedirs(os.path.dirname(SETTINGS_FILE) or ".", exist_ok=True)
with open(SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=2)
return True
app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32)