Automated Microsoft Teams Policy application to Azure AD Groups using PowerShell

28 Oct 2019

Introduction

I have worked at many organisations which have had a need to have users with different Teams meeting policies, this is normally a manual effort as a user is created and through role changes etc. The solution is a PowerShell script which iterates through a group and assigns licensed users the appropriate policies for their roles.

For the purpose of this article we will be focusing on Teams Meeting Policies, however this could easily be expanded out to other Teams policies and also Skype for Business Policies using the structure that is described below. We will mainly focus on PowerShell however it will lightly touch on Azure Automation accounts as well.

Solution

Where to run this?

Initially I thought about running this on a server as a scheduled task, but then I remembered that this is a cloud service that we are interacting with and some people simply don’t have any servers on-premise so need a way to run across all possible environments. To resolve this, I have it running as a job on an Azure Automation Account, it currently runs once a day, but can be run as many times as you like and can be triggered from other sources.

Credentials

The first problem to overcome is how to make it automatically and securely authenticate, fortunately within the Azure Automation account that is being used there is a section for credentials which can be called from runbooks within that account, so I created a credential that is called “Teams Admin”, inputting both the username and password which is then referenced from within the script by running the following

$teamsAdminCreds = Get-AutomationPSCredential -Name "Teams Admin"

This can then be used to authenticate without needing the password in plaintext in the script, the only requirement for this is that the user is excluded from needing MFA.

Functions

From here the next step is to build out some of the functions.

Policy Change Logs

The first decision is where should the logs go (if anywhere), the changes that are being made to. There are the logs that come with an automation account that can be used, however for other people to access this it can be easier to log the changes into a Teams Channel. Programmatically sending messages to Teams is super easy, all you need to do is generate a webhook for a Channel and then you can fire json at the webhook uri that you are given.

function Write-ToTeams {
    [CmdletBinding()]
    param (
        [parameter (Position = 1)][string]$uri,
        [parameter (Position = 2)][string]$title,
        [parameter (Position = 3)][string]$text,
        [parameter (Mandatory, Position = 4)][string]$message
    )
    $body = ConvertTo-Json -Depth 4 @{
        title = "$title"
        text = "$text"
        sections = @(
            @{
                activityTitle = "$title"
                activityImage = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPHDXiAytKAkLQQtbB3j5BDqGpinq0Uh1rjDGnKCinrS17bwRD" # this value would be a path to a nice image you would like to display in notifications
            },
            @{
                title = 'Report'
                text = "$message"
            }
        )
    }
    Invoke-RestMethod -uri $uri -Method Post -body $body -ContentType 'application/json'
}

This function when run will give you output similar to this

Write-ToTeams example output

This can take HTML in the message as it is simple card which support some HTML tags.

Teams License Check

The next function to write is one that can check through an Azure AD group and check for users that have a license with Teams enabled. To start we will need to pass a group name, and credentials through to the function. This will then return an array of UPN’s for all the members of the group ready to use later on in the script. For this function we will need to add the Azure AD module to the Automation account, this can be done by going into the modules and finding it within the “Browse Gallery”

function Invoke-TeamsLicenseCheck {
    #Requires -Modules AzureAD

    <#
    .SYNOPSIS
        Enumerates the given group and checks for Teams licenses
    
    .DESCRIPTION
        Enumerates the given group and checks for Teams licenses and returns those who are licensed. 
    
    .PARAMETER group
        This is a string with the name of the group that you wish to apply a policy to. 
    .PARAMETER credentials
        This is the credentials for someone with access to read Azure AD in a PSCredential Object
    #>
    [CmdletBinding()]
    param(
        [parameter (Mandatory, Position = 1)][string]$group, 
        [parameter (Position = 2)][System.Management.Automation.PSCredential]$credentials
    )
    try {
        Write-Verbose "Checking if AzureAD module already connected"
        Get-AzureADTenantDetail -ErrorAction SilentlyContinue | Out-Null
    }
    catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException]{
        Write-Verbose "Connecting to AzureAD"
        Connect-AzureAD -Credential $credentials
    }
    Write-Verbose "Checking $group"
    $output = @()
    $groupObject = Get-AzureADGroup -SearchString $group
    $members = Get-AzureADGroupMember -ObjectId $groupObject.ObjectId -All $true
    $count = $members.count
    Write-Verbose $count
    $x=1
    foreach ($user in $members) {
        Write-Verbose "Group member $x of $count"
        Write-Verbose $user.UserPrincipalName
        $userObject = Get-AzureADUserLicenseDetail -ObjectId $user.ObjectId
        if ($userObject.ServicePlans | Where-Object {$_.ProvisioningStatus -eq "Success" -and $_.ServicePlanName -like "*Teams*"}) {
           $tempuser = New-Object PSObject
           $mailaddress = $user.UserPrincipalName
           $tempuser | Add-Member -MemberType NoteProperty -Name "UPN" -Value $mailaddress
           $output += $tempuser
        }
        $x++
    }
    Write-Output $output
}

Policy Application

Now you have got your users, it is time to deploy a policy! This is where the issues started with the Azure Automation account, as it wants the modules to be within the PowerShell Gallery and the Skype for Business module isn’t. To get around this install the SkypeOnlineConnector module and then copy the module to a zip file. The modules default install location is C:\Program Files\Common Files\Skype for Business Online\Modules\SkypeOnlineConnector. Once this is done you go to modules with Azure Automation and upload this zip file.

Within the function you need to connect to Skype for Business Online PowerShell, to do this you run this:

$sfbSession = New-CsOnlineSession -Credential $creds

Then normally you would run

Import-PSSession $sfbSession -AllowClobber

however due to limitations of the automation account there is a need to be more selective over the commands which are imported into the session. The syntax used to only import the relevant commands is:

Import-PSSession $sfbSession -AllowClobber -CommandName Get-CsOnlineUser,Grant-CsTeamsMeetingPolicy

This can be expanded to include other functions to set other policies, however for this example we only need the two.

For this function we wil pass the creds in a PSCredential object, the array of licensed users and the name of the policy to apply. It will return the UPN of the users that have had the policy ammended and the name of the policy that they have had applied to them.

function Invoke-TeamsPolicyApplication {
    #Requires -Modules SkypeOnlineConnector
    [CmdletBinding()]
    param (
        [parameter (Position = 1, Mandatory, ParameterSetName = "PSCredObject")][System.Management.Automation.PSCredential]$creds,
        [parameter (Position = 2, Mandatory)]$users,
        [parameter (Position = 3, Mandatory)]$policyToApply
    )
    $VerbosePreference = 'Continue'
    Write-Verbose "Starting to set $policyToApply on Teams"
    Import-Module SkypeOnlineConnector
    Write-Verbose "Creating Skype Online PS Session"
    $sfbSession = New-CsOnlineSession -Credential $creds
    Write-Verbose "Created Skype Online Session"
    Import-PSSession $sfbSession -AllowClobber -CommandName Get-CsOnlineUser,Grant-CsTeamsMeetingPolicy | Out-Null
    Write-Verbose "Imported Skype PS Session"
    $policyApplied = @()
    foreach ($user in $users) {
        $upn = $user.upn
        if (Get-CsOnlineUser -Identity $upn | Where-Object {$_.TeamsMeetingPolicy -eq $policyToApply}) {
            Write-Verbose "Teams Policy is already correct on $upn"
        }
        else {
            Write-Verbose "Setting Teams $policyToApply on $upn"
            $temp = New-Object PSObject
            $temp | Add-Member -MemberType NoteProperty -Name User -Value $upn
            $temp | Add-Member -MemberType NoteProperty -Name Policy -Value $policyToApply
            [array]$policyApplied += $temp
            Grant-CsTeamsMeetingPolicy -Identity $upn -PolicyName $policyToApply
            Remove-Variable temp
            Remove-Variable upn
        }
    }
    Write-Output $policyApplied
    Remove-PSSession $sfbSession
}

Running the script

After the functions you first call the credentials as above and then you run the functions, getting the users for the appropriate policies. This can take a while depending on how many users you have.

$policy1Users = Invoke-TeamsLicenseCheck -group "Teams Policy 1 group" -credentials $teamsAdminCreds
$policy2Users = Invoke-TeamsLicenseCheck -group "Teams Policy 2 group" -credentials $teamsAdminCreds
$policy3Users = Invoke-TeamsLicenseCheck -group "Teams Policy 3 group" -credentials $teamsAdminCreds

Once you have got the users you then pass this to Invoke-TeamsPolicyApplication, the output of which you capture in a variable.

$teamsChanges = Invoke-TeamsPolicyApplication -creds $teamsAdminCreds -users $policy1Users -policyToApply "Teams Meeting Policy 1" -Verbose
$teamsChanges += Invoke-TeamsPolicyApplication -creds $teamsAdminCreds -users $policy2Users -policyToApply "Teams Meeting Policy 2" -Verbose
$teamsChanges += Invoke-TeamsPolicyApplication -creds $teamsAdminCreds -users $policy3Users -policyToApply "Teams Meeting Policy 3" -Verbose

Once you have a variable with the users that have had their policies changed you can then check if there have been any, convert this into a HTML fragment. The reason for converting to a HTML fragment is so that you get a table in the card within Teams.

if (!$teamsChanges) {
    $teamsChanges = "No changes have been made today"
}
else {
    [string]$teamsChanges = $teamsChanges | ConvertTo-Html -Fragment
}
Write-Verbose $teamsChanges
Write-ToTeams -uri $teamsURI -title "Teams Policy Automation Notification" -text "Teams Policy deployment task successful" -message $teamsChanges

The complete script can be found here.