gendesign/data/sql/144_audit_log.sql
Light1YT 86828d0388
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
feat(rbac): add analyst role + §19 audit-log middleware (#962)
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.
2026-06-08 12:16:19 +05:00

65 lines
3.6 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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;