Files
ayurishchevandClaude Sonnet 5 a846d30872 IPAM Manager: API, UI-админка, журнал аудита
Backend (FastAPI, SQLAlchemy 2, Alembic, PostgreSQL 16):
- организации, VRF, префиксы (дерево, использование, автоназначение),
  адреса, операторы связи, устройства и типы устройств; JWT, роли admin/viewer;
- VRF принадлежит организации (составной FK), смена VRF у префикса
  переносит поддерево, имя VRF уникально в организации;
- журнал аудита: поиск и фильтры, ротация (срок/количество), очистка
  по паролю с блокировкой, IP клиента и метаданные запроса
  (X-Forwarded-For только от TRUSTED_PROXIES).

UI (web/, без сборки): экраны и диалоги по макетам «IPAM Manager»,
кликабельные строки реестров, локальные шрифты IBM Plex,
собственные выпадающие списки.

Окружение: docker-compose (postgres + app), миграции Alembic 0001-0004,
scripts/gen_env.py, scripts/seed_demo.py, 11 автотестов (pytest).
Документация: README.md и docs/changes/001-005 (планы и итоги).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 12:27:47 +03:00

11 KiB
Raw Permalink Blame History

Журнал: поиск по событиям, ротация, очистка и UI (изменение 003)

Context

Сейчас audit_log — сырая таблица (ts, username, entity_type, entity_id, entity_label, action, diff), экран «Журнал» — простая таблица без фильтров и без управления размером; журнал растёт бесконечно. В макетах канваса (страница «ROS Manager», листы Journal / JournalEntry / JournalSettings / JournalClear) уже нарисован целевой журнал: фильтры и поиск, окно записи, настройки ротации, очистка с паролем. Цель — реализовать это для IPAM (backend + UI), адаптировав поля под IPAM. Решения из блока «Журнал: вопросы для ревью» канваса берём как принятые: короткий ID 8 символов; «Показать ещё 100»; ротация по умолчанию 90 дней и 100 000 записей (0 — без ограничения); очистка по паролю пользователя, 5 неверных попыток за 10 минут → блокировка на 10 минут; настройки ротации сохраняются без пароля; строка таблицы целиком кликабельна и открывает окно записи; «Очистить журнал» — белая кнопка с красным текстом.

Модель данных (миграция 0003)

  • audit_log: +uid UUID (default gen_random_uuid(), уникальный; в UI evt_<uuid>, короткий вид — первые 8 символов), +message TEXT (человекочитаемое: «Префикс 10.30.0.0/24 создан», для изменений — с перечнем полей); бэкфилл существующих строк. Тип события = <entity_type>.<action> (вычисляется, отдельной колонки нет). Актор = username для system/anonymous, иначе ui:<username>.
  • app_settings(key PK, value JSONB) — ключ journal = {retention_days: 90, max_entries: 100000} (создаётся с дефолтами).
  • clear_attempts(id, user_id FK, ts) — неудачные попытки подтверждения пароля при очистке (счётчик и блокировка в БД, переживают рестарт).
  • Индексы: (entity_type, action); поиск по сообщению/ID — ILIKE (при лимите 100 000 записей достаточно; pg_trgm — при необходимости позже).

Backend

Новый app/api/v1/journal.py (перенос /audit из overview.py), app/rotation.py.

  • GET /audit — фильтры event_type, entity_type, actor, date_from, date_to (UTC, включительно), q (message / entity_label / начало uid), limit, offset; сортировка id DESC. Ответ: items[{uid, short_id, ts, event_type, entity_type, entity_id, entity_label, actor, message, diff}], total.
  • GET /audit/summary — total, oldest_ts, retention_days, max_entries (для строки «1 284 записи · старейшая … · ротация …»).
  • GET /audit/facets — списки значений для фильтров: типы событий, акторы, сущности.
  • GET /audit/{uid} — запись целиком (окно записи).
  • GET|PUT /journal/settings — чтение всем, запись только admin; валидация: целые ≥ 0 (дни ≤ 3650, записей ≤ 10 000 000); после сохранения — немедленная ротация.
  • POST /journal/clear {password} — только admin: проверка пароля текущего пользователя (argon2); неверный → 403 {attempts_left}; 5 неверных за 10 минут → 429 {retry_after_seconds}; успех: удалить все записи и в той же транзакции записать journal.cleared (кто, сколько удалено). Обработчик ошибок расширяется: detail может быть dict (доп. поля в тело ответа).
  • Ротация (rotate(db)): удаляет записи старше retention_days и самые старые сверх max_entries; при удалении >0 пишет journal.rotated (system, кол-во и причина). Запуск: фоновая задача раз в час (asyncio в lifespan, работа в потоке) и при сохранении настроек; защита от параллельного запуска в нескольких репликах — pg_try_advisory_lock.
  • Новые события в журнал: auth.login, auth.failed (anonymous), journal.settings_updated, journal.cleared, journal.rotated; services.audit() получает генерацию message и uid.
  • Обратная совместимость: поля /audit, используемые Обзором (entity_label, action, username, ts), сохраняются.

UI (web/app.js, web/styles.css) — по макетам Journal*

  • Экран «Журнал»: заголовок + строка «N записей · старейшая <дата> · ротация: 90 дней / 100 000 записей»; кнопки «Обновить», «Настройки», «Очистить журнал» (admin; для viewer скрыты/неактивны). Фильтры: «Тип», «Актор», «Сущность» (выпадающие списки в стиле меню — уже есть filterBtn), период «с … по …» (input type=date), поиск «Сообщение или ID» (debounce, как в остальных экранах). Таблица: Время (UTC, с секундами), Тип (бейдж), Сущность (тип · метка), Сообщение, Актор, ID записи (8 симв., моно); «Показать ещё 100», «Показано X из N записей».
  • Окно записи (640 px): ID с кнопкой «Копировать» (fallback через execCommand, т.к. по http navigator.clipboard недоступен), время, тип, актор, сущность, сообщение, блок «Данные» (JSON diff).
  • Окно «Настройки журнала» (560 px): текущее число записей и старейшая, поля «Хранить записи, дней» и «Максимум записей», пояснение о ротации.
  • Окно «Очистить журнал» (540 px): предупреждение, поле пароля, кнопка неактивна до ввода; состояния «Неверный пароль. Осталось попыток: N» и блокировка «Повторите через N мин.» (поле отключено).
  • Бейджи типов: created — синий, assigned — зелёный, updated — янтарный, deleted/auth.failed — красный, остальные — серый.

Порядок работ

  1. Скопировать этот план в docs/changes/003-journal-search-rotation/PLAN.md (требование проекта).
  2. Миграция 0003 + модели; бэкфилл message/uid.
  3. services.audit() (message, uid) и новые события (auth.*).
  4. journal.py: список/поиск/summary/facets/запись; перенос /audit.
  5. Настройки, ротация (rotation.py, фоновая задача, advisory lock), очистка с блокировкой.
  6. UI: экран, окна записи/настроек/очистки.
  7. Тесты, прогон сценария UI, сверка окон с макетами, README, SUMMARY.

Тесты (+3, всего 9; против контейнеров, свои учётные данные из .env)

  1. Поиск и фильтры: создать префикс → находится по q (метка, начало uid), по event_type, actor; date_from в будущем → пусто; /audit/{uid}.
  2. Ротация: запись с ts старше срока (вставка через БД по DSN из .env) и превышение max_entries удаляются при PUT /journal/settings; появляется journal.rotated; настройки в конце восстанавливаются к 90/100 000.
  3. Очистка: отдельный временный пользователь (вставка в БД, удаляется после теста, чтобы не блокировать admin): неверный пароль → 403 с attempts_left, 5 неверных → 429; успешную очистку в общей базе не гоняем — она проверяется вручную в сценарии UI на сбрасываемой базе.

Проверка end-to-end

docker compose up -d --build (миграции автоматически) → pytest → сценарий в браузере: создать/изменить объекты → найти событие по тексту, типу, актору и периоду → открыть запись и скопировать ID → сменить ротацию на малый лимит и увидеть усечение и запись journal.rotated → очистка: неверный пароль (счётчик попыток), блокировка, успешная очистка → в журнале одна запись journal.cleared; после этого база пересоздаётся и заполняется seed_demo.py. Сверка окон записи/настроек/очистки с макетами Journal* (отличия шапки и названий — из-за другого приложения, ROS Manager).

Допущения (скажите, если не так)

  • Запись journal.rotated создаётся только когда что-то удалено (иначе каждый час по записи).
  • Смена регистра/формата: поиск q без учёта регистра, подстрока в сообщении и метке, префикс для ID.
  • Записи auth.failed не ограничиваются отдельно; рост от подбора паролей сдерживает ротация.
  • Экспорт журнала (CSV) и права viewer на очистку/настройки не входят.