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 : 2.1.5 #> [CmdletBinding()] param( [switch]$SkipAuth ) $displayNameProd = "365TUNE - Security and Compliance" $displayNameBeta = "365TUNE - Security and Compliance - Beta" 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 -AllowClobber } } Import-Module Az.Accounts, Az.Resources Write-Host " ✅ Modules ready." -ForegroundColor Green # Step 2 — Authenticate Write-Host "`n[2/5] Authenticating..." -ForegroundColor Cyan $inCloudShell = ($env:ACC_CLOUD -eq "PROD") -or ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like "*CloudShell*") -or ($env:AZUREPS_HOST_ENVIRONMENT -like "*cloud-shell*") if (-not $SkipAuth) { if ($inCloudShell) { Write-Host " Cloud Shell detected — using existing session." -ForegroundColor Gray } else { Disconnect-AzAccount -ErrorAction SilentlyContinue | Out-Null Connect-AzAccount -WarningAction SilentlyContinue | Out-Null } } $context = Get-AzContext if (-not $context) { throw "Not authenticated. Please try again." } Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Gray Write-Host " Account : $($context.Account.Id)" -ForegroundColor Gray Write-Host " ✅ Authenticated." -ForegroundColor Green # Step 3 — Fetch Service Principal via Graph API (avoids Az.MSGraph dependency issues) Write-Host "`n[3/5] Looking up 365TUNE Service Principal..." -ForegroundColor Cyan $graphTokenObj = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" if ($graphTokenObj.Token -is [System.Security.SecureString]) { $graphToken = [System.Net.NetworkCredential]::new("", $graphTokenObj.Token).Password } else { $graphToken = $graphTokenObj.Token } $graphHeaders = @{ Authorization = "Bearer $graphToken"; "Content-Type" = "application/json" } function Find-365TuneSP ($name) { $encoded = [Uri]::EscapeDataString("displayName eq '$name'") $response = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=$encoded" -Headers $graphHeaders -Method GET $response.value | Select-Object -First 1 | Select-Object id, appId, displayName } $sp = Find-365TuneSP $displayNameProd if (-not $sp) { Write-Host " '$displayNameProd' not found — trying Beta..." -ForegroundColor Yellow $sp = Find-365TuneSP $displayNameBeta } if (-not $sp) { throw "Service Principal not found. Tried '$displayNameProd' and '$displayNameBeta'. Ensure the 365TUNE app has been consented to in this tenant." } $displayName = $sp.displayName $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 foreach ($scope in @("/", "/providers/Microsoft.aadiam")) { try { New-AzRoleAssignment ` -ObjectId $servicePrincipalId ` -Scope $scope ` -RoleDefinitionName "Reader" ` -SkipClientSideScopeValidation ` -ErrorAction Stop Write-Host " ✅ 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 " ⚠️ 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 aadiam scope $verifiedAadiam = Invoke-AzRestMethod ` -Path "/providers/Microsoft.aadiam/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$servicePrincipalId'" ` -Method GET $aadIamValues = ($verifiedAadiam.Content | ConvertFrom-Json).value if ($aadIamValues.Count -gt 0) { Write-Host " Verified scope: /providers/Microsoft.aadiam" -ForegroundColor Green } else { Write-Warning " Could not verify /providers/Microsoft.aadiam assignment — check Azure Portal." } # Verify root scope $rootAssignment = Get-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Reader" -Scope "/" -ErrorAction SilentlyContinue | Where-Object { $_.Scope -eq "/" } if ($rootAssignment) { Write-Host " Verified scope: /" -ForegroundColor Green } else { Write-Warning " Could not verify / assignment — check Azure Portal." } Remove-365TuneElevation Write-Host "`n══════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " 365TUNE Azure permissions configured. ✅" -ForegroundColor Green Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Green Write-Host " Account : $($context.Account.Id)" -ForegroundColor Green Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan } |