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 : 1.9.0
    #>


    [CmdletBinding()]
    param(
        [switch]$SkipAuth
    )

    $displayName = "365TUNE - Security and Compliance"

    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 -Force -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) {
            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 - Fetch Service Principal via Graph REST (avoids Az.MSGraph dependency issues)
    Write-Host "`n[3/5] Looking up 365TUNE Service Principal..." -ForegroundColor Cyan
    $graphToken    = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" -ErrorAction Stop)
    $graphTokenStr = if ($graphToken.Token -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new("", $graphToken.Token).Password } else { $graphToken.Token }
    $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
    $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 - Pre-flight check
    Write-Host "`n[4/5] Checking for existing assignments..." -ForegroundColor Cyan

    # Check root "/" assignment via standard RBAC
    $rootAssignment = Get-AzRoleAssignment `
        -ObjectId           $servicePrincipalId `
        -RoleDefinitionName "Reader" `
        -Scope              "/" `
        -ErrorAction SilentlyContinue |
        Where-Object { $_.Scope -eq "/" }

    # Check aadiam assignment via standard RBAC
    $aadIamAssignment = Get-AzRoleAssignment `
        -ObjectId           $servicePrincipalId `
        -RoleDefinitionName "Reader" `
        -Scope              "/providers/Microsoft.aadiam" `
        -ErrorAction SilentlyContinue |
        Where-Object { $_.Scope -eq "/providers/Microsoft.aadiam" }

    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 = (($rootAssignment ? 1 : 0) + ($aadIamAssignment ? 1 : 0))
    Write-Host " Found $foundCount assignment(s) - proceeding with removal." -ForegroundColor Yellow

    # Step 5 - Elevate and remove
    Write-Host "`n[5/5] Elevating and removing Reader permissions..." -ForegroundColor Cyan
    Invoke-365TuneElevation

    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 }
    }

    if ($aadIamAssignment) {
        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"
    }

    $remainRoot   = Get-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Reader" -Scope "/" -ErrorAction SilentlyContinue | Where-Object { $_.Scope -eq "/" }
    $remainAadiam = Get-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Reader" -Scope "/providers/Microsoft.aadiam" -ErrorAction SilentlyContinue | Where-Object { $_.Scope -eq "/providers/Microsoft.aadiam" }

    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."
    }

    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
}