Public/Permissions/Invoke-365TuneConnectAzure.ps1
|
function Invoke-365TuneConnectAzure { <# .SYNOPSIS Assigns Reader permissions to the 365TUNE Enterprise App in Azure. .DESCRIPTION Grants Reader access at root scope "/" and AAD IAM scope "/providers/Microsoft.aadiam". Temporarily elevates to User Access Administrator then self-cleans after assignment. Run from local PowerShell or Cloud Shell. Your account must have Global Administrator rights and "Access management for Azure resources" enabled in Entra ID > Properties. .EXAMPLE Invoke-365TuneConnectAzure .NOTES Author : Metawise Consulting LLC Module : 365TUNE Version : 1.9.0 #> [CmdletBinding()] param( [switch]$SkipAuth ) $displayName = "365TUNE - Security and Compliance" Write-Host "`n======================================================" -ForegroundColor Cyan Write-Host " 365TUNE - Assign Azure Permissions" -ForegroundColor Cyan Write-Host "======================================================`n" -ForegroundColor Cyan # Step 1 - Check modules Write-Host "[1/5] Checking required modules..." -ForegroundColor Cyan foreach ($module in @("Az.Accounts", "Az.Resources")) { if (-not (Get-Module -ListAvailable -Name $module)) { Write-Host " Installing $module..." -ForegroundColor Yellow Install-Module -Name $module -Force -Scope CurrentUser } } Import-Module Az.Accounts, Az.Resources Write-Host " [OK] Modules ready." -ForegroundColor Green # Step 2 - Authenticate Write-Host "`n[2/5] Authenticating..." -ForegroundColor Cyan if (-not $SkipAuth) { $inCloudShell = ($env:ACC_CLOUD -eq "PROD") -or ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like "*CloudShell*") -or ($env:AZUREPS_HOST_ENVIRONMENT -like "*cloud-shell*") if ($inCloudShell) { Connect-AzAccount -Identity -WarningAction SilentlyContinue | Out-Null } else { Disconnect-AzAccount -ErrorAction SilentlyContinue | Out-Null Connect-AzAccount -WarningAction SilentlyContinue | Out-Null } } $context = Get-AzContext if (-not $context) { throw "Not authenticated. Run without -SkipAuth or log in manually first." } Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Gray Write-Host " Account : $($context.Account.Id)" -ForegroundColor Gray Write-Host " [OK] Authenticated." -ForegroundColor Green # Step 3 - Fetch Service Principal via Graph REST (avoids Az.MSGraph dependency issues) Write-Host "`n[3/5] Looking up 365TUNE Service Principal..." -ForegroundColor Cyan $graphToken = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" -ErrorAction Stop) $graphTokenStr = if ($graphToken.Token -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new("", $graphToken.Token).Password } else { $graphToken.Token } $graphHeaders = @{ Authorization = "Bearer $graphTokenStr"; "Content-Type" = "application/json" } $spFilter = [Uri]::EscapeDataString("displayName eq '$displayName'") $spResponse = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=$spFilter" -Headers $graphHeaders -Method GET $sp = $spResponse.value | Select-Object -First 1 if (-not $sp) { throw "Service Principal '$displayName' not found. Ensure the 365TUNE app has been consented to in this tenant." } $servicePrincipalId = $sp.id Write-Host " Display Name : $($sp.displayName)" Write-Host " Object ID : $servicePrincipalId" Write-Host " App ID : $($sp.appId)" # Step 4 - Elevate and assign Reader Write-Host "`n[4/5] Elevating and assigning Reader permissions..." -ForegroundColor Cyan Write-Host " (Elevation is temporary - removed at end of script)" Invoke-365TuneElevation # Reconnect for fresh token with elevated role baked in Write-Host " Refreshing session with elevated permissions..." -ForegroundColor Gray $currentTenantId = $context.Tenant.Id Disconnect-AzAccount -ErrorAction SilentlyContinue | Out-Null $inCloudShell = ($env:ACC_CLOUD -eq "PROD") -or ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like "*CloudShell*") -or ($env:AZUREPS_HOST_ENVIRONMENT -like "*cloud-shell*") if ($inCloudShell) { Connect-AzAccount -Identity -WarningAction SilentlyContinue | Out-Null } else { Connect-AzAccount -TenantId $currentTenantId -WarningAction SilentlyContinue | Out-Null } Write-Host " [OK] Session refreshed with elevated permissions." -ForegroundColor Green foreach ($scope in @("/", "/providers/Microsoft.aadiam")) { try { New-AzRoleAssignment ` -ObjectId $servicePrincipalId ` -Scope $scope ` -RoleDefinitionName "Reader" ` -ObjectType "ServicePrincipal" ` -SkipClientSideScopeValidation ` -ErrorAction Stop Write-Host " [OK] Reader assigned at '$scope'" -ForegroundColor Green } catch { if ($_.Exception.Message -like "*Conflict*" -or $_.Exception.Message -like "*Forbidden*" -or $_.Exception.Message -like "*already exists*") { Write-Warning " [WARN] Reader at '$scope' already exists - skipping" } else { throw } } } # Step 5 - Verify and remove elevation Write-Host "`n[5/5] Verifying and removing elevation..." -ForegroundColor Cyan # Verify root assignment (visible without elevation) $verifyRoot = Get-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Reader" -Scope "/" -ErrorAction SilentlyContinue | Where-Object { $_.Scope -eq "/" } if ($verifyRoot) { Write-Host " Verified scope: /" -ForegroundColor Green } # Note: aadiam assignment requires elevation to verify - trusted as assigned above Remove-365TuneElevation Write-Host "`n======================================================" -ForegroundColor Cyan Write-Host " 365TUNE Azure permissions configured. [OK]" -ForegroundColor Green Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Green Write-Host " Account : $($context.Account.Id)" -ForegroundColor Green Write-Host "======================================================`n" -ForegroundColor Cyan } |