""" bootstrap_admin.py — one-off: create (or promote) the first Iris admin account. Because there is no public signup, the very first admin must be provisioned out-of-band. This script creates the Firebase Auth user, sets the custom claim {role: "admin"} (which the backend's require_auth reads), and writes the iris_staff profile doc. Run it once. It is idempotent: if the user already exists, it just (re)sets the password + admin claim. CREDENTIALS — the Firebase service account is read from either: * env var FIREBASE = the full service-account JSON string (same secret the app uses), OR * a path to a serviceAccount.json file passed with --service-account USAGE (PowerShell example): $env:FIREBASE = Get-Content serviceAccount.json -Raw python bootstrap_admin.py --email admin@example.com --password '' --name "Iris Admin" USAGE (bash example): export FIREBASE="$(cat serviceAccount.json)" python bootstrap_admin.py --email admin@example.com --password '' --name "Iris Admin" The password is taken from --password or the ADMIN_PASSWORD env var; never hardcode it here. """ import os import sys import json import argparse from datetime import datetime, timezone import firebase_admin from firebase_admin import credentials, auth, firestore def load_credentials(service_account_path: str): """Build a firebase_admin credential from the FIREBASE env var or a JSON file.""" if os.environ.get("FIREBASE"): return credentials.Certificate(json.loads(os.environ["FIREBASE"])) if service_account_path and os.path.exists(service_account_path): return credentials.Certificate(service_account_path) sys.exit("ERROR: provide credentials via the FIREBASE env var or --service-account .") def main() -> None: parser = argparse.ArgumentParser(description="Create or promote the first Iris admin.") parser.add_argument("--email", required=True, help="Admin email address.") parser.add_argument("--password", default=os.environ.get("ADMIN_PASSWORD", "")) parser.add_argument("--name", default="Iris Admin") parser.add_argument("--service-account", default="serviceAccount.json", help="Path to a service account JSON (ignored if FIREBASE env is set).") args = parser.parse_args() if not args.password: sys.exit("ERROR: supply the password via --password or the ADMIN_PASSWORD env var.") cred = load_credentials(args.service_account) if not firebase_admin._apps: firebase_admin.initialize_app(cred) db = firestore.client() email = args.email.strip().lower() # Create the user, or update it if it already exists (idempotent). try: user = auth.create_user(email=email, password=args.password, display_name=args.name) print(f"Created user {email} (uid={user.uid}).") except auth.EmailAlreadyExistsError: user = auth.get_user_by_email(email) auth.update_user(user.uid, password=args.password, display_name=args.name, disabled=False) print(f"User {email} already existed (uid={user.uid}); password + profile updated.") # Grant the admin role via custom claim — this is what the backend checks. auth.set_custom_user_claims(user.uid, {"role": "admin"}) print("Set custom claim {role: 'admin'}.") # Write the staff profile doc the dashboard lists. db.collection("iris_staff").document(user.uid).set({ "uid": user.uid, "email": email, "name": args.name, "role": "admin", "disabled": False, "created_at": datetime.now(timezone.utc).isoformat(), "created_by": "bootstrap_admin.py", }, merge=True) print("Wrote iris_staff profile.") print("\nDone. Sign in to the dashboard with this email/password.") print("Note: if the user was already signed in somewhere, the new admin claim") print("takes effect after the next token refresh (re-login).") if __name__ == "__main__": main()