Files
ipam_control/docs/changes/026-login-lockout-policy/SUMMARY.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

6.1 KiB
Raw Blame History

Итог: политика блокировки входа без блокировки администратора анонимом (изменение 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, настроенном на реальный прокси (не задан на стенде).