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