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
13
app.py
13
app.py
|
|
@ -45,6 +45,19 @@ def save_settings(data):
|
||||||
with open(SETTINGS_FILE, "w") as f:
|
with open(SETTINGS_FILE, "w") as f:
|
||||||
json.dump(existing, f, indent=2)
|
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 = Flask(__name__)
|
||||||
app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32)
|
app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 add <username>
|
||||||
docker exec -it arr-summary python3 manage_users.py passwd <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 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
|
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
|
per-user logins. Until then the old LOGIN_PASSWORD keeps working, so you
|
||||||
can't lock yourself out by deploying this.
|
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
|
import sys
|
||||||
|
|
@ -24,7 +31,10 @@ import getpass
|
||||||
from app import (
|
from app import (
|
||||||
SETTINGS_FILE,
|
SETTINGS_FILE,
|
||||||
PASSWORD_HASH_METHOD,
|
PASSWORD_HASH_METHOD,
|
||||||
|
LOGIN_PASSWORD,
|
||||||
save_settings,
|
save_settings,
|
||||||
|
delete_setting,
|
||||||
|
load_settings,
|
||||||
load_users,
|
load_users,
|
||||||
generate_password_hash,
|
generate_password_hash,
|
||||||
)
|
)
|
||||||
|
|
@ -122,7 +132,49 @@ def cmd_remove(args):
|
||||||
return 0
|
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):
|
def main(argv):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue