Power BI Interview Questions and Answers Part 8 - Row-level Security and Governance

Power BI Interview Q&A · Part 8 of 10

In this post we are going to cover row-level security and governance in Power BI: static and dynamic RLS, who RLS applies to, object-level security, workspace roles and sharing, and the governance features like sensitivity labels and endorsement. These are 7 real Power BI interview questions, and the last one is a scenario a manager asks: a regional manager says she can see other regions' sales.

Security is the topic where a wrong answer can cost a company real trouble. One report, many users, and each person must see only their own data. Interviewers ask about it because the details matter: where roles are defined, who bypasses them, and how you prove the filter works.

Every example below runs on a small sample data set, so you can rebuild it in Power BI Desktop and check each number yourself.

Part 8 of the Real Time Power BI Interview Questions and Answers series on TechBrothersIT.

The short answer

Say this first

RLS filters rows with DAX rules in roles, static or dynamic with USERPRINCIPALNAME(), and applies only to Viewers. OLS hides tables and columns. Share with apps, and govern with sensitivity labels, endorsement, tenant settings and lineage.

Keep that summary in mind. Every question below is one piece of it.

Let's create some sample data

We use a tiny shop: 5 products, 4 customers in two regions, a date table for 2025 and 2026, and 12 sales in January to March of each year. The script below is written for PostgreSQL (the date table uses generate_series), and it works on any database with small changes. Load the four tables into Power BI Desktop and relate fact_sales to each dimension on its key.

Sample data - SQL script (PostgreSQL)
DROP TABLE IF EXISTS fact_sales;
DROP TABLE IF EXISTS dim_product;
DROP TABLE IF EXISTS dim_customer;
DROP TABLE IF EXISTS dim_date;

CREATE TABLE dim_product (product_key INT PRIMARY KEY, product VARCHAR(30), category VARCHAR(20));
INSERT INTO dim_product VALUES
  (1, 'Laptop Stand', 'Accessories'), (2, 'USB-C Cable', 'Accessories'),
  (3, 'Monitor', 'Electronics'),      (4, 'Keyboard', 'Electronics'),
  (5, 'Desk Lamp', 'Home');

CREATE TABLE dim_customer (customer_key INT PRIMARY KEY, customer VARCHAR(30), city VARCHAR(20), region VARCHAR(10), email VARCHAR(40));
INSERT INTO dim_customer VALUES
  (1, 'Sara Khan', 'Toronto', 'East', 'sara@tbshop.com'),
  (2, 'Omar Ali',  'Dallas',  'West', 'omar@tbshop.com'),
  (3, 'Lina Park', 'Seattle', 'West', 'lina@tbshop.com'),
  (4, 'Mei Chen',  'Boston',  'East', 'mei@tbshop.com');

-- one row per day, 2025-01-01 .. 2026-12-31 (calendar year = fiscal year here)
CREATE TABLE dim_date (date DATE PRIMARY KEY, year INT, quarter VARCHAR(2), month_no INT, month VARCHAR(3));
INSERT INTO dim_date
SELECT d, EXTRACT(YEAR FROM d), 'Q' || EXTRACT(QUARTER FROM d), EXTRACT(MONTH FROM d), TO_CHAR(d, 'Mon')
FROM generate_series(DATE '2025-01-01', DATE '2026-12-31', INTERVAL '1 day') AS g(d);

CREATE TABLE fact_sales (
  sale_id      INT PRIMARY KEY,
  order_date   DATE REFERENCES dim_date(date),
  product_key  INT REFERENCES dim_product(product_key),
  customer_key INT REFERENCES dim_customer(customer_key),
  qty          INT,
  unit_price   DECIMAL(10,2)
);
INSERT INTO fact_sales VALUES
  ( 1, '2025-01-10', 1, 1, 2,  40.00), ( 2, '2025-01-22', 3, 2, 1, 250.00),
  ( 3, '2025-02-05', 2, 3, 5,  10.00), ( 4, '2025-02-18', 5, 4, 1,  60.00),
  ( 5, '2025-03-03', 4, 1, 2,  50.00), ( 6, '2025-03-27', 3, 3, 1, 250.00),
  ( 7, '2026-01-08', 1, 2, 3,  40.00), ( 8, '2026-01-25', 3, 1, 1, 250.00),
  ( 9, '2026-02-11', 2, 4, 4,  10.00), (10, '2026-02-20', 4, 3, 2,  50.00),
  (11, '2026-03-09', 5, 2, 2,  60.00), (12, '2026-03-30', 3, 4, 2, 250.00);

Here are the 12 sales. Each row is one sale line: a date, a product, a customer, a quantity and a unit price.

fact_sales12 rows
sale_idorder_dateproduct_keycustomer_keyqtyunit_price
12025-01-1011240.00
22025-01-22321250.00
32025-02-0523510.00
42025-02-1854160.00
52025-03-0341250.00
62025-03-27331250.00
72026-01-0812340.00
82026-01-25311250.00
92026-02-1124410.00
102026-02-2043250.00
112026-03-0952260.00
122026-03-30342250.00

And the customers. Notice the email column: we reuse those emails as user names when we build dynamic RLS in Question 2.

dim_customer4 rows
customer_keycustomercityregionemail
1Sara KhanTorontoEastsara@tbshop.com
2Omar AliDallasWestomar@tbshop.com
3Lina ParkSeattleWestlina@tbshop.com
4Mei ChenBostonEastmei@tbshop.com

Question 1: What is row-level security, and what is the difference between static and dynamic RLS?

Row-level security (RLS) filters which rows a user can see, so everyone can use the same report. You create a role and give it a DAX filter on a table.

Let's write a static rule. The role East gets the rule region equals East on the Customer table.

DAX - Measures.dax
// Modeling > Manage roles
// Role: East    Table: Customer
[Region] = "East"

The filter flows from Customer to Sales through the relationship. Without RLS, everyone sees 1920.00. Here is what a user in the East role sees.

Result on the sample data3 rows
customercitysales
Mei ChenBoston600.00
Sara KhanToronto430.00
Total (East)1030.00
Check this result yourself with SQL

The role filter on Customer flows to Sales, so an East user sees 1030 instead of the full 1920.

SQL
SELECT customer, city, sales FROM (
  SELECT c.customer, c.city, SUM(f.qty * f.unit_price) AS sales, 0 AS srt
  FROM fact_sales f JOIN dim_customer c ON c.customer_key = f.customer_key
  WHERE c.region = 'East'
  GROUP BY c.customer, c.city
  UNION ALL
  SELECT 'Total (East)', '', SUM(f.qty * f.unit_price), 1
  FROM fact_sales f JOIN dim_customer c ON c.customer_key = f.customer_key
  WHERE c.region = 'East') t
ORDER BY srt, customer;

Only the two East customers are left: Mei Chen with 600.00 and Sara Khan with 430.00, so the total is 1030.00 instead of 1920.00.

That is static RLS: the value is hard-coded, so you need one role per region. Dynamic RLS uses one role for everyone, and the rule looks up the signed-in user to decide what each person sees.

Static RLSDynamic RLS
RULE[Region] = "East"[Email] = USERPRINCIPALNAME ()
ROLESone per value: East, West ...one role for everyone
NEW USERadd to a role (or a new role)add a row to a mapping table
Tip

Static is fine for a handful of fixed groups. With many users, or a mapping that changes often, go dynamic: adding a user becomes adding a row, not a new role.

Question 2: How do you implement dynamic RLS with USERPRINCIPALNAME()?

You build a security mapping table with one row per user and region, create one role with a filter on that table, and let USERPRINCIPALNAME() pick the rows for whoever is viewing.

Here is the mapping table. In our sample, Sara and Mei map to East, Omar and Lina map to West.

UserRegion4 rows
emailregion
mei@tbshop.comEast
sara@tbshop.comEast
lina@tbshop.comWest
omar@tbshop.comWest

Now the role. The rule says email equals USERPRINCIPALNAME(), which returns the sign-in name of the viewer, usually their email address.

DAX - Measures.dax
// Role: Regional    Table: UserRegion
[Email] = USERPRINCIPALNAME ()

// UserRegion[Region] filters Customer[Region]
// Customer filters Sales

Relate the mapping table to Customer on region, as a many-to-many relationship filtering in one direction. The filter then flows from UserRegion to Customer, and on to Sales. Here is what Omar sees when he opens the report.

Result on the sample data4 rows
categorysales
Electronics600.00
Accessories170.00
Home120.00
Total890.00
Check this result yourself with SQL

USERPRINCIPALNAME() returns omar@tbshop.com, the mapping keeps West, and Omar sees 890 of the 1920.

SQL
WITH user_region (email, region) AS (
  VALUES ('sara@tbshop.com', 'East'), ('mei@tbshop.com', 'East'),
         ('omar@tbshop.com', 'West'), ('lina@tbshop.com', 'West'))
SELECT category, sales FROM (
  SELECT p.category, SUM(f.qty * f.unit_price) AS sales, 0 AS srt
  FROM fact_sales f
  JOIN dim_customer c ON c.customer_key = f.customer_key
  JOIN dim_product p ON p.product_key = f.product_key
  WHERE c.region IN (SELECT region FROM user_region WHERE email = 'omar@tbshop.com')
  GROUP BY p.category
  UNION ALL
  SELECT 'Total', SUM(f.qty * f.unit_price), 1
  FROM fact_sales f
  JOIN dim_customer c ON c.customer_key = f.customer_key
  WHERE c.region IN (SELECT region FROM user_region WHERE email = 'omar@tbshop.com')) t
ORDER BY srt, sales DESC;

Omar sees only West: Electronics 600.00, Accessories 170.00, Home 120.00, and 890.00 in total out of the full 1920.00.

Two tips for the interview:

  • Use USERPRINCIPALNAME(), not USERNAME(). In Desktop, USERNAME() returns DOMAIN\user.
  • A manager who covers two regions simply gets two rows in the mapping table.

Question 3: Where are RLS roles defined and members assigned, and who bypasses RLS?

You define roles and their DAX rules in Desktop, under Modeling, Manage roles, or in web modeling in the service. Then you publish the model. You assign members in the service, in the semantic model's security settings: users or security groups.

Now the part interviewers really want. RLS only filters people with view access.

Workspace roleRLS applied?
AdminNo: sees all data
MemberNo: sees all data
ContributorNo: sees all data
Viewer / app userYes: filtered

Admins, Members and Contributors can edit the model, so they see all the data. Only Viewers and app users are filtered.

A few more rules worth saying out loud:

  • Security groups are supported. Microsoft 365 groups are not.
  • Service principals can't be members of a role.
  • Roles are additive: someone in two roles sees both sets of rows.
  • A user in no role sees no data at all.

To test, use View as in Desktop or Test as role in the service, then confirm with a real test user. View as uses your own identity, so it can't simulate a guest user.

Question 4: What is object-level security (OLS), and how is it different from RLS?

RLS answers "which rows?". OLS answers "can this user see this table or column at all?". For users in the role, the object simply does not exist: it is not in the field list, and a visual that uses it shows an error that the field can't be found.

A classic use is hiding a salary column. In our sample, we hide the customer email column from sales reps, while they still see the rest of the Customer table. You set this in TMDL view in Desktop, or with an external tool like Tabular Editor, by setting the permission to none. Here is one role that combines both: a row filter on Customer, plus OLS on the email column.

TMDL - model.tmdl
role 'Sales Rep'
    modelPermission: read

    tablePermission Customer = [Region] = "East"
        columnPermission Email = none
RLSOLS
HIDESrowstables, columns
SET INManage rolesTMDL view, Tabular Editor
USER SEESfewer rowsfield not found

Like RLS, OLS restricts Viewers.

Tip

Design pages carefully. A visual built on a hidden column breaks for the users who can't see that column.

Question 5: What are the workspace roles, and how do you share content: apps, direct sharing or Build permission?

There are four workspace roles.

RoleCan do
Adminmanage workspace and access
Memberadd people, share, publish app
Contributorcreate, edit, schedule refresh
Viewerview only; RLS applies

For sharing, pick the tool by audience:

  • App for a big audience. There is one app per workspace, and inside it you can create up to 25 audiences, each seeing different reports.
  • Direct sharing, with a link or with named people, for a few people and ad hoc requests. It does not scale and it is harder to audit.
  • Build permission on the semantic model, so analysts can create their own reports and use Analyze in Excel on your model without joining the workspace.
Licensing

In a shared capacity workspace, every user needs Pro or Premium Per User. On F64 or larger, Viewers can use a free license.

Question 6: How do you govern Power BI content with sensitivity labels, endorsement, tenant settings and lineage?

Governance has four parts, and a good answer names all four.

Sensitivity labels come from Microsoft Purview. A label can be inherited from sources like Azure SQL and Synapse, flows down to downstream items, and travels with exports to Excel, PDF, PowerPoint and PBIX. CSV exports are not protected. And in the service, a label alone does not block access.

Endorsement tells people what to trust.

EndorsementWho sets it
Promotedanyone with write permission
Certifiedreviewers set by an admin
Master datadesignated users, data items

Master data marks core data items, like a customer semantic model, as the authoritative source.

Tenant settings in the admin portal apply to the whole organization: for example, who can export data, who can publish to the web, and who can use Copilot.

Lineage view shows where every item gets its data.

Tip

Before you change a semantic model, run impact analysis to see every report, dashboard and workspace that depends on it.

Question 7: Scenario - a regional manager says she can see other regions' sales. How do you investigate and fix it?

This is the scenario managers love. Our East manager should see 1030.00. Here is what she sees when RLS is not filtering her.

Result on the sample data3 rows
regionsales
East1030.00
West890.00
Total1920.00
Check this result yourself with SQL

Without RLS she sees West too (890) and the grand total 1920; with the East role she should see 1030 only.

SQL
SELECT region, sales FROM (
  SELECT c.region, SUM(f.qty * f.unit_price) AS sales, 0 AS srt
  FROM fact_sales f JOIN dim_customer c ON c.customer_key = f.customer_key
  GROUP BY c.region
  UNION ALL
  SELECT 'Total', SUM(f.qty * f.unit_price), 1 FROM fact_sales f) t
ORDER BY srt, region;

She sees West too, 890.00, and the grand total of 1920.00. Here is how to work through it, in order:

  • Check her workspace role. If she is an Admin, Member or Contributor, RLS does not apply to her at all. This is the most common cause. Make her a Viewer, or give her access through the app.
  • Check role membership. Is she really in the East role, and not also in a broader one through a security group? Roles are additive, so one extra role opens everything.
  • Check that the filter reaches Sales. If the rule's table only reaches Sales against the relationship direction, for example through a bridge table, turn on apply security filter in both directions.
  • Test. Use View as in Desktop, or Test as role in the service. She should see East, 1030.00, not 1920.00.
  • Check the source model. If the report uses a live connection to Analysis Services or another semantic model, the roles live in that model. Fix them there, then confirm with her own login.
Tip

Start with the workspace role. Saying that first tells the manager you know RLS only applies to Viewers.

Key points to remember

  • RLS filters rows: static rules or dynamic with USERPRINCIPALNAME()
  • RLS applies only to Viewers; editors bypass it
  • OLS hides tables and columns (TMDL view)
  • Share with apps; Build for self-service
  • Labels, endorsement, tenant settings, lineage

The full SQL script

If you want to rebuild everything at once, here is the whole script: the sample data and the SQL check behind every result above.

Show the full SQL script (sample data and every check)
Full script - copy and run top to bottom
-- ========================================================================
-- Power BI Interview Q&A #8 - Row-level security and governance
-- TechBrothersIT. Sample data is invented for teaching.
-- Standard SQL, tested on PostgreSQL 16. Runs top to bottom in one go.
-- Run it in an empty database or schema: other videos reuse table names
-- like dim_customer, and their leftover tables can block the DROPs below.
-- PostgreSQL example:  CREATE SCHEMA pbi_q08;  SET search_path TO pbi_q08;
-- SQL Server: write DATE '2026-06-01' as CAST('2026-06-01' AS DATE).
-- ========================================================================

-- Step 0: create and load the sample data
DROP TABLE IF EXISTS fact_sales;
DROP TABLE IF EXISTS dim_product;
DROP TABLE IF EXISTS dim_customer;
DROP TABLE IF EXISTS dim_date;

CREATE TABLE dim_product (product_key INT PRIMARY KEY, product VARCHAR(30), category VARCHAR(20));
INSERT INTO dim_product VALUES
  (1, 'Laptop Stand', 'Accessories'), (2, 'USB-C Cable', 'Accessories'),
  (3, 'Monitor', 'Electronics'),      (4, 'Keyboard', 'Electronics'),
  (5, 'Desk Lamp', 'Home');

CREATE TABLE dim_customer (customer_key INT PRIMARY KEY, customer VARCHAR(30), city VARCHAR(20), region VARCHAR(10), email VARCHAR(40));
INSERT INTO dim_customer VALUES
  (1, 'Sara Khan', 'Toronto', 'East', 'sara@tbshop.com'),
  (2, 'Omar Ali',  'Dallas',  'West', 'omar@tbshop.com'),
  (3, 'Lina Park', 'Seattle', 'West', 'lina@tbshop.com'),
  (4, 'Mei Chen',  'Boston',  'East', 'mei@tbshop.com');

-- one row per day, 2025-01-01 .. 2026-12-31 (calendar year = fiscal year here)
CREATE TABLE dim_date (date DATE PRIMARY KEY, year INT, quarter VARCHAR(2), month_no INT, month VARCHAR(3));
INSERT INTO dim_date
SELECT d, EXTRACT(YEAR FROM d), 'Q' || EXTRACT(QUARTER FROM d), EXTRACT(MONTH FROM d), TO_CHAR(d, 'Mon')
FROM generate_series(DATE '2025-01-01', DATE '2026-12-31', INTERVAL '1 day') AS g(d);

CREATE TABLE fact_sales (
  sale_id      INT PRIMARY KEY,
  order_date   DATE REFERENCES dim_date(date),
  product_key  INT REFERENCES dim_product(product_key),
  customer_key INT REFERENCES dim_customer(customer_key),
  qty          INT,
  unit_price   DECIMAL(10,2)
);
INSERT INTO fact_sales VALUES
  ( 1, '2025-01-10', 1, 1, 2,  40.00), ( 2, '2025-01-22', 3, 2, 1, 250.00),
  ( 3, '2025-02-05', 2, 3, 5,  10.00), ( 4, '2025-02-18', 5, 4, 1,  60.00),
  ( 5, '2025-03-03', 4, 1, 2,  50.00), ( 6, '2025-03-27', 3, 3, 1, 250.00),
  ( 7, '2026-01-08', 1, 2, 3,  40.00), ( 8, '2026-01-25', 3, 1, 1, 250.00),
  ( 9, '2026-02-11', 2, 4, 4,  10.00), (10, '2026-02-20', 4, 3, 2,  50.00),
  (11, '2026-03-09', 5, 2, 2,  60.00), (12, '2026-03-30', 3, 4, 2, 250.00);


-- Step 1: Static RLS: what a user in the East role sees (rule [Region] = "East" on Customer)
SELECT customer, city, sales FROM (
  SELECT c.customer, c.city, SUM(f.qty * f.unit_price) AS sales, 0 AS srt
  FROM fact_sales f JOIN dim_customer c ON c.customer_key = f.customer_key
  WHERE c.region = 'East'
  GROUP BY c.customer, c.city
  UNION ALL
  SELECT 'Total (East)', '', SUM(f.qty * f.unit_price), 1
  FROM fact_sales f JOIN dim_customer c ON c.customer_key = f.customer_key
  WHERE c.region = 'East') t
ORDER BY srt, customer;

-- Step 2: Dynamic RLS: what omar@tbshop.com sees through the user-region mapping table
WITH user_region (email, region) AS (
  VALUES ('sara@tbshop.com', 'East'), ('mei@tbshop.com', 'East'),
         ('omar@tbshop.com', 'West'), ('lina@tbshop.com', 'West'))
SELECT category, sales FROM (
  SELECT p.category, SUM(f.qty * f.unit_price) AS sales, 0 AS srt
  FROM fact_sales f
  JOIN dim_customer c ON c.customer_key = f.customer_key
  JOIN dim_product p ON p.product_key = f.product_key
  WHERE c.region IN (SELECT region FROM user_region WHERE email = 'omar@tbshop.com')
  GROUP BY p.category
  UNION ALL
  SELECT 'Total', SUM(f.qty * f.unit_price), 1
  FROM fact_sales f
  JOIN dim_customer c ON c.customer_key = f.customer_key
  WHERE c.region IN (SELECT region FROM user_region WHERE email = 'omar@tbshop.com')) t
ORDER BY srt, sales DESC;

-- Step 3: Scenario: what the manager sees when RLS does not filter her (all regions)
SELECT region, sales FROM (
  SELECT c.region, SUM(f.qty * f.unit_price) AS sales, 0 AS srt
  FROM fact_sales f JOIN dim_customer c ON c.customer_key = f.customer_key
  GROUP BY c.region
  UNION ALL
  SELECT 'Total', SUM(f.qty * f.unit_price), 1 FROM fact_sales f) t
ORDER BY srt, region;
Next in this series

Part 9: Deployment, Git and Refresh

The sample data on this page is invented for teaching. The result tables were produced by running the same calculation in SQL on PostgreSQL 16 over that sample data (open "Check this result yourself" under any result), so you can rebuild every number in Power BI Desktop and compare.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.