Пентест (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>
78 lines
11 KiB
Markdown
78 lines
11 KiB
Markdown
# Итог: исправление находок ревью изменения 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, не проверено визуально в браузере.
|