Files
ipam_control/docs/reviews/2026-09-27-codebase-review.md
ayurishchevandClaude Opus 5.5 744a025960 Задачи 032-033: ролевая модель с привязкой к организации, исправления по ревью
Пентест (docs/reviews/2026-09-27-pentest.md) и план 031 (Swagger, TLS) — план, не реализован.
032 Роль superadmin (без организации) и привязка admin/viewer к одной организации:
    users.organization_id + CHECK, audit_log.organization_id (миграции 0010-0012);
    require_org/scope_org во всех чтениях и записях, журнал и «Обзор» в границах
    организации; пользователи, организации, типы устройств, настройки журнала — только superadmin.
033 Исправление находок ревью 032 (docs/reviews/2026-09-27-changes-032-review.md,
    docs/reviews/2026-09-27-codebase-review.md):
    - FK audit_log.organization_id ON DELETE SET NULL (миграция 0013) — удаление организаций;
    - проверка организации в предпросмотре подсети;
    - инвариант «роль — организация» по итоговому состоянию (повышение снимает организацию,
      понижение требует её), 422/404 вместо обезличенных 409;
    - одинаковый 404 для чужих и несуществующих объектов (VRF, устройство, parent_id, оператор);
    - отказы удаления в журнале организации, счётчики типов в пределах организации;
    - UI: живое поле «Организация» в диалоге пользователя, бейдж superadmin; род в текстах 404.
README актуализирован под ролевую модель.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:26:57 +03:00

11 KiB
Raw Permalink Blame History

Ревью кодовой базы — 2026-09-27

Объём: рабочее дерево поверх 5210ba3 (незакоммиченные изменения 031–032, миграции 0010–0012). Метод: сверка с docs/reviews/2026-09-27-changes-032-review.md → построчное чтение app/, alembic/versions/0010–0012, diff web/app.js → воспроизведение на стенде ipam_control_006 (API + SQL) → pytest. Временные данные удалены.

Итог

Находки ревью 032 не исправлены, все подтверждены повторно (pytest: 3 failed / 11 passed — те же три теста). Собственное ревью добавляет одну уязвимость изоляции организаций (№ 9, подтверждена на стенде) и несколько мелких замечаний. Изменение 031 существует только как план — в коде не реализовано.

Рекомендация: не коммитить 032 до исправления № 1–4 и № 9.

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

# Серьёзность Статус Проверка
1 Критическая Не исправлено FK audit_log.organization_id без ON DELETE SET NULL (модель + 0012). Уточнение: одного SET NULL по существующим строкам недостаточно — delete_org сам пишет audit(..., organization_id=id) в той же транзакции перед DELETE; на стенде даже после ручного обнуления — 409. Нужен именно ondelete="SET NULL"
2 Высокая Не исправлено UserIn(role="admin") без ключа organization_id проходит валидацию (проверено в venv); явный null — отклоняется
3 Высокая Не исправлено update_user не реконсилирует organization_id при смене роли
4 Средняя Не исправлено refuse_delete без organization_id
5–8 Низкая/инфо Не исправлено Без изменений

Побочный эффект № 1: на стенде накоплено 18 организаций test-* — фикстура org не может их удалить. После исправления — почистить.

Новые находки

# Серьёзность Файл Кратко
9 Высокая app/api/v1/prefixes.py:286 GET /prefixes/{id}/subnets/next без require_org — чтение чужой организации
10 Средняя app/api/v1/users.py, app/services.py Несуществующий organization_id / нарушение CHECK → 409 «логин уже существует» или обезличенный 409
11 Низкая prefixes.py:241,355, refs.py:354-355 Различимые ответы 404/422 раскрывают существование объектов чужой организации
12 Низкая app/api/v1/refs.py:174 /device-types отдаёт devices_count по всем организациям
13 Низкая app/models.py, 0011 CheckConstraint в модели без имени, в миграции — ck_users_role_org_scope
14 Инфо app/api/v1/journal.py Админ организации видит IP и User-Agent суперадминистратора в событиях своей организации
15 Инфо README.md Устаревшие/неточные формулировки

9. Утечка данных чужой организации через предпросмотр подсети — ВЫСОКАЯ

preview_subnet не принимает user и не вызывает require_org — единственный GET по id префикса, пропущенный в 032. Воспроизведено: viewer организации B: GET /prefixes/{id_A} → 404 (изоляция), GET /prefixes/{id_A}/subnets/next?length=24 → 200 {"prefix":"10.77.0.0/24","length_min":17,"length_max":32}; несуществующий id → 404. Раскрывается: существование префикса, его длина (length_min − 1), занятость (перебором length восстанавливается раскладка подсетей). Исправление: user: User = Depends(current_user) + require_org(user, p.organization_id, "Префикс").

10. Ошибки целостности маскируются под «логин уже существует»

create_user оборачивает flush/commit текстом «Пользователь с таким логином уже существует» — им же подписываются нарушение FK (organization_id несуществующей организации) и CHECK (№ 2). update_user при тех же причинах отдаёт общий CONFLICT_MSG. Отдельно: повторная активация мигрированного viewer (organization_id=NULL) через «Разрешить доступ» (в т. ч. групповое) → непонятный 409. Исправление: в create_user/update_user — get_or_404(Organization, organization_id) до записи; проверку инварианта роль/организация делать по итоговому состоянию (роль, организация, активность) с 422 и понятным текстом — это же закрывает № 2–3.

11. Оракул существования объектов чужой организации

Для чужих объектов ответ отличается от несуществующих: create_prefix — 422 «VRF принадлежит другой организации» против 404; _check_device — 422 против 404; update_isp — get_or_404(Organization) до require_org (разные тексты 404). Также create_prefix берёт advisory-lock чужого VRF до require_org. Противоречит принципу 032 «404, не подтверждать существование». Исправление: для не-superadmin отвечать 404 при чужой организации (или вызывать require_org по организации объекта до остальных проверок).

12. Глобальные счётчики типов устройств

list_types считает устройства по всем организациям — admin/viewer видит объём чужих данных (на стенде видно devices_count=1 у типа без устройств своей организации). Решение 2 плана относится к справочнику, не к счётчикам. Исправление: scope_org в подсчёте _type_outs.

13. Имя CHECK-ограничения расходится

Модель: CheckConstraint(...) без name; миграция: ck_users_role_org_scope. alembic check CHECK не сравнивает, поэтому расхождение не видно, но create_all (и любая будущая автогенерация/drop) получит другое имя. Исправление: name="ck_users_role_org_scope" в модели.

14. Метаданные запросов суперадминистратора в журнале организации

События, выполненные superadmin в организации, несут client_ip и meta (User-Agent, путь) и видны админу/просмотрщику этой организации. Не баг плана, но стоит решить осознанно (например, скрывать client_ip/meta чужих акторов для не-superadmin).

15. README

  • Строка 135: «Пользователи» (только admin) → только superadmin.
  • Строка 96: «защищена от отключения/удаления при наличии других активных superadmin» — смысл обратный: защищён последний активный.
  • Нет раздела о 031 (Swagger, TLS-профиль) — ожидаемо, изменение не реализовано.

Прочее

  • 031: в docs/changes/031-expose-hardening/ только PLAN.md; docs_enabled, app_bind, Caddyfile, профиль tls отсутствуют — находки пентеста № 1–2 открыты.
  • Артефакты 032: нет итогового файла (суммаризации) — ожидаемо до завершения цикла правок.
  • Тесты: после исправлений обновить test_users (явный organization_id) и прямую SQL-вставку в test_journal; добавить один тест изоляции (viewer чужой организации → 404 на GET /prefixes/{id} и /subnets/next) — закрывает № 9 от регрессии.

Что проверено без замечаний

  • Все мутации (VRF, префиксы, адреса, устройства, операторы, организации) проверяют организацию до изменения; *Update-схемы не принимают organization_id — перенос объектов между организациями невозможен; перенос префикса между VRF проверяет организацию целевого VRF.
  • current_user читает роль и организацию из БД на каждый запрос — смена роли/организации действует сразу, без перевыпуска токена.
  • Журнал: list/summary/facets/get фильтруются по организации; settings/clear — только superadmin.
  • UI: loadOrgs сбрасывает сохранённую в localStorage организацию, если она недоступна пользователю.
  • Миграции 0010 (enum в autocommit_block) и 0011 (CHECK после backfill) — корректный порядок.

Порядок исправлений

  1. № 1 (ondelete="SET NULL" в модели и новой миграции 0013 — 0012 уже применена на стенде), № 9.
  2. № 2, 3, 10 — единая проверка инварианта по итоговому состоянию в users.py + правка userDialog (№ 3, 8).
  3. № 4, 11, 12, 13.
  4. № 5–7, 15; обновление тестов; очистка организаций test-* на стенде.