Повторный анализ кодовой базы (docs/reviews/2026-09-26-codebase-review-2.md) и доработки:
025 Ёмкость префикса — размер его подсети (а не сумма листьев); «Обзор» считает ёмкость
по корневым активным IPv4-префиксам и адреса внутри них.
026 Политика блокировки входа: 5 неудач на логин+IP, 20 на IP, 50 на логин со всех IP,
кроме известных IP (known_logins, миграция 0009) — владельца нельзя заблокировать анонимно.
027 Сериализация попыток входа по IP (advisory-lock после блокировки логина).
028 UI «Префиксы»: загрузка всех страниц (до 20 000), счётчики по total, предупреждение об усечении.
029 Advisory-lock по VRF для операций, меняющих дерево префиксов и раскладку адресов.
030 Исправление замечаний ревью 025-029: _lock_prefix (VRF блокируется до чтения префикса,
409 при одновременном переносе), константы политики входа перенесены в app/services.py.
Тесты: 14 passed (проверка ёмкости родителя приведена к семантике 025); сквозные сценарии
и гонки — docs/reviews/2026-09-26-changes-025-029-review.md, 2026-09-27-changes-030-review.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
35 lines
4.2 KiB
Markdown
35 lines
4.2 KiB
Markdown
# Политика блокировки входа без блокировки администратора анонимом (изменение 026)
|
||
|
||
Находка № 2 из `docs/reviews/2026-09-26-codebase-review-2.md`, серьёзность — средняя.
|
||
|
||
## Context
|
||
Лимит по логину (5 неудач за 10 минут, изменение 012) действует для всех IP, и во время блокировки отклоняется и верный пароль.
|
||
Любой, кто знает логин (например, `admin`), может отправлять 5 неверных попыток раз в 10 минут и держать учётную запись заблокированной.
|
||
|
||
## Решение (`app/api/v1/auth.py`, `app/models.py`, миграция)
|
||
1. **Три области лимита** (окно 10 минут, блокировка — окно после последней неудачи, как сейчас):
|
||
| Область | Порог | Кого блокирует |
|
||
|---|---|---|
|
||
| логин + IP | 5 | этот логин с этого IP |
|
||
| IP | 20 | любые логины с этого IP (как сейчас) |
|
||
| логин (все IP) | 50 | этот логин со всех IP, **кроме «известных» IP** |
|
||
Константы `MAX_PER_LOGIN_IP = 5`, `MAX_PER_IP = 20`, `MAX_PER_LOGIN = 50` в `auth.py`.
|
||
2. **Известные IP:** таблица `known_logins (username, client_ip, last_seen)` с первичным ключом `(username, client_ip)` — новая миграция `0009` и модель `KnownLogin`.
|
||
При успешном входе — upsert (`INSERT … ON CONFLICT (username, client_ip) DO UPDATE SET last_seen = now()`). IP считается известным, если `last_seen` не старше 30 дней (`KNOWN_IP_DAYS = 30`).
|
||
Блокировка по области «логин» не применяется к запросам с известного IP. Области «логин + IP» и «IP» действуют всегда.
|
||
Записи старше 30 дней удалять в часовом цикле ротации (`app/rotation.py`, рядом с очисткой `login_attempts`).
|
||
3. `_retry_after` возвращает `(секунды, область, счётчики)`, где область — `login_ip` | `ip` | `login`, счётчики — по каждой области. Запись `session.failed` — по-прежнему только первая неудача
|
||
этого логина в окне (счётчик по логину во всех IP). `session.locked` — один раз при первом пересечении порога любой областью, в `diff` — `scope` (`login_ip`/`ip`/`login`), `attempts`, `retry_after_seconds`,
|
||
для `ip` — `distinct_logins` (как сейчас).
|
||
4. Сериализацию попыток по логину (`pg_advisory_xact_lock(hashtext(name))`, изменение 024) и выравнивание времени ответа сохранить.
|
||
5. Успешный вход удаляет неудачи этого логина (по всем IP), как сейчас.
|
||
6. `README.md`, раздел «Журнал» (абзац «Вход»): новые пороги, известные IP, почему анонимный клиент не может заблокировать вход с рабочего места пользователя.
|
||
|
||
## Файлы
|
||
`app/api/v1/auth.py`, `app/models.py`, `alembic/versions/0009_known_logins.py`, `app/rotation.py`, `README.md`.
|
||
|
||
## Проверка
|
||
- 5 неверных попыток логина с IP A → 429 для A; верный пароль с IP B, с которого этот пользователь входил ранее, — 200.
|
||
- 50 неверных попыток логина с разных IP (эмуляция через `TRUSTED_PROXIES` недоступна на стенде → проверка по коду или прямыми вставками в `login_attempts`) → вход с нового IP 429, с известного — 200.
|
||
- `alembic check` без расхождений.
|