Files
ipam_control/docs/changes/008-blocked-delete-audit/PLAN.md
T
ayurishchevandClaude Sonnet 5 cd09ef0805 Задачи 006-010: пользователи, исправление удаления, журнал отказов, групповые операции, автовыделение префиксов
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>
2026-09-26 13:25:33 +03:00

5.8 KiB
Raw Blame History

Журнал: события отклонённого удаления (изменение 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), миграция БД не нужна:
    {"reason": "у организации есть префиксы, устройства или операторы",
     "blocked_by": {"prefixes": {"total": 13, "items": ["10.0.0.0/8 (default)", "…"]},
                    "devices":  {"total": 2, "items": ["db-master.internal", "…"]},
                    "isps":     {"total": 1, "items": ["Ростелеком"]}}}
    
    В списке не более 20 элементов на группу, total — полное число. Пустые группы не выводятся. Для отказов без связанных объектов (свой аккаунт, тип по умолчанию, последний админ) — только reason.
  • Сообщение: «Организация ООО «X»: удаление отклонено — есть связанные объекты (префиксы: 13, устройства: 2, операторы: 1)».

Реализация

  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()).
  2. Вызовы вместо прямых 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-цепочки на три подсчёта, чтобы собрать все группы сразу.
  3. web/app.js: eventBadge — цвет amber для delete_blocked; в actionBadge обзора — delete_blocked: ["amber", "отклонено"]. Блок «Данные» (entryDialog) уже выводит diff как JSON — отдельная вёрстка не нужна.
  4. _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.

Проверка

  1. docker compose -p ipam_control_006 up -d --build app (БД и прежняя поставка не затрагиваются); venv/bin/python -m pytest -q.
  2. В UI (http://192.168.5.9:8088): «Организации» → «Удалить» у «ООО «Технологии связи»» → 409; в «Журнале» появилась запись organization.delete_blocked (жёлтый бейдж), в окне записи блок «Данные» показывает списки префиксов/устройств/операторов; фильтр по типу события находит её.
  3. Отказ на удаление VRF с префиксами и своего аккаунта — аналогичные записи.