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.1.9
    #>


    [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
    if (-not (Get-Module -ListAvailable -Name "Az.Accounts")) {
        Write-Host " Installing Az.Accounts..." -ForegroundColor Yellow
        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. 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 " [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"
    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
    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." }

    $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
    $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
    $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

        Invoke-RestMethod `
            -Uri     "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" `
            -Headers $headers -Method POST -Body $body | 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
}