#!/usr/bin/env python3 """ seed-load.py — Load users into the ACS (Access Control System) database. CSV/XLSX columns: username, full_name, email, department, password, must_change_password, roles - roles: semicolon-separated, e.g. "ADMIN;SECURITY" (values: ADMIN, SECURITY, HOST) Usage: python scripts/seed-load.py --file scripts/seeds/users.csv Connection via env (PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE) or --dsn. Idempotent: existing usernames are updated (password re-hashed), roles replaced. """ import argparse import os import pandas as pd import psycopg2 from passlib.hash import bcrypt VALID_ROLES = {"ADMIN", "SECURITY", "HOST"} def read_sheet(path): if path.lower().endswith(".csv"): return pd.read_csv(path, dtype=str).fillna("") return pd.read_excel(path, dtype=str).fillna("") def get_conn(dsn=None): if dsn: return psycopg2.connect(dsn) return psycopg2.connect( host=os.getenv("PGHOST", "localhost"), port=os.getenv("PGPORT", "5432"), user=os.getenv("PGUSER", "acs"), password=os.getenv("PGPASSWORD", ""), dbname=os.getenv("PGDATABASE", "acs"), ) def parse_bool(v): return str(v).strip().lower() in ("1", "true", "yes", "y") def upsert_user(cur, row): username = row["username"].strip() if not username: return False password = row.get("password", "").strip() or "ChangeMe123!" pw_hash = bcrypt.hash(password) roles = [r.strip().upper() for r in str(row.get("roles", "")).split(";") if r.strip()] roles = [r for r in roles if r in VALID_ROLES] or ["HOST"] cur.execute("SELECT id FROM users WHERE username = %s", (username,)) existing = cur.fetchone() if existing: user_id = existing[0] cur.execute( """UPDATE users SET updated_at = now(), password_hash = %s, full_name = %s, email = %s, department = %s, must_change_password = %s WHERE id = %s""", (pw_hash, row.get("full_name", "").strip() or username, row.get("email", "").strip() or None, row.get("department", "").strip() or None, parse_bool(row.get("must_change_password", "true")), user_id), ) else: cur.execute( """INSERT INTO users (created_at, updated_at, username, password_hash, full_name, email, department, must_change_password, enabled, locked) VALUES (now(), now(), %s, %s, %s, %s, %s, %s, TRUE, FALSE) RETURNING id""", (username, pw_hash, row.get("full_name", "").strip() or username, row.get("email", "").strip() or None, row.get("department", "").strip() or None, parse_bool(row.get("must_change_password", "true"))), ) user_id = cur.fetchone()[0] cur.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,)) for role in set(roles): cur.execute("INSERT INTO user_roles (user_id, role) VALUES (%s, %s)", (user_id, role)) return True def main(): parser = argparse.ArgumentParser() parser.add_argument("--file", required=True) parser.add_argument("--dsn", default=None) args = parser.parse_args() df = read_sheet(args.file) conn = get_conn(args.dsn) count = 0 try: with conn, conn.cursor() as cur: for _, row in df.iterrows(): if upsert_user(cur, row): count += 1 print(f"[seed-load] upserted {count} user(s).") finally: conn.close() if __name__ == "__main__": main()