Skip to content

Instantly share code, notes, and snippets.

@7effrey89
Created August 12, 2026 07:30
Show Gist options
  • Select an option

  • Save 7effrey89/94ee40adff13d4b18bbba11fde3323aa to your computer and use it in GitHub Desktop.

Select an option

Save 7effrey89/94ee40adff13d4b18bbba11fde3323aa to your computer and use it in GitHub Desktop.
Powershell - Create Fabric Onelake Security - Roles and RLS
# Purpose: Create or replace a Fabric OneLake security role that grants a
# specified user read access only to table rows matching the RLS rule.
# The script reads the existing role collection first so unrelated roles are preserved.
#
# Prerequisites: Before running this script, create the target Fabric workspace
# and a sample lakehouse (Public holidays). The sample lakehouse will automatically
# create and populate the table publicholidays with a countryOrRegion column.
# This script configures security only; it does not create the workspace or lakehouse.
[CmdletBinding(SupportsShouldProcess)]
param(
# Fabric workspace containing the target lakehouse.
[string]$WorkspaceId = "6a4aa5f3-xxxx-xxxx-xxxx-xxxxa92c84d",
# Fabric lakehouse where the OneLake security role is managed.
[string]$LakehouseId = "369xxxx-xxxx-xxxx-9073-be5c332b8454",
# Name of the custom OneLake security role to create or update.
[string]$RoleName = "OneLakeSecurityLimited2",
# Microsoft Entra object ID of the user assigned to the role.
[string]$UserObjectId = "a1291ea4-xxxx-xxxx-xxxx-3fd656662ec4",
# Microsoft Entra tenant used for interactive login and role membership.
[string]$TenantId = "eb75a7b4-xxxx-xxxx-xxxx-xxxxf1c3fea2",
# Fabric capacity resource ID. It is validated for configuration completeness;
# capacity assignment is handled by a different Fabric API.
[string]$CapacityResourceId = "/subscriptions/cc4deffa-xxxx-xxxx-xxxx-xxxxc648e883/resourceGroups/Fabric/providers/Microsoft.Fabric/capacities/fabcapswedencentral"
)
# Stop on errors so a failed read or write cannot be reported as success.
$ErrorActionPreference = "Stop"
# Fabric REST API base URL and OAuth resource used to acquire the access token.
$baseUrl = "https://api.fabric.microsoft.com/v1"
$resourceUrl = "https://api.fabric.microsoft.com"
function Convert-SecureStringToPlainText {
param([System.Security.SecureString]$SecureString)
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
try {
[Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer)
}
finally {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer)
}
}
function Get-FabricHeaders {
# Reuse the current Az session when it is already connected to the requested tenant.
$context = Get-AzContext -ErrorAction SilentlyContinue
if (-not $context) {
if ($TenantId) {
Connect-AzAccount -TenantId $TenantId | Out-Null
}
else {
Connect-AzAccount | Out-Null
}
}
elseif ($TenantId -and $context.Tenant.Id -ne $TenantId) {
Connect-AzAccount -TenantId $TenantId | Out-Null
}
try {
$accessToken = Get-AzAccessToken -ResourceUrl $resourceUrl
}
catch {
Write-Host "Fabric token requires an interactive authentication scope; signing in again..." -ForegroundColor Cyan
if ($TenantId) {
Connect-AzAccount -TenantId $TenantId -AuthScope $resourceUrl | Out-Null
}
else {
Connect-AzAccount -AuthScope $resourceUrl | Out-Null
}
$accessToken = Get-AzAccessToken -ResourceUrl $resourceUrl
}
$token = if ($accessToken.Token -is [System.Security.SecureString]) {
Convert-SecureStringToPlainText -SecureString $accessToken.Token
}
else {
[string]$accessToken.Token
}
return @{
Authorization = "Bearer $token"
"Content-Type" = "application/json"
}
}
function Get-ResponseError {
param([System.Management.Automation.ErrorRecord]$ErrorRecord)
if ($ErrorRecord.ErrorDetails.Message) {
return $ErrorRecord.ErrorDetails.Message
}
return $ErrorRecord.Exception.Message
}
function Get-DataAccessRoles {
param([hashtable]$Headers)
# OneLake roles are stored as a complete collection, so the collection must be read
# before a PUT to avoid overwriting roles that this script does not own.
$uri = "$baseUrl/workspaces/$WorkspaceId/items/$LakehouseId/dataAccessRoles?preview=true"
try {
$response = Invoke-RestMethod -Headers $Headers -Uri $uri -Method Get
if ($null -eq $response) {
return @()
}
if ($null -ne $response.value) {
return @($response.value)
}
return @($response)
}
catch {
throw "Failed to read OneLake data access roles: $(Get-ResponseError -ErrorRecord $_)"
}
}
function New-LimitedRoleDefinition {
# The Path permission scopes access to one table and the Action permission makes it read-only.
# The row constraint must use the same table path as the Path permission.
return @{
name = $RoleName
decisionRules = @(
@{
effect = "Permit"
permission = @(
@{
attributeName = "Path"
attributeValueIncludedIn = @("/Tables/publicholidays")
},
@{
attributeName = "Action"
attributeValueIncludedIn = @("Read")
}
)
constraints = @{
rows = @(
@{
tablePath = "/Tables/publicholidays"
value = "SELECT * FROM publicholidays WHERE countryOrRegion='Austalia'"
}
)
}
}
)
members = @{
microsoftEntraMembers = @(
@{
objectId = $UserObjectId
objectType = "User"
tenantId = (Get-AzContext).Tenant.Id
}
)
fabricItemMembers = @()
}
}
}
function Set-DataAccessRoles {
param(
[hashtable]$Headers,
[object[]]$Roles
)
# The Fabric API replaces the role collection in one operation.
$uri = "$baseUrl/workspaces/$WorkspaceId/items/$LakehouseId/dataAccessRoles?preview=true"
$body = @{ value = @($Roles) } | ConvertTo-Json -Depth 30
try {
Invoke-RestMethod -Headers $Headers -Uri $uri -Method Put -Body $body | Out-Null
}
catch {
throw "Failed to write OneLake data access roles: $(Get-ResponseError -ErrorRecord $_)"
}
}
# Validate the supplied capacity identifier even though this script does not assign capacity.
if ($CapacityResourceId -notmatch '^/subscriptions/[0-9a-f-]{36}/resourceGroups/[^/]+/providers/Microsoft\.Fabric/capacities/[^/]+$') {
throw "CapacityResourceId is not a valid Fabric capacity resource ID."
}
# Authenticate, read the current roles, and construct the desired limited role.
$headers = Get-FabricHeaders
$roles = @(Get-DataAccessRoles -Headers $headers)
$role = New-LimitedRoleDefinition
$existingRole = $roles | Where-Object { $_.name -ieq $RoleName } | Select-Object -First 1
if ($existingRole) {
$roles = @($roles | Where-Object { $_.name -ine $RoleName })
Write-Host "Updating existing OneLake role '$RoleName'." -ForegroundColor Yellow
}
else {
Write-Host "Creating OneLake role '$RoleName'." -ForegroundColor Cyan
}
$roles += $role
if ($PSCmdlet.ShouldProcess("$WorkspaceId/$LakehouseId", "Persist OneLake role '$RoleName' and assign $UserObjectId")) {
Set-DataAccessRoles -Headers $headers -Roles $roles
Write-Host "OneLake role '$RoleName' applied to table publicholidays." -ForegroundColor Green
Write-Host "RLS: SELECT * FROM publicholidays WHERE countryOrRegion='Austalia'" -ForegroundColor Green
Write-Warning "Remove this user from DefaultReader if it grants broader access; OneLake roles combine with UNION semantics."
}
else {
Write-Host "WhatIf: no API write performed." -ForegroundColor Yellow
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment