Automatically send alerts to the last owner of a team! (If you enforce a 2 or more owner policy)

I’ve seen many customers try to enforce a written rule of having at least 2 owners of a Microsoft Teams team. Many have been doing this semi-manually, by just taking out a report of teams that has 1 or 0 owners. How would you fully automate this?

I have created a routine that with the help of Azure Automation and a SharePoint List, sends X amount of warnings to the sole owner of a team before deleting the team. It helps to put some pressure on the users 🙂 Joking aside, some wouldn’t see the meaning of keeping the team if the team owner doesn’t take action on these reminders. Others maybe rather have a notification to be sent to IT after X amount of reminders to the owner, so they can call him to see if the team can be deleted. In the script below we send a warning to IT, so they can take action if all other warnings are ignored. The script sends 5 warnings with 7 days between each warning, and after the final warning, it waits 14 days before notifying IT. The routine also sends a notification direct to IT if there are teams without any owner.

You will need an email address you can send emails from.
The PowerShell script in Azure Automation uses Graph API to gather and post all the information.

Below you have all the steps needed to set up an automated solution to send reminders.

In the Teams client click the 3 dots and select Manage team on the team you want to receive the final warning for IT in:

Click Apps.

Click More Apps.

Search for Incoming Webhook and click on the app.

Click Add to a team.

Select the channel you want to use and click Set up a connector.

Give the webhook a name and click Create.

Copy the url of the webhook, as you will need this in the PowerShell script. Click Done.

Go to a channel of your choice in the teams client, and click + to add a tab. We are now creating a SharePoint List to store the records of all warnings sent.

Search for and click on Lists.

Click Save.

Click Create a list.

Click Blank list.

Give the list a name and click Create.

Click Add column, and create the following entires:
TeamID – Single line of text
UPN – Single line of text
Warning1 – Date and time
Warning2 – Date and time
Warning3 – Date and time
Warning4 – Date and time
Warning5 – Date and time
WarningAdmin – Date and time

After creating all the columns, click the 3 dots and select Open in SharePoint. We are now going to find the SharePoint site id and List id.

Copy the URL, looks something like this:
https://alexholmeset.sharepoint.com/sites/CompanyCats/Lists/TeamOwnersList/AllItems.aspx

Remove every thing from Lists and to the right, then add _api/site/id to the url, so i looks like this:
https://alexholmeset.sharepoint.com/sites/CompanyCats/_api/site/id
Go to that URL and copy the ID you find after Edm.Guid. This is the Site ID. You will need this in the PowerShell script.

Go back to the SharePoint List, click Settings and select List settings.

Copy the URL, which looks something like this:
https://alexholmeset.sharepoint.com/sites/CompanyCats/_layouts/15/listedit.aspx?List=%7Bfc40e0a7-7b22-408c-b25e-a6f006b2b0ea%7D

Copy the id between %7B and %7D, which will look something like this:
fc40e0a7-7b22-408c-b25e-a6f006b2b0ea
This is the List id. You will need this in the PowerShell script.


Go to portal.azure.com and open the App Registrations menu:

Click New registration.

Give the application a name and click Register.

Take note of the application/client id and directory/tenant id, you will need this in the PowerShell script. Click API permissions.

Click Add a permission.

Click Microsoft Graph.

Click Application permissions.

Search and select the following permissions:
* Directory.Read.All
* Group.Read.All
* User.Read.All
* Sites.ReadWrite.All
* Mail.Send

Click Add permissions.

Click Grant admin consent for ….

Click Yes.

Click Certificates & secrets.

Click New client secret

Give the secret a name and select expiration. Click Add.

Copy the Value of the client secret, you will need it at a later step.


Go to the Azure Automation menu.


Click Create.


Fill in the needed and click “Review and create”.


Click Create.


Wait about 30 seconds, and click refresh

Now click Go to Resource.

On the left menu scroll down and click Variables.

Click Add a variable


Give the secret the name secret, as this will be used in the PowerShell script. Select String type, paste the secret in Value and click Yes under encrypted. Click Create.

Click Runbooks.

Click Create a runbook.

Give the runbook a name, and enter the same as below. Click Create.

Copy/Paste the PowerShell script below into the runbook editor.

### Team Owner Routine
### Version 1.0
### Author: Alexander Holmeset
### Email: [email protected]
### Twitter: twitter.com/alexholmeset
### Blog: alexholmeset.blog
$TenantId = "Populate variable"
$ClientID = "Populate variable"
$ClientSecret = Get-AutomationVariable -Name 'secret'
$SharePointSiteID = "Populate variable"
$SharePointListID = "Populate variable"
$SharePointListURL = "https://graph.microsoft.com/v1.0/sites/$($SharePointSiteID)/lists/$($SharePointListID)"
$EmailUPN = "Populate variable"
$TeamsWebhookURL = "Populate variable"
#Number of days between warnings.
$DaysBewteenReminders = 7
$WarningAdminDays = 14
function Get-MSGraphAppToken{
<# .SYNOPSIS
Get an app based authentication token required for interacting with Microsoft Graph API
.PARAMETER TenantID
A tenant ID should be provided.
.PARAMETER ClientID
Application ID for an Azure AD application. Uses by default the Microsoft Intune PowerShell application ID.
.PARAMETER ClientSecret
Web application client secret.
.EXAMPLE
# Manually specify username and password to acquire an authentication token:
Get-MSGraphAppToken -TenantID $TenantID -ClientID $ClientID -ClientSecert = $ClientSecret
.NOTES
Author: Jan Ketil Skanke
Contact: @JankeSkanke
Created: 2020-15-03
Updated: 2020-15-03
Version history:
1.0.0 - (2020-03-15) Function created
#>[CmdletBinding()]
param (
[parameter(Mandatory = $true, HelpMessage = "Your Azure AD Directory ID should be provided")]
[ValidateNotNullOrEmpty()]
[string]$TenantID,
[parameter(Mandatory = $true, HelpMessage = "Application ID for an Azure AD application")]
[ValidateNotNullOrEmpty()]
[string]$ClientID,
[parameter(Mandatory = $true, HelpMessage = "Azure AD Application Client Secret.")]
[ValidateNotNullOrEmpty()]
[string]$ClientSecret
)
Process {
$ErrorActionPreference = "Stop"
# Construct URI
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token&quot;
# Construct Body
$body = @{
client_id = $clientId
scope = "https://graph.microsoft.com/.default&quot;
client_secret = $clientSecret
grant_type = "client_credentials"
}
try {
$MyTokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
$MyToken =($MyTokenRequest.Content | ConvertFrom-Json).access_token
If(!$MyToken){
Write-Warning "Failed to get Graph API access token!"
Exit 1
}
$MyHeader = @{"Authorization" = "Bearer $MyToken" }
}
catch [System.Exception] {
Write-Warning "Failed to get Access Token, Error message: $($_.Exception.Message)"; break
}
return $MyHeader
}
}
$Count=0
#Generate Graph API access token
$global:Header = Get-MSGraphAppToken -TenantID $TenantId -ClientID $ClientID -ClientSecret $ClientSecret
#URL to SharePoint list with entries of sent reminders.
$currentUriSharePoint = "$($SharePointListURL)/items?expand=fields"
#Get all entries in SharePoint list.
$ListItems = while (-not [string]::IsNullOrEmpty($currentUriSharePoint)) {
# API Call
# Write-Host "`r`nQuerying $currentUri..." -ForegroundColor Yellow
$apiCall = Invoke-WebRequest -Method "GET" -Uri $currentUriSharePoint -ContentType "application/json" -Headers $global:Header -ErrorAction Stop -UseBasicParsing
$nextLink = $null
$currentUriSharePoint = $null
if ($apiCall.Content) {
# Check if any data is left
$nextLink = $apiCall.Content | ConvertFrom-Json | Select-Object '@odata.nextLink'
$currentUriSharePoint = $nextLink.'@odata.nextLink'
$apiCall.Content | ConvertFrom-Json
}
}
$ListItems = ($ListItems.value).fields
#Checks if Team in SharePoint list has 1 or 2 owners. If team has 2 owners, then the entry is deleted from the list.
foreach($item in $ListItems){
#Get list of owners of team.
$TeamOwners = @()
try{$TeamOwners = (Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/beta/groups/$($item.TeamID)/owners&quot; -Headers $global:Header).value}
catch{$deleteUri = '$($SharePointListURL)/items/' + $($item.id)
$deleteApiCall = Invoke-WebRequest -Method "DELETE" -Uri $deleteUri -ContentType "application/json" -Headers $global:Header -ErrorAction Stop -UseBasicParsing}
if($TeamOwners.Count -gt 1){
$deleteUri = "$($SharePointListURL)/items/$($item.id)"
$deleteApiCall = Invoke-WebRequest -Method "DELETE" -Uri $deleteUri -ContentType "application/json" -Headers $global:Header -ErrorAction Stop -UseBasicParsing
}
}
$currentUri = 'https://graph.microsoft.com/beta/groups?$select=id,resourceProvisioningOptions,Displayname,mail&#39;
$Teams = while (-not [string]::IsNullOrEmpty($currentUri)) {
# API Call
# Write-Host "`r`nQuerying $currentUri..." -ForegroundColor Yellow
$apiCall = Invoke-WebRequest -Method "GET" -Uri $currentUri -ContentType "application/json" -Headers $global:Header -ErrorAction Stop -UseBasicParsing
$nextLink = $null
$currentUri = $null
if ($apiCall.Content) {
# Check if any data is left
$nextLink = $apiCall.Content | ConvertFrom-Json | Select-Object '@odata.nextLink'
$currentUri = $nextLink.'@odata.nextLink'
$apiCall.Content | ConvertFrom-Json
}
}
$Teams = $Teams.value
#Filters out all groups that are teams.
$Teams = $Teams | Where-Object { $_.resourceProvisioningOptions -eq 'Team' }
$count2 = 0
foreach($team in $teams){
$count2++
"Team $count2"
$TeamDisplayName = @()
$TeamDisplayName = ($($Team.displayname).replace("'","")).replace('"','')
#Get list of owners of team.
$TeamOwners = @()
$TeamOwners = (Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/beta/groups/$($team.id)/owners&quot; -Headers $global:Header).value
#If team has only 1 owner.
If($TeamOwners.Count -eq 1){
$Title="Action Required – Your team is breaking our guidelines"
$bodyHTML ='<doctype html><html><head><title>' + $Title +' </title></head><body><font face="Calibri" size="3">'
$bodyHTML+="<p>Hi $($TeamOwners.givenname)!</p><p>the team<b><i> $TeamDisplayName</i></b>,"
$bodyHTML+=' that you are the owner of is breaking our policy for two owners or more for a team. Please add another owner to the team.
<ul><li>More info here: <a href=https://alexholmeset.blog#for-eiere-og-superbrukere-av-teams>Info about team ownership</a> <br /> <p>Best regards <br /> <b><i> IT Team</i></b></p></font></body></html>'
$BodyEmail = @"
{
"message": {
"subject": "$Title",
"body": {
"contentType": "HTML",
"content": '$bodyHTML'
},
"toRecipients": [
{
"emailAddress": {
"address": "$($teamowners.userPrincipalName)"
}
}
]
},
"saveToSentItems": "false"
}
"@
#Check if team already has an entry in the SharePoint list.
$TeamListItem = @()
$TeamListItem = $ListItems | Where-Object { $_.TeamID -eq $team.id }
#If entry exists in SharePoint list, go on and send new reminder.
If($TeamListItem.TeamID -eq $Team.ID){
#Send 2. warning.
if ($TeamListItem.Warning1 -and !$TeamListItem.Warning2) {
$Days =@()
$Days = ($TeamListItem.Warning1 | New-TimeSpan).Days
#If first warning has been sent, send second warning if enough time has passed.
If($Days -eq $DaysBewteenReminders){
#Send email to team owner.
Invoke-RestMethod -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$($EmailUPN)/sendMail&quot; -Headers $global:Header -body $BodyEmail -ContentType "application/json;charset=utf-8"
$BodyNewListItem = @()
$BodyNewListItem = @"
{
'Warning2': '$(Get-Date)'
}
"@
Invoke-RestMethod -Method Patch -Uri "https://graph.microsoft.com/v1.0/sites/a5c40278-b214-4a00-8a9c-49cfa85e1ce8/lists/e5d86ef4-22e8-46a9-9bc2-901a322cf38b/items/$($TeamListItem.id)/fields&quot; -Headers $global:Header -body $BodyNewListItem -ContentType "application/json"
$teamowners.userPrincipalName
/}
}
#Send 3. warning.
if ($TeamListItem.Warning2 -and !$TeamListItem.Warning3) {
$Days =@()
$Days = ($TeamListItem.Warning2 | New-TimeSpan).Days
#If second warning has been sent, send third warning if enough time has passed.
If($Days -eq $DaysBewteenReminders){
#Send email to team owner.
Invoke-RestMethod -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$($EmailUPN)/sendMail&quot; -Headers $global:Header -body $BodyEmail -ContentType "application/json;charset=utf-8"
$BodyNewListItem = @()
$BodyNewListItem = @"
{
'Warning3': '$(Get-Date)'
}
"@
Invoke-RestMethod -Method Patch -Uri "$($SharePointListURL)/items/$($TeamListItem.id)/fields" -Headers $global:Header -body $BodyNewListItem -ContentType "application/json"
$teamowners.userPrincipalName
}
}
#Send 4. warning.
if ($TeamListItem.Warning3 -and !$TeamListItem.Warning4) {
$Days =@()
$Days = ($TeamListItem.Warning3 | New-TimeSpan).Days
#If third warning has been sent, send fourth warning if enough time has passed.
If($Days -eq $DaysBewteenReminders){
#Send email to team owner.
Invoke-RestMethod -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$($EmailUPN)/sendMail&quot; -Headers $global:Header -body $BodyEmail -ContentType "application/json;charset=utf-8"
$BodyNewListItem = @()
$BodyNewListItem = @"
{
'Warning4': '$(Get-Date)'
}
"@
Invoke-RestMethod -Method Patch -Uri "$($SharePointListURL)/items/$($TeamListItem.id)/fields" -Headers $global:Header -body $BodyNewListItem -ContentType "application/json"
$teamowners.userPrincipalName
}
}
#Send 5. warning.
if ($TeamListItem.Warning4 -and !$TeamListItem.Warning5) {
$Days =@()
$Days = ($TeamListItem.Warning4 | New-TimeSpan).Days
#Om fjerde varsel er sendt, send femte varsel om det har gått lang nokk tid.
#If fourth warning has been sent, send fifth warning if enough time has passed.
If($Days -eq $DaysBewteenReminders){
#Siste epost varsling til eier av Team.
#Final email warning to team owner.
$Title="Last Warning – Your team is breaking our guidelines"
$bodyHTMLsiste ='<doctype html><html><head><title>' + $Title +' </title></head><body><font face="Calibri" size="3">'
$bodyHTML+="<p>Hi $($TeamOwners.givenname)!</p><p>the team<b><i> $TeamDisplayName</i></b>,"
$bodyHTML+=' that you are the owner of is breaking our policy for two owners or more for a team. Please add another owner to the team, or the team will be deleted in 14 days.
<ul><li>More info here: <a href=https://alexholmeset.blog#for-eiere-og-superbrukere-av-teams>Info about team ownership</a> <br /> <p>Best regards <br /> <b><i> IT Team</i></b></p></font></body></html>'
$BodyEmailsiste = @"
{
"message": {
"subject": "$Title",
"body": {
"contentType": "HTML",
"content": '$bodyHTMLsiste'
},
"toRecipients": [
{
"emailAddress": {
"address": "$($teamowners.userPrincipalName)"
}
}
]
},
"saveToSentItems": "false"
}
"@
Invoke-RestMethod -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$($EmailUPN)/sendMail&quot; -Headers $global:Header -body $BodyEmailSiste -ContentType "application/json;charset=utf-8"
$BodyNewListItem = @()
$BodyNewListItem = @"
{
'Warning5': '$(Get-Date)'
}
"@
Invoke-RestMethod -Method Patch -Uri "$($SharePointListURL)/items/$($TeamListItem.id)/fields" -Headers $global:Header -body $BodyNewListItem -ContentType "application/json"
$teamowners.userPrincipalName
}
}
if ($TeamListItem.Warning5 -and !$TeamListItem.WarningAdmin) {
$Days =@()
$Days = ($TeamListItem.Warning5 | New-TimeSpan).Days
#Om femte varsel er sendt, send IT varsel om det har gått lang nokk tid.
If($Days -ge $WarningAdminDays){
$BodyNewListItem = @()
$BodyNewListItem = @"
{
'WarningAdmin': '$(Get-Date)'
}
"@
Invoke-RestMethod -Method Patch -Uri "$($SharePointListURL)/items/$($TeamListItem.id)/fields" -Headers $global:Header -body $BodyNewListItem -ContentType "application/json"
#Team still has 1 owner after 14 days, send message to IT in Teams.
$body = @"
{
"type":"message",
"attachments":[
{
"contentType":"application/vnd.microsoft.card.adaptive",
"contentUrl":null,
"content":{
"`$schema":"http://adaptivecards.io/schemas/adaptive-card.json&quot;,
"type":"AdaptiveCard",
"version":"1.2",
"msteams": {
"width": "Full"
},
"body":[
{
"type": "TextBlock",
"text": "$($team.displayname) / $($team.id) can be deleted today!"
}
]
}
}
]
}
"@
Invoke-RestMethod -Method POST -Body $body -Uri $TeamsWebhookURL -ContentType "application/json;charset=utf-8"
}
}
}
Else{
$Count++
$Count
#If not warning has been sent, send first warning and create entry in SharePoint list.
Invoke-RestMethod -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$($EmailUPN)/sendMail&quot; -Headers $global:Header -body $BodyEmail -ContentType "application/json;charset=utf-8"
$BodyNewListItem = @()
$BodyNewListItem = @"
{
'fields': {
'Title': "$TeamDisplayName",
'TeamID': '$($Team.ID)',
'UPN': '$($teamowners.userPrincipalName)',
'Warning1': '$(Get-Date)'
}
}
"@
$TeamOwners
$BodyNewListItem
Invoke-RestMethod -Method POST -Uri "$($SharePointListURL)/items" -Headers $global:Header -body $BodyNewListItem -ContentType "application/json;charset=utf-8"
$teamowners.userPrincipalName
}
}
#If the team has now owner, send message to IT in Teams.
If($TeamOwners.Count -eq 0){
$TeamListItem = @()
$TeamListItem = $ListItems | Where-Object { $_.TeamID -eq $team.id }
#If entry does not exist in SharePoint list, create a entry and warning to IT in Teams.
If($TeamListItem.TeamID -ne $Team.ID){
$body = @"
{
"type":"message",
"attachments":[
{
"contentType":"application/vnd.microsoft.card.adaptive",
"contentUrl":null,
"content":{
"`$schema":"http://adaptivecards.io/schemas/adaptive-card.json&quot;,
"type":"AdaptiveCard",
"version":"1.2",
"msteams": {
"width": "Full"
},
"body":[
{
"type": "TextBlock",
"text": "$($team.displayname) / $($team.id) has no owner, and can be deleted today!"
}
]
}
}
]
}
"@
Invoke-RestMethod -Method POST -Body $body -Uri $TeamsWebhookURL -ContentType "application/json;charset=utf-8"
#Create entry in SharePoint list.
$BodyNewListItem = @()
$BodyNewListItem = @"
{
'fields': {
'Title': "$($team.displayName)",
'TeamID': '$($Team.ID)',
'WarningAdmin': '$(Get-Date)'
}
}
"@
Invoke-RestMethod -Method POST -Uri "$($SharePointListURL)/items" -Headers $global:Header -body $BodyNewListItem -ContentType "application/json;charset=utf-8"
}
}
}


Variables needed to be populated in the script:
* TenantID
* ClientID
* EmailUPN
* SharePointSiteID
* SharePointListID
* TeamsWebHookURL

Click Publish.

Click Yes.

Click Link to schedule.

Click Link a schedule to your runbook.

Click Add a schedule.

I sugest that this run once a day, so copy the settings in the screenshot and click Create.

Click OK.

The entries in the SharePoint List will look like this:

Example of the first warning a team owner will receive:

The warning IT receives in Teams will look like this:

2 thoughts on “Automatically send alerts to the last owner of a team! (If you enforce a 2 or more owner policy)

  1. Thanks for sharing. Deployed AvePoint Cloud Governance to enforce two owner rule and pester remaining owner to find a buddy. It then escalated to members before locking the Team.

    Like

Leave a comment