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-паттернов
Территориальный 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).
Иерархический 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 работает быстрее.
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. Пересборка модели не нужна, права обновляются при следующем входе.
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 с этой колонкой вернёт ошибку доступа. Защита жёсткая, но требует аккуратного тестирования: любая мера, использующая скрытую колонку, сломается для пользователей без прав.
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 не распространился на агрегат, или в агрегате данные устарели. Оба случая — баг, нужно чинить.
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
- RLS на уровне отчёта, не модели. Каждый новый отчёт нужно настраивать заново, быстро расходится. Правильно — RLS в SSAS, отчёт не задумывается о безопасности.
- Hard-coded список пользователей в DAX. Добавили нового сотрудника — нужна пересборка модели. Правильно — таблица dim_employee с AD-login, роль читает её.
- Забыть скрыть bridge-таблицу. rls_user_region должна быть hidden=true, иначе пользователи увидят её в отчётах и смогут узнать, кто имеет доступ к их данным.
- Не применить RLS к агрегатам. См. паттерн 05. Самая коварная ошибка: числа «правильные» на первый взгляд, но пользователь видит больше, чем должен.
- Тестирование только под админом. У админа нет фильтров. Всегда проверяйте под реальным AD-логином (через Effective Username или локальным пользователем).
- USERNAME() вместо USERPRINCIPALNAME(). USERNAME возвращает DOMAIN\user, UPN — user@domain. При миграции доменов (или в Power BI Service) старые роли перестают работать.
- Неочевидная утечка через DISTINCT/COUNTROWS. Если пользователь не видит строки региона, но может получить количество уникальных customer_id в этом регионе — это уже утечка. Решается dim-мерой, к которой тоже применён RLS.
Как тестировать RLS перед продакшном
Любая RLS-модель должна проходить 4 проверки перед тем, как уйти в прод:
- Smoke-test под админом. Видно всё. Базовый sanity-check, что ничего не сломано.
- Test под типовым пользователем (например, maria.petrova@). Через View as role + Effective Username в DAX Studio. Проверяете, что видит строки своего региона и не видит чужих.
- Контрольная сумма. Суммарный
SUM(fact_sales[revenue])под пользователем должен равняться сумме выручка по его региону из SQL. Любое расхождение — повод разбираться. - Тест утечек через 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-минутный звонок, обсудим специфику и сроки.
Связанные материалы:
- Power BI для среднего бизнеса: полное руководство — роль SSAS в BI-архитектуре
- Рефакторинг Power BI в 10 раз — про производительность, в том числе RLS + агрегаты
- DAX для управленцев — базовые формулы, которые стоит понимать