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