lab3
This commit is contained in:
378
lab3/usermgr.py
Normal file
378
lab3/usermgr.py
Normal file
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import getpass
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from config import (
|
||||
ACCESS_MODE_FILE,
|
||||
ETC_DIR,
|
||||
LOG_DIR,
|
||||
OBJECT_LABELS_FILE,
|
||||
PASSWD_FILE,
|
||||
SUBJECT_LABELS_FILE,
|
||||
)
|
||||
|
||||
ALLOWED_CHARS = set(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"
|
||||
)
|
||||
FIRST_CHAR_ALLOWED = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||
VALID_PERM_CHARS = set("rwd")
|
||||
VALID_LABELS = frozenset({"0", "1", "2"})
|
||||
|
||||
|
||||
def require_root() -> None:
|
||||
"""Exit if not running as root."""
|
||||
if os.geteuid() != 0:
|
||||
print("Must run as root", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
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 read_access_mode() -> str:
|
||||
"""Read access mode; default BOTH if file missing."""
|
||||
if not ACCESS_MODE_FILE.exists():
|
||||
return "BOTH"
|
||||
raw = ACCESS_MODE_FILE.read_text().strip().upper()
|
||||
if raw in ("BOTH", "DAC_ONLY", "MAC_ONLY"):
|
||||
return raw
|
||||
return "BOTH"
|
||||
|
||||
|
||||
def write_access_mode(mode: str) -> None:
|
||||
ETC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ACCESS_MODE_FILE.write_text(mode + "\n")
|
||||
|
||||
|
||||
def read_subject_labels() -> dict[str, int]:
|
||||
"""Read subject labels; login -> level (0, 1, 2)."""
|
||||
labels: dict[str, int] = {}
|
||||
if not SUBJECT_LABELS_FILE.exists():
|
||||
return labels
|
||||
for line in SUBJECT_LABELS_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2 and parts[1] in VALID_LABELS:
|
||||
labels[parts[0]] = int(parts[1])
|
||||
return labels
|
||||
|
||||
|
||||
def write_subject_labels(labels: dict[str, int]) -> None:
|
||||
ETC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
lines = [f"{login}:{level}" for login, level in sorted(labels.items())]
|
||||
SUBJECT_LABELS_FILE.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def read_object_labels() -> dict[str, int]:
|
||||
"""Read object labels; path (relative to confdata) -> level."""
|
||||
labels: dict[str, int] = {}
|
||||
if not OBJECT_LABELS_FILE.exists():
|
||||
return labels
|
||||
for line in OBJECT_LABELS_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2 and parts[1] in VALID_LABELS:
|
||||
labels[parts[0]] = int(parts[1])
|
||||
return labels
|
||||
|
||||
|
||||
def write_object_labels(labels: dict[str, int]) -> None:
|
||||
ETC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
lines = [f"{path}:{level}" for path, level in sorted(labels.items())]
|
||||
OBJECT_LABELS_FILE.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
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:
|
||||
if args.label is not None:
|
||||
require_root()
|
||||
|
||||
users = read_users()
|
||||
user = find_user(users, args.login)
|
||||
if not user:
|
||||
print(f"User '{args.login}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
changed: list[str] = []
|
||||
|
||||
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 args.label is not None:
|
||||
if args.label not in VALID_LABELS:
|
||||
print(f"Invalid label {args.label!r}; allowed: 0, 1, 2")
|
||||
sys.exit(1)
|
||||
labels = read_subject_labels()
|
||||
labels[args.login] = int(args.label)
|
||||
write_subject_labels(labels)
|
||||
changed.append("label")
|
||||
|
||||
if not changed:
|
||||
print("Nothing to change. Use --full-name, --permissions, and/or --label.")
|
||||
sys.exit(0)
|
||||
|
||||
if "full_name" in changed or "permissions" in changed:
|
||||
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 cmd_set_mode(args: argparse.Namespace) -> None:
|
||||
require_root()
|
||||
mode = args.mode.upper()
|
||||
if mode not in ("BOTH", "DAC_ONLY", "MAC_ONLY"):
|
||||
print(f"Invalid mode {args.mode!r}; allowed: BOTH, DAC_ONLY, MAC_ONLY")
|
||||
sys.exit(1)
|
||||
write_access_mode(mode)
|
||||
log_action(f"SET_MODE mode={mode}")
|
||||
print(f"Access mode set to {mode}.")
|
||||
|
||||
|
||||
def cmd_show_mode(_args: argparse.Namespace) -> None:
|
||||
require_root()
|
||||
mode = read_access_mode()
|
||||
print(f"Current access mode: {mode}")
|
||||
|
||||
|
||||
def cmd_set_label(args: argparse.Namespace) -> None:
|
||||
require_root()
|
||||
if args.label not in VALID_LABELS:
|
||||
print(f"Invalid label {args.label!r}; allowed: 0, 1, 2")
|
||||
sys.exit(1)
|
||||
path = args.path
|
||||
if path.startswith("./"):
|
||||
path = path[2:]
|
||||
path = "/".join(p for p in path.split("/") if p)
|
||||
labels = read_object_labels()
|
||||
labels[path] = int(args.label)
|
||||
write_object_labels(labels)
|
||||
log_action(f"SET_LABEL path={path} label={args.label}")
|
||||
print(f"Object label for '{path}' set to {args.label}.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="User management utility for practice3")
|
||||
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.add_argument("--label", help="Security label (0, 1, 2) - root only")
|
||||
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)
|
||||
|
||||
p_set_mode = sub.add_parser("set-mode", help="Set access mode (BOTH/DAC_ONLY/MAC_ONLY) - root only")
|
||||
p_set_mode.add_argument("mode", help="BOTH, DAC_ONLY, or MAC_ONLY")
|
||||
p_set_mode.set_defaults(func=cmd_set_mode)
|
||||
|
||||
p_show_mode = sub.add_parser("show-mode", help="Show current access mode - root only")
|
||||
p_show_mode.set_defaults(func=cmd_show_mode)
|
||||
|
||||
p_set_label = sub.add_parser("set-label", help="Set object security label - root only")
|
||||
p_set_label.add_argument("path", help="Path relative to confdata")
|
||||
p_set_label.add_argument("label", help="0, 1, or 2")
|
||||
p_set_label.set_defaults(func=cmd_set_label)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user