#!/usr/bin/env python3 """ Admin password management utility. Usage: reset_admin_password.py --generate # generate a random password and set it reset_admin_password.py # set a specific password """ import sys import os import secrets import string _here = os.path.dirname(os.path.abspath(__file__)) # Find auth_manager: host layout has api/ sibling, container has it one level up at /app for _api_path in [os.path.join(_here, '..', 'api'), os.path.join(_here, '..'), '/app']: if os.path.isfile(os.path.join(_api_path, 'auth_manager.py')): sys.path.insert(0, os.path.normpath(_api_path)) break ROOT = os.path.normpath(os.path.join(_here, '..')) # data dir: host uses ROOT/data/api, container mounts data/api at ROOT/data for _data in [os.path.join(ROOT, 'data', 'api'), os.path.join(ROOT, 'data'), '/app/data']: if os.path.isdir(_data): _DATA_DIR = _data break else: _DATA_DIR = os.path.join(ROOT, 'data', 'api') INIT_PW_FILE = os.path.join(_DATA_DIR, '.admin_initial_password') TEST_PW_FILE = os.path.join(_DATA_DIR, '.test_admin_pass') def _generate_password(length: int = 20) -> str: alphabet = string.ascii_letters + string.digits + '!@#$%^&*' while True: pw = ''.join(secrets.choice(alphabet) for _ in range(length)) # Ensure at least one of each character class required by the validator if (any(c.isupper() for c in pw) and any(c.islower() for c in pw) and any(c.isdigit() for c in pw) and any(c in '!@#$%^&*' for c in pw)): return pw def _set_password(new_password: str) -> None: from auth_manager import AuthManager data_dir = _DATA_DIR os.makedirs(data_dir, exist_ok=True) mgr = AuthManager(data_dir=data_dir, config_dir='/tmp') if mgr.set_password_admin('admin', new_password): print('[OK] Admin password updated in auth_users.json') else: print('[WARN] Admin user not found — creating it now') mgr.create_user('admin', new_password, 'admin') print('[OK] Admin user created') def _print_banner(password: str) -> None: border = '=' * 60 print(border) print(' ADMIN PASSWORD') print(border) print(f' Username : admin') print(f' Password : {password}') print(border) print(' Save this password — it will NOT be shown again.') print(border) def main() -> None: if len(sys.argv) < 2: print(__doc__, file=sys.stderr) sys.exit(1) arg = sys.argv[1] if arg == '--show': # Show the initial password file if the API hasn't consumed it yet if os.path.exists(INIT_PW_FILE): pw = open(INIT_PW_FILE).read().strip() _print_banner(pw) print() print(f' (file: {INIT_PW_FILE})') print(' The API will delete this file on first start.') else: print('Initial password file not found.') print('The API has already consumed it, or setup has not been run.') print() print('To set a new password run:') print(' make reset-admin-password') return if arg == '--generate': password = _generate_password() else: password = arg if len(password) < 10: print('Error: password must be at least 10 characters', file=sys.stderr) sys.exit(1) _set_password(password) # Write the initial password file (API reads it on first start, then deletes it) os.makedirs(os.path.dirname(INIT_PW_FILE), exist_ok=True) with open(INIT_PW_FILE, 'w') as f: f.write(password) # Write the persistent test password file (never deleted by the API) with open(TEST_PW_FILE, 'w') as f: f.write(password) os.chmod(TEST_PW_FILE, 0o600) _print_banner(password) print(f'\n Also saved to: {INIT_PW_FILE}') print(f' Test file: {TEST_PW_FILE} (persists across API restarts)') print(' Restart the API container for the change to take effect:') print(' docker restart cell-api') if __name__ == '__main__': main()