Files
portal/db/anon_masking_labels.sql
T

100 lines
7.0 KiB
SQL
Raw 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.
-- =============================================================================
-- anon_masking_labels.sql — правила динамического маскирования ПДн (pg_anonymizer)
-- =============================================================================
-- Назначение: закрыть блокер B2. На боевом liderra.ru расширение anon 3.0.13
-- установлено (22.05.2026), но НЕ задано ни одного правила маскирования
-- (anon.pg_masking_rules → 0 строк), поэтому pg_dump выгружает персональные
-- данные в открытом виде. Этот файл декларативно навешивает SECURITY LABEL на
-- все колонки-носители ПДн.
--
-- БЕЗОПАСНОСТЬ ПРИМЕНЕНИЯ НА ПРОД:
-- * SECURITY LABEL ON COLUMN активирует маскирование ТОЛЬКО для ролей с меткой
-- 'MASKED'. Роли приложения (crm_app_user, crm_app_admin, crm_supplier_worker,
-- crm_readonly, crm_migrator) видят реальные данные без изменений — работа
-- портала НЕ затрагивается.
-- * Реальные данные в таблицах НЕ переписываются: это динамическое маскирование,
-- не статическое anon.anonymize_database() (его на проде НЕ запускаем).
-- * RLS не затрагивается: маскирование (anon) и tenant-изоляция (RLS) —
-- независимые механизмы. Колонки tenant_id / id / ключи партиций
-- (received_at, created_at) / любые FK-ключи НЕ маскируются.
-- * FK-целостность: маскируются только не-ключевые колонки. Для UNIQUE-колонок
-- (users.email) применяется детерминированная anon.partial_email (сохраняет
-- уникальность по входу); для phone — anon.partial (сохраняет форму).
--
-- ПРИМЕНЕНИЕ (на проде, под postgres):
-- LOAD 'anon';
-- \i anon_masking_labels.sql
-- ПРОВЕРКА:
-- SELECT relname, attname, masking_function
-- FROM anon.pg_masking_rules ORDER BY relname, attname;
-- ИДЕМПОТЕНТНОСТЬ: повторный SECURITY LABEL перезаписывает прежний для той же
-- колонки — файл можно применять повторно.
--
-- NB по синтаксису anon 3.0.13: в `MASKED WITH VALUE …` долларовые кавычки
-- ($$…$$) не принимаются — используются одинарные (внутри метки удваиваются).
-- JSONB с NOT NULL (supplier_leads.raw_payload) маскируется редакцией
-- '{}'::jsonb (не NULL — иначе нарушился бы NOT NULL при restore дампа);
-- nullable JSONB/INET маскируются в NULL.
-- =============================================================================
-- ── users ────────────────────────────────────────────────────────────────────
SECURITY LABEL FOR anon ON COLUMN users.email
IS 'MASKED WITH FUNCTION anon.partial_email(email)';
SECURITY LABEL FOR anon ON COLUMN users.phone
IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$******$$,2)';
SECURITY LABEL FOR anon ON COLUMN users.first_name
IS 'MASKED WITH FUNCTION anon.fake_first_name()';
SECURITY LABEL FOR anon ON COLUMN users.last_name
IS 'MASKED WITH FUNCTION anon.fake_last_name()';
SECURITY LABEL FOR anon ON COLUMN users.totp_secret
IS 'MASKED WITH VALUE NULL';
SECURITY LABEL FOR anon ON COLUMN users.avatar_path
IS 'MASKED WITH VALUE NULL';
-- ── deals (партиционированная — метка на родителе наследуется партициями) ─────
SECURITY LABEL FOR anon ON COLUMN deals.phone
IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$******$$,2)';
SECURITY LABEL FOR anon ON COLUMN deals.phones
IS 'MASKED WITH VALUE NULL';
SECURITY LABEL FOR anon ON COLUMN deals.contact_name
IS 'MASKED WITH FUNCTION anon.fake_last_name()';
SECURITY LABEL FOR anon ON COLUMN deals.comment
IS 'MASKED WITH VALUE NULL';
-- ── supplier_leads (SaaS-уровень, без RLS) ───────────────────────────────────
SECURITY LABEL FOR anon ON COLUMN supplier_leads.phone
IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$******$$,2)';
SECURITY LABEL FOR anon ON COLUMN supplier_leads.raw_payload
IS 'MASKED WITH FUNCTION pg_catalog.jsonb_build_object()'; -- NOT NULL → {} через функцию (anon не берёт ::jsonb-каст в VALUE)
-- ── pd_subject_requests (обращения субъектов ПДн, без RLS) ───────────────────
SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.subject_email
IS 'MASKED WITH FUNCTION anon.partial_email(subject_email)';
SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.subject_phone
IS 'MASKED WITH FUNCTION anon.partial(subject_phone,2,$$******$$,2)';
SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.subject_full_name
IS 'MASKED WITH FUNCTION anon.fake_last_name()';
SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.description
IS 'MASKED WITH VALUE NULL';
SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.response_text
IS 'MASKED WITH VALUE NULL';
-- ── pd_processing_log (партиц.; прямой ПДн — только IP) ──────────────────────
SECURITY LABEL FOR anon ON COLUMN pd_processing_log.ip_address
IS 'MASKED WITH VALUE NULL';
-- ── auth_log (журнал входов) ─────────────────────────────────────────────────
SECURITY LABEL FOR anon ON COLUMN auth_log.email
IS 'MASKED WITH FUNCTION anon.partial_email(email)';
-- ── tenants (контактная почта арендатора) ────────────────────────────────────
SECURITY LABEL FOR anon ON COLUMN tenants.contact_email
IS 'MASKED WITH FUNCTION anon.partial_email(contact_email)';
-- =============================================================================
-- КОНЕЦ. Перечень колонок — по контракту спеки
-- docs/superpowers/specs/2026-06-17-anon-masking-labels-prod-spec.md (раздел D1).
-- Рецепт маскированного дампа и план применения — в
-- docs/security/pgaudit-anonymizer-setup.md.
-- =============================================================================