279 lines
7.9 KiB
Python
279 lines
7.9 KiB
Python
#!/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 <file> create new empty file [requires: w]
|
|
read <file> print file contents [requires: r]
|
|
append <file> <text> append text line to file [requires: w]
|
|
copy <src> <dst> copy file into confdata [requires: r, w]
|
|
remove <file> 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 <file>")
|
|
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 <file>")
|
|
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 <file> <text>")
|
|
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 <src> <dst>")
|
|
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 <file>")
|
|
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()
|