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 : 1.9.1 #> [CmdletBinding()] param( [switch]$SkipAuth ) $displayName = "365TUNE - Security and Compliance" 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 -Force -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").Token $token = [System.Net.NetworkCredential]::new("", $secureToken).Password $headers = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } # Fetch 365TUNE SP $sp = Get-AzADServicePrincipal -DisplayName $displayName | Select-Object Id, AppId, DisplayName 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 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 $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 Invoke-RestMethod ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$objectId/appRoleAssignments" ` -Headers $headers -Method POST -Body $consentBody | 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 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 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 } |