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

117 lines
15 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.
# Повторный анализ кодовой базы 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 — по желанию.