006 Пользователи: API /users (CRUD, смена своего пароля), раздел UI «Пользователи»,
события журнала user.*, защита от отключения/удаления себя и последнего админа.
007 Исправление удаления организации: VRF удаляются явным DELETE до организации
(без relationship() порядок DELETE не гарантирован → ложный 409).
008 Журнал фиксирует отказы в удалении (<entity>.delete_blocked) со списком
мешающих объектов в «Данных»: организация, VRF, тип устройства, префикс, пользователь.
009 Выбор строк чекбоксами и групповые операции в UI (удаление, смена типа устройств,
статус префиксов и адресов, доступ пользователей); цикл запросов из UI, итог и список отказов.
010 Автовыделение следующего вложенного префикса: POST/GET /prefixes/{id}/subnets/next,
первый свободный выровненный блок; пункт «Добавить вложенный (авто)» в меню префикса.
Тесты: 14 (добавлены сценарии для 006, 007/008, 010); исправлена нестабильность
тестов журнала (IPv6-группы с ведущими нулями нормализуются PostgreSQL).
Документация: README.md, docs/changes/006-010 (планы и итоги).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5.8 KiB
Журнал: события отклонённого удаления (изменение 008)
Context
Когда удаление запрещено бизнес-правилом (например, у организации есть префиксы/устройства/операторы), API отвечает 409 через raise HTTPException,
а запись в журнал не пишется: audit() вызывается только на успешном пути, сессия закрывается без commit (app/db.py:get_db).
Нужно фиксировать такие попытки как предупреждения и показывать в блоке «Данные» окна записи журнала, какие именно связанные сущности мешают удалению.
Решения
- Тип события:
<entity>.delete_blocked(напримерorganization.delete_blocked) — вписывается в существующую схемуentity_type+action, фильтры и facets журнала подхватывают его без изменений (app/api/v1/journal.pyразбираетevent_typeпо точке). - Охват: все отказы в удалении по бизнес-правилам (409): организация, VRF, тип устройства (в т.ч. «по умолчанию»), префикс с адресами, пользователь (свой аккаунт / последний администратор). Прочие 409/422 (дубли, валидация) не журналируются.
- Содержимое «Данных» — уже существующее поле
audit_log.diff(JSONB), миграция БД не нужна:В списке не более 20 элементов на группу,{"reason": "у организации есть префиксы, устройства или операторы", "blocked_by": {"prefixes": {"total": 13, "items": ["10.0.0.0/8 (default)", "…"]}, "devices": {"total": 2, "items": ["db-master.internal", "…"]}, "isps": {"total": 1, "items": ["Ростелеком"]}}}total— полное число. Пустые группы не выводятся. Для отказов без связанных объектов (свой аккаунт, тип по умолчанию, последний админ) — толькоreason. - Сообщение: «Организация ООО «X»: удаление отклонено — есть связанные объекты (префиксы: 13, устройства: 2, операторы: 1)».
Реализация
app/services.py: хелперrefuse_delete(db, user, entity_type, entity, label, reason, blocked_by=None)— пишетaudit(..., "delete_blocked", label, diff, message=...), делаетcommit(db)(в транзакции только запись журнала, других изменений к этому моменту нет) и поднимаетHTTPException(409, reason_text). Текст ответа API остаётся прежним. Хелпер_blockers(db, stmt, label_col, limit=20)—{"total", "items"}по запросу (переиспользуемcount()).- Вызовы вместо прямых
raise HTTPException(409, …)в:app/api/v1/refs.py:delete_org(префиксы:prefix (vrf), устройства:name, операторы:name),delete_vrf(префиксы),delete_type(устройства + случайis_default);app/api/v1/prefixes.py:delete_prefix(адреса, до 20 шт.);app/api/v1/users.py:delete_user(два отказа: свой аккаунт, единственный активный админ). Для организации проверка «занято» переписывается сor-цепочки на три подсчёта, чтобы собрать все группы сразу.
web/app.js:eventBadge— цветamberдляdelete_blocked; вactionBadgeобзора —delete_blocked: ["amber", "отклонено"]. Блок «Данные» (entryDialog) уже выводитdiffкак JSON — отдельная вёрстка не нужна._VERBSвmake_messageне трогаем: сообщение передаётся явно.
Артефакты (правила проекта)
docs/changes/008-blocked-delete-audit/PLAN.md (копия этого плана) и SUMMARY.md; README.md — в раздел «Журнал»: событие *.delete_blocked, состав «Данных».
Тест (минимум)
Расширить test_delete_organization в tests/test_api.py: после 409 на организации с префиксом найти в /audit?event_type=organization.delete_blocked&q=<имя>
запись, проверить diff.blocked_by.prefixes.total == 1 и наличие CIDR в items.
Проверка
docker compose -p ipam_control_006 up -d --build app(БД и прежняя поставка не затрагиваются);venv/bin/python -m pytest -q.- В UI (http://192.168.5.9:8088): «Организации» → «Удалить» у «ООО «Технологии связи»» → 409; в «Журнале» появилась запись
organization.delete_blocked(жёлтый бейдж), в окне записи блок «Данные» показывает списки префиксов/устройств/операторов; фильтр по типу события находит её. - Отказ на удаление VRF с префиксами и своего аккаунта — аналогичные записи.