Пентест (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>
11 KiB
Итог: исправление находок ревью изменения 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.downgrade0013 → 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).
- удаление организации — 204, записи журнала этой организации сохраняются с
Не проверено
- Живое переключение поля «Организация» в диалоге пользователя при смене роли в открытом select (№ 8) — проверено чтением кода, не проверено в браузере.
- Цвет бейджа
superadmin(класс.badge.purple, № 6) — проверено чтением CSS, не проверено визуально в браузере.