77 lines
11 KiB
Markdown
77 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, не проверено визуально в браузере.
|