<# .SYNOPSIS Onboard a Microsoft Entra tenant for Icon Map for Fabric in one run: prepare the tenant, consent both workload applications for every user, and pre-approve the on-demand OneLake (and optionally Eventhouse) permission - so nobody in the tenant ever sees a consent prompt. .DESCRIPTION Does, through Microsoft Graph, everything an administrator would otherwise do by accepting the prompts inside Fabric or opening the admin-consent URLs: 1. Creates the tenant's entry for Azure Storage (and Azure Data Explorer with -IncludeEventhouse). Entra needs it before any consent for the service can be recorded, and Fabric does not create it. 2. Creates the service principals for Tekantis.IconMap (frontend) and Tekantis.IconMap-Backend in the tenant. 3. Records a tenant-wide (AllPrincipals) grant for each application's declared permissions: Microsoft Graph sign-in and the Fabric / Power BI scopes. This is what the consent prompts and the admin-consent URLs do. 4. Assigns the backend application's single app-only permission, Microsoft Graph CrossTenantInformation.ReadBasic.All (directory metadata only: your tenant's display name and domain). 5. Pre-approves the on-demand Azure Storage permission (OneLake) for both applications, and Azure Data Explorer (Eventhouse) for the frontend with -IncludeEventhouse, so the first Lakehouse or Eventhouse does not prompt either. 6. Lists every grant it can see, so the result can be checked. Every step is idempotent: existing entries and grants are reported, missing scopes are added to an existing grant, nothing is removed. Requires the Azure CLI (az) - Azure Cloud Shell (https://shell.azure.com) has it - and a Global Administrator or Privileged Role Administrator. A Cloud Application Administrator can do everything except step 4; the script reports that step as skipped rather than failing. .PARAMETER TenantId The tenant to onboard. Omit to use the tenant az is already signed in to. .PARAMETER IncludeEventhouse Also create the Azure Data Explorer entry and pre-approve Eventhouse access. .PARAMETER SkipLogin Use the current az session instead of signing in to -TenantId. .EXAMPLE ./Onboard-IconMapTenant.ps1 -TenantId 00000000-0000-0000-0000-000000000000 -WhatIf ./Onboard-IconMapTenant.ps1 -TenantId 00000000-0000-0000-0000-000000000000 -IncludeEventhouse #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [string]$TenantId, [switch]$IncludeEventhouse, [switch]$SkipLogin ) $ErrorActionPreference = "Stop" # --- Microsoft services (resource applications) -------------------------- $GRAPH = @{ Name = "Microsoft Graph"; AppId = "00000003-0000-0000-c000-000000000000" } $PBI = @{ Name = "Power BI Service"; AppId = "00000009-0000-0000-c000-000000000000" } $STORAGE = @{ Name = "Azure Storage"; AppId = "e406a681-f3d4-42a8-90b6-c2b029497af1" } $ADX = @{ Name = "Azure Data Explorer"; AppId = "2746ea77-4702-4b45-80ca-3c97e680e8b7" } # Backend's one app-only permission: Graph CrossTenantInformation.ReadBasic.All $GRAPH_CROSS_TENANT_ROLE = "cac88765-0581-4025-9725-5ebc13f729ee" # --- Icon Map applications and what each declares ------------------------- # Mirrors scripts/Setup/EnsureIconMapAppPermissions.ps1 (the source of truth for # the registrations). Azure Storage / Azure Data Explorer are NOT declared; they # are the on-demand permissions handled in step 5. $APPS = @( @{ Name = "Tekantis.IconMap" AppId = "8ac140d7-88a0-4857-84b4-73c708afd645" Graph = "openid profile offline_access User.Read" Pbi = "Fabric.Extend Workspace.Read.All Capacity.Read.All Item.Read.All Item.ReadWrite.All Item.Execute.All OneLake.Read.All OneLake.ReadWrite.All Dataset.Read.All Connection.Read.All" OnDemandAdx = $true AppRole = $null }, @{ Name = "Tekantis.IconMap-Backend" AppId = "81c50c7d-ca21-414c-b7d9-524410f38339" Graph = "openid profile offline_access User.Read" Pbi = "Fabric.Extend Workspace.Read.All Item.Read.All Item.ReadWrite.All Item.Execute.All" OnDemandAdx = $false AppRole = $GRAPH_CROSS_TENANT_ROLE } ) # --- helpers --------------------------------------------------------------- function Invoke-Graph([string]$Method, [string]$Url, $Body) { if ($null -eq $Body) { return az rest --method $Method --url $Url --only-show-errors -o json | ConvertFrom-Json } $tmp = New-TemporaryFile try { Set-Content -Path $tmp -Value ($Body | ConvertTo-Json -Compress -Depth 5) -Encoding utf8 -NoNewline $out = az rest --method $Method --url $Url --headers "Content-Type=application/json" --body "@$tmp" --only-show-errors -o json if ($LASTEXITCODE -ne 0) { throw "Graph $Method $Url failed." } if ($out) { return $out | ConvertFrom-Json } return $null } finally { Remove-Item $tmp -ErrorAction SilentlyContinue } } function Get-SpId([string]$appId) { $id = az ad sp list --filter "appId eq '$appId'" --query "[0].id" -o tsv 2>$null if ($id) { return $id } return $null } function Ensure-Sp([string]$appId, [string]$label) { $id = Get-SpId $appId if ($id) { Write-Host (" present {0}" -f $label); return $id } if ($PSCmdlet.ShouldProcess($label, "create service principal")) { az ad sp create --id $appId --only-show-errors | Out-Null if ($LASTEXITCODE -ne 0) { throw "Could not create the service principal for $label. You need the Cloud Application Administrator, Application Administrator or Global Administrator role." } Start-Sleep -Seconds 2 $id = Get-SpId $appId Write-Host (" created {0}" -f $label) -ForegroundColor Green return $id } return $null } # Record (or extend) an AllPrincipals delegated grant client -> resource. function Ensure-Grant([string]$clientSpId, [string]$resourceSpId, [string]$scopes, [string]$label) { $wanted = $scopes -split ' ' | Where-Object { $_ } $filter = "clientId eq '$clientSpId' and resourceId eq '$resourceSpId' and consentType eq 'AllPrincipals'" $existing = (Invoke-Graph GET "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=$([uri]::EscapeDataString($filter))").value | Select-Object -First 1 if ($existing) { $have = $existing.scope -split ' ' | Where-Object { $_ } $missing = $wanted | Where-Object { $have -notcontains $_ } if (-not $missing) { Write-Host (" present {0}" -f $label); return } if ($PSCmdlet.ShouldProcess($label, "add scopes: $($missing -join ' ')")) { Invoke-Graph PATCH "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$($existing.id)" @{ scope = (($have + $missing) -join ' ') } | Out-Null Write-Host (" extended {0} (+{1})" -f $label, ($missing -join ' ')) -ForegroundColor Green } return } if ($PSCmdlet.ShouldProcess($label, "grant for all users: $scopes")) { Invoke-Graph POST "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" @{ clientId = $clientSpId consentType = "AllPrincipals" resourceId = $resourceSpId scope = $scopes } | Out-Null Write-Host (" granted {0}" -f $label) -ForegroundColor Green } } # Assign an app-only role (application permission) to a service principal. function Ensure-AppRole([string]$spId, [string]$resourceSpId, [string]$roleId, [string]$label) { $existing = (Invoke-Graph GET "https://graph.microsoft.com/v1.0/servicePrincipals/$spId/appRoleAssignments").value | Where-Object { $_.resourceId -eq $resourceSpId -and $_.appRoleId -eq $roleId } if ($existing) { Write-Host (" present {0}" -f $label); return } if ($PSCmdlet.ShouldProcess($label, "assign application permission")) { try { Invoke-Graph POST "https://graph.microsoft.com/v1.0/servicePrincipals/$spId/appRoleAssignments" @{ principalId = $spId resourceId = $resourceSpId appRoleId = $roleId } | Out-Null Write-Host (" assigned {0}" -f $label) -ForegroundColor Green } catch { Write-Warning "Could not assign $label - this needs a Global Administrator or Privileged Role Administrator. Everything else is done; an administrator with that role can open the backend admin-consent URL to complete it." } } } # --- sign in --------------------------------------------------------------- if (-not (Get-Command az -ErrorAction SilentlyContinue)) { throw "The Azure CLI (az) is not installed. Run this in Azure Cloud Shell (https://shell.azure.com) or install it from https://aka.ms/installazurecli." } if (-not $SkipLogin -and $TenantId) { Write-Host "Signing in to tenant $TenantId ..." az login --tenant $TenantId --allow-no-subscriptions --only-show-errors | Out-Null if ($LASTEXITCODE -ne 0) { throw "az login failed." } } $tenantNow = az account show --query tenantId -o tsv 2>$null if ($TenantId -and $tenantNow -and ($tenantNow -ne $TenantId)) { throw "az is signed in to tenant $tenantNow, not $TenantId. Run 'az login --tenant $TenantId --allow-no-subscriptions' first." } Write-Host "Onboarding Icon Map for Fabric in tenant $($tenantNow ?? $TenantId)" # --- 1. Microsoft service entries ----------------------------------------- Write-Host "" Write-Host "1. Microsoft service entries" -ForegroundColor Cyan $graphSp = Ensure-Sp $GRAPH.AppId "$($GRAPH.Name) ($($GRAPH.AppId))" $pbiSp = Ensure-Sp $PBI.AppId "$($PBI.Name) ($($PBI.AppId))" $storageSp = Ensure-Sp $STORAGE.AppId "$($STORAGE.Name) ($($STORAGE.AppId))" $adxSp = $null if ($IncludeEventhouse) { $adxSp = Ensure-Sp $ADX.AppId "$($ADX.Name) ($($ADX.AppId))" } # --- 2. Icon Map service principals --------------------------------------- Write-Host "" Write-Host "2. Icon Map applications" -ForegroundColor Cyan $spIds = @{} foreach ($app in $APPS) { $spIds[$app.AppId] = Ensure-Sp $app.AppId "$($app.Name) ($($app.AppId))" } if ($WhatIfPreference) { Write-Host "" Write-Host "WhatIf: service principals may not exist yet, so the grant steps below are described, not evaluated." -ForegroundColor Yellow } # --- 3. Declared permissions ---------------------------------------------- Write-Host "" Write-Host "3. Consent the declared permissions for all users" -ForegroundColor Cyan foreach ($app in $APPS) { $sp = $spIds[$app.AppId] if (-not $sp -or -not $graphSp -or -not $pbiSp) { Write-Host " (skipped in WhatIf) $($app.Name)"; continue } Ensure-Grant $sp $graphSp $app.Graph "$($app.Name) -> Microsoft Graph" Ensure-Grant $sp $pbiSp $app.Pbi "$($app.Name) -> Power BI Service" } # --- 4. Backend app-only permission --------------------------------------- Write-Host "" Write-Host "4. Backend application permission (Graph CrossTenantInformation.ReadBasic.All)" -ForegroundColor Cyan foreach ($app in $APPS | Where-Object { $_.AppRole }) { $sp = $spIds[$app.AppId] if (-not $sp -or -not $graphSp) { Write-Host " (skipped in WhatIf) $($app.Name)"; continue } Ensure-AppRole $sp $graphSp $app.AppRole "$($app.Name) -> Microsoft Graph (application)" } # --- 5. On-demand permissions --------------------------------------------- Write-Host "" Write-Host "5. Pre-approve the on-demand permissions" -ForegroundColor Cyan foreach ($app in $APPS) { $sp = $spIds[$app.AppId] if (-not $sp -or -not $storageSp) { Write-Host " (skipped in WhatIf) $($app.Name)"; continue } Ensure-Grant $sp $storageSp "user_impersonation" "$($app.Name) -> Azure Storage (OneLake)" if ($IncludeEventhouse -and $app.OnDemandAdx -and $adxSp) { Ensure-Grant $sp $adxSp "user_impersonation" "$($app.Name) -> Azure Data Explorer (Eventhouse)" } } # --- 6. Verify -------------------------------------------------------------- Write-Host "" Write-Host "6. Grants recorded for all users" -ForegroundColor Cyan foreach ($app in $APPS) { $sp = $spIds[$app.AppId] if (-not $sp) { continue } $grants = (Invoke-Graph GET "https://graph.microsoft.com/v1.0/servicePrincipals/$sp/oauth2PermissionGrants").value | Where-Object { $_.consentType -eq 'AllPrincipals' } Write-Host " $($app.Name)" foreach ($g in $grants) { $res = (Invoke-Graph GET "https://graph.microsoft.com/v1.0/servicePrincipals/$($g.resourceId)?`$select=displayName").displayName Write-Host (" {0,-22} {1}" -f $res, $g.scope) } if ($app.AppRole) { $roles = (Invoke-Graph GET "https://graph.microsoft.com/v1.0/servicePrincipals/$sp/appRoleAssignments").value foreach ($r in $roles) { Write-Host (" {0,-22} application permission {1}" -f "Microsoft Graph", $r.appRoleId) } } } Write-Host "" Write-Host "Done. Allow 10-30 minutes for consent to propagate. Users can now add Icon Map items, Lakehouse sources$(if ($IncludeEventhouse) { ' and Eventhouse sources' }) without being prompted." -ForegroundColor Yellow Write-Host "Next: enable the Fabric tenant settings - https://www.icon-map.com/documentation/fabric/onboarding/tenant-settings/"