#!/usr/bin/env python3 import argparse import getpass import hashlib import shutil import signal import sys from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from config import CONFDATA_DIR, LOG_DIR, PASSWD_FILE HELP_TEXT = """\ Commands: create create new empty file [requires: w] read print file contents [requires: r] append append text line to file [requires: w] copy copy file into confdata [requires: r, w] remove delete file from confdata [requires: d] help show this help exit exit""" def hash_password(password: str) -> str: return hashlib.sha256(password.encode("ascii")).hexdigest() def log_action(login: str, action: str) -> None: LOG_DIR.mkdir(parents=True, exist_ok=True) timestamp = datetime.now().isoformat(sep=" ", timespec="seconds") with open(LOG_DIR / "access.log", "a") as f: f.write(f"{timestamp} [{login}] {action}\n") def read_users() -> dict[str, dict]: users: dict[str, 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[parts[0]] = { "password_hash": parts[1], "id": parts[2], "permissions": parts[3], "full_name": parts[4], } return users def authenticate() -> tuple[str, dict]: users = read_users() while True: try: login = input("Login: ").strip() password = getpass.getpass("Password: ") except (EOFError, KeyboardInterrupt): print("\nBye.") sys.exit(0) user = users.get(login) if user and user["password_hash"] == hash_password(password): return login, user log_action(login if login else "-", "LOGIN_FAILED") print("Invalid credentials. Try again.") def confdata_path(arg: str) -> Path: p = Path(arg) if not p.is_absolute(): p = CONFDATA_DIR / p return p.resolve() def is_in_confdata(path: Path) -> bool: try: path.relative_to(CONFDATA_DIR.resolve()) return True except ValueError: return False def cmd_read(args: list[str], login: str, perms: str) -> None: if "r" not in perms: print("Permission denied (requires: r)") return if len(args) != 1: print("Usage: read ") return path = confdata_path(args[0]) if not is_in_confdata(path): print("Access denied: file must be inside confdata") return if not path.exists(): print(f"File not found: {path.name}") return print(path.read_text(), end="") log_action(login, f"READ {path}") def cmd_create(args: list[str], login: str, perms: str) -> None: if "w" not in perms: print("Permission denied (requires: w)") return if len(args) != 1: print("Usage: create ") return path = confdata_path(args[0]) if not is_in_confdata(path): print("Access denied: file must be inside confdata") return if path.exists(): print(f"File already exists: {path.name}") return path.parent.mkdir(parents=True, exist_ok=True) path.touch() log_action(login, f"CREATE {path}") print("Done.") def cmd_append(args: list[str], login: str, perms: str) -> None: if "w" not in perms: print("Permission denied (requires: w)") return if len(args) < 2: print("Usage: append ") return path = confdata_path(args[0]) if not is_in_confdata(path): print("Access denied: file must be inside confdata") return if not path.exists(): print(f"File not found: {path.name}. Use 'create' first.") return text = " ".join(args[1:]) with open(path, "a") as f: f.write(text + "\n") log_action(login, f"APPEND {path}") print("Done.") def cmd_copy(args: list[str], login: str, perms: str) -> None: if "r" not in perms or "w" not in perms: print("Permission denied (requires: r, w)") return if len(args) != 2: print("Usage: copy ") return src = Path(args[0]).resolve() dst = confdata_path(args[1]) if not is_in_confdata(dst): print("Access denied: destination must be inside confdata") return if dst.is_dir(): dst = dst / src.name if dst.exists(): print(f"Destination already exists: {dst.name}") return if not src.exists(): print(f"Source not found: {args[0]}") return if src.is_dir(): print("Copying directories is not supported") return shutil.copy2(src, dst) log_action(login, f"COPY {src} -> {dst}") print("Done.") def cmd_remove(args: list[str], login: str, perms: str) -> None: if "d" not in perms: print("Permission denied (requires: d)") return if len(args) != 1: print("Usage: remove ") return path = confdata_path(args[0]) if not is_in_confdata(path): print("Access denied: file must be inside confdata") return if not path.exists(): print(f"File not found: {path.name}") return path.unlink() log_action(login, f"REMOVE {path}") print("Done.") def check_credentials(login: str, password: str) -> bool: """Check login+password against passwd. Used by --check mode.""" users = read_users() user = users.get(login) if user and user["password_hash"] == hash_password(password): return True return False def main() -> None: parser = argparse.ArgumentParser(description="Access confidential data") parser.add_argument( "--check", metavar="LOGIN", help="Batch mode: read passwords line-by-line from stdin, output 0 or 1 per line; exit 0 on first match", ) args, _ = parser.parse_known_args() if args.check is not None: # --check: one process, many checks; read_users once users = read_users() user = users.get(args.check) target_hash = user["password_hash"] if user else None for line in sys.stdin: password = line.rstrip("\n") if target_hash and hash_password(password) == target_hash: sys.stdout.write("1\n") sys.stdout.flush() sys.exit(0) sys.stdout.write("0\n") sys.stdout.flush() sys.exit(1) signal.signal(signal.SIGINT, lambda _s, _f: (print("\nBye."), sys.exit(0))) login, user = authenticate() perms = user["permissions"] full_name = user["full_name"] log_action(login, "LOGIN") print(f"\nПривет, {full_name}") print(HELP_TEXT) while True: try: line = input(f"\n{login}> ").strip() except (EOFError, KeyboardInterrupt): log_action(login, "EXIT") print("\nBye.") break if not line: continue parts = line.split() command, args = parts[0], parts[1:] if command == "exit": log_action(login, "EXIT") print("Bye.") break elif command == "help": print(HELP_TEXT) elif command == "create": cmd_create(args, login, perms) elif command == "read": cmd_read(args, login, perms) elif command == "append": cmd_append(args, login, perms) elif command == "copy": cmd_copy(args, login, perms) elif command == "remove": cmd_remove(args, login, perms) else: print(f"Unknown command: {command!r}. Type 'help' for available commands.") if __name__ == "__main__": main()