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 only (not 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 -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 - 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 ` -TimeoutSec 30 -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=displayName eq 'Office 365 Exchange Online'" ` -Headers $headers -Method GET -ErrorAction Stop 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 ` -TimeoutSec 30 -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$objectId/appRoleAssignments" ` -Headers $headers -Method GET -ErrorAction Stop $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 ` -TimeoutSec 30 -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$objectId/appRoleAssignments" ` -Headers $headers -Method POST -Body $consentBody | Out-Null -ErrorAction Stop 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 } |