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


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

    $displayName = "365TUNE - Security and Compliance"

    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
        }
    }
    Import-Module Az.Accounts, Az.Resources
    Write-Host " ✅ 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 " ✅ Authenticated." -ForegroundColor Green

    # Step 3 — Fetch Service Principal
    Write-Host "`n[3/5] Looking up 365TUNE Service Principal..." -ForegroundColor Cyan
    $sp = Get-AzADServicePrincipal -DisplayName $displayName | Select-Object Id, AppId, DisplayName
    if (-not $sp) {
        throw "Service Principal '$displayName' not found. Ensure the 365TUNE app has been consented to in this tenant."
    }
    $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" `
                -ObjectType         "ServicePrincipal" `
                -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
    $verified       = Invoke-AzRestMethod `
        -Path   "providers/Microsoft.aadiam/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01" `
        -Method GET
    $verifiedValues = ($verified.Content | ConvertFrom-Json).value |
        Where-Object { $_.properties.principalId -eq $servicePrincipalId }
    if ($verifiedValues.Count -gt 0) {
        $verifiedValues | Select-Object -ExpandProperty properties |
            ForEach-Object { Write-Host " Verified scope: $($_.scope)" -ForegroundColor Green }
    } else {
        Write-Warning " Could not verify assignments — 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
}