Public/Permissions/Invoke-365TuneConnectAzure.ps1

function Invoke-365TuneConnectAzure {
    <#
    .SYNOPSIS
        Assigns Reader permissions to the 365TUNE Enterprise App in Azure.

    .DESCRIPTION
        Grants Reader access at root scope "/" and AAD IAM scope
        "/providers/Microsoft.aadiam". Temporarily elevates to User Access
        Administrator then self-cleans after assignment.

        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-365TuneConnectAzure

    .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 — Assign 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 -AllowClobber
        }
    }
    Import-Module Az.Accounts, Az.Resources
    Write-Host " ✅ Modules ready." -ForegroundColor Green

    # Step 2 — Authenticate
    Write-Host "`n[2/5] Authenticating..." -ForegroundColor Cyan

    $inCloudShell = ($env:ACC_CLOUD -eq "PROD") -or
                    ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like "*CloudShell*") -or
                    ($env:AZUREPS_HOST_ENVIRONMENT -like "*cloud-shell*")

    if (-not $SkipAuth) {
        if ($inCloudShell) {
            Write-Host " Cloud Shell detected — using existing session." -ForegroundColor Gray
        } 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 — Fetch Service Principal via Graph API (avoids Az.MSGraph dependency issues)
    Write-Host "`n[3/5] Looking up 365TUNE Service Principal..." -ForegroundColor Cyan
    $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 | Select-Object id, appId, displayName
    }

    $sp = Find-365TuneSP $displayNameProd
    if (-not $sp) {
        Write-Host " '$displayNameProd' not found — trying Beta..." -ForegroundColor Yellow
        $sp = Find-365TuneSP $displayNameBeta
    }
    if (-not $sp) {
        throw "Service Principal not found. Tried '$displayNameProd' and '$displayNameBeta'. Ensure the 365TUNE app has been consented to in this tenant."
    }
    $displayName        = $sp.displayName
    $servicePrincipalId = $sp.id
    Write-Host " Display Name : $($sp.displayName)"
    Write-Host " Object ID : $servicePrincipalId"
    Write-Host " App ID : $($sp.appId)"

    # Step 4 — Elevate and assign Reader
    Write-Host "`n[4/5] Elevating and assigning Reader permissions..." -ForegroundColor Cyan
    Write-Host " (Elevation is temporary — removed at end of script)"
    Invoke-365TuneElevation

    foreach ($scope in @("/", "/providers/Microsoft.aadiam")) {
        try {
            New-AzRoleAssignment `
                -ObjectId           $servicePrincipalId `
                -Scope              $scope `
                -RoleDefinitionName "Reader" `
                -SkipClientSideScopeValidation `
                -ErrorAction Stop
            Write-Host " ✅ Reader assigned at '$scope'" -ForegroundColor Green
        } catch {
            if ($_.Exception.Message -like "*Conflict*"  -or
                $_.Exception.Message -like "*Forbidden*" -or
                $_.Exception.Message -like "*already exists*") {
                Write-Warning " ⚠️ Reader at '$scope' already exists — skipping"
            } else { throw }
        }
    }

    # Step 5 — Verify and remove elevation
    Write-Host "`n[5/5] Verifying and removing elevation..." -ForegroundColor Cyan

    # Verify aadiam scope
    $verifiedAadiam  = Invoke-AzRestMethod `
        -Path   "/providers/Microsoft.aadiam/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$servicePrincipalId'" `
        -Method GET
    $aadIamValues = ($verifiedAadiam.Content | ConvertFrom-Json).value
    if ($aadIamValues.Count -gt 0) {
        Write-Host " Verified scope: /providers/Microsoft.aadiam" -ForegroundColor Green
    } else {
        Write-Warning " Could not verify /providers/Microsoft.aadiam assignment — check Azure Portal."
    }

    # Verify root scope
    $rootAssignment = Get-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Reader" -Scope "/" -ErrorAction SilentlyContinue |
        Where-Object { $_.Scope -eq "/" }
    if ($rootAssignment) {
        Write-Host " Verified scope: /" -ForegroundColor Green
    } else {
        Write-Warning " Could not verify / assignment — check Azure Portal."
    }

    Remove-365TuneElevation

    Write-Host "`n══════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " 365TUNE Azure permissions configured. ✅" -ForegroundColor Green
    Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Green
    Write-Host " Account : $($context.Account.Id)" -ForegroundColor Green
    Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan
}