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
        in 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.1.6
    #>


    [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 — Assign Teams Permissions" -ForegroundColor Cyan
    Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan

    # Step 1 — Check modules
    Write-Host "[1/4] Checking required modules..." -ForegroundColor Cyan
    foreach ($module in @("Az.Accounts")) {
        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
    Write-Host " ✅ Modules ready." -ForegroundColor Green

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

    # Step 2 — Authenticate
    Write-Host "`n[2/4] Authenticating..." -ForegroundColor Cyan
    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 — Resolve IDs via Graph
    Write-Host "`n[3/4] Resolving IDs..." -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" }

    # Find 365TUNE SP
    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. Tried '$displayNameProd' and '$displayNameBeta'. Ensure the app has been consented to in this tenant." }
    $displayName = $sp.displayName
    $spId        = $sp.id
    Write-Host " Display Name : $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
    $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 " ✅ All IDs resolved." -ForegroundColor Green

    # Step 4 — Assign Teams Reader role
    Write-Host "`n[4/4] Assigning $teamsRoleName role..." -ForegroundColor Cyan

    # Check if already assigned
    $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

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

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

    Write-Host "`n══════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " 365TUNE Teams 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
}