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:
This is an expert-level lesson. You should arrive with:
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.
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.
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.
OLS operates at the metadata level. When a column or table has an OLS restriction applied for a given role:
This is a categorically different kind of restriction. You're not filtering data — you're hiding the schema.
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.
OLS enforcement requires a Premium or Fabric capacity. More specifically:
The practical implication: if your organization is on shared capacity, OLS is not available. This is a hard architectural constraint, not a configuration issue.
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:
For this lesson, we'll use Tabular Editor 2, which is the most accessible option.
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.
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.
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, EmergencyContactCompensation table: EmployeeID, BaseSalary, BonusAmount, BonusPercentage, StockGrantValue, TotalCompensation, EffectiveDatePerformance table: EmployeeID, ReviewPeriod, PerformanceScore, ReviewerID, NotesDepartment table: DepartmentID, DepartmentName, CostCenter, HeadCountDateDim table: standard date dimensionThe 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.
OLS works through the roles system in the tabular model. Each role you define in your model can have permissions applied at three levels:
None (table is invisible) or Read (table is visible with all accessible columns)None or ReadThe 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.
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:
HR_BusinessPartnerReadNow, 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.
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 perspectiveThere'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.
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
});
}
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:
executeQueries endpointPro 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.
OLS roles use the same membership mechanism as RLS roles. You can add:
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
SocialSecurityNumberisNonebut Role B says it'sRead(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.
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.
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).
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:
USERPRINCIPALNAME() or role-based logic in DAX to route between full and safe versionsFor 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.
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 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.
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:
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
filterExpressionon thePerformancetable handles row filtering, while thecolumnPermissionsonEmployeehandle column hiding. These are independent mechanisms evaluated at different stages of query processing.
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.
After publishing your model to a Premium/Fabric workspace:
To test as a specific user persona, the most reliable method is:
test.hrpartner@contoso.com, test.manager@contoso.com)This is the only fully accurate test — it exercises the actual enforcement path in the Analysis Services engine backing the semantic model.
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.
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.
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):
Your objective is to implement three OLS roles:
Role 1: SalesAnalyst
Sales and DepartmentEmployee but NOT BaseSalary or SSNBudget column in DepartmentRole 2: HRAdmin
Role 3: ExecutiveViewer
Sales data and Department (including Budget)Employee table at allCustomerName in SalesStep 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 listSSN does not appear in the field list Employee[BaseSalary] throws an error, not a blankEmployee table is not visible at all when signed in as ExecutiveViewerStep 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.
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.
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.
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.
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:
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.
If your OLS roles don't appear in the Power BI service Security settings for the semantic model:
roles array contains your definitionsNote: 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.
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?"
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 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.
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.
With OLS implemented, the natural next steps in your enterprise Power BI security architecture are:
***-**-1234 for SSNs in lookup scenarios) rather than hiding fields entirely — useful when you need to show the shape of data without the actual valuesThe 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