Some checks failed
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy Trade-In / changes (push) Successful in 7s
Deploy / changes (push) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 36s
Deploy / build-backend (push) Successful in 1m39s
Deploy / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 48s
Deploy / build-worker (push) Successful in 2m41s
Deploy / deploy (push) Failing after 4s
Deploy Trade-In / deploy (push) Successful in 47s
CI / backend-tests (push) Successful in 6m38s
CI / backend-tests (pull_request) Successful in 6m30s
EPIC18/§19. analyst sees everything (deals, insights, exports, site-finder, analytics, concept) EXCEPT admin/data-management. Enforcement is backend-hard (the existing rbac_guard already 403s any non-admin role on /api/v1/admin/*, so adding the role auto-blocks it) + frontend (deny_paths via /me) + audit. §19 audit: new best-effort HTTP middleware logs the sensitive actions (analyze / forecast / forecast-export) to a new audit_log table after the response. Audit failures never break or delay-fail the request (2-layer try/except + finally close). Registered INNER to rbac_guard so only authorized requests are audited (a 403 short-circuits before audit). classify_path matches export before forecast (anchored). - auth/roles.yaml: analyst role (paths /**, deny admin-mgmt) + analysttest QA user - core/auth.py (+ tradein mirror): Role Literal += analyst - core/audit_middleware.py (new) + main.py registration - data/sql/144_audit_log.sql (idempotent; auto-applies) - tests: analyst rbac (403 admin / 200 parcels) + 11 audit cases No data-level ACL (analyst sees data per policy) -> no #948 dependency. No new deps. parcels.py untouched. Real analyst logins still need adding to caddy/users.caddy.snippet (devops). Refs #962.
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;
|