Вход админа: защита от перебора (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:
@@ -417,3 +417,41 @@ def test_player_cannot_set_passwords_via_admin(client: TestClient):
|
||||
headers=csrf_headers(client),
|
||||
)
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
# ─── Защита от перебора пароля администратора (#56, F1) ───────────────────────
|
||||
|
||||
def test_admin_login_throttled_after_failures(client: TestClient, make_admin, monkeypatch):
|
||||
import app.core.ratelimit as ratelimit
|
||||
|
||||
now = [2000.0]
|
||||
monkeypatch.setattr(ratelimit.time, "monotonic", lambda: now[0])
|
||||
make_admin("boss", "secret123")
|
||||
|
||||
for _ in range(5):
|
||||
r = client.post("/api/admin/auth/login", json={"username": "boss", "password": "nope"})
|
||||
assert r.status_code == 401
|
||||
|
||||
blocked = client.post("/api/admin/auth/login", json={"username": "boss", "password": "secret123"})
|
||||
assert blocked.status_code == 429 # даже верный пароль не проверяется
|
||||
assert blocked.json()["error"]["code"] == "TOO_MANY_ATTEMPTS"
|
||||
|
||||
now[0] += 15 * 60 # окно истекло
|
||||
ok = client.post("/api/admin/auth/login", json={"username": "boss", "password": "secret123"})
|
||||
assert ok.status_code == 200
|
||||
|
||||
|
||||
def test_failed_admin_login_is_audited_without_password(client: TestClient, make_admin, engine):
|
||||
from app.models import AuditLog
|
||||
|
||||
make_admin("boss", "secret123")
|
||||
assert client.post(
|
||||
"/api/admin/auth/login", json={"username": "boss", "password": "nope-secret-guess"}
|
||||
).status_code == 401
|
||||
|
||||
with Session(engine) as s:
|
||||
logs = s.exec(select(AuditLog).where(AuditLog.action == "login_failed")).all()
|
||||
assert any(
|
||||
log.entity_type == "admin" and (log.payload or {}).get("username") == "boss" for log in logs
|
||||
)
|
||||
assert all("nope-secret-guess" not in str(log.payload) for log in logs)
|
||||
|
||||
Reference in New Issue
Block a user