Public/Permissions/Invoke-365TuneTestTeams.ps1

function Invoke-365TuneTestTeams {
    <#
    .SYNOPSIS
        Tests whether Teams Reader Entra ID role is correctly assigned to the 365TUNE Enterprise App.

    .DESCRIPTION
        Checks that the 365TUNE Service Principal has the Teams Reader directory role assigned
        in Entra ID. Read-only - makes no changes.

    .EXAMPLE
        Invoke-365TuneTestTeams

    .NOTES
        Author : Metawise Consulting LLC
        Module : 365TUNE
        Version : 2.2.7
    #>


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

    $displayNameProd = "365TUNE - Security and Compliance"
    $displayNameBeta = "365TUNE - Security and Compliance - Beta"
    $teamsRoleName   = "Teams Reader"

    Write-Host "`n======================================================" -ForegroundColor Cyan
    Write-Host " 365TUNE - Test Teams Permissions" -ForegroundColor Cyan
    Write-Host "======================================================`n" -ForegroundColor Cyan

    # Step 1 - Modules
    Write-Host "[1/4] Checking required modules..." -ForegroundColor Cyan
    if (-not (Get-Module -ListAvailable -Name "Az.Accounts")) {
        Install-Module -Name "Az.Accounts" -Force -Scope CurrentUser
    }
    Import-Module Az.Accounts
    Write-Host " [OK] Modules ready." -ForegroundColor Green

    # Step 2 - Authenticate
    Write-Host "`n[2/4] 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." }
    Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Gray
    Write-Host " Account : $($context.Account.Id)" -ForegroundColor Gray
    Write-Host " [OK] Authenticated." -ForegroundColor Green

    # Step 3 - Resolve SP and role via Graph
    Write-Host "`n[3/4] Resolving IDs via Graph..." -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
    }
    $headers = @{ 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 $headers -Method GET
        $response.value | Select-Object -First 1
    }

    $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. Ensure the 365TUNE app has been consented to in this tenant." }
    $spId = $sp.id
    Write-Host " Display Name : $($sp.displayName)"
    Write-Host " Object ID : $spId"

    $encoded      = [Uri]::EscapeDataString("displayName eq '$teamsRoleName'")
    $roleResponse = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?`$filter=$encoded" -Headers $headers -Method GET
    $roleDef      = $roleResponse.value | Select-Object -First 1
    if (-not $roleDef) { throw "Entra ID role '$teamsRoleName' not found." }
    $roleDefId = $roleDef.id
    Write-Host " Role : $($roleDef.displayName)"
    Write-Host " [OK] IDs resolved." -ForegroundColor Green

    # Step 4 - Check assignment
    Write-Host "`n[4/4] Checking Teams role assignment..." -ForegroundColor Cyan
    $existingFilter   = [Uri]::EscapeDataString("principalId eq '$spId' and roleDefinitionId eq '$roleDefId' and directoryScopeId eq '/'")
    $existingResponse = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?`$filter=$existingFilter" -Headers $headers -Method GET
    $existing         = $existingResponse.value | Select-Object -First 1

    Write-Host ""
    if ($existing) {
        Write-Host " [OK] Teams Reader role ASSIGNED" -ForegroundColor Green
    } else {
        Write-Host " [FAIL] Teams Reader role MISSING" -ForegroundColor Red
    }

    Write-Host ""
    Write-Host "======================================================" -ForegroundColor Cyan
    if ($existing) {
        Write-Host " Teams permissions OK. [OK]" -ForegroundColor Green
    } else {
        Write-Host " Teams permissions INCOMPLETE. Run Invoke-365TuneConnectTeams." -ForegroundColor Red
    }
    Write-Host "======================================================`n" -ForegroundColor Cyan

    return [bool]$existing
}