Source code for seamm_webui.manage
"""CLI for managing seamm_webui local-mode user accounts.
There's no admin UI yet (that's Phase 6) -- this is the only way to create
a "local"-auth-mode account for now. New accounts default to the admin
role (full visibility, same as everyone gets today) rather than
seamm_datastore's real owner/group filtering: Phase 3 is "prove who you
are," not per-user data partitioning -- see dashboard-rewrite-plan.md.
"""
import argparse
import getpass
from pathlib import Path
from seamm_webui.db import get_datastore, init_datastore
def _add_common_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--root",
default="~/SEAMM",
help=(
"The general SEAMM config root; NOT the datastore itself "
"(default: ~/SEAMM). Same meaning as seamm-webui's own --root."
),
)
parser.add_argument(
"--datastore",
default=None,
help="The datastore directory (default: '<root>/Jobs').",
)
def _resolve_datastore_dir(args: argparse.Namespace) -> str:
if args.datastore:
return args.datastore
return str(Path(args.root).expanduser() / "Jobs")
[docs]
def cmd_create(args: argparse.Namespace) -> None:
# init_datastore() must run before anything else in this process
# imports seamm_datastore.database.models -- see db.py's module
# docstring for why.
init_datastore(_resolve_datastore_dir(args))
from seamm_datastore.database.models import User
ds = get_datastore()
password = args.password
if password is None:
password = getpass.getpass("Password: ")
if password != getpass.getpass("Confirm password: "):
raise SystemExit("Passwords did not match.")
try:
user = User.create(
username=args.username,
password=password,
first_name=args.first_name,
last_name=args.last_name,
email=args.email,
roles=["admin"],
)
except ValueError as exc:
# e.g. the username already exists.
raise SystemExit(str(exc))
ds.Session.add(user)
ds.Session.commit()
print(f"Created user {user.username!r}.")
[docs]
def cmd_set_password(args: argparse.Namespace) -> None:
"""Reset an existing user's password.
Deliberately no --password flag (unlike create, which allows one for
scripting) -- this always prompts via getpass, so the new password
never has to be typed anywhere it could be logged or captured (a
shell's command history, a chat transcript run through a shared
terminal session, etc.), only into a genuinely hidden terminal prompt.
"""
init_datastore(_resolve_datastore_dir(args))
from seamm_datastore.database.models import User
ds = get_datastore()
user = User.query.filter_by(username=args.username).one_or_none()
if user is None:
raise SystemExit(f"No such user {args.username!r}.")
password = getpass.getpass("New password: ")
if password != getpass.getpass("Confirm new password: "):
raise SystemExit("Passwords did not match.")
user.password = password
ds.Session.commit()
print(f"Password updated for {user.username!r}.")
[docs]
def cmd_delete(args: argparse.Namespace) -> None:
init_datastore(_resolve_datastore_dir(args))
from seamm_datastore.database.models import User
ds = get_datastore()
user = User.query.filter_by(username=args.username).one_or_none()
if user is None:
raise SystemExit(f"No such user {args.username!r}.")
if not args.yes:
confirm = input(f"Delete user {user.username!r}? [y/N] ")
if confirm.strip().lower() not in ("y", "yes"):
raise SystemExit("Not deleted.")
ds.Session.delete(user)
ds.Session.commit()
print(f"Deleted user {user.username!r}.")
[docs]
def cmd_list(args: argparse.Namespace) -> None:
init_datastore(_resolve_datastore_dir(args))
from seamm_datastore.database.models import User
for user in User.query.order_by(User.username).all():
roles = ", ".join(role.name for role in user.roles)
print(f"{user.username}\t{user.email or ''}\t{roles}")
[docs]
def run() -> None:
parser = argparse.ArgumentParser(
description="Manage seamm_webui local-auth-mode user accounts."
)
subparsers = parser.add_subparsers(dest="command", required=True)
create = subparsers.add_parser("create", help="Create a new user account.")
_add_common_args(create)
create.add_argument("username")
create.add_argument(
"--password", default=None, help="Prompted securely if not given."
)
create.add_argument("--email", default=None)
create.add_argument("--first-name", default=None)
create.add_argument("--last-name", default=None)
create.set_defaults(func=cmd_create)
set_password = subparsers.add_parser(
"set-password", help="Reset an existing user's password."
)
_add_common_args(set_password)
set_password.add_argument("username")
set_password.set_defaults(func=cmd_set_password)
delete = subparsers.add_parser("delete", help="Delete a user account.")
_add_common_args(delete)
delete.add_argument("username")
delete.add_argument(
"--yes", action="store_true", help="Skip the confirmation prompt."
)
delete.set_defaults(func=cmd_delete)
list_cmd = subparsers.add_parser("list", help="List existing user accounts.")
_add_common_args(list_cmd)
list_cmd.set_defaults(func=cmd_list)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
run()