Вход админа: защита от перебора (throttle)
admin_login проходит через LoginThrottle (пара IP+логин, IP и сам аккаунт), как вход игрока; исчерпание лимита -> 429. Неудачные попытки пишутся в аудит (без пароля). Пароль админа — прямой путь к полному контролю, лимиты строже. #56 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
"""Вход администратора под защитой от перебора (#56).
|
||||
|
||||
Тонкий слой поверх `admin_service.authenticate_admin`: throttle по IP, по паре «IP + логин»
|
||||
и по самому аккаунту через тот же `LoginThrottle`, что и вход игрока (`core/ratelimit`).
|
||||
Сервис остаётся чистым от инфраструктуры лимитов. Пароль администратора — единственный
|
||||
барьер к полному контролю приложения, поэтому перебор здесь ограничиваем строже игроцкого.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.core.errors import InvalidCredentialsError
|
||||
from app.core.ratelimit import login_throttle
|
||||
from app.models import User
|
||||
from app.services import admin_service
|
||||
|
||||
# Неудач за окно LoginThrottle (15 минут):
|
||||
_PAIR_LIMIT = 5 # на пару «IP + логин» — против перебора пароля с одного адреса
|
||||
_IP_LIMIT = 20 # на IP — против перебора по разным логинам с одного адреса
|
||||
# На сам аккаунт (IP-независимо, #60): распределённый перебор с ротацией IP всё равно
|
||||
# упирается в этот предел. Щедрее пары, чтобы случайный поток ошибок не запирал вход
|
||||
# админа насовсем (лимит на аккаунт — вектор lockout-DoS, поэтому не слишком строгий).
|
||||
_ACCOUNT_LIMIT = 50
|
||||
|
||||
|
||||
def _keys(ip: str, username: str) -> dict[str, int]:
|
||||
pair_key = f"admin-login:{ip}:{username.casefold()}"
|
||||
return {
|
||||
pair_key: _PAIR_LIMIT,
|
||||
f"admin-login-ip:{ip}": _IP_LIMIT,
|
||||
f"admin-login-user:{username.casefold()}": _ACCOUNT_LIMIT,
|
||||
}
|
||||
|
||||
|
||||
def login_admin(session: Session, username: str, password: str, ip: str | None) -> User:
|
||||
"""authenticate_admin под защитой от перебора. Сессию открывает вызывающий."""
|
||||
username = (username or "").strip()
|
||||
ip = ip or "unknown"
|
||||
limits = _keys(ip, username)
|
||||
login_throttle.check(limits)
|
||||
try:
|
||||
admin = admin_service.authenticate_admin(session, username, password)
|
||||
except InvalidCredentialsError:
|
||||
login_throttle.fail(limits)
|
||||
raise
|
||||
# Успех: снимаем счётчики этого аккаунта, чтобы законный вход не копил лимит.
|
||||
for key in limits:
|
||||
login_throttle.reset(key)
|
||||
return admin
|
||||
Reference in New Issue
Block a user