Public/Permissions/Invoke-365TuneConnectExchange.ps1
|
function Invoke-365TuneConnectExchange { <# .SYNOPSIS Assigns Exchange Online View-Only Configuration role to the 365TUNE Enterprise App. .DESCRIPTION Grants admin consent for Exchange.ManageAsApp in the customer tenant, registers the Service Principal in Exchange Online, and assigns the View-Only Configuration management role. Operates entirely within the customer tenant - no changes made to the 365TUNE app registration in the Metawise tenant. Run from local PowerShell or Cloud Shell. Your account must have Global Administrator and Exchange Administrator rights. .EXAMPLE Invoke-365TuneConnectExchange .NOTES Author : Metawise Consulting LLC Module : 365TUNE Version : 2.3.4 #> [CmdletBinding()] param( [switch]$SkipAuth ) $displayName = "365TUNE - Security and Compliance" $context = Get-AzContext Write-Host "`n======================================================" -ForegroundColor Cyan Write-Host " 365TUNE - Assign Exchange Online 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", "ExchangeOnlineManagement")) { if (-not (Get-Module -ListAvailable -Name $module)) { Write-Host " Installing $module..." -ForegroundColor Yellow Install-Module -Name $module -Scope CurrentUser } } Import-Module Az.Accounts, Az.Resources, ExchangeOnlineManagement Write-Host " [OK] All 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 -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 - Resolve IDs from customer tenant only Write-Host "`n[3/5] Resolving IDs..." -ForegroundColor Cyan $secureToken = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" -ErrorAction Stop).Token $token = [System.Net.NetworkCredential]::new("", $secureToken).Password $headers = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } # Fetch 365TUNE SP $spFilter = [Uri]::EscapeDataString("displayName eq '$displayName'") $spResponse = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=$spFilter" -Headers $headers -Method GET -ErrorAction Stop -TimeoutSec 30 $sp = $spResponse.value | Select-Object -First 1 if (-not $sp) { throw "Service Principal '$displayName' not found. Ensure the app has been consented to in this tenant." } $objectId = $sp.id $appId = $sp.appId Write-Host " Display Name : $($sp.displayName)" Write-Host " Object ID : $objectId" Write-Host " App ID : $appId" # Fetch Exchange Online SP $exchangeSPResponse = Invoke-RestMethod ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=displayName eq 'Office 365 Exchange Online'" ` -Headers $headers -Method GET -ErrorAction Stop -TimeoutSec 30 if (-not $exchangeSPResponse.value) { throw "Office 365 Exchange Online SP not found in tenant." } $exchangeSP = $exchangeSPResponse.value[0] $exchangeSPId = $exchangeSP.id $exchangeManageAsAppRole = $exchangeSP.appRoles | Where-Object { $_.value -eq "Exchange.ManageAsApp" } if (-not $exchangeManageAsAppRole) { throw "Exchange.ManageAsApp role not found." } $exchangeManageAsAppId = $exchangeManageAsAppRole.id Write-Host " Exchange SP ID : $exchangeSPId" Write-Host " Exchange.ManageAsApp ID : $exchangeManageAsAppId" Write-Host " [OK] All IDs resolved." -ForegroundColor Green # Step 4 - Grant admin consent directly in customer tenant # No app registration changes - consent granted directly on the SP Write-Host "`n[4/5] Granting admin consent..." -ForegroundColor Cyan $existingGrant = Invoke-RestMethod ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$objectId/appRoleAssignments" ` -Headers $headers -Method GET -ErrorAction Stop -TimeoutSec 30 $alreadyGranted = $existingGrant.value | Where-Object { $_.appRoleId -eq $exchangeManageAsAppId } if ($alreadyGranted) { Write-Warning " [WARN] Admin consent already granted - skipping" } else { $consentBody = @{ principalId = $objectId resourceId = $exchangeSPId appRoleId = $exchangeManageAsAppId } | ConvertTo-Json -Depth 10 Invoke-RestMethod ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$objectId/appRoleAssignments" ` -Headers $headers -Method POST -Body $consentBody -ErrorAction Stop | Out-Null Write-Host " [OK] Admin consent granted." -ForegroundColor Green } # Step 5 - Connect Exchange Online, register SP and assign role Write-Host "`n[5/5] Connecting to Exchange Online and assigning role..." -ForegroundColor Cyan Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop Start-Sleep -Milliseconds 500 Write-Host " [OK] Connected to Exchange Online." -ForegroundColor Green try { New-ServicePrincipal -AppId $appId -ObjectId $objectId -DisplayName $displayName -ErrorAction Stop | Out-Null Write-Host " [OK] Service Principal registered in Exchange Online." -ForegroundColor Green } catch { if ($_.Exception.Message -like "*already exists*" -or $_.Exception.Message -like "*Conflict*" -or $_.Exception.Message -like "*ExternalDirectoryObjectId*") { Write-Warning " [WARN] Service Principal already registered - skipping" } else { throw } } try { New-ManagementRoleAssignment -Role "View-Only Configuration" -App $displayName -ErrorAction Stop | Out-Null Write-Host " [OK] View-Only Configuration role assigned." -ForegroundColor Green } catch { if ($_.Exception.Message -like "*already exists*" -or $_.Exception.Message -like "*Conflict*") { Write-Warning " [WARN] Role already assigned - skipping" } else { throw } } Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 500 Write-Host " Exchange Online session closed." -ForegroundColor Gray Write-Host "`n======================================================" -ForegroundColor Cyan Write-Host " 365TUNE Exchange Online 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 } |