Phase 1 — multi-user auth: - server/auth.py: bcrypt passwords, itsdangerous signed-cookie sessions, sha256 bearer tokens, FastAPI current_user/require_owner deps, login rate limit - users + api_tokens tables + jobs/assets.user_id (additive migrations) - HARD RULE enforced server-side: guests are local-only — /api/operators filters out requires_env operators, POST /api/jobs 403s cloud ops for non-owners (proven via direct POST in smoke.sh, not just UI). Settings owner-only. auth_secret hidden from the settings API. Per-user active-job cap (owner exempt). Own-asset/ own-job checks. WS auth via cookie or ?token=. Owner bootstrap prints pw once. - mb-ready: bearer MB_TOKEN; scripts/users.py for out-of-band management - Frontend: Login gate, header user chip + logout, guest note, username on jobs, Users panel in Settings (owner) Phase 2 — dashboard: - server/sysinfo.py: psutil CPU/RAM/disk + macmon Apple GPU (util/power/temp, no sudo), computed lane occupancy, 24h job summary, recent jobs w/ output thumbs; all cached (5s stats, 5min du). /api/system + /api/jobs/recent. - Dashboard.jsx: snapshot-on-refresh (no polling) — stat cards, per-core strip, lane strip, running/queued, recent grid. tests/smoke.sh rewritten for auth: 28 checks passing incl. all guest-security rules. Browser-verified owner + guest + dashboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
#!/usr/bin/env python
|
|
"""Manage MODELBEAST users directly against the DB — for bootstrapping or when
|
|
you're locked out of the UI. Run from the repo root: uv run python scripts/users.py ...
|
|
"""
|
|
import argparse
|
|
import getpass
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
from server import auth, db # noqa: E402
|
|
|
|
|
|
def main():
|
|
con = db.connect()
|
|
ap = argparse.ArgumentParser()
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
a = sub.add_parser("add")
|
|
a.add_argument("username")
|
|
a.add_argument("--owner", action="store_true")
|
|
a.add_argument("--max-jobs", type=int, default=4)
|
|
sub.add_parser("list")
|
|
p = sub.add_parser("passwd")
|
|
p.add_argument("username")
|
|
d = sub.add_parser("delete")
|
|
d.add_argument("username")
|
|
args = ap.parse_args()
|
|
|
|
if args.cmd == "add":
|
|
pw = getpass.getpass("password: ")
|
|
u = auth.create_user(con, args.username, pw,
|
|
role="owner" if args.owner else "guest",
|
|
max_active_jobs=args.max_jobs)
|
|
print(f"created {u['username']} ({u['role']})")
|
|
elif args.cmd == "list":
|
|
for u in auth.list_users(con):
|
|
print(f"{u['username']:<20} {u['role']:<7} max_jobs={u['max_active_jobs']}")
|
|
elif args.cmd == "passwd":
|
|
u = auth.get_user_by_name(con, args.username)
|
|
if not u:
|
|
sys.exit("no such user")
|
|
auth.set_password(con, u["id"], getpass.getpass("new password: "))
|
|
print("updated")
|
|
elif args.cmd == "delete":
|
|
u = auth.get_user_by_name(con, args.username)
|
|
if not u:
|
|
sys.exit("no such user")
|
|
auth.delete_user(con, u["id"])
|
|
print("deleted")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|