Семантический слой и Governance

Порядок и права доступа

|

Сто таблиц, триста пользователей и ни одной внятной записи, кому что можно видеть. Доступы раздавали «по кнопке» и «на всякий случай», так что теперь даже вы не уверены, кто из аналитиков дотягивается до зарплат, а у какого отдела своя «единственно верная» выручка.

Так выглядит компания без Data Governance. И дело не в том, что кто-то злой, — просто раздавать права руками на сотне таблиц невозможно в принципе. Нужен механизм, который сделает это за вас и не забудет.

Этот механизм — семантический слой. Он не только хранит метрики, но и следит, чтобы каждый видел ровно свои данные: чужая строка или чужая колонка физически не попадали ему в руки. Так как я уже несколько лет работаю с Cube, то решил показать примеры реализации на этом стеке.

Кто владелец метрики? Или почему у нас три разные выручки

Главная проблема любой растущей компании — метрический хаос. Маркетинг считает выручку по отгруженным товарам, финансы — по поступившим деньгам, а продажи — по подписанным контрактам. В итоге на планёрке каждый защищает свою версию реальности, и начинается самая настоящая драма.

Data Governance отвечает на вопрос: кто владеет метрикой? Кто тот человек, который может сказать: «Да, вот эта формула расчёта LTV — единственно верная, а всё остальное — галлюцинации»? Это называется Data Stewardship.

Но назначить ответственного мало. Нужно заставить всех пользоваться его определениями. Тут на сцену выходит семантический слой. Он выступает в роли единой точки истины. Вместо того чтобы каждый аналитик писал свой «грязный» SQL-запрос в BI-инструменте, все обращаются к семантической модели.

Семантический слой жёстко фиксирует: «Вот эта метрика называется Выручка, считается вот по этому алгоритму, и владелец у неё — финансовый директор». Если вы хотите получить отчёт, вы берёте эту метрику из слоя. Это исключает ситуацию, когда два отдела приходят на встречу с разными цифрами и начинают обвинять друг друга в некомпетентности.

Безопасность доступа: семантический слой как контролёр

Теперь поговорим о правах доступа. Когда у вас сотни таблиц и тысячи пользователей, раздавать права напрямую в базе данных или хранилище — это как выдать ключи от всех номеров отеля всем гостям. Рано или поздно кто-то зайдёт не в ту комнату и увидит то, что видеть не должен.

Семантический слой — это контролёр на входе. Да, он может слегка замедлить аналитика, которому хочется быстро запихнуть сырой SQL-запрос прямо в дашборд, минуя все правила. Но зато он спасает от крайне неприятных последствий: финансовых потерь, утечек персональных данных и репутационного ущерба.

Вся магия безопасности в семантическом слое реализуется через два механизма: RLS и CLS. Главное преимущество в том, что вам не нужно прописывать эти правила в каждой витрине данных или BI-отчёте. Вы настраиваете их один раз на уровне семантической модели.

flowchart LR
  A[Пользователь] --> B[Семантический слой]
  B --> C{RLS<br/>фильтр строк}
  B --> D{CLS<br/>скрытие колонок}
  C --> E[(Хранилище)]
  D --> E
  E --> F[Только свои данные]

Row-Level Security (RLS)

Row-Level Security, или безопасность на уровне строк, работает как аккуратный официант: он приносит вам ровно то блюдо, что вы заказали, и ни крошки чужого.

Например, у вас есть таблица с продажами по всей стране. Региональный менеджер по Уралу заходит в BI-систему и тянет общую метрику «Продажи». Благодаря RLS, настроенному в семантическом слое, система незаметно подставляет фильтр WHERE region = 'Ural'. Менеджер счастлив — он видит свои данные, и ему не нужно знать, как дела у соседней области или сколько продала Москва. Семантический слой сам фильтрует строки на лету, основываясь на роли или атрибутах пользователя.

Как это делается в Cube

В Cube для этого есть декларативные политики доступа (access_policy): описываете правила один раз в модели данных — и слой применяет их ко всем исходящим запросам. Никакого дублирования фильтров в каждом дашборде, никаких «а ты не забыл дописать WHERE».

Политика — это двухмерная матрица прав. member_level решает, какие метрики и измерения видит пользователь (это та самая CLS, до неё доберёмся чуть ниже), а row_level — какие строки ему вообще разрешено увидеть. Строки фильтруются прямо в SQL на лету, из атрибутов пользователя (userAttributes), которые слой получает при аутентификации. Вот политика для куба сделок:

cube(`deals`, {  sql: `SELECT * FROM public.deals`,   // Политики доступа (Data Access Policies)  access_policy: [    {      // Обычные менеджеры по продажам      group: `sales`,      member_level: {        includes: `*`, // Доступ ко всем метрикам и измерениям куба      },      row_level: {        filters: [          {            member: `salesPersonId`,            operator: `equals`,            values: [`{userAttributes.userId}`], // Только свои сделки          },        ],      },    },    {      // Региональные руководители      group: `sales_manager`,      member_level: {        includes: `*`,      },      row_level: {        filters: [          {            member: `region`,            operator: `equals`,            values: [`{userAttributes.region}`], // Все сделки своего региона          },        ],      },    },  ],   measures: {    count: { type: `count` },  },   dimensions: {    salesPersonId: { sql: `sales_person_id`, type: `string` },    region: { sql: `region`, type: `string` },  },})

Обычный менеджер видит только свои сделки, руководитель региона — весь регион, а пользователь, которого нет ни в одной группе, — вообще ничего: политики работают по принципу default deny. Если человек состоит сразу в нескольких группах, Cube пересекает их правила — доступ остаётся только к пересечению разрешённых областей. Так что «случайно вытащить чужую строку через хитрый отчёт» больше не получится: слой просто не даст.

Column-Level Security (CLS)

Column-Level Security, или безопасность на уровне колонок, — это как витрина магазина: вы видите общий ассортимент, но в подсобку, где хранятся самые чувствительные товары, без особого пропуска не пройти.

Допустим, аналитику нужно посчитать среднюю зарплату по департаментам. Ему не обязательно видеть конкретные суммы каждого сотрудника. В семантическом слое вы можете скрыть колонку salary от всех, кроме HR-директора. Аналитик сможет использовать метку «Есть данные о зарплате» для группировки, но физически достать сами цифры из колонки не сможет. BI-инструмент даже не узнает, что эта колонка существует для данного пользователя.

Как это делается в Cube

Если RLS отвечает на вопрос «какие строки видит пользователь», то CLS — «какие метрики и измерения для него вообще существуют». В терминах Cube это Member-Level Security, тот самый member_level из политики доступа выше. В классических СУБД права на колонки — это простыни GRANT-ов на каждую таблицу. Здесь — декларативное правило в коде модели, которое одинаково работает и для API, и для BI.

Есть нюанс, который стоит держать в голове: по умолчанию все кубы и их члены публичны. Но как только вы определили хоть одну политику, Cube переключается на default deny — видно только то, что явно разрешено. Метрику, которую забыли выдать группе, невозможно запросить: для этой группы её просто не существует.

Видимость настраивается через includes (белый список) и excludes (чёрный список) внутри member_level. Тот же сценарий с зарплатами, но на витрине заказов: менеджерам скрываем абсолютный показатель count, наблюдателям — ещё и оперативную детализацию за 7 дней, а гостям оставляем одну агрегированную метрику.

view(`orders_view`, {  // Подключаем нужные элементы из базового куба  cubes: [    {      join_path: `orders`,      includes: [`status`, `created_at`, `count`, `count_7d`, `count_30d`],    },  ],   // Политики доступа на уровне членов (Column-Level Security)  access_policy: [    {      // Менеджеры видят всё, кроме абсолютного показателя `count`      group: `manager`,      member_level: {        excludes: [`count`],      },    },    {      // Наблюдатели не видят `count` и детализацию за 7 дней      group: `observer`,      member_level: {        excludes: [`count`, `count_7d`],      },    },    {      // Гости — строго одна агрегированная метрика (белый список)      group: `guest`,      member_level: {        includes: [`count_30d`],      },    },  ],})

Гость попробовал запросить count — и получил отказ ещё до генерации SQL. Слой даже не дошёл до хранилища: этого члена для его группы не существует.

Маскирование: третий инструмент

RLS прячет строки, CLS — колонки. Но есть ещё один сценарий: пользователь законно видит колонку, только значения внутри неё должны быть «испорчены». Номер телефона — +7-9**-***-**-**, зарплата — диапазон 100–150 тыс. ₽ вместо точной цифры.

Механизм Что прячет Пример
RLS Целые строки Менеджер видит только свой регион
CLS Колонки Аналитик не видит salary
Маскирование Значения внутри колонки Телефон +7-9**-***-**-**

Чем маскирование отличается от CLS

CLS убирает колонку целиком: поле просто исчезает из схемы в BI-инструменте. Маскирование работает мягче — пользователь видит колонку, строит по ней дашборды и агрегации, но вместо реальных значений получает обезличенные: ***, NULL или -1. Отчёты не «ломаются» из-за пропавших полей, а персональные данные (PII) остаются под замком — что прямым текстом закрывает вопросы GDPR и ФЗ-152.

Семантические слои маскированием «из коробки» обычно не занимаются — это работа хранилища (Dynamic Data Masking в Snowflake, policy tags в BigQuery) или производных полей в модели: создаёте измерение salary_masked, которое возвращает диапазон, и выдаёте его вместо настоящего. Хорошая новость: когда RLS и CLS уже настроены, маскирование превращается в точечную задачу для пары полей, а не в проект на тысячу таблиц.

Как это делается в Cube

В Cube маскирование собирается из двух деталей: параметр mask на уровне измерения или меры задаёт правило подмены (статическая строка или SQL-выражение), а member_masking внутри политики доступа говорит, к каким полям и для какой группы применять маску.

Самая вкусная фишка — условное маскирование в связке с RLS. Если политика даёт полный доступ к полю, но ограничивает его правилом row_level, Cube генерирует SQL вида CASE WHEN {условие_RLS} THEN {реальное_значение} ELSE {маска} END. Менеджер видит реальный email только в своих строках, а чужие — замаскированными.

cube(`customers`, {  sql: `SELECT * FROM public.customers`,   dimensions: {    email: {      sql: `email`,      type: `string`,      // Динамическое маскирование: первые 3 символа и домен      mask: {        sql: `CONCAT(LEFT(${CUBE}.email, 3), '***@', SPLIT_PART(${CUBE}.email, '@', 2))`,      },    },    phone: {      sql: `phone`,      type: `string`,      mask: `***-***-**-**`, // Статическое маскирование для всех случаев    },  },   measures: {    count: { type: `count` },  },   access_policy: [    {      // Аналитики видят реальные email, но телефон всегда замаскирован      group: `analyst`,      member_level: {        includes: [`email`, `count`],      },      member_masking: {        includes: [`phone`], // Поле доступно, но значения подменяются маской      },    },    {      // Менеджеры видят реальные данные только по своим клиентам      group: `manager`,      member_level: {        includes: [`email`, `phone`, `count`],      },      row_level: {        filters: [          {            member: `assigned_manager_id`,            operator: `equals`,            values: [`{userAttributes.userId}`],          },        ],      },      member_masking: {        includes: `*`, // По умолчанию маскируем всё      },      // В строках, где assigned_manager_id совпадает с userId,      // менеджер увидит реальные email и phone. В остальных — маску.    },  ],})

Такой подход превращает семантический слой в единую точку применения политик безопасности:

  • Для инженеров — не нужно дублировать логику маскирования в десятках SQL-представлений или настраивать её отдельно в каждом BI-инструменте.
  • Для безопасности — правила неотделимы от метаданных: через API, SQL-интерфейс или дашборд запросил пользователь данные, маска сработает всегда, потому что встроена в переписывание запросов на уровне движка.
  • Для пользователей — интерфейс остаётся бесшовным: привычные столбцы и агрегации на месте, а конфиденциальные значения надёжно спрятаны на уровне отдельных строк.

Как внедрить и не сойти с ума

Внедрение Data Governance через семантический слой — процесс болезненный, как переход на здоровое питание. Но вот пошаговый план, как пережить эту перестройку:

  1. Проведите инвентаризацию. Выпишите все метрики, которые используются в компании. Найдите дубликаты и противоречия.
  2. Назначьте стюардов (Data Stewards). Для каждого домена (финансы, маркетинг, продажи) назначьте ответственного, который будет утверждать определения метрик.
  3. Опишите семантическую модель. Используйте инструменты вроде dbt Semantic Layer, Cube, AtScale или встроенные семантические слои в современных BI. Опишите физические таблицы, связи, измерения и меры.
  4. Настройте матрицу доступов. Определите, кто какие роли имеет. Интегрируйте семантический слой с корпоративной системой аутентификации (Active Directory, Okta), чтобы права подтягивались автоматически.
  5. Запретите прямой доступ. Отрежьте аналитикам возможность писать сырые SQL-запросы к витринам в обход семантического слоя. Да, они будут ныть первые две недели, но потом привыкнут и полюбят вас за то, что им больше не нужно помнить структуру всех таблиц.

Резюме

Data Governance без семантического слоя — это просто набор красивых презентаций и скучных регламентов, которые никто не читает. Семантический слой без Data Governance — это быстрый способ построить красивую витрину, которая всё равно показывает три разных версии выручки.

Только вместе они создают экосистему, где данные безопасны, метрики едины, а бизнес перестаёт тратить время на споры о том, чья цифра правильнее, и начинает наконец зарабатывать деньги. Так что наведите порядок в своих данных — и пусть аналитика будет управляемой и приносящей удовольствие.