Код для lab2

This commit is contained in:
2026-03-03 10:21:13 +03:00
parent 07e02401eb
commit 60a4471a8c
11 changed files with 1050 additions and 0 deletions

1
lab2/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.pyc

1
lab2/.python-version Normal file
View File

@@ -0,0 +1 @@
3.14

99
lab2/README.md Normal file
View File

@@ -0,0 +1,99 @@
# Lab 2 — Authentication, Authorization & Brute Force Research
Система доступа к конфиденциальным данным с управлением пользователями и исследованием стойкости паролей.
## Структура каталогов
```
$PRACTICE2_DIR/ # по умолчанию /usr/local/practice2
├── etc/passwd # логин:sha256:id:права:ФИО
├── confdata/ # конфиденциальные файлы
├── bin/ # утилиты (usermgr, confaccess, bruteforce)
└── log/ # usermgr.log, access.log
```
Базовый каталог задаётся через переменную окружения `PRACTICE2_DIR`.
Если переменная не задана — используется `/usr/local/practice2`.
## Установка
```bash
chmod +x setup.sh
# для пути по умолчанию (/usr/local/practice2) нужен root:
sudo ./setup.sh
# для тестирования без root:
PRACTICE2_DIR=/tmp/practice2 ./setup.sh
```
Скрипт создаёт структуру каталогов, копирует утилиты в `bin/` и выставляет права доступа.
### Добавить bin во временный PATH
```bash
export PATH="/usr/local/practice2/bin:$PATH"
# или для тестовой директории:
export PATH="/tmp/practice2/bin:$PATH"
```
После этого утилиты доступны без полного пути:
```bash
usermgr add alice
confaccess
bruteforce alice
```
## Использование
### usermgr — управление пользователями
```bash
usermgr add alice # добавить пользователя (интерактивный ввод)
usermgr list # список пользователей
usermgr edit alice --permissions rw
usermgr edit alice --full-name "Иванов Иван"
usermgr passwd alice # сменить пароль
usermgr delete alice # удалить пользователя
```
Права: `r` — чтение, `w` — запись, `d` — удаление.
Требования к паролю: первый символ — буква (AZ, az), далее — буквы, цифры и `!@#$%^&*()`.
### confaccess — доступ к данным
```bash
confaccess
# или с явным указанием базовой директории:
PRACTICE2_DIR=/tmp/practice2 confaccess
```
После аутентификации доступны команды:
```
create <file> создать новый пустой файл в confdata [requires: w]
read <file> вывести содержимое файла [requires: r]
append <file> <text> дописать строку в файл [requires: w]
copy <src> <dst> скопировать файл в confdata [requires: r, w]
remove <file> удалить файл из confdata [requires: d]
help / exit
```
Пути к файлам указываются относительно `confdata/` (или абсолютные).
Для `copy`: src — любой путь, dst — внутри confdata, перезапись запрещена.
Выход — `exit` или Ctrl+C.
### bruteforce — взлом пароля
```bash
bruteforce alice
bruteforce alice --max-length 4
```
Алгоритм хэширования: **SHA-256**.
Перебор выполняется напрямую по хэшу из passwd-файла.
Фиксируется время перебора и количество итераций до нахождения пароля.
Перебор останавливается автоматически при достижении лимита 8 часов.

244
lab2/access.py Normal file
View File

@@ -0,0 +1,244 @@
#!/usr/bin/env python3
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 main() -> None:
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()

106
lab2/bruteforce.py Normal file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
import argparse
import hashlib
import itertools
import string
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from config import PASSWD_FILE
CHARSET = string.ascii_letters + string.digits + "!@#$%^&*()"
FIRST_CHARS = string.ascii_letters
MAX_HOURS = 8
def hash_password(password: str) -> str:
return hashlib.sha256(password.encode("ascii")).hexdigest()
def get_target_hash(login: str) -> str | None:
if not PASSWD_FILE.exists():
return None
with open(PASSWD_FILE) as f:
for line in f:
parts = line.strip().split(":", 4)
if len(parts) == 5 and parts[0] == login:
return parts[1]
return None
def max_combinations(length: int) -> int:
if length == 1:
return len(FIRST_CHARS)
return len(FIRST_CHARS) * (len(CHARSET) ** (length - 1))
def brute_force_length(target_hash: str, length: int) -> tuple[str, int, float] | None:
count = 0
start = time.perf_counter()
if length == 1:
for first in FIRST_CHARS:
count += 1
if hash_password(first) == target_hash:
return first, count, time.perf_counter() - start
return None
for first in FIRST_CHARS:
for rest in itertools.product(CHARSET, repeat=length - 1):
if time.perf_counter() - start > MAX_HOURS * 3600:
return None
count += 1
password = first + "".join(rest)
if hash_password(password) == target_hash:
return password, count, time.perf_counter() - start
return None
def main() -> None:
parser = argparse.ArgumentParser(description="Brute force password cracker (SHA-256)")
parser.add_argument("login", help="Target username")
parser.add_argument(
"--max-length",
type=int,
default=6,
help="Maximum password length to try (default: 6)",
)
args = parser.parse_args()
target_hash = get_target_hash(args.login)
if not target_hash:
print(f"User '{args.login}' not found in passwd file.")
return
print(f"Target: {args.login}")
print(f"Hash: {target_hash}")
print(f"Charset size: {len(CHARSET)} ({len(FIRST_CHARS)} valid for first char)")
print(f"Algorithm: SHA-256")
print()
for length in range(1, args.max_length + 1):
total = max_combinations(length)
print(f"Length {length}: max {total:>15,} combinations")
result = brute_force_length(target_hash, length)
if result is not None:
password, count, elapsed = result
print(f" >>> FOUND: '{password}'")
print(f" Iterations: {count:,}")
print(f" Time: {elapsed:.4f}s")
print(f" Speed: {count / elapsed:,.0f} hashes/s")
return
else:
print(f" Not found at length {length} (timeout or exhausted)")
print(f"\nPassword not found within length {args.max_length}.")
if __name__ == "__main__":
main()

11
lab2/config.py Normal file
View File

@@ -0,0 +1,11 @@
import os
from pathlib import Path
BASE_DIR = Path(os.environ.get("PRACTICE2_DIR", "/usr/local/practice2"))
ETC_DIR = BASE_DIR / "etc"
CONFDATA_DIR = BASE_DIR / "confdata"
BIN_DIR = BASE_DIR / "bin"
LOG_DIR = BASE_DIR / "log"
PASSWD_FILE = ETC_DIR / "passwd"

277
lab2/lab2.md Normal file
View File

@@ -0,0 +1,277 @@
# Практическая работа №2
по дисциплине «Защита информации»
**Тема работы:** «Разработка и исследование системы аутентификации и авторизации»
**Преподаватель:** Силиненко А.В.
**Email:** [a_silinenko@mail.ru](mailto:a_silinenko@mail.ru)
---
## 1. Цели работы
* Разработать систему доступа пользователей к конфиденциальным данным;
* Исследовать стойкость паролей к атаке методом грубой силы.
---
## 2. Задачи работы
### 2.1.
При необходимости установить на компьютер целевую ОС (Linux или MacOS), в которой производится разработка и использование системы.
### 2.2.
Разработать систему доступа пользователей к конфиденциальным данным, включающую:
* Выделенный каталог для хранения всех файлов системы;
* Утилиту для работы с данными аутентификации и авторизации (паролями и правами доступа);
* Утилиту доступа к конфиденциальным данным, обеспечивающую аутентификацию и авторизацию пользователя.
### 2.3.
Разработать программу взлома паролей методом грубой силы и исследовать стойкость паролей в зависимости от длины пароля и алгоритма хэширования.
---
## 3. Требования к работе
### 3.1. ОС
Работа выполняется в ОС **Linux** или **MacOS**.
Необходим доступ с правами суперпользователя.
Если доступа нет — установить гипервизор **VirtualBox** ([https://www.virtualbox.org/](https://www.virtualbox.org/)) и развернуть гостевую ОС.
Допускается установка ОС как второй системы без VirtualBox.
---
### 3.2. Структура каталогов
Необходимо создать дерево каталогов:
```
/usr/local/practice2/
├── etc # хранение данных аутентификации и авторизации
├── confdata # хранение конфиденциальных файлов
├── bin # разработанные утилиты
└── log # файлы регистрации
```
**Права доступа:** чтение, запись и выполнение только для пользователя `root`.
---
### 3.3. Требования к утилите управления пользователями
#### Файл хранения данных
Файл:
```
/usr/local/practice2/etc/passwd
```
Структура записи:
```
<логин>:<хэш_пароля>:<идентификатор>:<права>:<ФИО>
```
Одна строка — один пользователь.
**Права на файл:** чтение и запись только для `root`.
#### Права доступа
* `r` — чтение
* `w` — запись
* `d` — удаление
#### Требования к функционалу
Утилита должна обеспечивать:
* Добавление нового пользователя:
* логин
* ФИО
* права доступа
* пароль + подтверждение
* Проверку корректности данных
* Хранение пароля в виде **хэш-значения**
* Использование алгоритма хэширования согласно индивидуальному заданию
* Редактирование существующего пользователя
* Изменение пароля
* Удаление пользователя
* Регистрацию всех действий с файлом `passwd`
#### Требования к паролю
* Кодировка: ASCII
* Разрешены:
* AZ
* az
* 09
* `!@#$%^&*()`
* Первый символ не может быть цифрой или спецсимволом
---
### 3.4. Требования к утилите доступа к конфиденциальным данным
#### Авторизация
При запуске:
1. Запрос логина
2. Запрос пароля
3. Проверка корректности
Если данные корректны:
* Вывод: `Привет, <ФИО>`
* Краткая справка
* Приглашение к вводу команд
Если данные некорректны:
* Повторный запрос
Остановка — по сигналу **SIGINT (Ctrl+C)**.
---
### Поддерживаемые команды
| Команда | Описание | Требуемые права |
| -------- | ------------------------ | --------------- |
| `read` | Вывод содержимого файла | r |
| `append` | Добавление данных в файл | w |
| `copy` | Копирование файла | r + w |
| `remove` | Удаление файла | d |
| `exit` | Выход | — |
| `help` | Справка | — |
#### Ограничения copy
* Разрешено копирование:
* в каталог `confdata`
* внутри `confdata`
* Запрещено:
* из `confdata` в другие каталоги
* перезапись существующих файлов внутри `confdata`
#### Дополнительные требования
* Утилита доступна для запуска всем пользователям ОС
* Все действия с конфиденциальными данными логируются
---
### 3.5. Программа взлома паролей
Должна:
* Запускать утилиту доступа
* Перебирать пароли методом brute force
* Учитывать используемый алгоритм хэширования
* Фиксировать:
* время перебора
* количество итераций до взлома
---
### 3.6. Общие требования
* Язык программирования — любой
* Код должен быть снабжен комментариями
---
### 3.7. Требования к исследованию
Провести исследование для длин паролей:
```
3, 4, 5, 6, 7, 8 символов
```
Необходимо:
* Рассчитать максимальное количество итераций
* По экспериментам (34 символа) оценить время для 58
* Проверить теорию экспериментально
* Прервать эксперимент, если длительность > 8 часов
* Проводить тестирование без активных задач на ПК
---
## 4. Требования к отчету
В отчете необходимо указать:
* Актуальность темы
* Цель и задачи
* Требования к системе
* Характеристики ПК (процессор, память)
* ОС и среду разработки
* Используемый язык
* Алгоритм хэширования
* Примеры сборки (если применимо)
* Примеры работы утилит
* Пример расчета количества итераций и времени взлома
* Таблицу или график с результатами:
* рассчитанное количество итераций и время
* полученные экспериментальные данные
* Выводы
---
## 5. Справочная информация
### Поддержка алгоритмов хэширования
**Linux:**
* `md5sum`
* `sha1sum`
* `sha256sum`
* `sha512sum`
* `b2sum`
* библиотека `openssl`
**C++:**
* `std::hash`
**Python:**
* модуль `hashlib`
**Java:**
* `java.security.MessageDigest`
* Spring Security
**Go:**
* пакет `hash`

19
lab2/pyproject.toml Normal file
View File

@@ -0,0 +1,19 @@
[project]
name = "lab2"
version = "0.1.0"
description = "Authentication, authorization and brute force research system"
readme = "README.md"
requires-python = ">=3.14"
dependencies = []
[project.scripts]
usermgr = "usermgr:main"
access = "access:main"
bruteforce = "bruteforce:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
include = ["*.py"]

38
lab2/setup.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_DIR="${PRACTICE2_DIR:-/usr/local/practice2}"
echo "Setting up directory structure at: $BASE_DIR"
mkdir -p "$BASE_DIR/etc" "$BASE_DIR/confdata" "$BASE_DIR/bin" "$BASE_DIR/log"
touch "$BASE_DIR/etc/passwd"
cp "$SCRIPT_DIR/config.py" "$BASE_DIR/bin/config.py"
cp "$SCRIPT_DIR/usermgr.py" "$BASE_DIR/bin/usermgr"
cp "$SCRIPT_DIR/access.py" "$BASE_DIR/bin/confaccess"
cp "$SCRIPT_DIR/bruteforce.py" "$BASE_DIR/bin/bruteforce"
chmod +x "$BASE_DIR/bin/usermgr" "$BASE_DIR/bin/confaccess" "$BASE_DIR/bin/bruteforce"
if [ "$(id -u)" -eq 0 ]; then
chmod 700 "$BASE_DIR" "$BASE_DIR/etc" "$BASE_DIR/confdata" "$BASE_DIR/bin" "$BASE_DIR/log"
chmod 600 "$BASE_DIR/etc/passwd"
echo "Permissions set (root-only)."
else
echo "Warning: not running as root; skipping permission hardening."
fi
echo ""
echo "Done. Directory layout:"
ls -la "$BASE_DIR"
echo ""
echo "Next steps:"
echo " $BASE_DIR/bin/usermgr add <login>"
echo " $BASE_DIR/bin/confaccess"
echo " $BASE_DIR/bin/bruteforce <login>"
echo ""
echo "To add bin to PATH temporarily:"
echo " export PATH=\"$BASE_DIR/bin:\$PATH\""

246
lab2/usermgr.py Normal file
View File

@@ -0,0 +1,246 @@
#!/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()

8
lab2/uv.lock generated Normal file
View File

@@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.14"
[[package]]
name = "lab2"
version = "0.1.0"
source = { editable = "." }