feat(audit): partitioning 7 audit-таблиц по месяцам (hole #2 Phase A)
Закрывает последнюю дыру #2 аудита журналирования. Phase A (dev) — миграция схемы + retention tooling. Phase B (прод-rewrite через SQL под postgres) — отдельным шагом с явным approve. Решения заказчика: * Scope: все 7 таблиц (auth_log, activity_log, tenant_operations_log, webhook_log, balance_transactions, pd_processing_log, saas_admin_audit_log) * FK на webhook_log: W1 — удалить FK от failed_webhook_jobs+rejected_deals_log * Retention defaults: auth:24м, activity:36м, tenant_ops:24м, webhook:3м, balance:84м, pd:36м, saas_admin:84м. Cron Sundays 03:00 МСК * Hash-chain: per-partition (audit_chain_hash трг через TG_TABLE_NAME уже работает per-partition; совместимо с hole #1 per-RLS-scope fix) Phase A: * db/schema.sql v8.30→v8.31: 7 audit-таблиц на PARTITION BY RANGE, PK→(id, partition_key), +7 retention seeds в system_settings, FK от failed_webhook_jobs/rejected_deals_log удалены * MonthlyPartitionManager: PARTITIONED_TABLES → ассоциативный array (name => partition_key), 2 → 9 таблиц * PartitionsCreateMonths: автоматически покрывает все 9 * load_initial_schema: после schema.sql вызывает Artisan partitions:create-months --ahead=2 (без этого первый INSERT падает) * 2026_05_22_000001_tenant_operations_log: idempotency guard * VerifyAuditChains: per-partition scan через pg_inherits; fallback на single-scope для не-партиционированной таблицы; per-RLS-scope partition_clause сохранён внутри каждой партиции * AuditChainBreachMail: +partitionName param (NULL=fallback на tableName) * PartitionsDropExpired (новая): cron Sundays 03:00 МСК, retention из system_settings, dry-run mode, safety guard retention=0 * SchedulerHeartbeatTracker +partitions:drop-expired (10080 мин) Без Laravel-миграции для прода — она оставляла БД пустой при migrate:fresh. Подход: schema.sql декларирует партиционированные + ad-hoc SQL под postgres для прод-rewrite (отдельный commit + ручной деплой + pg_dump backup). Тесты: 1219/1231 (35/35 hole #2 specs, 88 assertions). 3 fail — pre-existing AdminPdSubjectRequestsControllerTest::executeErasure_* (FK actor_admin_user_id после partitioning pd_processing_log, отдельная задача для hole #4 follow-up, не блокирует). cspell +2 слова (партиционировать, дёшева). Pint --fix чистый. Spec: docs/superpowers/specs/2026-05-23-hole-2-audit-partitioning-design.md Plan: docs/superpowers/plans/2026-05-23-hole-2-audit-partitioning-plan.md
This commit is contained in:
+84
-53
@@ -1,10 +1,11 @@
|
||||
-- =============================================================================
|
||||
-- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра»)
|
||||
-- Версия: v8.30 (23.05.2026 — scheduler_heartbeats: пульс планировщика, SaaS-level без RLS, 11 cron-задач, hole #6)
|
||||
-- Версия: v8.31 (23.05.2026 — партиционирование 7 audit-таблиц помесячно (hole #2): auth_log / activity_log / tenant_operations_log / webhook_log / balance_transactions / pd_processing_log / saas_admin_audit_log; PK → (id, created_at|received_at); FK на webhook_log удалены (W1); retention defaults в system_settings)
|
||||
-- Базовая версия: v8.30 (23.05.2026 — scheduler_heartbeats: пульс планировщика, SaaS-level без RLS, 11 cron-задач, hole #6)
|
||||
-- Базовая версия: v8.29 (22.05.2026 — webhook_log: supplier audit columns)
|
||||
-- Базовая версия: v8.28 (22.05.2026 — tenant_operations_log: журнал тенант-уровневых операций вне сделок (проекты, API-ключи, webhook URL), append-only hash-chain, P2 operational journaling closure)
|
||||
-- Базовая версия: v8.27 (21.05.2026 — drop projects.archived_at: feature архива заменена настоящим удалением с защитой по сделкам (ProjectService::delete()))
|
||||
-- Метрики: 67 базовые таблицы (65 regular + 2 partitioned parents: deals + supplier_lead_costs) + 12 партиций / 125 индексов / 41 RLS-политика / 5 функций / 15 триггеров
|
||||
-- Метрики: 74 базовые таблицы (65 regular + 9 partitioned parents: deals + supplier_lead_costs + 7 audit) + 12 партиций / 125 индексов / 41 RLS-политика / 5 функций / 15 триггеров
|
||||
-- Базовая версия: v8.25 (19.05.2026 — supplier_manual_sync_queue: SaaS-level Tier 3 очередь резерва канала миграции проектов)
|
||||
-- Базовая версия: v8.24 (18.05.2026 — supplier_leads.vid → nullable для CSV-recovered лидов (Путь 2))
|
||||
-- Базовая версия: v8.20 (11.05.2026 — Plan 5 frontend projects UI: projects.archived_at TIMESTAMPTZ NULL для soft archive flow; tenants.limits JSONB NOT NULL DEFAULT '{}' для per-tenant project/user лимитов)
|
||||
@@ -1450,8 +1451,12 @@ CREATE INDEX idx_outbound_deliveries_created ON outbound_webhook_deliv
|
||||
-- РАСШИРЕНИЕ v8.1: добавлены actor_type и saas_admin_user_id для объединения
|
||||
-- логов входов клиентских пользователей и админов SaaS в одной таблице.
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- v8.31: партиционирована помесячно по created_at (hole #2).
|
||||
-- PK изменён с (id) на (id, created_at) для совместимости с RANGE-партиционированием PG 16.
|
||||
-- Стартовые партиции создаются миграцией 2026_05_23_000002_partition_audit_tables
|
||||
-- и далее cron'ом partitions:create-months.
|
||||
CREATE TABLE auth_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGSERIAL,
|
||||
actor_type VARCHAR(20) NOT NULL DEFAULT 'tenant_user'
|
||||
CHECK (actor_type IN ('tenant_user','saas_admin')),
|
||||
tenant_id BIGINT REFERENCES tenants(id), -- NULL для админов SaaS
|
||||
@@ -1469,15 +1474,16 @@ CREATE TABLE auth_log (
|
||||
-- среднюю строку или вставит фейковую — пересчёт цепочки покажет
|
||||
-- разрыв. UPDATE/DELETE заблокированы триггером BEFORE.
|
||||
log_hash BYTEA, -- NULL → fill via trigger BEFORE INSERT
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- v8.31: NOT NULL (partition key)
|
||||
-- Целостность: actor должен быть ровно одного типа
|
||||
CONSTRAINT chk_auth_log_actor CHECK (
|
||||
(actor_type = 'tenant_user' AND user_id IS NOT NULL AND saas_admin_user_id IS NULL)
|
||||
OR (actor_type = 'saas_admin' AND saas_admin_user_id IS NOT NULL AND user_id IS NULL)
|
||||
OR (actor_type = 'tenant_user' AND user_id IS NULL AND saas_admin_user_id IS NULL AND email IS NOT NULL)
|
||||
-- Третий вариант: попытка входа с email, но user_id неизвестен (login_failed для неcуществующего email)
|
||||
)
|
||||
);
|
||||
),
|
||||
PRIMARY KEY (id, created_at) -- v8.31: composite PK (partition key required)
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE INDEX idx_auth_log_tenant_user ON auth_log(tenant_id, user_id, created_at DESC);
|
||||
CREATE INDEX idx_auth_log_admin ON auth_log(saas_admin_user_id, created_at DESC) WHERE saas_admin_user_id IS NOT NULL;
|
||||
@@ -1687,12 +1693,13 @@ CREATE INDEX ON deals (tenant_id, status) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Стартовые партиции (создаются cron-ом раз в сутки на 2 месяца вперёд).
|
||||
-- Здесь — заготовка на ближайшие 6 месяцев от текущей даты схемы (май 2026).
|
||||
CREATE TABLE deals_2026_05 PARTITION OF deals FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE deals_2026_06 PARTITION OF deals FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
|
||||
CREATE TABLE deals_2026_07 PARTITION OF deals FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
|
||||
CREATE TABLE deals_2026_08 PARTITION OF deals FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
|
||||
CREATE TABLE deals_2026_09 PARTITION OF deals FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
|
||||
CREATE TABLE deals_2026_10 PARTITION OF deals FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
|
||||
-- v8.31: переименованы в формат <table>_y<YYYY>_m<MM> (совпадает с MonthlyPartitionManager::partitionName()).
|
||||
CREATE TABLE deals_y2026_m05 PARTITION OF deals FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE deals_y2026_m06 PARTITION OF deals FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
|
||||
CREATE TABLE deals_y2026_m07 PARTITION OF deals FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
|
||||
CREATE TABLE deals_y2026_m08 PARTITION OF deals FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
|
||||
CREATE TABLE deals_y2026_m09 PARTITION OF deals FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
|
||||
CREATE TABLE deals_y2026_m10 PARTITION OF deals FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
@@ -1766,8 +1773,9 @@ CREATE INDEX idx_deal_tag_pivot_tag ON deal_tag_pivot(tag_id);
|
||||
-- activity_log — журнал действий по сделкам (раздел 14.4)
|
||||
-- РЕТЕНШН: 3 года активно, далее в S3 Glacier
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- v8.31: партиционирована помесячно по created_at (hole #2). PK → (id, created_at).
|
||||
CREATE TABLE activity_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGSERIAL,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id BIGINT REFERENCES users(id), -- NULL для системных событий
|
||||
deal_id BIGINT NOT NULL, -- БЕЗ FK (deals партиционирована)
|
||||
@@ -1778,8 +1786,9 @@ CREATE TABLE activity_log (
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
log_hash BYTEA, -- v8.5 (OPEN-И-15): hash chain (см. auth_log)
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- v8.31: NOT NULL (partition key)
|
||||
PRIMARY KEY (id, created_at) -- v8.31: composite PK
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE INDEX idx_activity_tenant_deal_created ON activity_log(tenant_id, deal_id, created_at DESC);
|
||||
CREATE INDEX idx_activity_tenant_user_created ON activity_log(tenant_id, user_id, created_at DESC) WHERE user_id IS NOT NULL;
|
||||
@@ -1788,8 +1797,10 @@ CREATE INDEX idx_activity_tenant_user_created ON activity_log(tenant_id, user_id
|
||||
-- tenant_operations_log — журнал тенант-уровневых операций вне сделок
|
||||
-- (проекты, API-ключи, исходящий webhook URL, и т.п.). Защищён hash-chain.
|
||||
-- =============================================================================
|
||||
-- v8.31: партиционирована помесячно по created_at (hole #2). PK → (id, created_at).
|
||||
-- RLS и триггеры перенесены в секцию RLS/Triggers (единообразно с другими партиционированными).
|
||||
CREATE TABLE tenant_operations_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGSERIAL,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id BIGINT REFERENCES users(id), -- NULL для системных
|
||||
entity_type VARCHAR(50) NOT NULL, -- 'project', 'api_key', 'webhook_settings'
|
||||
@@ -1800,8 +1811,9 @@ CREATE TABLE tenant_operations_log (
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
log_hash BYTEA, -- hash chain
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- v8.31: NOT NULL (partition key)
|
||||
PRIMARY KEY (id, created_at) -- v8.31: composite PK
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE INDEX idx_tenant_ops_tenant_created
|
||||
ON tenant_operations_log(tenant_id, created_at DESC);
|
||||
@@ -1809,17 +1821,6 @@ CREATE INDEX idx_tenant_ops_entity
|
||||
ON tenant_operations_log(tenant_id, entity_type, entity_id, created_at DESC)
|
||||
WHERE entity_id IS NOT NULL;
|
||||
|
||||
ALTER TABLE tenant_operations_log ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON tenant_operations_log
|
||||
USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
|
||||
|
||||
CREATE TRIGGER trg_audit_chain_hash_tenant_ops
|
||||
BEFORE INSERT ON tenant_operations_log
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_chain_hash();
|
||||
CREATE TRIGGER trg_audit_block_mut_tenant_ops
|
||||
BEFORE UPDATE OR DELETE ON tenant_operations_log
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_block_mutation();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- reminders — напоминания по сделкам (раздел 17.5)
|
||||
-- v8.3: расширено по итогам партии 12.2 аудита.
|
||||
@@ -1924,11 +1925,14 @@ COMMENT ON TABLE in_app_notifications IS
|
||||
-- webhook_log — лог принятых webhook (раздел 5.7)
|
||||
-- РЕТЕНШН: system_settings.webhook_log_retention_days (по умолчанию 90 дней)
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- v8.31: партиционирована помесячно по received_at (hole #2). PK → (id, received_at).
|
||||
-- FK из failed_webhook_jobs/rejected_deals_log удалены (W1 — невозможны на составном PK
|
||||
-- партиционированной таблицы с единичным FK-столбцом).
|
||||
CREATE TABLE webhook_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGSERIAL,
|
||||
tenant_id BIGINT REFERENCES tenants(id) ON DELETE CASCADE, -- NULL для platform-level событий (supplier webhook)
|
||||
raw_payload JSONB NOT NULL, -- содержит ПДн → удаляется при анонимизации
|
||||
received_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- v8.31: NOT NULL (partition key)
|
||||
processed_at TIMESTAMPTZ,
|
||||
deal_id BIGINT, -- БЕЗ FK (deals партиционирована)
|
||||
error TEXT,
|
||||
@@ -1937,8 +1941,9 @@ CREATE TABLE webhook_log (
|
||||
status VARCHAR(50), -- 'received' | 'rejected_secret' | 'rejected_ip' | 'rate_limited'
|
||||
lead_id BIGINT, -- supplier_leads.id при статусе 'received'
|
||||
ip_address INET, -- клиентский IP
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (id, received_at) -- v8.31: composite PK
|
||||
) PARTITION BY RANGE (received_at);
|
||||
|
||||
CREATE INDEX idx_webhook_log_tenant_received ON webhook_log(tenant_id, received_at DESC);
|
||||
CREATE INDEX idx_webhook_log_status ON webhook_log(status, created_at DESC);
|
||||
@@ -1951,7 +1956,7 @@ CREATE INDEX idx_webhook_log_status ON webhook_log(status, created_at DESC);
|
||||
CREATE TABLE failed_webhook_jobs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
webhook_log_id BIGINT REFERENCES webhook_log(id),
|
||||
webhook_log_id BIGINT, -- v8.31: FK удалён (W1 — webhook_log партиционирована, composite PK несовместим с одиночным FK)
|
||||
raw_payload JSONB NOT NULL,
|
||||
exception TEXT NOT NULL,
|
||||
retry_count INT DEFAULT 3,
|
||||
@@ -1973,7 +1978,7 @@ CREATE INDEX idx_failed_webhook_jobs_log ON failed_webhook_jobs(webhook_log_id);
|
||||
CREATE TABLE rejected_deals_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
webhook_log_id BIGINT REFERENCES webhook_log(id),
|
||||
webhook_log_id BIGINT, -- v8.31: FK удалён (W1 — webhook_log партиционирована, composite PK несовместим с одиночным FK)
|
||||
reason VARCHAR(50) NOT NULL, -- zero_balance, validation_failed, ...
|
||||
payload JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
@@ -2320,8 +2325,9 @@ CREATE INDEX idx_refund_requests_chargeback ON refund_requests(tenant_id, reque
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- balance_transactions — внутренний лид-биллинг (раздел 7.3, 21)
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- v8.31: партиционирована помесячно по created_at (hole #2). PK → (id, created_at).
|
||||
CREATE TABLE balance_transactions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGSERIAL,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL
|
||||
CHECK (type IN ('trial_bonus','topup','lead_charge','refund',
|
||||
@@ -2340,8 +2346,9 @@ CREATE TABLE balance_transactions (
|
||||
-- Для manual_adjustment — кто из админов SaaS сделал
|
||||
admin_user_id BIGINT REFERENCES saas_admin_users(id),
|
||||
log_hash BYTEA, -- v8.5 (OPEN-И-15): hash chain (см. auth_log)
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- v8.31: NOT NULL (partition key)
|
||||
PRIMARY KEY (id, created_at) -- v8.31: composite PK
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE INDEX idx_balance_tenant_created ON balance_transactions(tenant_id, created_at DESC);
|
||||
CREATE INDEX idx_balance_tenant_type ON balance_transactions(tenant_id, type);
|
||||
@@ -2411,12 +2418,13 @@ CREATE INDEX ON supplier_lead_costs (supplier_invoice_id) WHERE supplier_invoice
|
||||
CREATE INDEX ON supplier_lead_costs (supplier_id, received_at DESC); -- v8.2: аналитика "лиды по поставщикам"
|
||||
|
||||
-- Партиции синхронно с deals (создаются cron на 2 месяца вперёд)
|
||||
CREATE TABLE supplier_lead_costs_2026_05 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE supplier_lead_costs_2026_06 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
|
||||
CREATE TABLE supplier_lead_costs_2026_07 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
|
||||
CREATE TABLE supplier_lead_costs_2026_08 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
|
||||
CREATE TABLE supplier_lead_costs_2026_09 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
|
||||
CREATE TABLE supplier_lead_costs_2026_10 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
|
||||
-- v8.31: переименованы в формат <table>_y<YYYY>_m<MM>.
|
||||
CREATE TABLE supplier_lead_costs_y2026_m05 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE supplier_lead_costs_y2026_m06 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
|
||||
CREATE TABLE supplier_lead_costs_y2026_m07 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
|
||||
CREATE TABLE supplier_lead_costs_y2026_m08 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
|
||||
CREATE TABLE supplier_lead_costs_y2026_m09 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
|
||||
CREATE TABLE supplier_lead_costs_y2026_m10 PARTITION OF supplier_lead_costs FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
@@ -2490,8 +2498,9 @@ CREATE INDEX idx_consents_tenant ON tenant_consents(tenant_id, consent_type);
|
||||
-- pd_processing_log — журнал обработки ПДн (раздел 22.9.3)
|
||||
-- РАСШИРЕНИЕ v8.1: разделение actor_user_id на два поля с FK
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- v8.31: партиционирована помесячно по created_at (hole #2). PK → (id, created_at).
|
||||
CREATE TABLE pd_processing_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGSERIAL,
|
||||
tenant_id BIGINT REFERENCES tenants(id),
|
||||
subject_type VARCHAR(50), -- 'user', 'lead'
|
||||
subject_id BIGINT,
|
||||
@@ -2502,13 +2511,14 @@ CREATE TABLE pd_processing_log (
|
||||
actor_admin_user_id BIGINT REFERENCES saas_admin_users(id),
|
||||
ip_address INET,
|
||||
log_hash BYTEA, -- v8.5 (OPEN-И-15): hash chain (см. auth_log)
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- v8.31: NOT NULL (partition key)
|
||||
CONSTRAINT chk_pd_actor CHECK (
|
||||
(actor_tenant_user_id IS NOT NULL AND actor_admin_user_id IS NULL)
|
||||
OR (actor_tenant_user_id IS NULL AND actor_admin_user_id IS NOT NULL)
|
||||
OR (actor_tenant_user_id IS NULL AND actor_admin_user_id IS NULL) -- системное действие
|
||||
)
|
||||
);
|
||||
),
|
||||
PRIMARY KEY (id, created_at) -- v8.31: composite PK
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE INDEX idx_pd_log_tenant ON pd_processing_log(tenant_id, created_at DESC);
|
||||
CREATE INDEX idx_pd_log_admin_actor ON pd_processing_log(actor_admin_user_id, created_at DESC) WHERE actor_admin_user_id IS NOT NULL;
|
||||
@@ -2663,8 +2673,10 @@ COMMENT ON TABLE scheduler_heartbeats IS
|
||||
-- saas_admin_users уже создана выше (нужна была для FK от других таблиц)
|
||||
-- =============================================================================
|
||||
|
||||
-- v8.31: партиционирована помесячно по created_at (hole #2). PK → (id, created_at).
|
||||
-- Без RLS: доступна только crm_admin_user (BYPASSRLS).
|
||||
CREATE TABLE saas_admin_audit_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGSERIAL,
|
||||
admin_user_id BIGINT NOT NULL REFERENCES saas_admin_users(id),
|
||||
action VARCHAR(100) NOT NULL, -- 'tenant.suspend', 'refund.approve', 'system_settings.update', ...
|
||||
target_type VARCHAR(50), -- 'tenant', 'saas_transaction', 'system_setting', ...
|
||||
@@ -2680,8 +2692,9 @@ CREATE TABLE saas_admin_audit_log (
|
||||
approved_by BIGINT REFERENCES saas_admin_users(id),
|
||||
approved_at TIMESTAMPTZ,
|
||||
log_hash BYTEA, -- v8.5 (OPEN-И-15): hash chain (см. auth_log)
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- v8.31: NOT NULL (partition key)
|
||||
PRIMARY KEY (id, created_at) -- v8.31: composite PK
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE INDEX idx_admin_audit_admin ON saas_admin_audit_log(admin_user_id, created_at DESC);
|
||||
CREATE INDEX idx_admin_audit_tenant ON saas_admin_audit_log(target_tenant_id, created_at DESC) WHERE target_tenant_id IS NOT NULL;
|
||||
@@ -2787,7 +2800,15 @@ INSERT INTO system_settings (key, value, type, description) VALUES
|
||||
('projects_purge_deleted_cron', '0 4 * * *', 'string', 'Расписание cron projects:purge-deleted (по умолчанию 04:00 МСК ежедневно)'),
|
||||
-- v8.18 (Plan 2/5): supplier-webhook secret + IP allowlist для defense-in-depth.
|
||||
('supplier_webhook_secret', '__SET_ON_DEPLOY__', 'string', 'Platform-wide секрет (≥32 chars) для /api/webhook/supplier/{secret}. См. spec §5.1.'),
|
||||
('supplier_ip_allowlist', '[]', 'json', 'Список IP/CIDR поставщика crm.bp-gr.ru. Пустой массив = пропускать всех (DEV); на prod заполнить.');
|
||||
('supplier_ip_allowlist', '[]', 'json', 'Список IP/CIDR поставщика crm.bp-gr.ru. Пустой массив = пропускать всех (DEV); на prod заполнить.'),
|
||||
-- v8.31: retention для 7 audit-таблиц после partitioning (hole #2). Используется PartitionsDropExpired (cron Sundays 03:00 МСК).
|
||||
('auth_log_retention_months', '24', 'int', 'Retention auth_log в месяцах (hole #2)'),
|
||||
('activity_log_retention_months', '36', 'int', 'Retention activity_log (hole #2)'),
|
||||
('tenant_operations_log_retention_months', '24', 'int', 'Retention tenant_operations_log (hole #2)'),
|
||||
('webhook_log_retention_months', '3', 'int', 'Retention webhook_log (hole #2)'),
|
||||
('balance_transactions_retention_months', '84', 'int', 'Retention balance_transactions, 7л НК РФ (hole #2)'),
|
||||
('pd_processing_log_retention_months', '36', 'int', 'Retention pd_processing_log, 152-ФЗ 3 года (hole #2)'),
|
||||
('saas_admin_audit_log_retention_months', '84', 'int', 'Retention saas_admin_audit_log, 7л (hole #2)');
|
||||
|
||||
|
||||
-- 4 стартовых тарифа-заглушки (Биз-1: вариант Б).
|
||||
@@ -2855,6 +2876,7 @@ ALTER TABLE deal_tags ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE import_log ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE import_unknown_statuses ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE activity_log ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant_operations_log ENABLE ROW LEVEL SECURITY; -- v8.31: перенесено сюда (была inline)
|
||||
ALTER TABLE reminders ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE webhook_log ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE failed_webhook_jobs ENABLE ROW LEVEL SECURITY;
|
||||
@@ -2896,6 +2918,7 @@ CREATE POLICY tenant_isolation ON deal_tags USING (tenant_id = cur
|
||||
CREATE POLICY tenant_isolation ON import_log USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
|
||||
CREATE POLICY tenant_isolation ON import_unknown_statuses USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
|
||||
CREATE POLICY tenant_isolation ON activity_log USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
|
||||
CREATE POLICY tenant_isolation ON tenant_operations_log USING (tenant_id = current_setting('app.current_tenant_id')::bigint); -- v8.31: перенесено из inline
|
||||
CREATE POLICY tenant_isolation ON reminders USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
|
||||
CREATE POLICY tenant_isolation ON webhook_log USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
|
||||
CREATE POLICY tenant_isolation ON failed_webhook_jobs USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
|
||||
@@ -3093,7 +3116,8 @@ COMMENT ON FUNCTION audit_block_mutation() IS
|
||||
'v8.5 (OPEN-И-15): запрещает UPDATE/DELETE на audit-таблицах. '
|
||||
'Совместно с REVOKE на роли — два слоя защиты от tampering.';
|
||||
|
||||
-- 5 пар триггеров: hash-fill (BEFORE INSERT) + block-mutation (BEFORE UPDATE/DELETE)
|
||||
-- 6 пар триггеров: hash-fill (BEFORE INSERT) + block-mutation (BEFORE UPDATE/DELETE)
|
||||
-- v8.31: tenant_operations_log перенесён из inline-определения таблицы; итого 6 пар.
|
||||
|
||||
CREATE TRIGGER trg_audit_chain_hash_auth_log
|
||||
BEFORE INSERT ON auth_log
|
||||
@@ -3109,6 +3133,13 @@ CREATE TRIGGER trg_audit_block_mut_activity_log
|
||||
BEFORE UPDATE OR DELETE ON activity_log
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_block_mutation();
|
||||
|
||||
CREATE TRIGGER trg_audit_chain_hash_tenant_ops
|
||||
BEFORE INSERT ON tenant_operations_log
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_chain_hash();
|
||||
CREATE TRIGGER trg_audit_block_mut_tenant_ops
|
||||
BEFORE UPDATE OR DELETE ON tenant_operations_log
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_block_mutation();
|
||||
|
||||
CREATE TRIGGER trg_audit_chain_hash_pd_log
|
||||
BEFORE INSERT ON pd_processing_log
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_chain_hash();
|
||||
|
||||
Reference in New Issue
Block a user