Power BI · Chapter 11 · Concept lesson · 7 min read

Row-level security is a filter on the model

Row-level security is not a hidden slicer on a page. It is a filter the model applies before any measure runs, which is why it follows the relationships, why it can be dynamic, and why it only protects the people it is applied to.

The idea

Row-level security (RLS) is a filter on the model that the reader cannot turn off. You write a rule for a table, a true/false expression such as customers[segment] = "business", and for anyone in that role the table keeps only the rows where the rule is true. Everything else is gone before a single measure runs.

The important word is model. RLS is not a slicer hidden on a page and it is not a filter on a visual. It is applied to the data the whole report reads, so a table, a chart, a tooltip and an export all see the same reduced rows.

Figure 1

An RLS rule on customers travels like any other filter

'Date'

  • Date
  • Year
  • Month

orders

  • order_id
  • customer_id
  • channel
  • status

customers

  • customer_id
  • segment
  • state

state in {CA, OR, WA, NV, AZ}

products

  • product_id
  • category

order_items

  • order_id
  • product_id
  • line_total

Step 1 of 4 The Harbor & Pine model. Ava Reed should only see customers in her states. Customer data lives in customers, so that is where the rule goes.

  1. The Harbor & Pine model. Ava Reed should only see customers in her states. Customer data lives in customers, so that is where the rule goes.
  2. The rule is a filter on customers, here state in her five states. It is applied before any measure or visual runs, and the reader cannot remove it.
  3. It flows down the relationships like a slicer: first to orders, then to order_items. Every sales figure Ava sees is now built from her customers' lines only.
  4. products and 'Date' are not filtered: she still sees every product name and date, with sales only for her customers. RLS filters rows through relationships, nothing more.

Why it exists

Harbor & Pine's forty managers open the same report each morning, and each should see only the customers they are responsible for. There are two ways to give them that: build forty reports and keep them in step, or build one and let the model decide what each person may see. The second is the only one that scales, and the only one that changes automatically when a territory moves.

It is also a safety property. If a number in a report is wrong because of a slicer, someone clicked the wrong thing. If a restricted row appears to the wrong person, the report has leaked. Security that depends on readers behaving is not security.

How it works under the hood

Evaluate a rule in four steps.

  1. The user signs in. The Service knows their email, which a rule can read with USERPRINCIPALNAME().
  2. The user's role applies. Roles are defined in Desktop (Modeling → Manage roles) and people are added to them in the Service (dataset Security).
  3. The rule filters its table. A static rule names the values (state in a list). A dynamic rule looks them up from a table, so the logic is written once.
  4. The filter flows along the relationships. Filtering customers filters orders and then order_items, exactly as a slicer would. Tables with no relationship to customers are not filtered.

Static and dynamic

A static role is one rule per group: West, Texas, B2B. It is simple and right for a handful of groups. It becomes a chore with twenty, and every quarter someone edits the model.

A dynamic rule asks "who is this person?" and looks up what they may see.

Figure 2

One dynamic rule, evaluated customer by customer

customers
customer_idstatesegmentSales
3CAbusiness$27,371.95
26CAbusiness$48,298.90
56NYbusiness$9,744.38
221TXbusiness$13,114.99
106FLbusiness$6,153.25
Territories (lookup)
statemanager_email
CAava.reed@harborandpine.com
NYgrace.kim@harborandpine.com
TXluis.ortega@harborandpine.com
What Ava sees
customer_idSales
3$27,371.95
26$48,298.90

Step 1 of 5 The rule is LOOKUPVALUE(Territories[manager_email], Territories[state], customers[state]) = USERPRINCIPALNAME(). For each customer it finds the owner of their state and compares it with the signed-in user.

  1. The rule is LOOKUPVALUE(Territories[manager_email], Territories[state], customers[state]) = USERPRINCIPALNAME(). For each customer it finds the owner of their state and compares it with the signed-in user.
  2. Ava Reed signs in. Customer 3 is in CA, whose manager is ava.reed@harborandpine.com. That matches, so the row stays.
  3. Customer 56 is in NY, owned by Grace, and 221 in TX, owned by Luis. The comparison fails and the rows are gone for Ava.
  4. Customer 106 is in FL, which is not in Territories. The lookup returns blank, a blank never equals an email, and the row disappears for everyone, until Sales Ops adds the state.
  5. Ava sees two customers and their orders: $27,371.95 + $48,298.90 = $75,670.85 of sales. Changing the rows in Territories changes who sees what, with no change to the model.

The mapping lives in a table (Territories here) that Sales Ops edits as plain data. The model and the rule never change. Two details matter: compare to USERPRINCIPALNAME(), and think about the person who is not in the table, because for them the rule is false on every row.

Figure 3

From sign-in to filtered numbers

  1. Sign in The Service knows the user's email
  2. Role Which role the user is a member of
  3. [state] in {CA, OR} The rule, one per table
  4. Model Filter flows along the relationships
  5. Visuals Measures run on the filtered rows

Step 1 of 5 Security starts at sign-in. The Service knows who is asking.

  1. Security starts at sign-in. The Service knows who is asking.
  2. In **Manage roles**, you define roles and add people or groups to them in the Service. The user's role decides which rules apply.
  3. A role holds rules, each a true/false expression on one table. Rows where it is true are visible.
  4. The rule's filter flows to related tables, the way a slicer does.
  5. Only then do the visuals evaluate their measures. The user never sees an unfiltered number.

Combining rules

Two combinations come up as soon as the report grows. Inside one role, a rule on customers and another on products both apply: a row must pass both, so they narrow each other. Across roles, the opposite holds. A person who belongs to two roles gets the union: a row is visible if either role allows it. A common mistake is to add someone to a broad role "just in case" and quietly cancel the narrow role that was meant to restrict them.

A single rule can also combine tests with &&. The B2B report uses one: customers[segment] = "business" AND the signed-in user is the rep or the manager listed for the customer's state in a Reps table. Business customers only, and only the states that person covers. Writing the lookups into VAR lines keeps a rule like that readable: one line for the user, one for the rep, one for the manager, and a short test in RETURN.

Who RLS applies to

RLS applies to people with the Viewer role in a workspace and to app users. It does not apply to people who can edit the content: Admin, Member and Contributor see all rows, because they can open the model and remove the rule anyway. So a test with your own admin account proves nothing. Use Modeling → View as in Desktop, or Test as role in the Service, to see what a user sees.

What RLS does not do

It does not hide columns (that is object-level security), and it does not make a model safe if a table that should be restricted has no relationship to the table carrying the rule. Check each table's path to the rule before you publish. What it does do is protect the model rather than the report: a Viewer with Build permission who queries the dataset from Excel gets the same filtered rows.

Common misconceptions

"I'll test the rule by opening the report myself"

If you are the owner or an admin, RLS does not apply to you and you will see everything. A rule you never viewed as another user is untested. Use View as with a real role and a real email, and also test someone who matches nothing.

"RLS hides rows in the visual"

It removes them from the model for that user, which is stronger: the rows are not hidden on the page, they do not exist for that person. That is why totals, tooltips and exports all agree with what the person sees, and why a dashboard total can differ between two people.

"One RLS role per region is the standard"

It is the simplest, and it works for three regions. At twenty it is a maintenance burden, and the roles drift from reality. A dynamic rule with a mapping table scales and is easier to audit, because the rule is written once and the data shows who sees what.

Check yourself

One question. Answering it ticks this lesson off on the map; it never locks a level.

The dynamic territory rule is live. Hannah Moore signs in; her email is not in Territories. What does she see in the report?

Pick what may run on this site. You can change this any time from "Cookie settings" at the bottom of every page.