Public/Permissions/Invoke-365TuneRevokeExchange.ps1
|
function Invoke-365TuneRevokeExchange { <# .SYNOPSIS Revokes Exchange Online View-Only Configuration role from the 365TUNE Enterprise App. .DESCRIPTION Removes the View-Only Configuration role assignment and Service Principal registration from Exchange Online. Safe to re-run — exits cleanly if nothing is found. Works in both local PowerShell and Azure Cloud Shell. - Local PowerShell : interactive browser login - Cloud Shell : uses the existing authenticated session — no extra login required Your account must have Global Administrator and Exchange Administrator rights. .EXAMPLE Invoke-365TuneRevokeExchange .NOTES Author : Metawise Consulting LLC Module : 365TUNE Version : 2.1.7 #> [CmdletBinding()] param( [switch]$SkipAuth ) $displayNameProd = "365TUNE - Security and Compliance" $displayNameBeta = "365TUNE - Security and Compliance - Beta" Write-Host "`n══════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " 365TUNE — Revoke Exchange Online Permissions" -ForegroundColor Cyan Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan # Step 1 — Check modules Write-Host "[1/4] Checking required modules..." -ForegroundColor Cyan foreach ($module in @("Az.Accounts", "ExchangeOnlineManagement")) { $installed = Get-Module -ListAvailable -Name $module | Sort-Object Version -Descending | Select-Object -First 1 $needsInstall = -not $installed $needsUpgrade = ($module -eq "ExchangeOnlineManagement") -and $installed -and ($installed.Version.Major -lt 3) if ($needsInstall -or $needsUpgrade) { Write-Host " Installing $module..." -ForegroundColor Yellow Install-Module -Name $module -Force -Scope CurrentUser -AllowClobber } } Import-Module Az.Accounts # Load EXO by explicit path to bypass any system-level version Cloud Shell ships with $exoModulePath = (Get-Module -ListAvailable -Name ExchangeOnlineManagement | Sort-Object Version -Descending | Select-Object -First 1).Path if (-not $exoModulePath) { throw "ExchangeOnlineManagement module not found. Run: Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force" } Import-Module $exoModulePath -Force Write-Host " ✅ All modules ready." -ForegroundColor Green # Detect Cloud Shell $inCloudShell = ($env:ACC_CLOUD -eq "PROD") -or ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like "*CloudShell*") -or ($env:AZUREPS_HOST_ENVIRONMENT -like "*cloud-shell*") # Step 2 — Authenticate Write-Host "`n[2/4] Authenticating..." -ForegroundColor Cyan if (-not $SkipAuth) { if ($inCloudShell) { Write-Host " Cloud Shell detected — using existing session." -ForegroundColor Gray # Cloud Shell is already authenticated; just ensure context is present } 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 — Connect to Exchange Online and check assignments Write-Host "`n[3/4] Connecting to Exchange Online..." -ForegroundColor Cyan if ($inCloudShell) { # Reuse the existing session token — no extra login prompt $exoTokenObj = Get-AzAccessToken -ResourceUrl "https://outlook.office365.com" if ($exoTokenObj.Token -is [System.Security.SecureString]) { $exoToken = [System.Net.NetworkCredential]::new("", $exoTokenObj.Token).Password } else { $exoToken = $exoTokenObj.Token } # MSI account has no UPN — resolve the tenant's default domain from Get-AzTenant $tenantDomain = (Get-AzTenant -TenantId $context.Tenant.Id).DefaultDomain Connect-ExchangeOnline -AccessToken $exoToken -DelegatedOrganization $tenantDomain -ShowBanner:$false } else { Connect-ExchangeOnline -ShowBanner:$false } Start-Sleep -Milliseconds 500 Write-Host " ✅ Connected." -ForegroundColor Green # Resolve which app name is present in this tenant via Graph API (avoids Az.MSGraph dependency issues) $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 } $spCheck = Find-365TuneSP $displayNameProd if (-not $spCheck) { Write-Host " '$displayNameProd' not found — trying Beta..." -ForegroundColor Yellow $spCheck = Find-365TuneSP $displayNameBeta } if (-not $spCheck) { throw "Service Principal not found. Tried '$displayNameProd' and '$displayNameBeta'. Ensure the app has been consented to in this tenant." } $displayName = $spCheck.displayName $existingAssignment = Get-ManagementRoleAssignment -ErrorAction SilentlyContinue | Where-Object { $_.RoleAssigneeName -eq $displayName -and $_.Role -eq "View-Only Configuration" } $existingSP = Get-ServicePrincipal -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -eq $displayName } if (-not $existingAssignment -and -not $existingSP) { Write-Host "`n══════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " No Exchange permissions found — nothing to revoke. ✅" -ForegroundColor Green Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan Disconnect-ExchangeOnline -Confirm:$false Start-Sleep -Milliseconds 500 return } # Step 4 — Remove role and Service Principal Write-Host "`n[4/4] Removing role and Service Principal..." -ForegroundColor Cyan if ($existingAssignment) { try { Remove-ManagementRoleAssignment -Identity "$($existingAssignment.Name)" -Confirm:$false -ErrorAction Stop Write-Host " ✅ View-Only Configuration role removed." -ForegroundColor Green } catch { if ($_.Exception.Message -like "*couldn't be found*" -or $_.Exception.Message -like "*NotFound*") { Write-Warning " ⚠️ Role assignment not found — already removed" } else { throw } } } else { Write-Warning " ⚠️ No role assignment found — already removed" } if ($existingSP) { try { Remove-ServicePrincipal -Identity $existingSP.Identity -Confirm:$false -ErrorAction Stop Write-Host " ✅ Service Principal removed from Exchange Online." -ForegroundColor Green } catch { if ($_.Exception.Message -like "*couldn't be found*" -or $_.Exception.Message -like "*NotFound*") { Write-Warning " ⚠️ Service Principal not found — already removed" } else { throw } } } else { Write-Warning " ⚠️ No Service Principal found — already removed" } 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 revoked. ✅" -ForegroundColor Green Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Green Write-Host " Account : $($context.Account.Id)" -ForegroundColor Green Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan } |