Оба места утверждали, что у Домклика один выделенный резидентный прокси и пула нет. Это перестало быть правдой ещё в #2800 (миграция 253 сняла резервацию узла), но текст остался — и именно на него опирался тикет #3189, поставленный под «калибровку» ограничения, которого не существует. Свип к тому же ходит через пул давно (serp.py:359), а бэкфилл подключён к нему в PR #3222. Заодно докстринг называл не тот ограничитель: свип кладёт не счётчик провалов lease, а break по первому DomClickBlockedError (#2854) — до ротации дело не доходит ни при каком счётчике, отсюда buckets_completed=0. Только комментарии, поведение не меняется. Миграция 175 уже применена, а _schema_migrations трекает по имени файла без checksum — правка текста её не перезапустит.
65 lines
3.6 KiB
PL/PgSQL
65 lines
3.6 KiB
PL/PgSQL
-- 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;
|