Rules / Performance

Check if dynamic row level security (RLS) is necessary

info CHECK_IF_DYNAMIC_ROW_LEVEL_SECURITY_(RLS)_IS_NECESSARY · ported · model layer · scope: TablePermission

What it checks

Row-level security filters that call USERNAME or USERPRINCIPALNAME. Reported per table permission, at info severity.

Each finding names the table the permission filters, as User Region, and carries the role beside it, as role Regional Users. The table name has no quotes around it here, because the object reported is the permission rather than the table.

Example

Fires the rule
table Sales
	column Region
		dataType: string
		sourceColumn: Region

	column Amount
		dataType: decimal
		summarizeBy: sum
		sourceColumn: Amount

table 'User Region'
	column 'User Email'
		dataType: string
		sourceColumn: UserEmail

	column Region
		dataType: string
		sourceColumn: Region

role 'Regional Users'
	modelPermission: read

	tablePermission 'User Region' = 'User Region'[User Email] = USERPRINCIPALNAME()
After the fix
table Sales
	column Region
		dataType: string
		sourceColumn: Region

	column Amount
		dataType: decimal
		summarizeBy: sum
		sourceColumn: Amount

role East
	modelPermission: read

	tablePermission Sales = 'Sales'[Region] = "East"

role West
	modelPermission: read

	tablePermission Sales = 'Sales'[Region] = "West"

Why it matters

A dynamic filter is evaluated for each user separately, so the engine cannot share a cached result between two people with the same access, and every query carries the lookup that maps the user to their rows. That is the right trade when the audience is large or changes often. When a handful of fixed groups each see a fixed slice, static roles with a plain filter are faster and simpler to audit.

How to fix it

Decide whether the mapping has to be dynamic, then act on the decision. Keep the dynamic filter where the user-to-rows mapping lives in a table that changes without a redeploy, or where there are more audiences than anyone would maintain by hand. Otherwise replace it: in Power BI Desktop, under Modeling, Manage roles, create one role per audience with a plain filter such as [Region] = "East", then delete the dynamic role in the same dialog. In the TMDL file each role is a role block in the roles folder, and each filter is the expression after the = on a tablePermission line under it, so a static filter is the same line with the USERPRINCIPALNAME call gone. Members are assigned after publishing, in the workspace under Security on the semantic model; Power BI Desktop writes roles but never members. Where the dynamic filter stays, the thing worth shortening is the lookup behind it: hold the user-to-key mapping in a small table with the key already computed, so the filter is one comparison instead of a search.

When to ignore it

Dynamic row-level security is a design, not a defect, and this rule is a prompt to confirm the design once rather than a queue to clear. Ignore it wherever the mapping table is the point: a model whose audience is every salesperson in the company, a hierarchy where a manager sees their reports' rows, anything where a new user should get access by appearing in a table rather than by someone editing the model. The finding is worth a second look in two places. One is a model with two or three fixed audiences, where static roles are simpler to explain to whoever audits access. The other is a dynamic filter over a large secured table, where the per-user lookup runs on every query and the fix is to precompute the key rather than to drop the pattern.

To ignore this rule on one object, add annotation pbiplint.ignore = CHECK_IF_DYNAMIC_ROW_LEVEL_SECURITY_(RLS)_IS_NECESSARY under the object in its TMDL file. Power BI Desktop keeps the annotation. To turn the rule off for a whole project, set "CHECK_IF_DYNAMIC_ROW_LEVEL_SECURITY_(RLS)_IS_NECESSARY": "off" under rules in pbiplint.config.json.

Quirks

Ported from Microsoft's Best Practice Analyzer ruleset (opens in a new tab).

Check a model for this Improve this page (opens in a new tab)