Public/Permissions/Invoke-365TuneConnectTeams.ps1

function Invoke-365TuneConnectTeams {
    <#
    .SYNOPSIS
        Assigns the Teams Reader Entra ID role to the 365TUNE Enterprise App.

    .DESCRIPTION
        Grants the "Teams Reader" directory role to the 365TUNE Service Principal
        as an Active, Permanently assigned role - equivalent to the manual steps:
        Entra ID > Roles and administrators > Teams Reader > Add assignment.

        Works in both local PowerShell and Azure Cloud Shell.
        Uses Microsoft Graph REST API - no additional modules required beyond Az.Accounts.

        Your account must have Global Administrator or Privileged Role Administrator rights.

    .EXAMPLE
        Invoke-365TuneConnectTeams

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


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

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

    $context = Get-AzContext
    Write-Host "`n======================================================" -ForegroundColor Cyan
    Write-Host " 365TUNE - Assign Teams Permissions" -ForegroundColor Cyan
    Write-Host "======================================================`n" -ForegroundColor Cyan

    # Step 1 - Check modules
    Write-Host "[1/4] Checking required modules..." -ForegroundColor Cyan
    if (-not (Get-Module -ListAvailable -Name "Az.Accounts")) {
        Write-Host " Installing Az.Accounts..." -ForegroundColor Yellow
        Install-Module -Name "Az.Accounts" -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) {
            # Cloud Shell pre-loads the signed-in user's context on startup.
            # Calling Connect-AzAccount overrides it with the Cloud Shell MSI - do not call it.
        } 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 "Cloud Shell is authenticated as Managed Service Identity (MSI), not as a user. Open a fresh Cloud Shell from the Azure Portal and 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 - Resolve IDs via Graph
    Write-Host "`n[3/4] Resolving IDs..." -ForegroundColor Cyan

    $graphTokenObj = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" -ErrorAction Stop
    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" }

    # Find 365TUNE SP - prod first, beta fallback
    $sp = Find-365TuneSP -Name $displayNameProd -Headers $headers
    if (-not $sp) {
        Write-Host " '$displayNameProd' not found - trying Beta..." -ForegroundColor Yellow
        $sp = Find-365TuneSP -Name $displayNameBeta -Headers $headers
    }
    if (-not $sp) { throw "Service Principal not found. Tried '$displayNameProd' and '$displayNameBeta'. Ensure the app has been consented to in this tenant." }

    $spId = $sp.id
    Write-Host " Display Name : $($sp.displayName)"
    Write-Host " Object ID : $spId"

    # Find Teams Reader role definition
    $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 -ErrorAction Stop -TimeoutSec 30
    $roleDef      = $roleResponse.value | Select-Object -First 1
    if (-not $roleDef) { throw "Entra ID role '$teamsRoleName' not found. Ensure it exists in this tenant." }
    $roleDefId = $roleDef.id
    Write-Host " Role : $($roleDef.displayName)"
    Write-Host " Role ID : $roleDefId"
    Write-Host " [OK] All IDs resolved." -ForegroundColor Green

    # Step 4 - Assign Teams Reader role
    Write-Host "`n[4/4] Assigning $teamsRoleName role..." -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 -ErrorAction Stop -TimeoutSec 30
    $existing         = $existingResponse.value | Select-Object -First 1

    if ($existing) {
        Write-Warning " [WARN] $teamsRoleName already assigned - skipping"
    } else {
        $body = @{
            principalId      = $spId
            roleDefinitionId = $roleDefId
            directoryScopeId = "/"
            justification    = "365TUNE Security and Compliance - automated assignment"
        } | ConvertTo-Json -Depth 10

        Invoke-RestMethod `
            -Uri     "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" `
            -Headers $headers -Method POST -Body $body -ErrorAction Stop | Out-Null
        Write-Host " [OK] $teamsRoleName role assigned (Active, Permanent)." -ForegroundColor Green
    }

    Write-Host "`n======================================================" -ForegroundColor Cyan
    Write-Host " 365TUNE Teams permissions configured. [OK]" -ForegroundColor Green
    Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Green
    Write-Host " Account : $($context.Account.Id)" -ForegroundColor Green
    Write-Host "======================================================`n" -ForegroundColor Cyan
}