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