Table of Contents
Manage MS Teams Membership
Summary: How to log in to the AWS console using your Entra ID credentials.
Date: 6 April 2021
Refactor: 6 March 2025: Checked links and formatting.
In this article I'll show the steps and script to manage team membership based on being a member of an Active Directory security group. I'll create two Azure Enterprise Applications, a script and an Azure DevOps Server pipeline. How fun.
Azure Enterprise Applications
For the purpose above, I'll create two Azure enterprise applications. Note that this could be done in one, but the company I currently work for has strict separation of permissions, so there is one app to be able to send out emails, and one app to connect and manage MS Teams.
Creating an Enterprise App is described in Register Azure Enterprise App for Graph so I'll just explain the permissions required.
The app that will be handling the email will need:
- Microsoft Graph → Application Permissions → Mail.Send (Send mail as any user)
MS Teams
The app that will be handling MS Teams will need:
- Microsoft Graph → all permissions below need to be assigned as both the delegated and application type:
- Directory.Read.All
- Directory.ReadWrite.All
- Group.Read.All
- Group.ReadWrite.All
- Team.ReadBasic.All
- TeamMember.ReadWrite.All
- TeamSettings.Read.All
- TeamSettings.ReadWrite.All
Note that these permissions are needed to ulimately add a member to the team. If you would need to do other tasks you'll also need different permissions:
* Get-Team: https://docs.microsoft.com/en-us/graph/api/team-get?view=graph-rest-1.0&tabs=http
Azure DevOps Server
Variables
We'll put the application IDs, secrets and tenant in a variable group so that's available for more than just one pipeline, in case we'll want to add additional pipelines to manage multiple groups:
- In Azure DevOps Server, go to pipelines and then go to Library
- Create a new Variable Group, and name it “Global Variable” (you can use any name you like, just don't forget to change it in the pipeline)
- Add variables for:
- EmailAppID (Application ID for the Azure Enterprise App for email)
- EmailAppSecret (don't forget to check the lock item to configure it as a secret)
- TeamsAppID (Application ID for the Azure Enterprise App for MS Teams)
- TeamsAppSecret (also a secret)
- Tenant
Note that in Register Azure Enterprise App for Graph you'll find explanantion on how to find the Values for these variables
Pipeline
Create a yaml pipeline:
# Pipeline # # Description: # Manage MS Teams membership # # References: # YAML: https://aka.ms/yaml # Input parameters: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/runtime-parameters # Variables: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch # - Do not use variables with _ in name, this will break yaml (works fine in classic pipelines) # - Pass variables to options within "" to preserve spaces # Variable Groups: https://docs.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops-2020&tabs=yaml # parameters: - name: adgroups displayName: Comma separated list of AD groups, use the Pre-Win 2000 names, and put the whole string into quotes. type: string default: '"ICT,ICT_ Data Management"' - name: msteams displayName: MS Teams display name. type: string default: '"Releases"' - name: environment displayName: Enter the environment connect to teams type: string default: PRD-DevOps values: - TST-DevOps - PRD-DevOps # Disable CI/CD trigger: none variables: - group: "Global Variable" schedules: - cron: "0 5 * * 1-5" displayName: Weekday run at 5 branches: include: - master always: true jobs: - job: AddUsers displayName: 'Add users to MS Teams' pool: ${{ parameters.environment }} steps: - task: PowerShell@2 displayName: 'Add users to MS Teams' inputs: targetType: filePath filePath: './msteams/adduserstoteams.ps1' arguments: '-adgroups ${{ parameters.adgroups }} -msteams ${{ parameters.msteams }} -teamsappid "$(TeamsAppID)" -teamsappsecret "$(TeamsAppSecret)" -emailappid "$(EmailAppID)" -emailappsecret "$(EmailAppSecret)" -tenant "$(Tenant)"'
Note: According to the documentation a schedule cannot be combined using variables, however, adding the variable group works
Note: The variables used to pass to the powershell script need to match the name of the variables in the variable group (obviously)
PowerShell Script
This is the script where the variables are passed to:
# Author: Sjoerd Hooft / https://www.linkedin.com/in/sjoerdhooft/ ### Versioning and Functionality ################################################################## ### Information on Script or Application ### # Name: Manage MS Teams membership based on On-Prem AD Security Groups ### Script & Application key users ### # Script maintained by Sjoerd Hooft # Key users: Sjoerd Hooft, MS Teams Business Analysts ### Versioning ### ### 2021 04 06 - Sjoerd Hooft - First Version ### # Get all users from a list of groups, including nested groups # Get all members of a MS Team # Add users from the AD groups to the MS Team if not already a member # Send a report by email ################################################################################################### ### Requirements ################################################################################## ### Powershell # The script is designed to run in Azure DevOps Server and has the following requirements: ### Azure AD Enterprise App with permissions to manage Teams ##### Get-Team: https://docs.microsoft.com/en-us/graph/api/team-get?view=graph-rest-1.0&tabs=http ##### Get-Member: https://docs.microsoft.com/en-us/graph/api/team-get-members?view=graph-rest-1.0&tabs=http ##### Add-Member: https://docs.microsoft.com/en-us/graph/api/team-post-members?view=graph-rest-1.0&tabs=http ### Azure Enterprise App with permissions te send email ##### Message-Send: https://docs.microsoft.com/en-us/graph/api/message-send?view=graph-rest-1.0&tabs=http ################################################################################################### ### Bugs ########################################################################################## ### No script bugs known yet # ################################################################################################### ### How-To ######################################################################################## ### When changing and testing ### # Set thebug variable to 1 instead of 0 ################################################################################################### ### Script Overview ############################################################################### ### Stage 1 ####################################################################################### # Set all Variables # Define Functions ### Stage 2 ####################################################################################### # Set Graph variables and verify graph connection(s) # Define Graph Functions ### Stage 3 ####################################################################################### # Run script # Send email report ################################################################################################### ################################################################################################### ########################################## Start Stage 1 ########################################## ################################################################################################### ### Input Variables ### # If used, the param statement MUST be the first thing in your script or function Param( [string]$adgroups, [string]$msteams, [string]$teamsappid, [string]$teamsappsecret, [string]$emailappid, [string]$emailappsecret, [string]$tenant ) ### Script Management $global:thebug = 1 ### Check Input if ($global:thebug -eq 1){ write-output "AD Groups: $adgroups" write-output "MS Teams: $msteams" write-output "MS Teams App ID: $teamsappid" write-output "MS Teams App Secret: $teamsappsecret" write-output "Email App ID: $emailappid" write-output "Email App Secret: $emailappsecret" write-output "Tenant: $tenant" } ### Fix Input: Convert string of adgroups to Array, added trim to remove spaces $adgroupsarray = $adgroups.Split(',').trim() ### Check Required Modules ### if (Get-Module -ListAvailable -Name ActiveDirectory) { if ($global:thebug -eq 1){write-output "PowerShell Module Active Directory exists"} Import-Module ActiveDirectory} else { write-output "PowerShell Module Active Directory does not exist, install using Install-Module ActiveDirectory. Now installing for the current user. " Install-Module ActiveDirectory -scope CurrentUser } if (Get-Module -ListAvailable -Name MicrosoftTeams) { if ($global:thebug -eq 1){write-output "PowerShell Module Microsoft Teams exists"} Import-Module MicrosoftTeams} else { write-output "PowerShell Module Microsoft Teams does not exist, install using Install-Module MicrosoftTeams" Install-Module MicrosoftTeams -scope CurrentUser } ### Email ### The to address is based on the requester of the build if started manually. If the build was scheduled or the email address is missing or not an Getshifting address, another email address is set based on the MS Teams name. If not configured, the email is sent to the email distribution list of ICT applications. $to = $env:BUILD_REQUESTEDFOREMAIL $cc = "" if ($global:thebug -eq 1){write-output "To email address: $to"} if (![string]::IsNullOrEmpty($requesteremail) -and ($requesteremail -match '@getshifting.com')){ if ($global:thebug -eq 1){write-output "To email address exists for build requester and matches an GetShifting email address"} }elseif ($msteams -eq "Release Calendar"){ if ($global:thebug -eq 1){write-output "Setting to email address to Release Calendar manager"} $to = "sjoerd@getshifting com" }else { if ($global:thebug -eq 1){write-output "Setting to email address to ICT-Applications"} $to = "ictteam@getshifting.com" } $bcc = "" $from = "master@getshifting.com" $emailtype = "html" $subject = "Report: $env:BUILD_DEFINITIONNAME for $msteams" $body = "Report information: <ul><li><strong>Agent:</strong> $($env:AGENT_NAME)</li><li><strong>Build Definition:</strong> $($env:BUILD_DEFINITIONNAME)</li><li><strong>Requestor:</strong> $($env:BUILD_QUEUEDBY)</li><li><strong>Build ID:</strong> $($env:BUILD_BUILDID)</li></ul> " ### Userlist $global:userlist = @() ### Functions function NestedGroupMembers($fgroup){ # Get all groupmemebrs $groupmembers = Get-ADGroupMember $fgroup ForEach ($groupmember in $groupmembers){ if ($groupmember.ObjectClass -eq "Group"){ if ($global:thebug -eq 1){write-output "$groupmember is a group"} NestedGroupMembers $groupmember }else{ if ($global:thebug -eq 1){write-output "$groupmember is a user, sam = $($groupmember.SamAccountName)"} #$user = "" | Select-Object username $user = $groupmember.Name if ($global:thebug -eq 1){write-output "SAM = $user"} $global:userlist += $user } } } ################################################################################################### ########################################## Start Stage 2 ########################################## ################################################################################################### ### Start Graph for Teams # https://docs.microsoft.com/en-us/powershell/module/teams/connect-microsoftteams?view=teams-ps try{ $teamsbodyGraph = @{grant_type="client_credentials";scope="https://graph.microsoft.com/.default";client_id=$teamsappid;client_secret=$teamsappsecret} $teamsoauthGraph = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$tenant/oauth2/v2.0/token -Body $teamsbodyGraph $teamsAccessToken = $teamsoauthGraph.access_token if ($global:thebug -eq 1){Write-Output "OauthGraph Teams: $teamsoauthGraph ; accessToken = $teamsAccessToken"} }catch{ write-output "Something went wrong with the Graph connection for Teams"; write-output "$($_.Exception.Message)"; exit 1 } # Connection needs both tokens filled in Connect-MicrosoftTeams -AadAccessToken $teamsAccessToken -MsAccessToken $teamsAccessToken -AccountId sa_automation@getshifting_com ### Start Graph for Email try{ $emailbodyGraph = @{grant_type="client_credentials";scope="https://graph.microsoft.com/.default";client_id=$emailappid;client_secret=$emailappsecret} $emailoauthGraph = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$tenant/oauth2/v2.0/token -Body $emailbodygraph if ($global:thebug -eq 1){Write-Output "OauthGraph Email: $emailoauthGraph"} }catch{ write-output "Something went wrong with the Graph connection for Email"; write-output "$($_.Exception.Message)"; exit 1 } Function SendGraphEmail(){ param( $To, $Cc, $Bcc, $subject, $emailtype, $body, $From, $emailoauthgraph ) if ($global:thebug -eq 1){ write-output "Verify input" Write-Output "To: $to" Write-Output "CC: $cc" Write-Output "BCC: $bcc" Write-Output "Subject: $subject" Write-Output "EmailType: $emailtype" Write-Output "Body: $body" Write-Output "From: $from" Write-Output "Emailoauthgraph: $emailoauthgraph" } ### Convert email addresses to JSON format # Convert To Address $tosplit = @($to.Split(',')) $ToinJSON = $tosplit | %{'{"EmailAddress": {"Address": "'+$_+'"}},'} $ToinJSON = ([string]$ToinJSON).Substring(0, ([string]$ToinJSON).Length - 1) # Convert CC Address if (![string]::IsNullOrEmpty($cc)){ if ($global:thebug -eq 1){Write-Output "cc not empty, start json format"} $ccsplit = @($cc.Split(',')) $CCinJSON = $ccsplit | %{'{"EmailAddress": {"Address": "'+$_+'"}},'} $CCinJSON = ([string]$CCinJSON).Substring(0, ([string]$CCinJSON).Length - 1) }else{ $CCinJSON = "" } # Comvert BCC address if (![string]::IsNullOrEmpty($bcc)){ if ($global:thebug -eq 1) {Write-Output "bcc not empty, start json format"} $bccsplit = @($bcc.Split(',')) $BCCinJSON = $bccsplit | %{'{"EmailAddress": {"Address": "'+$_+'"}},'} $BCCinJSON = ([string]$BCCinJSON).Substring(0, ([string]$BCCinJSON).Length - 1) }else{ $BCCinJSON = "" } if ($global:thebug -eq 1){ write-output "Display all email address in JSON" Write-Output "To: $to" Write-Output "ToSplit: $tosplit" Write-Output "ToJson: $ToinJSON" Write-Output "CC: $cc" Write-Output "CCSplit: $ccsplit" Write-Output "CCJson: $CCinJSON" Write-Output "BCC: $bcc" Write-Output "BCCSplit: $bccsplit" Write-Output "BCCJson: $BCCinJSON" } # Create email in JSON $reqBody='{ "message": { "subject": "", "body": { "contentType": "", "content": "" }, "toRecipients": [ PlaceHolderTo ], "ccRecipients": [ PlaceHolderCC ], "bccRecipients":[ PlaceHolderBCC ], "replyTo":[ ] } }' $reqBody = $reqBody -replace "PlaceholderTo",$ToinJSON $reqBody = $reqBody -replace "PlaceholderCC",$CCinJSON $reqBody = $reqBody -replace "PlaceholderBCC",$BCCinJSON if ($global:thebug -eq 1){Write-Output "Final Check reqBody Before JSON: $reqBody"} $reqBody = $reqBody | ConvertFrom-Json $reqBody.message.subject = $Subject $reqBody.message.body.contentType = $emailtype $reqBody.message.body.content = $Body if ($global:thebug -eq 1){Write-Output "Final Check reqBody After JSON: $reqBody"} ### Send email ### try{ Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/v1.0/users/$($From)/sendMail" -Headers @{'Authorization'="$($emailoauthgraph.token_type) $($emailoauthgraph.access_token)"; 'Content-type'="application/json"} -Body ($reqBody | ConvertTo-Json -Depth 4 | Out-String) }catch{ write-output "Something went wrong with sending email"; write-output "Error message: $($_.Exception.Message)"; } } ################################################################################################### ########################################## Start Stage 3 ########################################## ################################################################################################### ## Find all unique users in specified groups, including nested groups foreach ($group in $adgroupsarray) { NestedGroupMembers $group } $uniqueusers = $global:userlist | Sort-Object -Unique ## Find all members of the Release Calendar Team $rcalendarusers = get-team -DisplayName $msteams| get-teamuser | select -ExpandProperty Name ## Compare which users are member of one of the specified groups but not of the Team $newusers = Compare $rcalendarusers $uniqueusers | where {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject if ($global:thebug = 1){write-output "These users are not yet in the $msteams :"$newusers; } ## Find the groupid for the Teams if ($global:thebug = 1){get-team -DisplayName $msteams} $groupid = get-team -DisplayName $msteams| select -ExpandProperty groupid # get the upn for each user and add it to the Teams, and add the user to the report $body += "Added users: <ul>" foreach ($user in $newusers) { $upn = get-aduser -filter {name -eq $user} | select -ExpandProperty UserPrincipalName if ($global:thebug = 1){write-output "Adding $upn to $msteams"} add-teamuser -GroupId $groupid -User $upn -Role "Member" $body += "<li>$user ($upn)</li>" $upn = $null } $body += "</ul>" # Send out report SendGraphEmail -To $To -CC $CC -BCC $BCC -subject $subject -emailtype $emailtype -body $body -from $From -emailoauthgraph $emailoauthgraph