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
50 lines
2.6 KiB
Python
50 lines
2.6 KiB
Python
"""Вход администратора под защитой от перебора (#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
|