"""Bootstrap auth: initialize DB and seed admin user at install time. Idempotent — safe to run on every install (and re-install): - Writes config/auth/config.json only if it does not exist (existing JWT/WebAuthn settings are preserved). - Creates the admin user if missing; if the user already exists, updates the admin password to the provided value (docs/deployment.md: "On re-run, updates the admin password if already present"). - Suppresses the last-resort builtin admin seed (VACUUM_WALL_SEED_BUILTIN_ADMIN=0): bootstrap is the operator user's creator on a fresh install, so exactly one account exists and no hardcoded admin with an unrecoverable random password is left behind. Usage: python scripts/bootstrap_auth.py --project-dir /path/to/project \ --username admin \ --password secret \ --domain wall.example.com """ import argparse import json import os import sys from pathlib import Path # Ensure project lib is importable def main() -> None: parser = argparse.ArgumentParser(description="Bootstrap Vacuum Wall auth") parser.add_argument("--project-dir", required=True, help="Project root directory") parser.add_argument("--username", required=True, help="Admin username") parser.add_argument("--password", required=True, help="Admin password") parser.add_argument("--domain", required=True, help="Management domain (rp_id)") args = parser.parse_args() project_dir = Path(args.project_dir).resolve() sys.path.insert(0, str(project_dir)) # Set DB path before importing lib modules db_path = str(project_dir / "data" / "auth.db") os.environ["VACUUM_WALL_DB_BACKEND"] = "sqlite" os.environ["VACUUM_WALL_DB_PATH"] = db_path # Suppress the last-resort builtin admin seed in get_db(): bootstrap # creates the operator user itself, so no seeded admin may shadow it. os.environ["VACUUM_WALL_SEED_BUILTIN_ADMIN"] = "0" from lib.auth_users import ( ALL_SUBSYSTEMS, create_user, find_user, reset_password, ) # Write config — only if missing, so re-runs never clobber existing # JWT/WebAuthn settings (e.g. a customized rp_id/origin). config_dir = project_dir / "config" / "auth" config_dir.mkdir(parents=True, exist_ok=True) config_path = config_dir / "config.json" if not config_path.exists(): config = { "jwt": { "access_token_ttl": 300, "refresh_token_ttl": 604800, "algorithm": "HS256", }, "webauthn": { "rp_name": "Vacuum Wall", "rp_id": args.domain, "origin": f"https://{args.domain}", }, } with open(config_path, "w") as f: json.dump(config, f, indent=2) f.write("\n") print(f"Wrote auth config: {config_path}") else: print(f"Auth config already present, leaving unchanged: {config_path}") # Initialize DB and create the admin user, or sync the password on re-run if find_user(args.username) is not None: reset_password(args.username, args.password) print(f"Updated existing user: {args.username} (password synced)") else: permissions = {sub: "rw" for sub in ALL_SUBSYSTEMS} user = create_user(args.username, args.password, permissions) print(f"Created admin user: {user['username']} (id={user['id']})") print(f"Permissions: {len(permissions)} subsystems, all 'rw'") if __name__ == "__main__": main()