Повторный анализ кодовой базы (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>
15 KiB
Повторный анализ кодовой базы 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 — ёмкость префиксов: небольшая правка в
_capacitiesиoverview, заметна пользователю сразу. - № 2 и № 7 — политика блокировки входа.
- № 3 и № 5 — масштаб и целостность дерева префиксов.
- № 4 и № 9 — изоляция тестового окружения и порядок в поставках.
- № 6 и № 8 — по желанию.