Files
ros_control/app/security.py
T

81 lines
3.0 KiB
Python
Raw Normal View History

import hmac
import threading
import time
from cryptography.fernet import Fernet
from fastapi import Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.config import get_settings
_bearer = HTTPBearer(auto_error=False)
def _fernet() -> Fernet:
key = get_settings().secret_key
if not key:
raise RuntimeError("SECRET_KEY не задан (ключ Fernet)")
return Fernet(key.encode())
def encrypt(value: str) -> str:
return _fernet().encrypt(value.encode()).decode()
def decrypt(token: str) -> str:
return _fernet().decrypt(token.encode()).decode()
def check_admin(user: str, password: str) -> bool:
s = get_settings()
ok_user = hmac.compare_digest(user.encode(), s.admin_user.encode())
ok_pass = hmac.compare_digest(password.encode(), s.admin_password.encode())
return ok_user and ok_pass
# --- подтверждение действий паролем (очистка журнала) и защита от перебора ---
FAIL_LIMIT, WINDOW_S, LOCK_S = 5, 600, 600 # 5 неверных за 10 минут → блокировка на 10 минут
_guard = threading.Lock()
_fails: dict[str, list[float]] = {}
_locked_until: dict[str, float] = {}
def verify_password(user: str, password: str) -> bool:
"""Пароль пользователя сессии (единственный пользователь — ADMIN_USER); сравнение за постоянное время."""
s = get_settings()
return (hmac.compare_digest(user.encode(), s.admin_user.encode())
and hmac.compare_digest(password.encode(), s.admin_password.encode()))
def lockout_remaining(user: str) -> int:
"""Сколько секунд осталось до конца блокировки (0 — не заблокирован)."""
with _guard:
return max(0, int(_locked_until.get(user, 0) - time.monotonic() + 0.999))
def register_failure(user: str) -> int:
"""Учитывает неверный пароль; возвращает число оставшихся попыток (0 — пользователь заблокирован)."""
now = time.monotonic()
with _guard:
recent = [t for t in _fails.get(user, []) if now - t < WINDOW_S] + [now]
_fails[user] = recent
if len(recent) >= FAIL_LIMIT:
_locked_until[user] = now + LOCK_S
_fails[user] = []
return 0
return FAIL_LIMIT - len(recent)
def reset_failures(user: str) -> None:
with _guard:
_fails.pop(user, None)
_locked_until.pop(user, None)
async def require_api_token(cred: HTTPAuthorizationCredentials | None = Depends(_bearer)) -> None:
expected = get_settings().api_token
if cred is None or not hmac.compare_digest(cred.credentials.encode(), expected.encode()):
raise HTTPException(status_code=401, detail="Invalid or missing API token")
from app.services import events # локальный импорт: security загружается раньше сервисов
events.set_actor("api")