= 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. \\
{{tag>o365 azureDevops powershell entraid}}
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 [[azureenterpriseapp]] so I'll just explain the permissions required.
== Email ==
The app that will be handling the email will need:
* Microsoft Graph -> Application Permissions -> Mail.Send (Send mail as any user)
> https://docs.microsoft.com/en-us/graph/api/message-send?view=graph-rest-1.0&tabs=http
== 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
* 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 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 [[azureenterpriseapp]] 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:
- Agent: $($env:AGENT_NAME)
- Build Definition: $($env:BUILD_DEFINITIONNAME)
- Requestor: $($env:BUILD_QUEUEDBY)
- Build ID: $($env:BUILD_BUILDID)
"
### 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: "
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 += "- $user ($upn)
"
$upn = $null
}
$body += "
"
# Send out report
SendGraphEmail -To $To -CC $CC -BCC $BCC -subject $subject -emailtype $emailtype -body $body -from $From -emailoauthgraph $emailoauthgraph