Повторный анализ кодовой базы (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>
42 lines
6.1 KiB
Markdown
42 lines
6.1 KiB
Markdown
# Итог: политика блокировки входа без блокировки администратора анонимом (изменение 026)
|
||
|
||
Выполнено вместе с изменением 027 (один файл `app/api/v1/auth.py`); отдельный SUMMARY для 027 — там же, с разницей только в advisory-lock по IP.
|
||
|
||
## Что сделано
|
||
- `app/api/v1/auth.py`:
|
||
- три области лимита за окно 10 минут: `login_ip` (5, эта пара логин+IP), `ip` (20, любые логины с этого IP — как раньше), `login` (50, этот логин со всех IP, **кроме известных**);
|
||
константы `MAX_PER_LOGIN_IP`, `MAX_PER_IP`, `MAX_PER_LOGIN`;
|
||
- `_is_known(db, login, ip)` — IP считается известным для логина, если есть строка в `known_logins` с `last_seen` не старше `KNOWN_IP_DAYS = 30`;
|
||
- `_retry_after` переписана: считает счётчики по всем трём областям, блокировка возвращается для первой пересечённой, но `scope == "login"` пропускается (не блокирует), если `known=True`;
|
||
`login_ip`/`ip` блокируют всегда;
|
||
- `_remember_login(db, login, ip)` — upsert в `known_logins` (`INSERT … ON CONFLICT (username, client_ip) DO UPDATE SET last_seen = now()`, через `sqlalchemy.dialects.postgresql.insert`),
|
||
вызывается при успешном входе;
|
||
- `session.locked`: `diff.scope` теперь одно из `login_ip`/`ip`/`login` (а не только `login`/`ip`, как в изменении 024), сообщение в журнале — по-русски описывает область.
|
||
- `app/models.py`: модель `KnownLogin` (`username`, `client_ip` — составной первичный ключ; `last_seen`).
|
||
- `alembic/versions/0009_known_logins.py`: таблица `known_logins`.
|
||
- `app/rotation.py`: `known_logins` с `last_seen` старше `KNOWN_IP_DAYS` удаляются вместе с остальной ротацией (импорт `KNOWN_IP_DAYS` из `auth.py`).
|
||
- `README.md`, раздел «Журнал» → абзац «Вход»: описаны три области, известные IP и защита администратора от анонимной блокировки.
|
||
|
||
## Отклонения от плана
|
||
Нет. Пороги и пространства имён — как в плане (значения из примера плана: `MAX_PER_LOGIN_IP=5`, `MAX_PER_IP=20`, `MAX_PER_LOGIN=50`, `KNOWN_IP_DAYS=30`).
|
||
|
||
## Как проверено
|
||
1. `venv/bin/python -c 'import app.main'` — без ошибок (нет циклического импорта `rotation.py` → `auth.py`, так как `auth.py` не импортирует `rotation`).
|
||
2. Стенд пересобран, `healthy`; миграция `0009` применена (`alembic_version = 0009`); `alembic check` — без расхождений.
|
||
3. Сценарий из плана (временный пользователь `rv3-locka`, скрипт в scratchpad):
|
||
- 5 неверных попыток логина с одного IP (докер-шлюз, виден серверу как `172.31.0.1`) → 4×401, 5-я попытка сама пересекает порог `login_ip` и получает 429 (как и раньше для одиночного порога);
|
||
верный пароль с того же IP сразу после — тоже 429 (область `login_ip` активна);
|
||
- верный пароль с **другого** реального IP (127.0.0.1, изнутри контейнера приложения) для того же логина — **200**: анонимный клиент с одного IP не блокирует пользователя с другого. **PASS.**
|
||
4. Сценарий «перебор с разных IP» (план: «эмуляция через `TRUSTED_PROXIES` недоступна на стенде → проверка … прямыми вставками в `login_attempts`»), временный пользователь `rv3-lockb`:
|
||
- успешный вход с IP A (докер-шлюз) регистрирует его как известный;
|
||
- 55 неудачных попыток вставлены напрямую в `login_attempts` с 55 разными фиктивными IP (`203.0.113.1..55`) для этого логина;
|
||
- вход с верным паролем с нового, никогда не виденного IP (127.0.0.1 изнутри контейнера) → **429** (область `login`, IP не известен);
|
||
- вход с верным паролем с известного IP A → **200** (область `login` не применяется к известному IP; счётчик пересобирается заново после того, как предыдущая проверка его не сбросила). **PASS.**
|
||
5. `login_attempts` очищена после проверок (`delete from login_attempts`). Временные пользователи `rv3-locka`/`rv3-lockb` удалены. Осталась одна легитимная строка `known_logins`
|
||
(`admin`, IP докер-шлюза) — образовалась от обычных входов администратора при тестировании, не «rv3-» тестовые данные, не удалялась.
|
||
|
||
## Не проверено
|
||
- Реальный перебор с физически разных IP-адресов (сеть стенда не даёт больше двух различимых реальных адресов) — область `login` проверена вставкой записей напрямую в `login_attempts`,
|
||
как и предусмотрено планом.
|
||
- Поведение при `TRUSTED_PROXIES`, настроенном на реальный прокси (не задан на стенде).
|