8bb72b3430
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
7.0 KiB
SQL
100 lines
7.0 KiB
SQL
-- =============================================================================
|
||
-- 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.
|
||
-- =============================================================================
|