Skip to main content

Roles

In roles.yaml we define the roles that can be used in model.yaml. To add users to a role, define them in roles.yaml. Since this can be quite a long list of users, it's most advisable to use Entra-groups where those people can be added, so that roles.yaml only contains that Entra-group.

Next to specific rights on the model, there are also more general rights on PowerBI.com, like workspace access and dataset permissions (Build, Write, Admin etc).

For the latter, you are advised to use Entra-groups as well, but it is out of the scope of Tabular. To develop Power BI reports or connect with Excel you need to be in such a group.

Defining a role

- Role:
Name: NoFinance
Alias: NoFinance
Description: "Access without finance data"
Groups:
- NoFinance@easytabular

A role supports the following keys:

KeyTypeDescription
NamestringRole name. This is the value referenced from PermittedRoles, ProhibitedRoles and Daxfilters.
AliasstringFriendly alias for the role.
DescriptionstringFree-text description.
Groupsstring[]Entra (Azure AD) group names added as members of the role.
Membersstring[]Individual Entra (Azure AD) user principals added as members of the role.
DaxfilterslistRow-level security filters; see Row-level security.

Use Groups for Entra-groups and Members for individual users — both are added to the role as Azure AD members. Every role is granted model-level Read; the keys below narrow that access down.

Restricting tables and columns

In model.yaml a role can be set on a table or a column with the ProhibitedRoles or PermittedRoles key (string array). With ProhibitedRoles defined on a table, the whole table is no longer accessible for that role; with PermittedRoles set, the role has access to the table. For a column this works the same way. The default is that every role has access (permitted) to all objects in a Tabular model.

- Table:
Name: Finance
TableType: Fact
Description: "Fact table with sensitive finance data"
ProhibitedRoles: [NoFinance]

In this case the whole fact table Finance is not accessible for the role NoFinance.

Row-level security (Daxfilters)

Daxfilters restrict which rows a role can see, rather than hiding a whole table or column. Each entry maps a table to a DAX boolean expression; only rows for which the expression evaluates to true are visible to members of that role.

Define the filters under the role in roles.yaml:

- Role:
Name: SalesNL
Alias: SalesNL
Description: "Sales reps - Netherlands only"
Groups:
- SalesNL@easytabular
Daxfilters:
- Table: Customer
Expression: "Customer[Country] = \"NL\""
KeyTypeDescription
TablestringName of the Tabular table the filter applies to.
ExpressionstringDAX boolean expression; rows where it is true remain visible.

A Daxfilter stands on its own — you do not need to also list the role in the table's PermittedRoles. Every role already has read access to all tables (model-level Read); the filter narrows that access down to the matching rows.

With the role above, members of SalesNL can read the Customer table but only see rows where Customer[Country] = "NL".

note

The Table of a Daxfilter must match the Tabular table Name exactly (case-sensitive), and a role may define at most one Daxfilter per table. A filter pointing at a table that is not defined anywhere in the model, or more than one Daxfilter on the same table, fails generation with an error — so the mistake surfaces at build time rather than as missing or ambiguous row-level security at run time.

Derived models

A role can be shared across the base model and a derived model. When a Daxfilter targets a table that exists in the base but is left out of a derived model, the filter cannot be applied in that derived model. Generation does not fail (this is a valid setup), but it emits a build warning: the filter is simply not enforced there, so members of the role are not restricted by it in that model. Review these warnings — if the unfiltered table (or facts related to it) is present in the derived model, the role will see its data unrestricted.

Example: dynamic row-level security from Entra + source data

A static Daxfilter hard-codes the allowed values per role, so every change means editing roles.yaml. For security that follows your data — each user sees only the rows for their own department, managed centrally and refreshed with every load — drive the filter from a security bridge table instead of from a literal expression.

The idea: combine Entra (who the user is) with a source system (what they are allowed to see) into one helper dimension, then write a single Daxfilter that looks the current user up in that table at query time.

1. Build the security bridge table

In a notebook, load Entra into the data platform (the user's UserPrincipalName and group memberships) and join it to employee information from a source system — for example the department an employee works in. Materialise the result as a view, e.g. dm.D_UserRoleLevelSecurity, in an entity-attribute-value shape so one helper table can secure any dimension:

UserPrincipalNameTableNameAttributeAttributeValue
ann@contoso.comDepartmentDepartment code100
ann@contoso.comDepartmentDepartment code200
bob@contoso.comDepartmentDepartment codeALL

One row per user per allowed value. The sentinel value ALL grants unrestricted access — handy for managers and admins.

2. Add the helper dimension to the model

Add the table as a hidden helper dimension in model.yaml. IsHidden: True keeps it out of the report field list:

- Table:
Name: UserRoleLevelSecurity
SourceSchema: dbo
SourceTable: D_UserRoleLevelSecurity
TableType: Dim
IsHidden: True
Description: "Helper dimension: User Role Level Security"
Columns:
- SourceColumn: UserPrincipalName
Type: varchar(255)
- SourceColumn: TableName
Type: varchar(255)
- SourceColumn: Attribute
Type: varchar(255)
- SourceColumn: AttributeValue
Type: varchar(255)

Column display names are derived from the source columns, so UserPrincipalName becomes User Principal Name and AttributeValue becomes Attribute Value — these are the names the DAX below refers to.

3. Filter the real dimension with USERPRINCIPALNAME()

Add a role with a Daxfilter on the dimension you want to secure (here Department). The expression looks the signed-in user up in the helper table, honours the ALL escape hatch, and otherwise keeps only the departments listed for that user:

- Role:
Name: DepartmentSecurity
Alias: DepartmentSecurity
Description: "Each user sees only their own department(s)"
Groups:
- AllEmployees@contoso
Daxfilters:
- Table: Department
Expression: |
VAR UserRightsTable =
FILTER(
UserRoleLevelSecurity,
[User Principal Name] = USERPRINCIPALNAME() &&
UserRoleLevelSecurity[Table Name] = "Department" &&
UserRoleLevelSecurity[Attribute] = "Department code"
)

VAR HasAllAccess =
CALCULATE(
COUNTROWS(UserRightsTable),
UserRoleLevelSecurity[Attribute Value] = "ALL"
) > 0

RETURN
IF(
HasAllAccess,
TRUE(),
Department[Department code] IN
SELECTCOLUMNS(
UserRightsTable,
"AllowedValues",
UserRoleLevelSecurity[Attribute Value]
)
)

Now everyone in the AllEmployees@contoso group shares a single role, but each person sees only their own department's rows — with managers flagged ALL in the bridge table seeing everything. Access changes are made in the source/Entra data and take effect on the next load, with no edit to roles.yaml.