§ P14 — SSAS · Security

RLS на SSAS Tabular: 6 production-паттернов

DAX-роли · Real code · Тестирование

Row-Level Security в SSAS Tabular. Шесть реальных паттернов с DAX-ролями: территориальный RLS, иерархия менеджеров, dynamic RLS через bridge-таблицу, OLS, совместимость с агрегатами и impersonation для админов.

Row-Level Security — это правила, ограничивающие, какие строки таблицы видит конкретный пользователь. В SSAS Tabular реализуется через роли с DAX-фильтрами. Плюс модели RLS: правила живут в одном месте — в семантической модели. Все отчёты, подключённые к модели (Power BI, Excel, любой DAX-клиент), автоматически подчиняются этим правилам. Обойти нельзя: SSAS физически не возвращает запрещённые строки.

Ниже — шесть паттернов, которые покрывают 95% enterprise-сценариев. Каждый с реальной DAX-ролью, объяснением, когда применять, и типичными ошибками.

Базовая механика: как работают роли в SSAS

В SSAS Tabular вы создаёте роли. Каждая роль может иметь:

  • Row Filters — DAX-выражения на уровне таблиц, возвращающие TRUE/FALSE. Строка видна, если хотя бы одна роль пользователя возвращает TRUE (роли объединяются по OR).
  • Table Permissions — видна/не видна таблица целиком (OLS, Object-Level Security).
  • Members — список AD-логинов или групп, которые состоят в роли.

Ключевые функции в DAX-фильтрах: USERPRINCIPALNAME() возвращает UPN текущего пользователя (обычно user@domain.com), USERNAME() возвращает DOMAIN\user. На практике используйте UPN — это стабильнее при миграции доменов.

Шесть production-паттернов

Паттерн 01 · БазовыйДля: региональных менеджеров

Территориальный RLS: менеджер видит свой регион

Самый частый кейс. В dim_employee есть колонки user_principal_name и region_id. Правило: менеджер видит строки, где regions совпадает с его собственным.

Модель: таблица dim_employee (user_principal_name, region_id, employee_name, role).

DAX-фильтр на таблице dim_region:

[region_id] = LOOKUPVALUE(
    dim_employee[region_id],
    dim_employee[user_principal_name], USERPRINCIPALNAME()
)

Фильтр применяется к dim_region. Через связь dim_region → fact_sales он автоматически пробрасывается в факты. Пользователь видит только свои транзакции, отчёты работают как обычно, никаких изменений в DAX-мер не нужно.

Когда не работает: если один сотрудник отвечает за несколько регионов. Для этого — паттерн 03 (dynamic через bridge).

Паттерн 02 · ИерархияДля: оргструктуры, менеджер + команда

Иерархический RLS: руководитель видит всю свою вертикаль

Кейс посложнее: Иванов — директор региона. Он должен видеть продажи всех подчинённых (менеджеров, агентов, точек продаж). В оргструктуре может быть произвольная глубина.

Модель: dim_employee (employee_id, manager_id, user_principal_name, full_path) + на стороне SQL/ETL считаем path через рекурсивный CTE: для каждого сотрудника хранится цепочка вида |1|3|17|42| (ID всех руководителей вверх по иерархии).

DAX-фильтр на dim_employee:

VAR _current_user_id =
    LOOKUPVALUE(
        dim_employee[employee_id],
        dim_employee[user_principal_name], USERPRINCIPALNAME()
    )
RETURN
    CONTAINSSTRING(
        dim_employee[full_path],
        "|" & _current_user_id & "|"
    )
    || dim_employee[employee_id] = _current_user_id

Логика: пользователь видит строку, если в её full_path встречается его собственный ID (то есть он в цепочке руководителей этой строки), либо если это его собственная запись. Разделители |...| нужны, чтобы ID «1» не матчился с «10» или «100».

Альтернатива — PATH/PATHCONTAINS (встроенные функции DAX для parent-child иерархии). Но на больших таблицах (>100 тыс. сотрудников) строковое сравнение через CONTAINSSTRING работает быстрее.

Паттерн 03 · DynamicДля: many-to-many доступа

Dynamic RLS через bridge-таблицу

Сотрудник может отвечать за несколько регионов, стран или товарных категорий. Жёсткая 1-to-1 связь не работает. Решение — отдельная bridge-таблица, которая сопоставляет пользователей и объекты доступа.

Модель:

dim_employee  ──┐
                │
                ▼
        rls_user_region  (user_principal_name, region_id)
                │
                ▼
        dim_region
                │
                ▼
        fact_sales

Таблица rls_user_region скрыта от пользователей (hidden=true). Заполняется из ETL по данным AD или CRM: один логин — несколько строк с разными region_id.

DAX-фильтр на dim_region:

dim_region[region_id] IN
    CALCULATETABLE(
        VALUES(rls_user_region[region_id]),
        rls_user_region[user_principal_name] = USERPRINCIPALNAME()
    )

Или через TREATAS (быстрее на больших объёмах):

TREATAS(
    CALCULATETABLE(
        VALUES(rls_user_region[region_id]),
        rls_user_region[user_principal_name] = USERPRINCIPALNAME()
    ),
    dim_region[region_id]
)

Добавили менеджера к новому региону — добавили строку в bridge. Пересборка модели не нужна, права обновляются при следующем входе.

Паттерн 04 · OLSДля: скрытия чувствительных колонок

Object-Level Security: скрыть колонку целиком

RLS прячет строки. OLS прячет таблицы или колонки полностью. Типичный кейс — колонка salary в dim_employee должна быть видна только HR и финблоку.

В SSAS Tabular 2017+ OLS настраивается через XMLA или SSDT: для каждой роли указывается MetadataPermission=None для конкретной колонки. В Power BI Desktop нативной UI для OLS нет — настраивается через Tabular Editor.

Пример (Tabular Editor, C#-скрипт):

// Роль AllEmployees — все видят employee, но не salary
Model.Roles["AllEmployees"].TablePermissions["dim_employee"]
    .ColumnPermissions["salary"].MetadataPermission
    = MetadataPermission.None;

// Роль HR — видят всё
Model.Roles["HR"].TablePermissions["dim_employee"]
    .ColumnPermissions["salary"].MetadataPermission
    = MetadataPermission.Default;

Пользователь в роли AllEmployees не увидит колонку salary вообще: её не будет в панели Fields, её нельзя использовать в мерах. SUMMARIZECOLUMNS с этой колонкой вернёт ошибку доступа. Защита жёсткая, но требует аккуратного тестирования: любая мера, использующая скрытую колонку, сломается для пользователей без прав.

Паттерн 05 · PerformanceДля: моделей с агрегатами и RLS

RLS + агрегационные таблицы: держим согласованность

Если в модели есть aggregation tables (см. P13 про рефакторинг), RLS должен применяться и к ним. Иначе пользователь увидит суммы по регионам, которых формально не видит в детализации — типичная дырка.

Правильно: агрегационная таблица agg_sales_monthly содержит region_id, и RLS-фильтр применяется на dim_region, который связан и с fact_sales, и с agg_sales_monthly. Тогда при любом запросе — идёт ли он в агрегат или в факт — фильтр срабатывает одинаково.

Проверочный DAX-запрос:

// Под ролью Region_North (регион-ID 1)
// эти два числа должны быть равны:

// 1. Из детализации (fact_sales)
SUMX(fact_sales, fact_sales[revenue])

// 2. Из агрегата (agg_sales_monthly)
SUMX(agg_sales_monthly, agg_sales_monthly[revenue])

Если числа разные — или RLS не распространился на агрегат, или в агрегате данные устарели. Оба случая — баг, нужно чинить.

Частая ошибка: агрегационная таблица не связана с dim_region напрямую, а хранит поле region_name дубликатом. RLS на dim_region не фильтрует агрегат. Чтобы исправить: добавьте прямую связь agg_sales_monthly[region_id] → dim_region[region_id] (hidden=true, single direction).
Паттерн 06 · AdminДля: BI-админов и dev-команды

Impersonation: «посмотреть как пользователь X»

При отладке RLS нужно уметь проверить, что видит конкретный пользователь. В Power BI Desktop есть «View as role», но это проверка роли целиком. Для проверки «как конкретный AD-пользователь» есть EffectiveUserName.

В DAX Studio (подключение к SSAS):

Connection properties →
    Roles: Region_Sales
    Effective Username: maria.petrova@company.com

// Далее выполняете любой DAX-запрос — будете
// видеть результат как у Марии Петровой

Это требует, чтобы BI-админ был в группе Server Admin на SSAS (иначе impersonation запрещён). На продакшне используйте с осторожностью: impersonation логгируется, но имеет смысл согласовать с ИБ порядок применения.

Альтернатива — отдельная admin-роль с фильтром «TRUE()» (видит всё). Удобнее для повседневного дев-мониторинга, менее подозрительно для ИБ.

Типичные ошибки, которые ломают RLS

  1. RLS на уровне отчёта, не модели. Каждый новый отчёт нужно настраивать заново, быстро расходится. Правильно — RLS в SSAS, отчёт не задумывается о безопасности.
  2. Hard-coded список пользователей в DAX. Добавили нового сотрудника — нужна пересборка модели. Правильно — таблица dim_employee с AD-login, роль читает её.
  3. Забыть скрыть bridge-таблицу. rls_user_region должна быть hidden=true, иначе пользователи увидят её в отчётах и смогут узнать, кто имеет доступ к их данным.
  4. Не применить RLS к агрегатам. См. паттерн 05. Самая коварная ошибка: числа «правильные» на первый взгляд, но пользователь видит больше, чем должен.
  5. Тестирование только под админом. У админа нет фильтров. Всегда проверяйте под реальным AD-логином (через Effective Username или локальным пользователем).
  6. USERNAME() вместо USERPRINCIPALNAME(). USERNAME возвращает DOMAIN\user, UPN — user@domain. При миграции доменов (или в Power BI Service) старые роли перестают работать.
  7. Неочевидная утечка через DISTINCT/COUNTROWS. Если пользователь не видит строки региона, но может получить количество уникальных customer_id в этом регионе — это уже утечка. Решается dim-мерой, к которой тоже применён RLS.

Как тестировать RLS перед продакшном

Любая RLS-модель должна проходить 4 проверки перед тем, как уйти в прод:

  1. Smoke-test под админом. Видно всё. Базовый sanity-check, что ничего не сломано.
  2. Test под типовым пользователем (например, maria.petrova@). Через View as role + Effective Username в DAX Studio. Проверяете, что видит строки своего региона и не видит чужих.
  3. Контрольная сумма. Суммарный SUM(fact_sales[revenue]) под пользователем должен равняться сумме выручка по его региону из SQL. Любое расхождение — повод разбираться.
  4. Тест утечек через COUNT/DISTINCT. Повторяете проверку 3, но для разных агрегатов: COUNTROWS, DISTINCTCOUNT, MAX/MIN. Иногда именно здесь всплывает, что RLS не полный.

Автоматизировать это можно pytest-скриптом, который через ADOMD.NET выполняет набор DAX-запросов под разными Effective Users и сравнивает с ожидаемыми значениями. На крупных моделях делаем обязательным шагом CI/CD.

Что делать дальше

  • Если RLS ещё не настроен — начните с паттерна 01 (территориальный). Покрывает 70% кейсов, внедряется за 2-3 часа. Дальше по необходимости расширяйте.
  • Если RLS есть, но не уверены в корректности — BI-аудит за 5 дней включает ревизию RLS и тесты утечек. На выходе — отчёт с найденными проблемами и планом исправления.
  • Если нужен полный дизайн RLS с нуля (большая оргструктура, несколько уровней иерархии, OLS) — 30-минутный звонок, обсудим специфику и сроки.

Связанные материалы:

§ Консультация · 30 минут

Нужно спроектировать
RLS-модель?

30 минут — обсудим вашу оргструктуру, уровни доступа, источники данных. Через неделю — дизайн с DAX-кодом и планом внедрения.

Телефон+7 918 042 34 43