Files
portal/db/anon_masking_labels.sql
T

100 lines
7.0 KiB
SQL
Raw Normal View History

-- =============================================================================
-- 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.
-- =============================================================================