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:
parent
63ad19eb12
commit
2f73367c6b
2 changed files with 66 additions and 1 deletions
|
|
@ -12,10 +12,17 @@ Run it inside the container, where Werkzeug and the real settings.json are:
|
|||
docker exec -it arr-summary python3 manage_users.py add <username>
|
||||
docker exec -it arr-summary python3 manage_users.py passwd <username>
|
||||
docker exec -it arr-summary python3 manage_users.py remove <username>
|
||||
docker exec -it arr-summary python3 manage_users.py retire-shared-password
|
||||
|
||||
The first `add` switches the dashboard from its single shared password to
|
||||
per-user logins. Until then the old LOGIN_PASSWORD keeps working, so you
|
||||
can't lock yourself out by deploying this.
|
||||
|
||||
Once accounts exist and you've confirmed they sign in,
|
||||
`retire-shared-password` deletes the old plaintext login_password from
|
||||
settings.json. It refuses to run while no accounts exist, because with
|
||||
nothing configured at all the dashboard treats that as dev mode and lets
|
||||
anyone in without a password.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
|
@ -24,7 +31,10 @@ import getpass
|
|||
from app import (
|
||||
SETTINGS_FILE,
|
||||
PASSWORD_HASH_METHOD,
|
||||
LOGIN_PASSWORD,
|
||||
save_settings,
|
||||
delete_setting,
|
||||
load_settings,
|
||||
load_users,
|
||||
generate_password_hash,
|
||||
)
|
||||
|
|
@ -122,7 +132,49 @@ def cmd_remove(args):
|
|||
return 0
|
||||
|
||||
|
||||
COMMANDS = {"list": cmd_list, "add": cmd_add, "passwd": cmd_passwd, "remove": cmd_remove}
|
||||
def cmd_retire_shared_password(_args):
|
||||
"""Delete the plaintext login_password from settings.json.
|
||||
|
||||
Guarded, because removing it while nothing else is configured makes
|
||||
get_active_password() empty — and the login route reads "no credentials
|
||||
at all" as dev mode and lets anyone straight in.
|
||||
"""
|
||||
users = load_users()
|
||||
if not users:
|
||||
print("Refusing: no accounts exist yet, so the shared password is still",
|
||||
file=sys.stderr)
|
||||
print("the only way in. Run `add <username>` first and check you can",
|
||||
file=sys.stderr)
|
||||
print("sign in with it, then run this again.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if "login_password" not in load_settings():
|
||||
print("Nothing to do — settings.json has no login_password.")
|
||||
if LOGIN_PASSWORD:
|
||||
print()
|
||||
print("Note: LOGIN_PASSWORD is still set in the environment. It's")
|
||||
print("ignored while accounts exist, but clear it from .env too if")
|
||||
print("you want the old shared password gone entirely.")
|
||||
return 0
|
||||
|
||||
delete_setting("login_password")
|
||||
print(f"Removed the plaintext login_password from {SETTINGS_FILE}.")
|
||||
print(f"{len(users)} account(s) remain in use: {', '.join(sorted(users))}")
|
||||
if LOGIN_PASSWORD:
|
||||
print()
|
||||
print("LOGIN_PASSWORD is still set in the environment. It's ignored while")
|
||||
print("accounts exist, but clear it from .env and restart to be rid of it.")
|
||||
print("Don't clear it before you've confirmed the accounts work.")
|
||||
return 0
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
"list": cmd_list,
|
||||
"add": cmd_add,
|
||||
"passwd": cmd_passwd,
|
||||
"remove": cmd_remove,
|
||||
"retire-shared-password": cmd_retire_shared_password,
|
||||
}
|
||||
|
||||
|
||||
def main(argv):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue