Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles

Implementing Object-Level Security in Power BI to Restrict Table and Column Access for Sensitive Enterprise Data

Power BI🔥 Expert31 min readJun 28, 2026Updated Jun 28, 2026
Table of Contents
  • Introduction
  • Prerequisites
  • Understanding the Security Layers: RLS, OLS, and Why You Need Both
  • Row-Level Security: What It Does and What It Doesn't
  • Object-Level Security: What It Actually Does
  • The Third Layer: Field-Level Filtering (What OLS Is Not)
  • The Licensing Reality
  • Setting Up Your Development Environment
  • Connecting Tabular Editor to a Published Model via XMLA
  • Connecting Tabular Editor to Power BI Desktop
  • Your Scenario: The Contoso HR Analytics Model
  • Implementing OLS: The Core Mechanics

Implementing Object-Level Security in Power BI to Restrict Table and Column Access for Sensitive Enterprise Data

Introduction

Picture this: your organization has just completed a major data consolidation effort. You've built a beautiful, unified Power BI semantic model that pulls together HR data, financial records, customer PII, and operational metrics — all in one place. Your executives love it. Your analysts love it. And then your CISO walks over with that look on their face.

"Who can see the salary columns? Can an intern in marketing pull up executive compensation data? What about social security numbers — is that field visible to everyone who has access to the report?"

You confidently say "We have Row-Level Security in place." And your CISO says, "That's great. But can a Power BI user browse the schema and see that the SocialSecurityNumber column exists, even if they can't see the data?"

Silence.

This is the exact scenario that Object-Level Security (OLS) was built to solve. Row-Level Security (RLS) restricts which rows a user can see. OLS goes a step further — it hides entire tables or columns from specific users at the metadata level. The column doesn't just show blank data; it literally does not exist from the user's perspective. They cannot see it in field lists, they cannot reference it in DAX measures, and they cannot discover it through the Power BI REST API. That's the kind of granular, enterprise-grade access control that satisfies compliance auditors, satisfies your CISO, and actually reflects how sensitive data should be managed.

By the end of this lesson, you will be able to design, implement, test, and troubleshoot a complete Object-Level Security architecture in Power BI — including the integration points with Azure Active Directory groups, the Tabular Object Model, and the practical limitations that will affect your design decisions.

What you'll learn:

  • The architectural difference between RLS, OLS, and field-level filtering — and when each is the right tool
  • How to implement OLS using Tabular Editor 2 and the XMLA endpoint against a Power BI Premium or Fabric-capacity semantic model
  • How to define OLS roles, assign AAD users and groups, and validate access as a specific user persona
  • How measures, relationships, and dependent objects behave when a column or table is secured — and how to design around those constraints
  • How to integrate OLS with RLS in a unified security model and manage the result without losing your mind

Prerequisites

This is an expert-level lesson. You should arrive with:

  • Solid experience building Power BI semantic models (calculated columns, DAX measures, relationships)
  • Familiarity with Row-Level Security — you've implemented it before and understand how roles work
  • A Power BI Premium Per User (PPU), Premium capacity (P-SKU), or Microsoft Fabric F-SKU workspace — OLS requires XMLA read/write access, which is not available on shared (Pro-only) workspaces
  • Tabular Editor 2 (free, open-source) or Tabular Editor 3 installed on your machine — this is the primary tool for defining OLS because Power BI Desktop has no native UI for it
  • Basic comfort with JSON or the Tabular Object Model (TOM) structure — you don't need to be a developer, but you shouldn't panic when you see a JSON object

If you're on a shared capacity workspace, you will not be able to follow along with the XMLA deployment steps. Everything in Power BI Desktop will still work, but you won't be able to publish and enforce OLS in service without the right licensing.


Understanding the Security Layers: RLS, OLS, and Why You Need Both

Before writing a single line of OLS configuration, you need to understand where OLS fits in Power BI's security stack — because misunderstanding this leads to the most common architecture mistakes.

Row-Level Security: What It Does and What It Doesn't

RLS uses DAX filter expressions evaluated at query time. When a user runs a report, Power BI evaluates the RLS role assigned to that user, applies the filter to the relevant table, and propagates that filter through the model via relationships. A salesperson in the Eastern region only sees Eastern region rows.

But here's the critical limitation: RLS doesn't hide the existence of columns or tables. A user can open Power BI Desktop, connect to a shared dataset via "Connect to a dataset in the Power BI service," and browse the field list. Every column and table is visible in that field list, even if the user's RLS role filters the actual data to zero rows. If you have a Salary column, a savvy user can write SELECTEDVALUE(Employee[Salary]) in a new report, get a blank result due to RLS filtering everything out — but they now know that a Salary column exists in your model.

Object-Level Security: What It Actually Does

OLS operates at the metadata level. When a column or table has an OLS restriction applied for a given role:

  1. The field does not appear in the field list of any connected tool (Power BI Desktop, Excel, third-party tools using XMLA)
  2. Any DAX expression that references the secured object returns an error, not a blank — specifically, it throws an "OLS violation" error visible to the user building the expression
  3. The object is not returned by the Power BI REST API's schema endpoints for that user
  4. The restricted column does not appear in the model's metadata when queried via XMLA

This is a categorically different kind of restriction. You're not filtering data — you're hiding the schema.

The Third Layer: Field-Level Filtering (What OLS Is Not)

Some developers confuse OLS with filtering — like, "hide this column for users in Europe but show them a sanitized version." OLS doesn't work that way. OLS is binary: a user either has access to an object (can see it and query it) or they're in a role that restricts it (the object is invisible and inaccessible). There's no "show a masked version" natively in OLS — that pattern requires a different approach using DAX measures that perform masking logic, which we'll touch on later.

The Licensing Reality

OLS enforcement requires a Premium or Fabric capacity. More specifically:

  • Defining OLS roles: You need XMLA endpoint write access, which requires PPU, P-SKU, or F-SKU at the workspace level
  • Enforcing OLS: The semantic model must live in a Premium or Fabric workspace. Users accessing it need appropriate licenses (PPU or Pro/Fabric, depending on your licensing setup)
  • Testing OLS in Power BI Desktop: You can define roles in Power BI Desktop's Model view using the Security menu, but OLS role creation requires Tabular Editor connected to the model, or the PBIX file with OLS defined via Tabular Editor before publishing

The practical implication: if your organization is on shared capacity, OLS is not available. This is a hard architectural constraint, not a configuration issue.


Setting Up Your Development Environment

Before implementing OLS, you need the right tooling. Power BI Desktop as of mid-2024 supports viewing OLS roles through the Model view Security panel, but creating and editing OLS restrictions requires either:

  1. Tabular Editor 2 (free) — connected to the XMLA endpoint of a published semantic model, or editing the PBIX file directly via the external tool integration
  2. Tabular Editor 3 (paid) — same capabilities, better UX and debugging tools
  3. XMLA endpoint with TMSL/JSON scripts — for scripted deployments, useful for ALM pipelines

For this lesson, we'll use Tabular Editor 2, which is the most accessible option.

Connecting Tabular Editor to a Published Model via XMLA

When your model is published to a Premium/Fabric workspace, you can connect Tabular Editor directly to it using the XMLA endpoint. This lets you make live changes to the semantic model without republishing from Desktop.

Find your XMLA endpoint in Power BI service: open the workspace settings, navigate to the Premium tab, and copy the workspace connection string. It looks like this:

powerbi://api.powerbi.com/v1.0/myorg/YourWorkspaceName

In Tabular Editor 2, go to File > Open > From DB (or From Service), enter the XMLA connection string, authenticate with your Power BI credentials, and select the semantic model. You'll see the full tabular object model in the left panel.

Connecting Tabular Editor to Power BI Desktop

If you want to define OLS in the PBIX before publishing — which is the right approach for source-controlled deployments — open your model in Power BI Desktop first, then launch Tabular Editor 2 from the External Tools ribbon tab. Tabular Editor connects to Desktop's Analysis Services instance running on a local port. Changes you make are reflected live in the Desktop model.

Warning: When working through Desktop, Tabular Editor is connected to the in-memory model. Save your work in Tabular Editor (Ctrl+S pushes changes to Desktop's model), then save the PBIX from Desktop. If you close Desktop without saving the PBIX, your OLS definitions are lost.


Your Scenario: The Contoso HR Analytics Model

Throughout this lesson, we'll work with a realistic model: Contoso's HR Analytics semantic model, which contains:

  • Employee table: EmployeeID, FullName, Department, JobTitle, HireDate, TerminationDate, ManagerID, SocialSecurityNumber, HomeAddress, EmergencyContact
  • Compensation table: EmployeeID, BaseSalary, BonusAmount, BonusPercentage, StockGrantValue, TotalCompensation, EffectiveDate
  • Performance table: EmployeeID, ReviewPeriod, PerformanceScore, ReviewerID, Notes
  • Department table: DepartmentID, DepartmentName, CostCenter, HeadCount
  • DateDim table: standard date dimension

The access requirements coming from the CISO and HR leadership:

User Group What They Can See
HR Admins Everything
HR Business Partners Employee, Department, DateDim — but NOT SocialSecurityNumber, HomeAddress, EmergencyContact. Full access to Performance
Finance Analysts Department, DateDim, Compensation (all columns) — but NOT Employee PII columns
Managers Department, DateDim, Performance (their direct reports only via RLS), Employee (limited columns), NOT Compensation
General Staff Department, DateDim, Employee (name, department, job title only) — NO Compensation, NO Performance, NO PII

This is a realistic, multi-layered access matrix. We'll implement the OLS components of it — the parts that require hiding tables and columns — while noting where RLS takes over for row-based filtering.


Implementing OLS: The Core Mechanics

OLS works through the roles system in the tabular model. Each role you define in your model can have permissions applied at three levels:

  1. Model level: the role has read access to the entire model (default)
  2. Table level: the role's access to a specific table is set to None (table is invisible) or Read (table is visible with all accessible columns)
  3. Column level: within a table, a specific column's access is set to None or Read

The relationship between these levels is additive-from-none: if you set a table to None, all columns in that table are invisible regardless of column-level settings. If you set a table to Read but set specific columns to None, those columns are hidden while the rest are accessible.

Creating the HR Business Partner Role

Let's start with the HR Business Partner role. These users need to see the Employee table — but without SocialSecurityNumber, HomeAddress, and EmergencyContact.

In Tabular Editor 2, with your model open:

  1. In the left panel, expand the model tree until you see Roles
  2. Right-click Roles and select Add Role
  3. Name the role HR_BusinessPartner
  4. With the role selected, look at the right panel (properties). Set the Model Permission to Read

Now, configure column-level OLS for this role. Expand the Tables node, then expand Employee. You'll see all columns listed. Click on SocialSecurityNumber. In the properties panel, you'll see an Object-Level Security section (or in Tabular Editor, you navigate through the role's properties). Here's the pattern you need:

In Tabular Editor 2, the cleanest way to set OLS is through the role's property grid. With the HR_BusinessPartner role selected, look at its Table Permissions and Column Permissions in the right pane. You'll set these directly.

However, the most reliable approach — especially for multiple objects — is using Tabular Editor's scripting capability. Here's a C# script you can run in Tabular Editor (Tools > C# Script) to configure the HR_BusinessPartner role's OLS:

// Get the role we're configuring
var role = Model.Roles["HR_BusinessPartner"];

// Set column-level None access for PII columns in Employee table
var employeeTable = Model.Tables["Employee"];

// Columns to restrict
var restrictedColumns = new[] { 
    "SocialSecurityNumber", 
    "HomeAddress", 
    "EmergencyContact" 
};

foreach (var colName in restrictedColumns)
{
    var col = employeeTable.Columns[colName];
    // Set OLS to None for this role
    role.ColumnPermissions.Add(new ColumnPermission() 
    { 
        Column = col, 
        MetadataPermission = MetadataPermission.None 
    });
}

// Save changes to the model
Model.SaveChanges();

Note: In Tabular Editor 2's C# scripting, the exact API may vary slightly by version. If ColumnPermissions.Add() throws an error, you can alternatively set permissions through the GUI by navigating to each column under the role's permission tree.

Alternatively, you can set OLS through Tabular Editor's GUI by selecting the role, expanding its properties, and finding the Table Permissions and Column Permissions nodes. This involves right-clicking to add permission entries for each column you want to restrict.

Understanding MetadataPermission Values

OLS in the Tabular Object Model uses a MetadataPermission enum with two relevant values:

  • Read — the user can see and query this object (same as no restriction)
  • None — the object is completely hidden from this role's perspective

There's also Default, which means "inherit from the model-level permission," and is the starting state for all objects. You don't need to explicitly set Read for every column — Default behaves as Read unless you're explicitly restricting.

Configuring Table-Level OLS for General Staff

The General Staff role is more aggressive — they shouldn't see the Compensation or Performance tables at all. Set this at the table level:

var role = Model.Roles["GeneralStaff"];

// Hide entire tables
var tablesToHide = new[] { "Compensation", "Performance" };

foreach (var tableName in tablesToHide)
{
    var table = Model.Tables[tableName];
    role.TablePermissions.Add(new TablePermission() 
    { 
        Table = table, 
        MetadataPermission = MetadataPermission.None 
    });
}

// Within Employee table, restrict PII and sensitive columns
var employeeTable = Model.Tables["Employee"];
var restrictedEmployeeColumns = new[] { 
    "SocialSecurityNumber", 
    "HomeAddress", 
    "EmergencyContact",
    "ManagerID"  // Don't expose org chart to general staff
};

foreach (var colName in restrictedEmployeeColumns)
{
    var col = employeeTable.Columns[colName];
    role.ColumnPermissions.Add(new ColumnPermission() 
    { 
        Column = col, 
        MetadataPermission = MetadataPermission.None 
    });
}

The TMSL/JSON Alternative: Scripted Deployment

For production environments where you're deploying via pipelines (Azure DevOps, GitHub Actions), you typically don't click through Tabular Editor's GUI — you apply TMSL scripts. Here's the equivalent TMSL for creating the HR_BusinessPartner role with OLS:

{
  "createOrReplace": {
    "object": {
      "database": "ContosoHRAnalytics",
      "role": "HR_BusinessPartner"
    },
    "role": {
      "name": "HR_BusinessPartner",
      "modelPermission": "read",
      "tablePermissions": [
        {
          "name": "Employee",
          "columnPermissions": [
            {
              "name": "SocialSecurityNumber",
              "metadataPermission": "none"
            },
            {
              "name": "HomeAddress",
              "metadataPermission": "none"
            },
            {
              "name": "EmergencyContact",
              "metadataPermission": "none"
            }
          ]
        }
      ],
      "members": [
        {
          "memberName": "HR-BusinessPartners@contoso.com",
          "memberType": "Group"
        }
      ]
    }
  }
}

You can execute this TMSL via:

  • Tabular Editor's XMLA console
  • SQL Server Management Studio (SSMS) connected to the XMLA endpoint
  • The Power BI REST API's executeQueries endpoint
  • Azure Analysis Services PowerShell cmdlets (adapted for Power BI)

Pro tip: Store your TMSL scripts in source control. OLS role definitions should be treated as infrastructure-as-code. When your PBIX is republished, roles defined through the XMLA endpoint will persist on the semantic model in the service — they're not overwritten by a standard publish. But if you fully overwrite the dataset via the Power BI API, you'll need to reapply them.


Assigning Members to OLS Roles

OLS roles use the same membership mechanism as RLS roles. You can add:

  • Individual AAD user accounts (user@domain.com)
  • AAD security groups (recommended for enterprise scale)
  • Service principals (for automated processes that should have restricted access)

In Tabular Editor, with a role selected, the Members property lists the accounts in that role. You can add members directly here, or through the Power BI service UI after publishing:

In Power BI service, navigate to the semantic model settings (three dots > Settings on the semantic model), then go to Security. You'll see your OLS roles listed alongside any RLS roles. You can add members to each role through this interface without needing Tabular Editor.

Critical architectural note: A user can belong to multiple roles. When a user belongs to multiple OLS roles, their effective access is the union (most permissive) of all roles they belong to. If Role A says SocialSecurityNumber is None but Role B says it's Read (or Default), the user can see the column. Design your roles with this in mind — don't try to "add back" restrictions through additional roles.

This union behavior has a significant design implication: you cannot use OLS to restrict users who are already in a permissive role. If an HR Admin is in the HR_Admin role with full access, and someone also accidentally adds them to HR_BusinessPartner, they still see everything because the permissive role wins. Manage role membership carefully, and if possible, use AAD group membership to control it rather than manual assignment.


The Hard Part: Measures, Relationships, and Dependent Objects

This is where OLS gets genuinely complex — and where most implementations break down. When you secure a column or table, everything that depends on that object is affected, but not in obvious ways.

Measures That Reference Secured Columns

Suppose you have a measure defined in the model:

Total Compensation = 
SUMX(
    Compensation,
    Compensation[BaseSalary] + Compensation[BonusAmount]
)

If a user's OLS role restricts access to the Compensation table entirely, what happens when they encounter a report visual that uses Total Compensation?

The answer is that the visual throws an error — not a blank, not a zero, but an actual error message indicating an OLS violation. This behavior surprises many developers who expect it to silently return blank. The reasoning is that a silent blank would be semantically incorrect (it would look like the compensation is $0, which is a false statement), whereas an error correctly communicates "you don't have access to this calculation."

For users, this means a visual will show an error icon. The error message visible to the end user is typically: "An error occurred while loading the model. A security violation occurred." This is intentionally vague to avoid leaking information about what was secured.

Design implication: Don't put measures that depend on secured columns in reports that will be seen by restricted users. Either create separate reports per audience, or use role-aware measures (described below).

Role-Aware Measures: The Masking Pattern

If you want a measure to show something for restricted users rather than an error, you need to architect around OLS rather than through it. The pattern is:

  1. Create a column or table that is accessible to all users (a "safe" version)
  2. Use USERPRINCIPALNAME() or role-based logic in DAX to route between full and safe versions

For the Compensation scenario, if you want General Staff to see headcount-based cost estimates without exposing individual salaries, you create an alternative measure that uses the Department[HeadCount] table (which is accessible to everyone) rather than individual Compensation[BaseSalary] rows.

This isn't OLS doing the masking — OLS hides the object entirely. The masking happens through DAX design. OLS and RLS together protect the raw data; your DAX measures provide the "safe view" for restricted audiences.

Relationships Involving Secured Tables

If Compensation is secured at the table level for General Staff, and Compensation has a relationship to Employee via EmployeeID — what happens to that relationship from the General Staff perspective?

The relationship itself doesn't cause errors; it simply becomes irrelevant from the restricted user's perspective because they can't see the Compensation table to traverse from. RELATED() and RELATEDTABLE() functions in measures that cross from a visible table to a hidden table will return OLS violation errors, not blanks.

This is a critical point: if any measure in your model uses RELATED() or RELATEDTABLE() to traverse to a secured table, that measure will error for restricted users. You need to audit every measure that crosses table boundaries and ensure restricted users never encounter a visual using those measures.

Calculated Columns Referencing Secured Columns

Calculated columns are evaluated at model refresh time, not at query time. So you might think: if a calculated column in Employee uses SocialSecurityNumber to compute something, and SocialSecurityNumber is secured — what happens?

The calculated column is already computed and stored in the model. The result of the calculated column is a new column in Employee. If that new column is not explicitly secured, a restricted user can see the calculated column's values even though the source column is hidden. This is a data leakage risk you must actively manage.

Rule: Any calculated column that derives its value from a secured source column should also be explicitly secured via OLS. Use the same scripting approach to add these columns to your restriction list.

Similarly, calculated tables that source their data from secured tables must be explicitly secured as well. OLS doesn't propagate automatically through the DAX dependency graph — you must explicitly secure every object that could expose restricted data.


Combining OLS with RLS: The Unified Security Model

In the Contoso HR scenario, Managers need access to Performance data — but only for their direct reports (an RLS concern), and they should not see the Compensation table at all (an OLS concern). These requirements must coexist in the same role.

A single Power BI role can simultaneously define:

  • OLS restrictions (which tables/columns are visible)
  • RLS filter expressions (which rows are visible)

Here's how the Manager role looks when combining both:

In Tabular Editor, with the Manager role selected:

OLS Configuration:

var role = Model.Roles["Manager"];

// Hide Compensation table entirely
role.TablePermissions.Add(new TablePermission() 
{ 
    Table = Model.Tables["Compensation"], 
    MetadataPermission = MetadataPermission.None 
});

// In Employee table, hide PII columns
var employeeTable = Model.Tables["Employee"];
var restrictedColumns = new[] { 
    "SocialSecurityNumber", 
    "HomeAddress", 
    "EmergencyContact" 
};

foreach (var colName in restrictedColumns)
{
    var col = employeeTable.Columns[colName];
    role.ColumnPermissions.Add(new ColumnPermission() 
    { 
        Column = col, 
        MetadataPermission = MetadataPermission.None 
    });
}

RLS Filter Expression on Performance table: Set the Performance table's row filter (the DAX expression evaluated as a row filter) to:

[ReviewerID] = LOOKUPVALUE(
    Employee[EmployeeID],
    Employee[FullName], USERPRINCIPALNAME()
)

Or more robustly, if you have a manager mapping table:

[EmployeeID] IN 
    CALCULATETABLE(
        VALUES(Employee[EmployeeID]),
        Employee[ManagerID] = LOOKUPVALUE(
            Employee[EmployeeID],
            Employee[Email], USERPRINCIPALNAME()
        )
    )

In the TMSL combined definition, this looks like:

{
  "name": "Manager",
  "modelPermission": "read",
  "tablePermissions": [
    {
      "name": "Compensation",
      "metadataPermission": "none"
    },
    {
      "name": "Employee",
      "filterExpression": null,
      "columnPermissions": [
        { "name": "SocialSecurityNumber", "metadataPermission": "none" },
        { "name": "HomeAddress", "metadataPermission": "none" },
        { "name": "EmergencyContact", "metadataPermission": "none" }
      ]
    },
    {
      "name": "Performance",
      "filterExpression": "[ReviewerID] = LOOKUPVALUE(Employee[EmployeeID], Employee[Email], USERPRINCIPALNAME())"
    }
  ]
}

Architecture note: In this combined setup, the filterExpression on the Performance table handles row filtering, while the columnPermissions on Employee handle column hiding. These are independent mechanisms evaluated at different stages of query processing.


Testing OLS: Validating Your Implementation

Testing OLS is non-trivial because Power BI Desktop's "View as Role" feature doesn't fully enforce OLS — it partially simulates it but doesn't reproduce all of the service-side enforcement behaviors. For accurate testing, you need to test in the Power BI service.

Testing in Power BI Service via "View as Role"

After publishing your model to a Premium/Fabric workspace:

  1. Navigate to the semantic model in Power BI service
  2. Go to the semantic model's context menu (three dots) and select Security (or navigate through Settings)
  3. You'll see your OLS roles listed. You can add test users here.

To test as a specific user persona, the most reliable method is:

  • Create a test AAD account for each persona (e.g., test.hrpartner@contoso.com, test.manager@contoso.com)
  • Assign that account to the appropriate role in the semantic model security settings
  • Open an InPrivate/Incognito browser session and sign in as that test user
  • Open the report or connect to the dataset from that session

This is the only fully accurate test — it exercises the actual enforcement path in the Analysis Services engine backing the semantic model.

Using DAX Studio to Test OLS Enforcement

With DAX Studio connected to your published semantic model (via the XMLA endpoint), you can impersonate roles using the EffectiveUserName connection property. This lets you run DAX queries as if you were a specific user or role:

In DAX Studio's connection dialog, look for the Roles and EffectiveUserName fields. Enter the role name(s) you want to simulate and optionally the UPN of a user. Then run queries against the model — attempts to reference secured columns will return errors exactly as the real user would experience.

For example, running:

EVALUATE 
SELECTCOLUMNS(
    Employee,
    "SSN", Employee[SocialSecurityNumber]
)

as a user in the GeneralStaff role should return an OLS violation error, not data. If it returns data, your OLS is not configured correctly.

The Metadata Endpoint Test

A sophisticated test that your security auditors will want to see: confirm that the secured columns don't appear in the schema metadata. Using the Power BI REST API:

GET https://api.powerbi.com/v1.0/myorg/groups/{workspaceId}/datasets/{datasetId}/tables
Authorization: Bearer {user_access_token_for_restricted_user}

The response should not include secured tables. For column-level testing, inspect the columns array of the Employee table response — secured columns should be absent. This API call uses the authenticated user's identity, so a token obtained for a restricted user will return filtered metadata.


Hands-On Exercise

Now it's your turn to implement a complete OLS setup on a model you control. We'll build a simplified version of the Contoso HR scenario.

Setup: Create a Power BI Desktop file with three tables (you can import from Excel or create them manually using Enter Data):

  • Employee: EmployeeID (integer), FullName (text), Department (text), Email (text), BaseSalary (decimal), SSN (text — use fake data)
  • Sales: SaleID, EmployeeID, SaleDate, SaleAmount, CustomerName
  • Department: DepartmentID, DepartmentName, Budget

Your objective is to implement three OLS roles:

Role 1: SalesAnalyst

  • Can see all of Sales and Department
  • Can see Employee but NOT BaseSalary or SSN
  • Should NOT see the Budget column in Department

Role 2: HRAdmin

  • Full access to everything (no OLS restrictions)

Role 3: ExecutiveViewer

  • Can see aggregated Sales data and Department (including Budget)
  • Cannot see Employee table at all
  • Cannot see individual CustomerName in Sales

Step 1: Open Tabular Editor 2 via External Tools from Power BI Desktop

Step 2: Create the three roles with appropriate model permissions

Step 3: Apply OLS restrictions using either the GUI property panel or the C# scripting console

Step 4: Add yourself as a member of SalesAnalyst role

Step 5: Save from Tabular Editor, then save the PBIX from Desktop

Step 6: Publish to a Premium/Fabric workspace

Step 7: In Power BI service, open the report as your test user (use an InPrivate session or a test account) and verify:

  • BaseSalary does not appear in the field list
  • SSN does not appear in the field list
  • Creating a measure that references Employee[BaseSalary] throws an error, not a blank
  • The Employee table is not visible at all when signed in as ExecutiveViewer

Step 8: Export the TMSL for your model via Tabular Editor (Model > Export as TMSL) and inspect the roles section to confirm your OLS definitions are captured in the model definition

Bonus challenge: Create a measure called Safe Salary Band that categorizes employees into salary bands (Low/Medium/High) using only the Employee[Department] column (which is visible to SalesAnalysts) and a separate static reference table — without using Employee[BaseSalary] directly. This simulates the masking pattern where you provide value to restricted users without exposing raw sensitive data.


Common Mistakes & Troubleshooting

Mistake 1: Assuming OLS Works on Shared (Pro-Only) Workspaces

If you publish your model with OLS roles defined to a shared capacity workspace, OLS is simply not enforced. Users will see all columns and tables regardless of role membership. There's no error message warning you about this — the roles exist in the model definition, but the enforcement engine (Analysis Services with OLS support) is not active. This is the most dangerous mistake because it creates false confidence in your security model.

Fix: Always verify your workspace is Premium/Fabric. Check the workspace icon — it should show a diamond (Premium) indicator.

Mistake 2: Setting OLS on Calculated Columns but Not Measures

You secure BaseSalary column via OLS. You think you're done. But there's a measure in your model:

Average Salary = AVERAGE(Employee[BaseSalary])

This measure will error for any user whose OLS role restricts BaseSalary. But worse — if you have other derived measures that chain from this one, those will also error. You haven't secured those measures (you can't — measures cannot have OLS applied to them individually), but you've accidentally broken them for restricted users.

Fix: Audit all measures that reference secured columns. Either secure those measures from restricted users by not including them in restricted reports, or create alternative measures that use safe columns.

Mistake 3: The Union Rule Gotcha

A user is in both HR_BusinessPartner (which restricts SSN) and HR_Admin (which has no restrictions). The user can see SSN. You didn't expect this.

Fix: Never put power users in multiple roles if one of those roles has fewer restrictions. Use a single HR_Admin role that grants full access, and don't add those users to any other OLS role. Manage this through AAD group membership — put admins in the admin group only.

Mistake 4: Publishing from Desktop Overwrites XMLA-Applied OLS Definitions

If you defined your OLS roles via XMLA (connected to the published model) and then republish the PBIX from Desktop, what happens?

When you publish, Power BI checks whether a dataset with that name already exists in the workspace. If you choose to overwrite it, the published PBIX replaces the model definition — and any XMLA-applied OLS roles that weren't in the PBIX are overwritten.

Fix: Use one of these approaches:

  • Define OLS in the PBIX via Tabular Editor connected to Desktop before publishing, so the PBIX file contains the role definitions
  • Maintain a TMSL script for roles separately and reapply it after every publish as part of your deployment pipeline
  • Use the deployment pipeline (ALM toolkit) approach where the semantic model definition is managed as a complete artifact including roles

Mistake 5: Not Securing Calculated Tables That Derive From Secured Sources

You have a hidden SalaryHistory table derived via DAX:

SalaryHistory = 
SELECTCOLUMNS(
    Compensation,
    "EmpID", Compensation[EmployeeID],
    "Period", Compensation[EffectiveDate],
    "Salary", Compensation[BaseSalary]
)

You secured the Compensation table. But SalaryHistory is a separate calculated table — and unless you explicitly apply OLS to it, it's visible. Users can see SalaryHistory[Salary] even though Compensation[BaseSalary] is secured.

Fix: Apply the same OLS restrictions to every table and column that is derived from secured sources. Document your dependency graph carefully.

Troubleshooting: "I Can't See the Roles in Power BI Service Security Panel"

If your OLS roles don't appear in the Power BI service Security settings for the semantic model:

  • Confirm the model was published to a Premium/Fabric workspace (not promoted later — it must be published to that workspace)
  • Confirm the roles were properly saved in Tabular Editor before the PBIX was saved and published
  • Check the model's TMSL (Tabular Editor > Model > Export TMSL) to verify the roles array contains your definitions

Troubleshooting: "OLS Roles Appear But Aren't Restricting Anything"

  • Verify the user is actually in the role. In the service Security panel, check the role membership.
  • Verify the workspace is Premium/Fabric — OLS is silently unenforced on shared capacity
  • Verify you're testing via Power BI service, not just "View as Role" in Desktop (Desktop simulation is incomplete)
  • Check if the user is also a member of another role (like a workspace Admin role) that overrides OLS

Note: Workspace admin, member, and contributor roles in the Power BI service do NOT automatically bypass OLS. However, if a user is also a dataset owner or has Build permission on the dataset and there's no OLS role assigned to them at all, they may see the full model. Always test with a user who is only in the intended OLS role and has no other elevated dataset permissions.


Architecture Considerations for Scale

Managing Roles at Enterprise Scale

When you have 15+ roles across a model with 50+ tables and 500+ columns, manual OLS management becomes a serious operational burden. Use these patterns:

Template TMSL scripts: Maintain role definitions as parameterized TMSL JSON files in your repository. Use PowerShell to apply them during deployment.

AAD Group Strategy: Map roles 1:1 to AAD security groups. HR Business Partners → SG-PowerBI-HR-BusinessPartner. Finance Analysts → SG-PowerBI-Finance-Analyst. Role membership is then managed in Active Directory, not in Power BI — which means your IAM team controls access without touching the semantic model.

Role documentation matrix: Maintain a spreadsheet or markdown table that maps each role to its OLS restrictions. This becomes your audit artifact when compliance teams ask "who can see salary data?"

Performance Considerations

OLS adds negligible query-time overhead — the security check happens at the plan compilation stage, not the data retrieval stage. When the query engine compiles a DAX query for a user, it resolves OLS restrictions upfront and excludes secured objects from the query plan entirely. You won't see performance degradation from OLS in the way you sometimes see from complex RLS filter expressions.

However, model design decisions around OLS can have performance implications. If you create "safe" calculated columns or tables to substitute for restricted ones, those objects consume memory and refresh time. Design the safe alternatives to be as lean as possible.

OLS and DirectQuery

OLS behaves consistently in DirectQuery mode — the schema hiding works the same way. However, the OLS check cannot be pushed down to the source database. The Analysis Services engine handles OLS enforcement after receiving the column list from the source. There are no additional performance concerns specific to DirectQuery with OLS beyond the standard DirectQuery considerations.


Summary & Next Steps

You've covered a lot of ground in this lesson. Let's crystallize the key mental models:

OLS is schema-level, RLS is data-level. Use OLS when the existence of the column or table itself is sensitive. Use RLS when the data in a visible table needs to be filtered. Most sophisticated enterprise models need both.

Role union means most-permissive wins. Never rely on a restrictive OLS role to "override" a permissive one. Design your role membership so that users who need restriction are only in the restricted role.

Dependency management is your biggest ongoing responsibility. Measures, calculated columns, and calculated tables that source from secured objects must be explicitly managed. OLS doesn't cascade automatically through the DAX dependency graph.

Premium/Fabric is non-negotiable. OLS is a dead letter on shared capacity. Confirm your deployment target before promising OLS-based security to your compliance team.

Test with real service sessions, not Desktop simulation. Desktop's "View as Role" is a useful quick check but is not authoritative. Your security audit must include testing via the service with actual user accounts.

Where to Go Next

With OLS implemented, the natural next steps in your enterprise Power BI security architecture are:

  • Dynamic Data Masking via DAX: Build measures that return masked values (like ***-**-1234 for SSNs in lookup scenarios) rather than hiding fields entirely — useful when you need to show the shape of data without the actual values
  • Information Protection Labels: Apply Microsoft Purview sensitivity labels to your semantic models and reports, which works in conjunction with OLS to provide an additional governance layer
  • Monitoring and Auditing: Use the Power BI Activity Log and Azure Monitor to track who's accessing which semantic models, when, and from where — creating an audit trail that OLS alone doesn't provide
  • Workspace-Level Security Architecture: Design your workspace topology to complement your security model — sometimes isolating sensitive data into separate semantic models per audience is simpler than a complex multi-role OLS scheme
  • ALM Toolkit for Deployment: Learn the Analysis Services ALM Toolkit (available as a Tabular Editor extension) for managing semantic model deployments across dev/test/prod environments while preserving OLS role definitions

The fact that you've implemented OLS means you've graduated from "Power BI developer" to "Power BI security architect." These are not the same skill set, and the distinction matters when your organization's sensitive data is on the line.

Learning Path: Enterprise Power BI

Previous

Designing and Implementing Power BI Incremental Refresh for Large-Scale Enterprise Datasets

Related Articles

Power BI🔥 Expert

Writeback Patterns in DAX: Simulating What-If Scenarios with Parameter Tables and Disconnected Slicers

30 min
Power BI🔥 Expert

Optimizing Power BI Report Performance: Query Reduction, Aggregations, and DirectQuery Tuning

31 min
Power BI⚡ Practitioner

Designing and Implementing Power BI Incremental Refresh for Large-Scale Enterprise Datasets

22 min

On this page

  • Introduction
  • Prerequisites
  • Understanding the Security Layers: RLS, OLS, and Why You Need Both
  • Row-Level Security: What It Does and What It Doesn't
  • Object-Level Security: What It Actually Does
  • The Third Layer: Field-Level Filtering (What OLS Is Not)
  • The Licensing Reality
  • Setting Up Your Development Environment
  • Connecting Tabular Editor to a Published Model via XMLA
  • Connecting Tabular Editor to Power BI Desktop
  • Creating the HR Business Partner Role
  • Understanding MetadataPermission Values
  • Configuring Table-Level OLS for General Staff
  • The TMSL/JSON Alternative: Scripted Deployment
  • Assigning Members to OLS Roles
  • The Hard Part: Measures, Relationships, and Dependent Objects
  • Measures That Reference Secured Columns
  • Role-Aware Measures: The Masking Pattern
  • Relationships Involving Secured Tables
  • Calculated Columns Referencing Secured Columns
  • Combining OLS with RLS: The Unified Security Model
  • Testing OLS: Validating Your Implementation
  • Testing in Power BI Service via "View as Role"
  • Using DAX Studio to Test OLS Enforcement
  • The Metadata Endpoint Test
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Mistake 1: Assuming OLS Works on Shared (Pro-Only) Workspaces
  • Mistake 2: Setting OLS on Calculated Columns but Not Measures
  • Mistake 3: The Union Rule Gotcha
  • Mistake 4: Publishing from Desktop Overwrites XMLA-Applied OLS Definitions
  • Mistake 5: Not Securing Calculated Tables That Derive From Secured Sources
  • Troubleshooting: "I Can't See the Roles in Power BI Service Security Panel"
  • Troubleshooting: "OLS Roles Appear But Aren't Restricting Anything"
  • Architecture Considerations for Scale
  • Managing Roles at Enterprise Scale
  • Performance Considerations
  • OLS and DirectQuery
  • Summary & Next Steps
  • Where to Go Next
  • Your Scenario: The Contoso HR Analytics Model
  • Implementing OLS: The Core Mechanics
  • Creating the HR Business Partner Role
  • Understanding MetadataPermission Values
  • Configuring Table-Level OLS for General Staff
  • The TMSL/JSON Alternative: Scripted Deployment
  • Assigning Members to OLS Roles
  • The Hard Part: Measures, Relationships, and Dependent Objects
  • Measures That Reference Secured Columns
  • Role-Aware Measures: The Masking Pattern
  • Relationships Involving Secured Tables
  • Calculated Columns Referencing Secured Columns
  • Combining OLS with RLS: The Unified Security Model
  • Testing OLS: Validating Your Implementation
  • Testing in Power BI Service via "View as Role"
  • Using DAX Studio to Test OLS Enforcement
  • The Metadata Endpoint Test
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Mistake 1: Assuming OLS Works on Shared (Pro-Only) Workspaces
  • Mistake 2: Setting OLS on Calculated Columns but Not Measures
  • Mistake 3: The Union Rule Gotcha
  • Mistake 4: Publishing from Desktop Overwrites XMLA-Applied OLS Definitions
  • Mistake 5: Not Securing Calculated Tables That Derive From Secured Sources
  • Troubleshooting: "I Can't See the Roles in Power BI Service Security Panel"
  • Troubleshooting: "OLS Roles Appear But Aren't Restricting Anything"
  • Architecture Considerations for Scale
  • Managing Roles at Enterprise Scale
  • Performance Considerations
  • OLS and DirectQuery
  • Summary & Next Steps
  • Where to Go Next