"""Command-line user management for the Work Package Suite. Use this to bootstrap the FIRST admin (the /api/auth/users endpoint needs an existing admin, so one has to be made here) and for occasional account maintenance from a shell on the server. NO PASSWORDS. D13 moved authentication to an LDAPS bind against the domain, so this tool never sets or resets a credential — it only creates accounts and assigns roles. People sign in with their Windows password; the login page sends anyone who has forgotten it to Okta. Run from the PROJECT ROOT (same place you run uvicorn), so the package imports and .env resolve the same way the API does: python -m server.manage_users create-admin alice --name "Alice Smith" python -m server.manage_users create bob --role user --name "Bob Jones" python -m server.manage_users list python -m server.manage_users disable bob python -m server.manage_users enable bob `username` must be the person's sAMAccountName, because that is what the bind and the account match use. Creating an account is optional: anyone who authenticates against the domain and is not already here is provisioned on first sign-in, at project_user with no project access. """ import argparse import sys import uuid from .db import SessionLocal, Base, engine from . import models, auth def _gen_id() -> str: return f"user_{uuid.uuid4().hex[:12]}" def cmd_create(args, role: str | None = None) -> None: role = role or args.role # 'user' is the pre-roles spelling of 'project_user' and is still accepted so the # documented one-liners keep working; anything else has to be a current role. if role == "user": role = auth.ROLE_PROJECT_USER if role not in auth.ROLES: sys.exit(f"role must be one of {', '.join(auth.ROLES)}") with SessionLocal() as db: if auth.find_user(db, args.username): sys.exit(f"A user named '{args.username}' already exists.") u = models.User( id=_gen_id(), username=args.username.strip(), full_name=(args.name or "").strip(), email=(args.email or "").strip(), role=role, ) db.add(u) db.commit() print(f"Created {role}: {u.username} (id={u.id})") def cmd_list(args) -> None: with SessionLocal() as db: rows = db.query(models.User).order_by(models.User.username).all() if not rows: print("No users yet. Create one with: create-admin ") return print(f"{'USERNAME':<24}{'ROLE':<20}{'ACTIVE':<8}{'NAME'}") for u in rows: print(f"{u.username:<24}{auth.normalize_role(u.role):<20}" f"{('yes' if u.is_active else 'no'):<8}{u.full_name}") def _set_active(username: str, active: bool) -> None: with SessionLocal() as db: u = auth.find_user(db, username) if not u: sys.exit(f"No user named '{username}'.") u.is_active = active db.commit() print(f"{u.username} is now {'enabled' if active else 'disabled'}.") def main() -> None: # Ensure the users table exists even on a fresh database. Base.metadata.create_all(bind=engine) p = argparse.ArgumentParser(prog="manage_users", description="Work Package Suite user management") sub = p.add_subparsers(dest="cmd", required=True) def add_create(name, help_): sp = sub.add_parser(name, help=help_) sp.add_argument("username", help="the person's sAMAccountName") sp.add_argument("--name", default="", help="full name") sp.add_argument("--email", default="") return sp add_create("create-admin", "create an admin account") c = add_create("create", "create an account") c.add_argument("--role", choices=list(auth.ROLES) + ["user"], default=auth.ROLE_PROJECT_USER, help="permissions role ('user' is the legacy name for project_user)") sub.add_parser("list", help="list all accounts") dp = sub.add_parser("disable", help="disable an account (blocks login)") dp.add_argument("username") ep = sub.add_parser("enable", help="re-enable an account") ep.add_argument("username") args = p.parse_args() if args.cmd == "create-admin": cmd_create(args, role="admin") elif args.cmd == "create": cmd_create(args) elif args.cmd == "list": cmd_list(args) elif args.cmd == "disable": _set_active(args.username, False) elif args.cmd == "enable": _set_active(args.username, True) if __name__ == "__main__": main()