#!/usr/bin/env python3 import argparse import getpass import hashlib import sys from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from config import LOG_DIR, PASSWD_FILE ALLOWED_CHARS = set( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()" ) FIRST_CHAR_ALLOWED = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") VALID_PERM_CHARS = set("rwd") def hash_password(password: str) -> str: return hashlib.sha256(password.encode("ascii")).hexdigest() def validate_password(password: str) -> str | None: if not password: return "password cannot be empty" if password[0] not in FIRST_CHAR_ALLOWED: return "first character must be a letter (A-Z, a-z)" invalid = [ch for ch in password if ch not in ALLOWED_CHARS] if invalid: return f"invalid characters: {''.join(set(invalid))!r}" return None def validate_permissions(perms: str) -> str | None: if not perms: return "permissions cannot be empty" for ch in perms: if ch not in VALID_PERM_CHARS: return f"invalid permission {ch!r}; allowed: r, w, d" if len(set(perms)) != len(perms): return "duplicate permissions" return None def log_action(action: str) -> None: LOG_DIR.mkdir(parents=True, exist_ok=True) timestamp = datetime.now().isoformat(sep=" ", timespec="seconds") with open(LOG_DIR / "usermgr.log", "a") as f: f.write(f"{timestamp} {action}\n") def read_users() -> list[dict]: if not PASSWD_FILE.exists(): return [] users = [] with open(PASSWD_FILE) as f: for line in f: line = line.strip() if not line: continue parts = line.split(":", 4) if len(parts) != 5: continue users.append( { "login": parts[0], "password_hash": parts[1], "id": parts[2], "permissions": parts[3], "full_name": parts[4], } ) return users def write_users(users: list[dict]) -> None: PASSWD_FILE.parent.mkdir(parents=True, exist_ok=True) with open(PASSWD_FILE, "w") as f: for u in users: f.write( f"{u['login']}:{u['password_hash']}:{u['id']}:{u['permissions']}:{u['full_name']}\n" ) def find_user(users: list[dict], login: str) -> dict | None: return next((u for u in users if u["login"] == login), None) def next_uid(users: list[dict]) -> str: if not users: return "1" return str(max(int(u["id"]) for u in users) + 1) def prompt_password() -> str: while True: password = getpass.getpass("Password: ") err = validate_password(password) if err: print(f"Invalid password: {err}") continue confirm = getpass.getpass("Confirm password: ") if password != confirm: print("Passwords do not match.") continue return password def cmd_add(args: argparse.Namespace) -> None: users = read_users() login = args.login if find_user(users, login): print(f"User '{login}' already exists.") sys.exit(1) full_name = input("Full name: ").strip() if not full_name: print("Full name cannot be empty.") sys.exit(1) perms = input("Permissions (any combination of r, w, d): ").strip() err = validate_permissions(perms) if err: print(f"Invalid permissions: {err}") sys.exit(1) password = prompt_password() uid = next_uid(users) users.append( { "login": login, "password_hash": hash_password(password), "id": uid, "permissions": perms, "full_name": full_name, } ) write_users(users) log_action(f"ADD login={login} id={uid} permissions={perms} full_name='{full_name}'") print(f"User '{login}' added (id={uid}).") def cmd_edit(args: argparse.Namespace) -> None: users = read_users() user = find_user(users, args.login) if not user: print(f"User '{args.login}' not found.") sys.exit(1) changed = [] if args.full_name is not None: if not args.full_name: print("Full name cannot be empty.") sys.exit(1) user["full_name"] = args.full_name changed.append("full_name") if args.permissions is not None: err = validate_permissions(args.permissions) if err: print(f"Invalid permissions: {err}") sys.exit(1) user["permissions"] = args.permissions changed.append("permissions") if not changed: print("Nothing to change. Use --full-name and/or --permissions.") sys.exit(0) write_users(users) log_action(f"EDIT login={args.login} changed={','.join(changed)}") print(f"User '{args.login}' updated: {', '.join(changed)}.") def cmd_passwd(args: argparse.Namespace) -> None: users = read_users() user = find_user(users, args.login) if not user: print(f"User '{args.login}' not found.") sys.exit(1) password = prompt_password() user["password_hash"] = hash_password(password) write_users(users) log_action(f"PASSWD login={args.login}") print(f"Password for '{args.login}' updated.") def cmd_delete(args: argparse.Namespace) -> None: users = read_users() if not find_user(users, args.login): print(f"User '{args.login}' not found.") sys.exit(1) users = [u for u in users if u["login"] != args.login] write_users(users) log_action(f"DELETE login={args.login}") print(f"User '{args.login}' deleted.") def cmd_list(_args: argparse.Namespace) -> None: users = read_users() if not users: print("No users.") return print(f"{'Login':<16} {'ID':<4} {'Perms':<6} Full Name") print("-" * 52) for u in users: print(f"{u['login']:<16} {u['id']:<4} {u['permissions']:<6} {u['full_name']}") def main() -> None: parser = argparse.ArgumentParser(description="User management utility for practice2") sub = parser.add_subparsers(dest="command", required=True) p_add = sub.add_parser("add", help="Add a new user") p_add.add_argument("login", help="Username (login)") p_add.set_defaults(func=cmd_add) p_edit = sub.add_parser("edit", help="Edit an existing user") p_edit.add_argument("login", help="Username to edit") p_edit.add_argument("--full-name", dest="full_name", help="New full name") p_edit.add_argument("--permissions", help="New permissions (e.g. rwd)") p_edit.set_defaults(func=cmd_edit) p_passwd = sub.add_parser("passwd", help="Change user password") p_passwd.add_argument("login", help="Username") p_passwd.set_defaults(func=cmd_passwd) p_del = sub.add_parser("delete", help="Delete a user") p_del.add_argument("login", help="Username to delete") p_del.set_defaults(func=cmd_delete) p_list = sub.add_parser("list", help="List all users") p_list.set_defaults(func=cmd_list) args = parser.parse_args() args.func(args) if __name__ == "__main__": main()