Files
pic/scripts/reset_admin_password.py
T
roof 9677755b4f fix: e2e/integration test infrastructure and Makefile test targets
- Fix make test: was pointing to non-existent api/tests/, now runs unit tests
  correctly with --ignore=e2e --ignore=integration
- Remove dead phase test targets (test-phase1..4, test-all-phases) that all
  referenced cd api && pytest tests/ (non-existent path)
- Add .test_admin_pass file: reset_admin_password.py now writes a persistent
  test password file alongside .admin_initial_password; the API never deletes
  it (unlike .admin_initial_password which is consumed on first startup)
- Update both integration/conftest.py and e2e/helpers/admin_password.py to
  read .test_admin_pass before .admin_initial_password — so tests work after
  make restart without needing PIC_ADMIN_PASS env var
- Add AI collaboration rules to CLAUDE.md (auto-loaded every session)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 08:27:27 -04:00

111 lines
3.6 KiB
Python

#!/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 <new_password> # set a specific password
"""
import sys
import os
import secrets
import string
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'api'))
ROOT = os.path.join(os.path.dirname(__file__), '..')
INIT_PW_FILE = os.path.normpath(os.path.join(ROOT, 'data', 'api', '.admin_initial_password'))
TEST_PW_FILE = os.path.normpath(os.path.join(ROOT, 'data', 'api', '.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 = os.path.normpath(os.path.join(ROOT, 'data', 'api'))
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()