<# .SYNOPSIS Grants a TATER scanning app read-only Power BI admin API access so TATER can inventory Power BI workspaces / datasets / reports for GRC audit. .DESCRIPTION Repeatable per-client runbook. Does the two things the grant requires: 1. Creates (or reuses) a Microsoft Entra SECURITY group and adds the TATER scanning app's service principal to it. [fully automated] 2. Enables the Fabric/Power BI tenant setting "Service principals can access read-only admin APIs", scoped to that group, via the Fabric Admin "Update Tenant Setting" REST API. [best-effort + portal fallback] Then (optionally) verifies the grant by making an app-only admin API call. Authoritative reference: https://learn.microsoft.com/en-us/fabric/admin/enable-service-principal-admin-apis .PREREQUISITES - Run as a Microsoft Entra admin who is ALSO a Fabric/Power BI administrator (the tenant-setting step requires Fabric admin rights). - PowerShell 7+ recommended. Module: Az.Accounts Install-Module Az.Accounts -Scope CurrentUser - The TATER scanning app (the same app registration used for the Power Automate / Power Platform connection) must already exist in the tenant. - IMPORTANT (per Microsoft): the app must NOT have any admin-consent-required *Power BI Service* API permissions assigned. Graph/Exchange permissions are fine. This script warns if it detects Power BI Service permissions. .PARAMETER AppId The Application (client) ID of the TATER scanning app for THIS client. .PARAMETER GroupName Security group to create/use. Default: TATER-PowerBI-Readers .PARAMETER TenantId Optional tenant id to sign into (the client's tenant). .PARAMETER ClientSecret Optional. If supplied, the script verifies the grant by acquiring an app-only Power BI token and calling the admin/groups API. .PARAMETER SkipTenantSetting Only create the group + add the app. Toggle the tenant setting in the portal yourself (the script prints exact steps). .EXAMPLE ./Grant-PowerBIAdminAccess.ps1 -AppId d61f0d84-96a4-4504-8a38-100d14a2b189 -TenantId 51707517-6d54-48b7-b8e3-8fdd4447773d .EXAMPLE # also verify it reads, end to end: ./Grant-PowerBIAdminAccess.ps1 -AppId -TenantId -ClientSecret #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$AppId, [string]$GroupName = 'TATER-PowerBI-Readers', [string]$TenantId, [string]$ClientSecret, [switch]$SkipTenantSetting ) $ErrorActionPreference = 'Stop' $PBI_SERVICE_APP = '00000009-0000-0000-c000-000000000000' # Power BI Service (for the permission-conflict check) function Write-Step($m) { Write-Host "`n==> $m" -ForegroundColor Cyan } function Write-Ok($m) { Write-Host " [OK] $m" -ForegroundColor Green } function Write-Warn2($m){ Write-Host " [WARN] $m" -ForegroundColor Yellow } # Az.Accounts 5.x returns Get-AzAccessToken .Token as a SecureString; older # versions return a plain string. Return plaintext either way. function Get-PlainAccessToken([string]$Resource) { $t = (Get-AzAccessToken -ResourceUrl $Resource -WarningAction SilentlyContinue).Token if ($t -is [System.Security.SecureString]) { return [System.Net.NetworkCredential]::new('', $t).Password } return [string]$t } # --- 0. Prereqs --------------------------------------------------------------- if (-not (Get-Module -ListAvailable -Name Az.Accounts)) { throw "Az.Accounts module not found. Install it first: Install-Module Az.Accounts -Scope CurrentUser" } Import-Module Az.Accounts -ErrorAction Stop | Out-Null Write-Step "Signing in (use a Microsoft Entra + Fabric/Power BI admin account)" $connectArgs = @{} if ($TenantId) { $connectArgs['Tenant'] = $TenantId } Connect-AzAccount @connectArgs | Out-Null $graphTok = Get-PlainAccessToken 'https://graph.microsoft.com' $graphHdr = @{ Authorization = "Bearer $graphTok"; 'Content-Type' = 'application/json' } $GRAPH = 'https://graph.microsoft.com/v1.0' # --- 1. Resolve the app's service principal ---------------------------------- Write-Step "Resolving the TATER scanning app ($AppId)" $sp = (Invoke-RestMethod -Headers $graphHdr -Uri "$GRAPH/servicePrincipals?`$filter=appId eq '$AppId'").value | Select-Object -First 1 if (-not $sp) { throw "No service principal found for appId $AppId in this tenant. Confirm the app exists / has been consented here." } $spId = $sp.id Write-Ok "Service principal: $($sp.displayName) ($spId)" # Permission-conflict check (read-only admin API SPs must have NO admin-consent # Power BI Service permissions). try { $appReg = (Invoke-RestMethod -Headers $graphHdr -Uri "$GRAPH/applications?`$filter=appId eq '$AppId'").value | Select-Object -First 1 $pbiPerms = @($appReg.requiredResourceAccess | Where-Object { $_.resourceAppId -eq $PBI_SERVICE_APP }) if ($pbiPerms.Count -gt 0) { Write-Warn2 "This app has Power BI Service API permissions assigned. Microsoft requires read-only admin-API service principals to have NONE. Remove them (Entra > App registrations > $($appReg.displayName) > API permissions) or the admin API will reject the app." } else { Write-Ok "No conflicting Power BI Service permissions on the app." } } catch { Write-Warn2 "Could not verify app API permissions (non-fatal): $($_.Exception.Message)" } # --- 2. Security group + membership ------------------------------------------ Write-Step "Ensuring security group '$GroupName'" $grp = (Invoke-RestMethod -Headers $graphHdr -Uri "$GRAPH/groups?`$filter=displayName eq '$GroupName'").value | Select-Object -First 1 if (-not $grp) { $body = @{ displayName = $GroupName; mailEnabled = $false; mailNickname = ('tater' + [guid]::NewGuid().ToString('N').Substring(0,10)); securityEnabled = $true } | ConvertTo-Json $grp = Invoke-RestMethod -Headers $graphHdr -Method Post -Uri "$GRAPH/groups" -Body $body Write-Ok "Created group $GroupName ($($grp.id))" } else { Write-Ok "Using existing group $GroupName ($($grp.id))" } $groupId = $grp.id Write-Step "Adding the app to the group" try { $refBody = @{ '@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$spId" } | ConvertTo-Json Invoke-RestMethod -Headers $graphHdr -Method Post -Uri "$GRAPH/groups/$groupId/members/`$ref" -Body $refBody | Out-Null Write-Ok "Added service principal to the group." } catch { if ("$($_.Exception.Message)" -match 'already exist|added object references already') { Write-Ok "Service principal is already a member." } else { throw } } # --- 3. Tenant setting (best-effort via Fabric Admin API; portal fallback) ---- $portalSteps = @" Portal step (Fabric admin): 1. https://app.powerbi.com -> Settings (gear) -> Admin portal -> Tenant settings 2. Section 'Admin API settings' -> 'Service principals can access read-only admin APIs' 3. Toggle Enabled -> 'Specific security groups' -> add '$GroupName' -> Apply 4. Repeat for 'Service principals can call Fabric public APIs' (Developer settings) if you want Fabric APIs too. "@ if ($SkipTenantSetting) { Write-Step "Skipping tenant setting (per -SkipTenantSetting). Do it in the portal:" Write-Host $portalSteps -ForegroundColor Gray } else { Write-Step "Enabling the tenant setting via the Fabric Admin API (best-effort)" $tenantSettingDone = $false try { $fabricTok = Get-PlainAccessToken 'https://api.fabric.microsoft.com' $fabHdr = @{ Authorization = "Bearer $fabricTok"; 'Content-Type' = 'application/json' } $all = Invoke-RestMethod -Headers $fabHdr -Uri 'https://api.fabric.microsoft.com/v1/admin/tenantsettings' $setting = $all.tenantSettings | Where-Object { $_.title -match 'read.?only admin API' } | Select-Object -First 1 if (-not $setting) { throw "Could not find the 'read-only admin APIs' tenant setting via the API." } $upBody = @{ enabled = $true; enabledSecurityGroups = @(@{ graphId = $groupId; name = $GroupName }) } | ConvertTo-Json -Depth 5 Invoke-RestMethod -Headers $fabHdr -Method Post -Body $upBody ` -Uri "https://api.fabric.microsoft.com/v1/admin/tenantsettings/$($setting.settingName)/update" | Out-Null Write-Ok "Tenant setting '$($setting.title)' enabled and scoped to '$GroupName'." $tenantSettingDone = $true } catch { Write-Warn2 "Could not set the tenant setting via the API ($($_.Exception.Message))." Write-Host $portalSteps -ForegroundColor Gray } } # --- 4. Optional verification (app-only) ------------------------------------- if ($ClientSecret) { Write-Step "Verifying app-only read access (allow ~15 min for propagation)" try { $tid = $TenantId; if (-not $tid) { $tid = (Get-AzContext).Tenant.Id } $tok = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tid/oauth2/v2.0/token" -Body @{ client_id = $AppId; client_secret = $ClientSecret; grant_type = 'client_credentials' scope = 'https://analysis.windows.net/powerbi/api/.default' } $r = Invoke-WebRequest -Headers @{ Authorization = "Bearer $($tok.access_token)" } -Uri 'https://api.powerbi.com/v1.0/myorg/admin/groups?$top=1' -SkipHttpErrorCheck if ($r.StatusCode -eq 200) { Write-Ok "Power BI admin API returned 200 - the grant is LIVE." } elseif ($r.StatusCode -in 401,403) { Write-Warn2 "Power BI admin API returned $($r.StatusCode) - not propagated yet (wait ~15 min) or the tenant setting still needs the portal toggle." } else { Write-Warn2 "Power BI admin API returned $($r.StatusCode)." } } catch { Write-Warn2 "Verification call failed: $($_.Exception.Message)" } } # --- 5. Summary --------------------------------------------------------------- Write-Step "Done" Write-Host " App: $AppId" -ForegroundColor Gray Write-Host " Group: $GroupName ($groupId)" -ForegroundColor Gray Write-Host " Next: In TATER Manage > Connections > Power Platform Inventory, click 'Scan now'." -ForegroundColor Gray Write-Host " Power BI workspaces/datasets/reports appear once the grant has propagated (~15 min)." -ForegroundColor Gray