Files
ipam_control/docs/changes/033-review-fixes-032/SUMMARY.md
T
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 Blame History

Итог: исправление находок ревью изменения 032 (изменение 033)

Источники: docs/reviews/2026-09-27-changes-032-review.md (находки № 1–8) и docs/reviews/2026-09-27-codebase-review.md (№ 9–15). Изменение 032 в репозиторий не закоммичено — 033 доводит его до готовности, коммитятся вместе.

Находки → что сделано → файлы

# Серьёзность Что сделано Файлы
1 Критическая FK audit_log.organization_id → organizations.id получил ondelete="SET NULL" (модель + новая миграция) app/models.py, alembic/versions/0013_audit_log_org_fk_set_null.py
2 Высокая UserIn: кросс-полевая проверка «роль — организация» переведена с @field_validator на @model_validator(mode="after") (срабатывает и когда поле не передано); из UserUpdate валидатор убран — без текущего состояния записи инвариант не проверить app/schemas.py
3 Высокая update_user: реконсиляция «роль — организация» по итоговому состоянию до apply_update — повышение до superadmin само очищает organization_id, понижение без organization_id → 422; userDialog: отправка organization_id только при роли ≠ superadmin (одинаково для create/edit) app/api/v1/users.py, web/app.js
4 Средняя refuse_delete получил параметр organization_id, проброшен в audit(); проставлен в delete_org, delete_vrf, delete_prefix (там, где организация объекта известна) app/services.py, app/api/v1/refs.py, app/api/v1/prefixes.py
5 Низкая screens.devices: безусловный btn("Добавить устройство", …) вместо мёртвого ветвления с неподдерживаемым disabled web/app.js
6 Низкая Добавлен модификатор .badge.purple для бейджа роли superadmin web/styles.css
7 Инфо Локальные from app.models import Role в require_org/scope_org убраны — используется верхнеуровневый импорт app/services.py
8 Низкая Поле «Организация» в userDialog реагирует на живой выбор роли в открытом диалоге (hidden переключается в обработчике dd-pick для name="role"), а не на исходную роль записи web/app.js
9 Высокая preview_subnet получил user: User = Depends(current_user) и require_org(user, p.organization_id, "Префикс") до _find_subnet app/api/v1/prefixes.py
10 Средняя create_user: проверка organization_id через get_or_404(Organization) до db.add; текст «Пользователь с таким логином уже существует» на flush/commit убран (логин уже проверен явным запросом), гонку покрывает общий CONFLICT_MSG; update_user — проверка организации по № 3 app/api/v1/users.py
11 Низкая Устранены оракулы «422 для чужого объекта / 404 для несуществующего»: create_prefix — require_org по организации тела до _lock_vrf, затем по организации VRF до проверки 422, затем по организации родительского префикса; _check_device — require_org до 422; update_isp — require_org по телу до get_or_404(Organization); update_prefix/_move_to_vrf — require_org по целевому VRF до 422. Для superadmin прежние 422 сохранены app/api/v1/prefixes.py, app/api/v1/refs.py
12 Низкая _type_outs: подсчёт устройств через scope_org(..., Device.organization_id, user); list_types получил user: User = Depends(current_user) app/api/v1/refs.py
13 Низкая CheckConstraint в модели получил name="ck_users_role_org_scope" — совпадает с именем в миграции 0011; отдельная миграция не нужна app/models.py
14 Инфо Решено: вариант A (см. ниже) README.md
15 Инфо README актуализирован: «Пользователи» только superadmin, формулировка «последний активный superadmin защищён…», раздел о связке «роль — организация», строка изменения 033 в истории README.md

Попутно (выявлено на проверке этапа 2)

Согласование рода в текстах 404: get_or_404/require_org формировали «{what} не найден» для любого what, из-за чего получалось «Организация не найден» (женский род). Добавлена функция not_found(what) в app/services.py со словарями исключений по первому слову (_FEMININE_FIRST_WORDS = {"Организация", "Запись"}, _NEUTER_FIRST_WORDS = {"Устройство"}), используется в get_or_404 и require_org.

Дополнительно (выявлено на ревью этапа 3)

При проверке № 11 обнаружен ещё один не закрытый оракул: create_prefix не проверял организацию родительского префикса (parent_id) — чужой parent_id давал 422 вместо 404. Закрыто той же схемой: require_org(user, parent.organization_id, "Родительский префикс") сразу после get_or_404(db, Prefix, parent_id, ...), до проверки принадлежности VRF/подсети. Файл: app/api/v1/prefixes.py.

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

Код не меняется. В README (раздел «Журнал аудита») явно описано: события организации показывают IP-адрес и клиент (User-Agent) исполнителя, включая случаи, когда действие выполнил superadmin. Отклонённый вариант B (скрывать client_ip/meta чужих исполнителей от админа/просмотрщика организации) потребовал бы также ограничивать фильтры client_ip/q в /audit — иначе IP восстанавливается подбором через фильтр — и признан избыточным для этого цикла.

Миграция

alembic/versions/0013_audit_log_org_fk_set_null.py — пересоздаёт fk_audit_log_organization_id_organizations с ondelete="SET NULL" (0012 уже применена на стенде, её саму не правим). downgrade возвращает FK без ondelete.

Тесты

  • tests/test_api.py::test_in_use_objects_cannot_be_deleted — дополнен проверкой, что отказ в удалении VRF (vrf.delete_blocked) виден в /audit от имени admin организации, у которой VRF отклонён (закрывает № 4).
  • tests/test_api.py::test_prefix_isolation_between_organizations — новый тест: admin организации B получает 404 на GET /prefixes/{id_A}, GET /prefixes/{id_A}/subnets/next (№ 9), POST /prefixes с чужим VRF и с чужим parent_id (№ 11).
  • tests/test_users.py::test_users_management — создание viewer теперь передаёт organization_id (фикстура org); добавлены проверки: role="admin" без organization_id → 422 (№ 2), PATCH {"role": "superadmin"} → 200 и organization_id = null, обратное понижение без organization_id → 422, с organization_id → 200 (№ 3).
  • tests/test_journal.py::test_clear_requires_password_and_locks_out — прямая SQL-вставка пользователя теперь с ролью 'superadmin' (очистка журнала доступна только ему после 032/033).

Проверки

  • pytest — 15 passed.
  • alembic check — чисто (расхождений нет), включая миграцию 0013.
  • downgrade 0013 → 0012 и обратный upgrade — выполнены без ошибок.
  • Сценарии на стенде:
    • удаление организации — 204, записи журнала этой организации сохраняются с organization_id = NULL (№ 1);
    • предпросмотр чужой подсети (GET /prefixes/{id}/subnets/next) от имени admin/viewer чужой организации — 404 (№ 9);
    • повышение admin → superadmin и понижение обратно — 200/422 по сценарию № 3;
    • PATCH/POST с несуществующей организацией — 404 (№ 10);
    • чужие VRF, устройство, parent_id, организация оператора — 404 для admin своей организации, прежние 422 сохранены для superadmin (№ 11);
    • счётчики типов устройств (devices_count) — в пределах организации вызывающего (№ 12);
    • *.delete_blocked виден admin организации в его собственном журнале (№ 4);
    • тексты 404 согласованы по роду («Организация не найдена», «Устройство не найдено» и т. д.) — попутная правка;
    • накопленные на стенде 18 организаций test-* удалены через API (стало возможно после № 1).

Не проверено

  • Живое переключение поля «Организация» в диалоге пользователя при смене роли в открытом select (№ 8) — проверено чтением кода, не проверено в браузере.
  • Цвет бейджа superadmin (класс .badge.purple, № 6) — проверено чтением CSS, не проверено визуально в браузере.