Created
August 12, 2026 07:30
-
-
Save 7effrey89/94ee40adff13d4b18bbba11fde3323aa to your computer and use it in GitHub Desktop.
Powershell - Create Fabric Onelake Security - Roles and RLS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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