2026-06-17 19:33:58 +03:00
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
-- 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 перезаписывает прежний для той же
|
|
|
|
|
|
-- колонки — файл можно применять повторно.
|
|
|
|
|
|
--
|
2026-06-17 20:02:51 +03:00
|
|
|
|
-- NB по синтаксису anon 3.0.13: в `MASKED WITH VALUE …` долларовые кавычки
|
|
|
|
|
|
-- ($$…$$) не принимаются — используются одинарные (внутри метки удваиваются).
|
|
|
|
|
|
-- JSONB с NOT NULL (supplier_leads.raw_payload) маскируется редакцией
|
|
|
|
|
|
-- '{}'::jsonb (не NULL — иначе нарушился бы NOT NULL при restore дампа);
|
|
|
|
|
|
-- nullable JSONB/INET маскируются в NULL.
|
2026-06-17 19:33:58 +03:00
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
-- ── 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
|
2026-06-17 20:02:51 +03:00
|
|
|
|
IS 'MASKED WITH VALUE NULL';
|
2026-06-17 19:33:58 +03:00
|
|
|
|
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
|
2026-06-17 20:02:51 +03:00
|
|
|
|
IS 'MASKED WITH FUNCTION pg_catalog.jsonb_build_object()'; -- NOT NULL → {} через функцию (anon не берёт ::jsonb-каст в VALUE)
|
2026-06-17 19:33:58 +03:00
|
|
|
|
|
|
|
|
|
|
-- ── 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
|
2026-06-17 20:02:51 +03:00
|
|
|
|
IS 'MASKED WITH VALUE NULL';
|
2026-06-17 19:33:58 +03:00
|
|
|
|
|
|
|
|
|
|
-- ── 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.
|
|
|
|
|
|
-- =============================================================================
|