Files
ipam_control/docs/reviews/2026-09-26-codebase-review-2.md
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

15 KiB
Raw Permalink Blame History

Повторный анализ кодовой базы IPAM Manager — 2026-09-26

Состояние: коммит 13e17fb (задачи 001–024). Объём: app/ и web/app.js — около 3 400 строк, миграции 0001–0008. Метод: чтение кода с упором на участки, которые изменения 010–024 затронули или усилили. Подозрительные места проверены на стенде ipam_control_006 на временных данных, после проверки данные удалены. Предыдущие отчёты: 2026-09-26-codebase-review.md (13 находок) и 2026-09-26-changes-011-023-review.md (7 находок).

Пометки: ✅ — воспроизведено на стенде, 🔎 — вывод по коду.

Итог

Все 13 находок первого ревью закрыты, остаточные замечания указаны ниже. Все 7 находок ревью изменений 011–023 закрыты изменением 024. Состояние системы:

  • alembic check без расхождений;
  • 14 тестов проходят;
  • контейнер работает от непривилегированного пользователя и в статусе healthy;
  • UI работает под CSP без ошибок в консоли.

Новый анализ выявил одну заметную ошибку: неверная ёмкость и загрузка частично разбитого префикса (№ 1). Её усиливает сценарий автовыделения подсетей из изменения 010. Кроме неё — один риск доступности (№ 2) и несколько низких замечаний.

# Серьёзность Область Кратко
1 Средняя Данные / отображение Ёмкость родителя равна сумме ёмкостей листьев: после выделения одного /30 у /24 ёмкость 254 → 2, загрузка 16 % → 100 % ✅
2 Средняя Доступность Анонимный клиент может держать администратора заблокированным: 5 запросов каждые 10 минут, верный пароль тоже отклоняется ✅
3 Низкая UI Экран «Префиксы» загружает не больше 1 000 префиксов и показывает число загруженных, а не total; дерево молча усекается 🔎
4 Низкая Тесты / эксплуатация Тесты работают с БД рабочего стенда: создают данные и сбрасывают настройки журнала на значения по умолчанию, а не на исходные 🔎
5 Низкая Конкурентность Дерево префиксов (parent_id) поддерживается только кодом: параллельное создание пересекающихся префиксов может оставить неверного родителя 🔎
6 Низкая API Явный parent_id при создании префикса может указать не самого узкого родителя, и дерево разойдётся с вложенностью CIDR 🔎
7 Низкая Безопасность Лимит в 20 попыток по IP не сериализуется между разными логинами (остаточное замечание 024) 🔎
8 Инфо API PATCH /organizations/{id} требует полное тело (OrgIn), частичное обновление даёт 422 🔎
9 Инфо Эксплуатация Быстрый старт в README (docker compose up) поднимает проект ipam_control, а рабочий стенд — ipam_control_006; легко перепутать поставки 🔎

Находки

1. Ёмкость и загрузка частично разбитого префикса ✅

_capacities (app/api/v1/prefixes.py) считает ёмкость родителя как сумму ёмкостей листьев, а used (_usage) — по всем адресам поддерева, включая адреса самого родителя вне дочерних префиксов. После изменения 010 («Добавить вложенный (авто)») частичное разбиение стало обычным сценарием, и расхождение проявляется сразу. Воспроизведение: 172.20.0.0/24 с 40 адресами — used 40 / capacity 254 / 16 %. После автовыделения одного /30 — used 40 / capacity 2 / 100 %. Экран адресов того же префикса по-прежнему показывает ёмкость 254. Та же причина искажает «Обзор»: assigned считается по всем адресам IPv4, а capacity — только по листьям. Адреса в нелистовых префиксах завышают загрузку. Рекомендация:

  • ёмкость любого префикса — размер его собственной подсети (capacity(prefix));
  • на «Обзоре» ёмкость — сумма по корневым префиксам (без родителя) каждого VRF;
  • used оставить как есть: считается по поддереву, дублей нет благодаря изменению 011.

Так числа станут согласованы с экраном адресов.

2. Блокировка входа администратора анонимным клиентом ✅

Лимит по логину (5 неудач за 10 минут, изменение 012) действует для всех IP. Верный пароль во время блокировки тоже отклоняется. Проверено при сверке 012: 429 и Retry-After на верный пароль. Любой, кто знает логин (например, admin из README), может отправлять 5 неверных попыток раз в 10 минут и держать администратора заблокированным. Риск был отмечен в плане 012; ниже вариант смягчения. Рекомендация:

  • блокировать по паре «логин + IP» (5 попыток), а общий по логину порог сделать выше (например, 50) и рассматривать его как сигнал в журнале;
  • альтернатива: не блокировать вход с IP, с которого этот пользователь успешно входил за последние N дней.

3. Усечение списка префиксов в UI 🔎

screens.prefixes и экран адресов запрашивают /prefixes?limit=1000, а заголовок, вкладки и подвал показывают all.length, то есть число загруженных префиксов, а не total. Родительский список в «Новом префиксе» и умолчание размера в «Добавить вложенный» тоже строятся по усечённому набору. Организация, у которой /22 разбит на /30 (256 префиксов), быстро выходит за 1 000. Рекомендация:

  • минимум: показывать total и предупреждение «загружено 1 000 из N»;
  • лучше: догружать страницы (offset) или получать дерево для конкретного VRF и ветки.

4. Тесты работают с данными стенда 🔎

tests/conftest.py работает с API и БД из .env, то есть со стендом ipam_control_006, где лежат демо-данные.

  • test_rotation_by_age_and_count в finally выставляет retention_days=90, max_entries=100000 вместо значений, которые были до теста. Пользовательские настройки ротации теряются.
  • Остатки неудачных прогонов (организации test-*) видны в UI.

Рекомендация:

  • отдельный compose-проект или БД для тестов (например, docker compose -p ipam_test с другим .env);
  • в тесте ротации сохранять и восстанавливать исходные настройки.

5. Дерево префиксов при параллельных изменениях 🔎

attach_to_tree определяет родителя и забирает вложенные префиксы по снимку данных в транзакции. Два параллельных запроса могут создать в одном VRF пересекающиеся префиксы разной длины (ручное создание .0/29 и автовыделение .4/30). Уникальность (vrf_id, prefix) это не ловит, и parent_id останется неверным. FOR UPDATE в allocate_subnet блокирует только строку родителя. Рекомендация: в create_prefix, allocate_subnet и _move_to_vrf брать pg_advisory_xact_lock по vrf_id: изменения дерева одного VRF выполняются по одному. Дешёвый и полный вариант.

6. Явный parent_id против вложенности CIDR 🔎

create_prefix с parent_id (keep_parent=True) проверяет только, что новый префикс входит в указанный родитель. Если существует более узкий префикс, который тоже содержит новый, дерево будет указывать на более широкого родителя. _capacities и _depths работают по parent_id, _usage — по вложенности CIDR, и эти расчёты разойдутся. Рекомендация: если указан не самый узкий родитель, возвращать 422 или игнорировать parent_id и вычислять родителя автоматически, как без параметра.

7. Лимит по IP между разными логинами 🔎

Сериализация из изменения 024 работает по логину. Параллельный перебор многих разных логинов с одного IP проходит проверку лимита по IP одновременно и может превысить 20 попыток на степень параллелизма. Рекомендация: вторая advisory-блокировка по hashtext(ip), брать её после блокировки логина, всегда в одном порядке.

8. PATCH организации требует полное тело 🔎

update_org принимает OrgIn (обязательные name и inn), поэтому PATCH с одним полем возвращает 422. UI отправляет полную форму, так что пользователи этого не замечают, но семантика PATCH нарушена. Рекомендация: схема OrgUpdate с необязательными полями и exclude_unset, как у остальных PATCH.

9. Две поставки стенда 🔎

В README быстрый старт использует docker compose up -d --build, то есть проект ipam_control: он собирает старые контейнеры ipam_control-* на тех же портах. Актуальный стенд поднят как docker compose -p ipam_control_006. Рекомендация: задать имя проекта в docker-compose.yml (name: ipam_control) и перенести стенд на него, либо описать в README, какой проект рабочий.


Статус находок первого ревью

№ Находка Статус
1 Один IP дважды в VRF ✅ закрыта (011); остаток: старые адреса сети/broadcast блокируют охватывающий префикс (на стенде 0)
2 Перебор паролей и вытеснение журнала ✅ закрыта (012, 024); остаток — № 2 и № 7 этого отчёта
3 500 на отрицательных limit/offset ✅ закрыта (013)
4 Ресурсоёмкий экран адресов ✅ закрыта (014)
5 Адрес сети/broadcast ✅ закрыта (015, 024)
6 Роль по умолчанию admin ✅ закрыта (016)
7 Встроенный JWT-секрет ✅ закрыта (017)
8 null в PATCH → ложный 409 ✅ закрыта (018)
9 N+1 ✅ в основном закрыта (019); «Обзор» и _prefix_outs по-прежнему читают всё дерево организации
10 Автоназначение и вложенные префиксы ✅ закрыта (020)
11 Гонки ✅ закрыта (021); новая гонка по дереву префиксов — № 5
12 Эксплуатация ✅ закрыта (022); APP_BIND по умолчанию 0.0.0.0 (сознательно)
13 Мелочи ✅ закрыта (023)

Предлагаемый порядок

  1. № 1 — ёмкость префиксов: небольшая правка в _capacities и overview, заметна пользователю сразу.
  2. № 2 и № 7 — политика блокировки входа.
  3. № 3 и № 5 — масштаб и целостность дерева префиксов.
  4. № 4 и № 9 — изоляция тестового окружения и порядок в поставках.
  5. № 6 и № 8 — по желанию.