-- 144_audit_log.sql -- #962 (EPIC18, ТЗ §19): аудит-лог чувствительных действий аналитика/пользователей. -- -- Пишется best-effort FastAPI-middleware (app/core/audit_middleware.py) ПОСЛЕ -- ответа на запрос для ключевых действий: -- POST /api/v1/parcels/{cad}/analyze → action='analyze' -- GET /api/v1/parcels/{cad}/forecast → action='forecast' -- GET /api/v1/parcels/{cad}/forecast/export → action='export' -- Прочие matched-нестандартные пути → action='other'. /health, static, -- non-matched пути НЕ аудируются. -- -- Best-effort: сбой записи в audit_log НИКОГДА не ломает основной запрос -- (middleware оборачивает write в try/except). -- -- Deploy: auto-applied deploy.yml через _schema_migrations (ровно один раз NN=144). -- Idempotent: CREATE TABLE/INDEX IF NOT EXISTS + guarded DO-блок для CHECK. BEGIN; -- ── 1. Таблица audit_log ────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS audit_log ( id BIGSERIAL PRIMARY KEY, username TEXT NOT NULL, -- из X-Authenticated-User role TEXT, -- get_role(username) на момент запроса action TEXT NOT NULL, -- analyze | forecast | export | other method TEXT NOT NULL, -- HTTP-метод (GET/POST/...) path TEXT NOT NULL, -- request.url.path cad_num TEXT, -- кадастровый номер из пути (NULL если нет) status_code INTEGER, -- HTTP-статус ответа created_at TIMESTAMPTZ NOT NULL DEFAULT now(), detail JSONB -- произвольный контекст (резерв) ); COMMENT ON TABLE audit_log IS 'Аудит чувствительных действий (#962, ТЗ §19). Пишется best-effort ' 'middleware app/core/audit_middleware.py ПОСЛЕ ответа на запрос. ' 'Сбой записи не влияет на основной запрос.'; COMMENT ON COLUMN audit_log.action IS 'analyze | forecast | export | other — выводится из request path в middleware.'; COMMENT ON COLUMN audit_log.cad_num IS 'Кадастровый номер из /parcels/{cad}/... (напр. 66:41:0204016:10), NULL если нет.'; COMMENT ON COLUMN audit_log.detail IS 'Резервное JSONB-поле под доп. контекст (пока не заполняется).'; -- ── 2. CHECK на допустимые action (guarded — idempotent при ре-apply) ──────── DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'ck_audit_log_action' ) THEN ALTER TABLE audit_log ADD CONSTRAINT ck_audit_log_action CHECK (action IN ('analyze', 'forecast', 'export', 'other')); END IF; END $$; -- ── 3. Индексы: lookup по юзеру во времени + по кадастру ────────────────────── CREATE INDEX IF NOT EXISTS idx_audit_log_username_created ON audit_log (username, created_at); CREATE INDEX IF NOT EXISTS idx_audit_log_cad_num ON audit_log (cad_num); COMMIT;