Public/Permissions/Invoke-365TuneRevokeAzure.ps1
|
function Invoke-365TuneRevokeAzure { <# .SYNOPSIS Revokes Reader permissions previously granted to the 365TUNE Enterprise App in Azure. .DESCRIPTION Removes Reader access from root scope "/" and AAD IAM scope "/providers/Microsoft.aadiam". Safe to re-run - exits cleanly if no assignments are found. 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-365TuneRevokeAzure .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 - Revoke 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 -Scope CurrentUser } } Import-Module Az.Accounts, Az.Resources Write-Host " [OK] 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) { # Cloud Shell always has MSI loaded; the portal also injects the user's context. # Switch to the first non-MSI context if available, otherwise try interactive login. $context = Get-AzContext if (-not $context -or $context.Account.Type -eq 'ManagedService') { $userContext = Get-AzContext -ListAvailable | Where-Object { $_.Account.Type -ne 'ManagedService' } | Select-Object -First 1 if ($userContext) { Write-Host " Switching to user context..." -ForegroundColor Yellow Set-AzContext -Context $userContext | Out-Null } else { # az CLI in Cloud Shell is always authenticated as the portal user. Write-Host " Bridging from Azure CLI session..." -ForegroundColor Yellow try { $armToken = az account get-access-token --resource https://management.azure.com --query accessToken -o tsv 2>$null $graphToken = az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv 2>$null $tenantId = az account show --query tenantId -o tsv 2>$null $accountId = az account show --query user.name -o tsv 2>$null if ($armToken -and $tenantId -and $accountId) { Connect-AzAccount -AccessToken $armToken -GraphAccessToken $graphToken -TenantId $tenantId -AccountId $accountId -WarningAction SilentlyContinue | Out-Null } } catch { Write-Verbose "Azure CLI bridge failed: $_" } } } } 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." } if ($context.Account.Type -eq 'ManagedService') { throw "Authenticated as Managed Service Identity. Run 'Connect-AzAccount' in Cloud Shell then re-run." } Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Gray Write-Host " Account : $($context.Account.Id)" -ForegroundColor Gray Write-Host " [OK] Authenticated." -ForegroundColor Green # Step 3 - Fetch Service Principal via Graph REST (avoids Az.MSGraph dependency issues) Write-Host "`n[3/5] Looking up 365TUNE Service Principal..." -ForegroundColor Cyan try { $graphTokenObj = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" -ErrorAction Stop $graphTokenStr = if ($graphTokenObj.Token -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new("", $graphTokenObj.Token).Password } else { $graphTokenObj.Token } } catch { $graphTokenStr = az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv 2>$null if (-not $graphTokenStr) { throw } } $graphHeaders = @{ Authorization = "Bearer $graphTokenStr"; "Content-Type" = "application/json" } $spFilter = [Uri]::EscapeDataString("displayName eq '$displayName'") $spResponse = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=$spFilter" -Headers $graphHeaders -Method GET -ErrorAction Stop -TimeoutSec 30 $sp = $spResponse.value | Select-Object -First 1 if (-not $sp) { throw "Service Principal '$displayName' not found in this tenant." } $servicePrincipalId = $sp.id Write-Host " Object ID : $servicePrincipalId" # Step 4 - Elevate first (required to see and remove aadiam-scoped assignments) Write-Host "`n[4/5] Elevating and checking assignments..." -ForegroundColor Cyan Write-Host " (Elevation is temporary - removed at end of script)" Invoke-365TuneElevation try { # Short sleep then force-fresh token so elevation is reflected Start-Sleep -Seconds 10 # Query assignments via REST try { $mgmtTokenObj = Get-AzAccessToken -ResourceUrl "https://management.azure.com" -ErrorAction Stop $mgmtTokenStr = if ($mgmtTokenObj.Token -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new("", $mgmtTokenObj.Token).Password } else { $mgmtTokenObj.Token } } catch { $mgmtTokenStr = az account get-access-token --resource https://management.azure.com --query accessToken -o tsv 2>$null if (-not $mgmtTokenStr) { throw } } $mgmtHeaders = @{ Authorization = "Bearer $mgmtTokenStr"; "Content-Type" = "application/json" } $readerRoleId = "acdd72a7-3385-48ef-bd42-f606fba81ae7" $rootAssignment = (Invoke-RestMethod -Uri "https://management.azure.com/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$servicePrincipalId'" -Headers $mgmtHeaders -Method GET -ErrorAction Stop -TimeoutSec 30).value | Where-Object { $_.properties.scope -eq "/" -and $_.properties.roleDefinitionId -like "*$readerRoleId*" } $aadIamAssignment = (Invoke-RestMethod -Uri "https://management.azure.com/providers/Microsoft.aadiam/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$servicePrincipalId'" -Headers $mgmtHeaders -Method GET -ErrorAction Stop -TimeoutSec 30).value | Where-Object { $_.properties.roleDefinitionId -like "*$readerRoleId*" } if (-not $rootAssignment -and -not $aadIamAssignment) { Write-Host "`n======================================================" -ForegroundColor Cyan Write-Host " No assignments found - nothing to revoke. [OK]" -ForegroundColor Green Write-Host "======================================================`n" -ForegroundColor Cyan return } $foundCount = ($(if ($rootAssignment) { 1 } else { 0 }) + $(if ($aadIamAssignment) { 1 } else { 0 })) Write-Host " Found $foundCount assignment(s) - proceeding with removal." -ForegroundColor Yellow # Step 5 - Remove Write-Host "`n[5/5] Removing Reader permissions..." -ForegroundColor Cyan # Remove root scope assignment if ($rootAssignment) { $delPath1 = $rootAssignment.id.TrimStart('/') $delUri1 = "https://management.azure.com/$delPath1`?api-version=2022-04-01" $removed1 = $false # Method 1: Invoke-WebRequest DELETE with ARM token if ($mgmtTokenStr) { try { $delResp1 = Invoke-WebRequest -Uri $delUri1 -Method DELETE -Headers @{ Authorization = "Bearer $mgmtTokenStr" } -UseBasicParsing if ([int]$delResp1.StatusCode -in @(200, 204)) { Write-Host " [OK] Reader removed from '/'" -ForegroundColor Green $removed1 = $true } } catch { if ($_.Exception.Response -and [int]$_.Exception.Response.StatusCode -eq 404) { Write-Warning " [WARN] Reader at '/' not found - already removed" $removed1 = $true } } } # Method 2: az rest DELETE if (-not $removed1 -and (Get-Command az -ErrorAction SilentlyContinue)) { $azDel1 = az rest --method DELETE --url $delUri1 2>&1 if ($LASTEXITCODE -eq 0) { Write-Host " [OK] Reader removed from '/'" -ForegroundColor Green $removed1 = $true } elseif ($azDel1 -like "*NotFound*" -or $azDel1 -like "*404*") { Write-Warning " [WARN] Reader at '/' not found - already removed" $removed1 = $true } } # Method 3: Remove-AzRoleAssignment cmdlet if (-not $removed1) { try { Remove-AzRoleAssignment ` -ObjectId $servicePrincipalId ` -Scope "/" ` -RoleDefinitionName "Reader" ` -SkipClientSideScopeValidation ` -ErrorAction Stop Write-Host " [OK] Reader removed from '/'" -ForegroundColor Green } catch { if ($_.Exception.Message -like "*does not exist*" -or $_.Exception.Message -like "*NotFound*" -or $_.Exception.Message -like "*does not map*" -or $_.Exception.Message -like "*Forbidden*") { Write-Warning " [WARN] Reader at '/' not found - already removed" } else { throw } } } } else { Write-Warning " [WARN] Reader at '/' not found - already removed" } # Remove aadiam scope assignment if ($aadIamAssignment) { $delPath2 = $aadIamAssignment.id.TrimStart('/') $delUri2 = "https://management.azure.com/$delPath2`?api-version=2022-04-01" $removed2 = $false # Method 1: Invoke-WebRequest DELETE with ARM token if ($mgmtTokenStr) { try { $delResp2 = Invoke-WebRequest -Uri $delUri2 -Method DELETE -Headers @{ Authorization = "Bearer $mgmtTokenStr" } -UseBasicParsing if ([int]$delResp2.StatusCode -in @(200, 204)) { Write-Host " [OK] Reader removed from '/providers/Microsoft.aadiam'" -ForegroundColor Green $removed2 = $true } } catch { if ($_.Exception.Response -and [int]$_.Exception.Response.StatusCode -eq 404) { Write-Warning " [WARN] Reader at '/providers/Microsoft.aadiam' not found - already removed" $removed2 = $true } } } # Method 2: az rest DELETE if (-not $removed2 -and (Get-Command az -ErrorAction SilentlyContinue)) { $azDel2 = az rest --method DELETE --url $delUri2 2>&1 if ($LASTEXITCODE -eq 0) { Write-Host " [OK] Reader removed from '/providers/Microsoft.aadiam'" -ForegroundColor Green $removed2 = $true } elseif ($azDel2 -like "*NotFound*" -or $azDel2 -like "*404*") { Write-Warning " [WARN] Reader at '/providers/Microsoft.aadiam' not found - already removed" $removed2 = $true } } # Method 3: Remove-AzRoleAssignment cmdlet if (-not $removed2) { try { Remove-AzRoleAssignment ` -ObjectId $servicePrincipalId ` -RoleDefinitionName "Reader" ` -Scope "/providers/Microsoft.aadiam" ` -SkipClientSideScopeValidation ` -ErrorAction Stop Write-Host " [OK] Reader removed from '/providers/Microsoft.aadiam'" -ForegroundColor Green } catch { if ($_.Exception.Message -like "*does not exist*" -or $_.Exception.Message -like "*NotFound*" -or $_.Exception.Message -like "*does not map*" -or $_.Exception.Message -like "*Forbidden*") { Write-Warning " [WARN] Reader at '/providers/Microsoft.aadiam' not found - already removed" } else { throw } } } } else { Write-Warning " [WARN] Reader at '/providers/Microsoft.aadiam' not found - already removed" } # Verify via REST try { $mgmtTokenObj2 = Get-AzAccessToken -ResourceUrl "https://management.azure.com" -ErrorAction Stop $mgmtTokenStr2 = if ($mgmtTokenObj2.Token -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new("", $mgmtTokenObj2.Token).Password } else { $mgmtTokenObj2.Token } } catch { $mgmtTokenStr2 = az account get-access-token --resource https://management.azure.com --query accessToken -o tsv 2>$null if (-not $mgmtTokenStr2) { throw } } $mgmtHeaders2 = @{ Authorization = "Bearer $mgmtTokenStr2"; "Content-Type" = "application/json" } $readerRoleId = "acdd72a7-3385-48ef-bd42-f606fba81ae7" $remainRoot = (Invoke-RestMethod -Uri "https://management.azure.com/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$servicePrincipalId'" -Headers $mgmtHeaders2 -Method GET -ErrorAction Stop -TimeoutSec 30).value | Where-Object { $_.properties.scope -eq "/" -and $_.properties.roleDefinitionId -like "*$readerRoleId*" } $remainAadiam = (Invoke-RestMethod -Uri "https://management.azure.com/providers/Microsoft.aadiam/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$servicePrincipalId'" -Headers $mgmtHeaders2 -Method GET -ErrorAction Stop -TimeoutSec 30).value | Where-Object { $_.properties.roleDefinitionId -like "*$readerRoleId*" } if (-not $remainRoot -and -not $remainAadiam) { Write-Host " [OK] Verified - no assignments remain." -ForegroundColor Green } else { Write-Warning " [WARN] Assignments still remain - check Azure Portal." } } finally { Remove-365TuneElevation } Write-Host "`n======================================================" -ForegroundColor Cyan Write-Host " 365TUNE Azure permissions revoked. [OK]" -ForegroundColor Green Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Green Write-Host " Account : $($context.Account.Id)" -ForegroundColor Green Write-Host "======================================================`n" -ForegroundColor Cyan } |