wiki.getshifting.com

--- Sjoerd Hooft's InFormation Technology ---

User Tools

Site Tools


manageteammembership

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.

Email

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
manageteammembership.txt · Last modified: by 127.0.0.1