Row-Level Security restricts data access at the row level based on user identity. Users see only the data they're authorized to view.
Apply RLS to dimensions, not fact tables:
- More efficient (smaller tables)
- Filters propagate through relationships
- Easier to maintain
// On Customer dimension - filters propagate to Sales
[Region] = "West"
Avoid many role combinations:
- Each role = separate cache
- Roles are additive (union, not intersection)
- Consolidate where possible
Data-driven rules scale better:
- User mapping in a table
- USERPRINCIPALNAME() for identity
- No role changes when users change
Fixed rules per role:
// Role: West Region
[Region] = "West"
// Role: East Region
[Region] = "East"
Pros: Simple, clear Cons: Doesn't scale, requires role per group
User identity drives filtering:
// Single role filters based on logged-in user
[ManagerEmail] = USERPRINCIPALNAME()
Pros: Scales, self-maintaining Cons: Requires user mapping data
User email in dimension table:
// On Customer table
[CustomerEmail] = USERPRINCIPALNAME()
Separate table mapping users to data:
SecurityMapping table:
| UserEmail | Region |
|-----------|--------|
| joe@co.com | West |
| sue@co.com | East |
// On Region dimension
[Region] IN
SELECTCOLUMNS(
FILTER(SecurityMapping, [UserEmail] = USERPRINCIPALNAME()),
"Region", [Region]
)
Users see their data plus subordinates:
// Using PATH functions for hierarchy
PATHCONTAINS(Employee[ManagerPath],
LOOKUPVALUE(Employee[EmployeeID], Employee[Email], USERPRINCIPALNAME()))
Combine conditions:
// Users see their region OR if they're a global viewer
[Region] = LOOKUPVALUE(Users[Region], Users[Email], USERPRINCIPALNAME())
|| LOOKUPVALUE(Users[IsGlobal], Users[Email], USERPRINCIPALNAME()) = TRUE()
security_role_operations(operation: "List")
security_role_operations(
operation: "Create",
definitions: [{
name: "Regional Sales",
modelPermission: "Read",
description: "Restricts sales data by region"
}]
)
security_role_operations(
operation: "CreatePermissions",
permissionDefinitions: [{
roleName: "Regional Sales",
tableName: "Customer",
filterExpression: "[Region] = USERPRINCIPALNAME()"
}]
)
security_role_operations(
operation: "GetEffectivePermissions",
references: [{ name: "Regional Sales" }]
)
- Modeling tab > View As
- Select role(s) to test
- Optionally specify user identity
- Verify data filtering
For dynamic RLS, test:
- Valid users
- Unknown users (should see nothing or error gracefully)
- NULL/blank values
// Defensive pattern - returns no data for unknown users
IF(
USERPRINCIPALNAME() IN VALUES(SecurityMapping[UserEmail]),
[Region] IN SELECTCOLUMNS(...),
FALSE()
)
Problem: Large table scans, poor performance Solution: Apply to dimension tables, let relationships propagate
Problem: Expensive, doesn't scale Solution: Create proper relationships, let filters flow
Problem: Multiple roles = UNION (additive), not intersection Solution: Design roles with union behavior in mind
Problem: RLS filters become WHERE clauses Solution: Ensure source database can handle the query patterns
Problem: Users see unexpected data Solution: Test with: valid users, invalid users, multiple roles
For bidirectional relationships with RLS:
Enable "Apply security filter in both directions"
Only use when:
- RLS requires filtering through many-to-many
- Dimension-to-dimension security needed
Caution: Only one bidirectional relationship per path allowed.
- RLS adds WHERE clauses to every query
- Complex DAX in filters hurts performance
- Test with realistic user counts
- Consider aggregations for large models
Restrict access to entire tables or columns:
// Via XMLA/TMSL - not available in Desktop UI
Use for:
- Hiding sensitive columns (salary, SSN)
- Restricting entire tables
- Combined with RLS for comprehensive security
- RLS applied to dimension tables (not fact tables)
- Filters propagate correctly through relationships
- Dynamic RLS uses USERPRINCIPALNAME()
- Tested with valid and invalid users
- Edge cases handled (NULL, unknown users)
- Performance tested under load
- Role mappings documented
- Workspace roles understood (Admins bypass RLS)