Files
ipam_control/docs/changes/026-login-lockout-policy/PLAN.md
T
ayurishchevandClaude Opus 5.5 03d727e496 Задачи 025-030: ёмкость префиксов, политика входа, дерево префиксов
Повторный анализ кодовой базы (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>
2026-09-27 08:28:50 +03:00

35 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Политика блокировки входа без блокировки администратора анонимом (изменение 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` без расхождений.