<# .SYNOPSIS Grant tenant-wide admin consent for one of Icon Map's on-demand permissions (Azure Storage or Azure Data Explorer) without prompting users. .DESCRIPTION Icon Map asks for Azure Storage (OneLake) and Azure Data Explorer (Eventhouse) access on demand, the first time a user touches a feature that needs it, rather than declaring them up front - so that a tenant which has never used those services can still consent to Icon Map (see docs/specs/consent-clean-tenant.md). Because the permissions are not declared statically, the standard admin-consent URL does not cover them. An administrator can pre-approve them for everyone in any of three ways: 1. Tick "Consent on behalf of your organization" on the prompt the first time an administrator uses the feature. No tooling. 2. Open the admin-consent URL with an explicit scope, for example https://login.microsoftonline.com//adminconsent?client_id=&scope=https://storage.azure.com/user_impersonation 3. Run this script, which records the grant through Microsoft Graph. This script: - ensures the resource's service principal exists in the tenant (the same step as Prepare-IconMapTenant.ps1), - ensures the Icon Map application's service principal exists, - records an AllPrincipals oauth2PermissionGrant for `user_impersonation`. Requires the Cloud Application Administrator, Application Administrator or Global Administrator role, and the Azure CLI. Idempotent. .PARAMETER TenantId The customer tenant. .PARAMETER Resource Storage (default) or Eventhouse. .PARAMETER App Which Icon Map application to grant for. Workload (frontend, default), Backend (needed for Tileset Builder jobs and the Power BI visuals' catalog access) or EmbedViewer. .EXAMPLE ./Grant-IconMapResourceConsent.ps1 -TenantId -Resource Storage ./Grant-IconMapResourceConsent.ps1 -TenantId -Resource Storage -App Backend ./Grant-IconMapResourceConsent.ps1 -TenantId -Resource Eventhouse -WhatIf #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)][string]$TenantId, [ValidateSet('Storage', 'Eventhouse')][string]$Resource = 'Storage', [ValidateSet('Workload', 'Backend', 'EmbedViewer')][string]$App = 'Workload', [switch]$SkipLogin ) $ErrorActionPreference = "Stop" $RESOURCES = @{ Storage = @{ Name = "Azure Storage"; AppId = "e406a681-f3d4-42a8-90b6-c2b029497af1" } Eventhouse = @{ Name = "Azure Data Explorer"; AppId = "2746ea77-4702-4b45-80ca-3c97e680e8b7" } } $APPS = @{ Workload = @{ Name = "Tekantis.IconMap"; AppId = "8ac140d7-88a0-4857-84b4-73c708afd645" } Backend = @{ Name = "Tekantis.IconMap-Backend"; AppId = "81c50c7d-ca21-414c-b7d9-524410f38339" } EmbedViewer = @{ Name = "Icon Map Embed Viewer"; AppId = "31651b7a-301b-4581-871e-100cfd4d2099" } } $res = $RESOURCES[$Resource] $app = $APPS[$App] if (-not $SkipLogin) { 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." } } function Ensure-Sp([string]$appId, [string]$label) { $id = az ad sp list --filter "appId eq '$appId'" --query "[0].id" -o tsv 2>$null if ($id) { Write-Host " present $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." } Write-Host " created $label" -ForegroundColor Green return (az ad sp list --filter "appId eq '$appId'" --query "[0].id" -o tsv) } return $null } Write-Host "Granting $($res.Name) user_impersonation to $($app.Name) for all users in $TenantId" $resourceSpId = Ensure-Sp $res.AppId "$($res.Name) ($($res.AppId))" $clientSpId = Ensure-Sp $app.AppId "$($app.Name) ($($app.AppId))" if (-not $resourceSpId -or -not $clientSpId) { return } $existing = az rest --method GET ` --url "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$clientSpId' and resourceId eq '$resourceSpId' and consentType eq 'AllPrincipals'" ` --query "value[0]" -o json 2>$null | ConvertFrom-Json if ($existing -and ($existing.scope -split ' ') -contains 'user_impersonation') { Write-Host " present AllPrincipals grant: $($existing.scope)" return } $body = @{ clientId = $clientSpId consentType = "AllPrincipals" resourceId = $resourceSpId scope = if ($existing) { "$($existing.scope) user_impersonation".Trim() } else { "user_impersonation" } } | ConvertTo-Json -Compress if ($PSCmdlet.ShouldProcess("$($app.Name) -> $($res.Name)", "record AllPrincipals grant for user_impersonation")) { $tmp = New-TemporaryFile try { Set-Content -Path $tmp -Value $body -Encoding utf8 -NoNewline if ($existing) { az rest --method PATCH --url "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$($existing.id)" ` --headers "Content-Type=application/json" --body "@$tmp" | Out-Null } else { az rest --method POST --url "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" ` --headers "Content-Type=application/json" --body "@$tmp" | Out-Null } } finally { Remove-Item $tmp -ErrorAction SilentlyContinue } Write-Host " granted user_impersonation (AllPrincipals)" -ForegroundColor Green Write-Host "" Write-Host "Allow 10-30 minutes to propagate. Users will no longer be prompted for $($res.Name)." }