I was thinking of creating an automated teams mapping tool, which runs when a user logs on to a new machine to automatically sync all the Teams sites required and joined. I mostly wanted to do this because the current implementation of the registry/GPO method can take up to 8 hours.
During the creation of my tool I noticed I didn’t have an up to date document for all teams. So I figured I would create one. To do this, I’m using the Secure Application Model as always. The only difference is the method we are using to log onto the Graph API.
Logging onto the Graph API requires a token, normally we generate this token using the PartnerCenter module. This works fine for most tasks, but for using Teams with the Graph API we need a different claim, namely the “Client_Credentials” claim.
So instead of using the partner center module, we will create our own request for a token. I’ll show you how in the script.
I’ve created two versions again, one generic, and one for IT-Glue.
Generic version
The generic version creates 1 html file per team in C:\Temp. Feel free to modify this for your own documentation system. The result will look something like this:
################### Secure Application Model Information ###################
$ApplicationId = 'AppID'
$ApplicationSecret = 'AppSecret'
$TenantID = 'YourTenantID'
$RefreshToken = 'VeryLongRefreshToken'
################# /Secure Application Model Information ####################
write-host "Creating credentials and tokens." -ForegroundColor Green
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, ($ApplicationSecret | Convertto-SecureString -AsPlainText -Force))
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal
write-host "Creating body to request Graph access for each client." -ForegroundColor Green
$body = @{
'resource' = 'https://graph.microsoft.com'
'client_id' = $ApplicationId
'client_secret' = $ApplicationSecret
'grant_type' = "client_credentials"
'scope' = "openid"
}
write-host "Setting HTML Headers" -ForegroundColor Green
$head = @"
Audit Log Report
"@
write-host “Connecting to Office365 to get all tenants.” -ForegroundColor Green
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
foreach ($Customer in $Customers) {
$ClientToken = Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($customer.tenantid)/oauth2/token" -Body $body -ErrorAction Stop
$headers = @{ "Authorization" = "Bearer $($ClientToken.accesstoken)" }
write-host "Starting documentation process for $($customer.name)." -ForegroundColor Green
$headers = @{ "Authorization" = "Bearer $($CustgraphToken.AccessToken)" }
Write-Host "Grabbing all Teams" -ForegroundColor Green
$AllTeamsURI = "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&`$top=999"
$Teams = (Invoke-RestMethod -Uri $AllTeamsURI -Headers $Headers -Method Get -ContentType "application/json").value
Write-Host "Grabbing all Team Settings" -ForegroundColor Green
$TeamSettings = foreach ($Team in $Teams) {
$Settings = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/Teams/$($team.id)" -Headers $Headers -Method Get -ContentType "application/json")
$Members = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$($team.id)/members?`$top=999" -Headers $Headers -Method Get -ContentType "application/json").value
$Owners = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$($team.id)/Owners?`$top=999" -Headers $Headers -Method Get -ContentType "application/json").value
[PSCustomObject]@{
'Team Name' = $settings.displayname
'Team ID' = $settings.id
'Description' = $Settings.description
'Teams URL' = $settings.webUrl
'Messaging Settings' = $settings.messagingSettings
'Member Settings' = $Settings.memberSettings
'Guest Settings' = $Settings.guestSettings
'Fun Settings' = $settings.funSettings
'Discovery Settings' = $settings.discoverySettings
'Is Archived' = $Settings.isArchived
'Team Owners' = $Owners | Select-Object Displayname, UserPrincipalname
'Team Members' = $Members | Where-Object { $_.UserType -eq "Member" } | Select-Object Displayname, UserPrincipalname
'Team Guests' = $Members | Where-Object { $_.UserType -eq "Guest" } | Select-Object Displayname, UserPrincipalname
}
$SettingsHTML = $Settings | ConvertTo-Html -as List - -Fragment -PreContent "<h1>Settings<h2>" | Out-String
$OwnersHTML = $Owners | Select-Object Displayname, UserPrincipalname | ConvertTo-Html -Fragment -PreContent "<h1>Owners<h2>" | Out-String
$MembersHTML = $Members | Where-Object { $_.UserType -eq "Member" } | Select-Object Displayname, UserPrincipalname | ConvertTo-Html -Fragment -PreContent "<h1>Members<h2>" | Out-String
$GuestsHTML = $Members | Where-Object { $_.UserType -eq "Guest" } | Select-Object Displayname, UserPrincipalname | ConvertTo-Html -Fragment -PreContent "<h1>Guests<h2>" | Out-String
$head, $SettingsHTML, $OwnersHTML, $MembersHTML, $GuestsHTML -replace "<th>", "<th style=`"background-color:#4CAF50`">" | Out-File "C:\Temp\$($Customer.name) - $($Settings.displayname).html"
}
}
### IT-Glue version
So the IT-Glue version is quite a bit longer; I’ve also improved the previous domain matching logic we used to make the script run much faster. This script creates the Flexible Asset for you when running it the first time. We match the teams based on the Team Name. This script can also run from an Azure Function.
```powershell
################### Secure Application Model Information ###################
$ApplicationId = 'YourApplicationID'
$ApplicationSecret = 'YourApplicationSecret'
$RefreshToken = 'YourVeryLongRefreshToken'
################# /Secure Application Model Information ####################
################# IT-Glue Information ######################################
$ITGkey = "YourITGAPIKEY"
$ITGbaseURI = "https://api.eu.itglue.com"
$FlexAssetName = "Teams - Autodoc"
$Description = "Teams information automatically retrieved."
$TableStyling = "<th>", "<th style=`"background-color:#4CAF50`">"
################# /IT-Glue Information #####################################
write-host "Checking if IT-Glue Module is available, and if not install it." -ForegroundColor Green
#Settings IT-Glue logon information
If (Get-Module -ListAvailable -Name "ITGlueAPI") {
Import-module ITGlueAPI
}
Else {
Install-Module ITGlueAPI -Force
Import-Module ITGlueAPI
}
#Setting IT-Glue logon information
Add-ITGlueBaseURI -base_uri $ITGbaseURI
Add-ITGlueAPIKey $ITGkey
write-host "Getting IT-Glue contact list" -ForegroundColor Green
$i = 0
do {
$AllITGlueContacts += (Get-ITGlueContacts -page_size 1000 -page_number $i).data.attributes
$i++
Write-Host "Retrieved $($AllITGlueContacts.count) Contacts" -ForegroundColor Yellow
}while ($AllITGlueContacts.count % 1000 -eq 0 -and $AllITGlueContacts.count -ne 0)
write-host "Generating unique ID List" -ForegroundColor Green
$DomainList = foreach ($Contact in $AllITGlueContacts) {
$ITGDomain = ($contact.'contact-emails'.value -split "@")[1]
[PSCustomObject]@{
Domain = $ITGDomain
OrgID = $Contact.'organization-id'
Combined = "$($ITGDomain)$($Contact.'organization-id')"
}
}
$DomainList = $DomainList | sort-object -Property Combined -Unique
write-host "Checking if Flexible Asset exists in IT-Glue." -foregroundColor green
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
if (!$FilterID) {
write-host "Does not exist, creating new." -foregroundColor green
$NewFlexAssetData =
@{
type = 'flexible-asset-types'
attributes = @{
name = $FlexAssetName
icon = 'sitemap'
description = $description
}
relationships = @{
"flexible-asset-fields" = @{
data = @(
@{
type = "flexible_asset_fields"
attributes = @{
order = 1
name = "Team Name"
kind = "Text"
required = $true
"show-in-list" = $true
"use-for-title" = $true
}
},
@{
type = "flexible_asset_fields"
attributes = @{
order = 2
name = "Team URL"
kind = "Text"
required = $true
"show-in-list" = $false
"use-for-title" = $false
}
},
@{
type = "flexible_asset_fields"
attributes = @{
order = 3
name = "Team Message settings"
kind = "Textbox"
required = $false
"show-in-list" = $false
}
},
@{
type = "flexible_asset_fields"
attributes = @{
order = 4
name = "Team Member settings"
kind = "Textbox"
required = $false
"show-in-list" = $false
}
},
@{
type = "flexible_asset_fields"
attributes = @{
order = 5
name = "Team Guest settings"
kind = "Textbox"
required = $false
"show-in-list" = $false
}
},
@{
type = "flexible_asset_fields"
attributes = @{
order = 6
name = "Team Fun Settings"
kind = "Textbox"
required = $false
"show-in-list" = $false
}
},
@{
type = "flexible_asset_fields"
attributes = @{
order = 7
name = "Team Owners"
kind = "Textbox"
required = $false
"show-in-list" = $false
}
},
@{
type = "flexible_asset_fields"
attributes = @{
order = 8
name = "Team Members"
kind = "Textbox"
required = $false
"show-in-list" = $false
}
}, @{
type = "flexible_asset_fields"
attributes = @{
order = 9
name = "Team Guests"
kind = "Textbox"
required = $false
"show-in-list" = $false
}
}
)
}
}
}
New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
}
write-host "Creating credentials and tokens." -ForegroundColor Green
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, ($ApplicationSecret | Convertto-SecureString -AsPlainText -Force))
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal
write-host "Creating body to request Graph access for each client." -ForegroundColor Green
$body = @{
'resource' = 'https://graph.microsoft.com'
'client_id' = $ApplicationId
'client_secret' = $ApplicationSecret
'grant_type' = "client_credentials"
'scope' = "openid"
}
write-host "Connecting to Office365 to get all tenants." -ForegroundColor Green
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
foreach ($Customer in $Customers) {
write-host "Grabbing domains for client $($Customer.name)." -ForegroundColor Green
$CustomerDomains = Get-MsolDomain -TenantId $Customer.TenantId
write-host "Finding possible organisation IDs" -ForegroundColor Green
$orgid = foreach ($customerDomain in $customerdomains) {
($domainList | Where-Object { $_.domain -eq $customerDomain.name }).'OrgID' | Select-Object -Unique
}
write-host "Documenting in the following organizations." -ForegroundColor Green
$ClientToken = Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($customer.tenantid)/oauth2/token" -Body $body -ErrorAction Stop
$headers = @{ "Authorization" = "Bearer $($ClientToken.access_token)" }
write-host "Starting documentation process for $($customer.name)." -ForegroundColor Green
Write-Host "Grabbing all Teams" -ForegroundColor Green
$AllTeamsURI = "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&`$top=999"
$Teams = (Invoke-RestMethod -Uri $AllTeamsURI -Headers $Headers -Method Get -ContentType "application/json").value
Write-Host "Grabbing all Team Settings" -ForegroundColor Green
foreach ($Team in $Teams) {
$Settings = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/Teams/$($team.id)" -Headers $Headers -Method Get -ContentType "application/json")
$Members = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$($team.id)/members?`$top=999" -Headers $Headers -Method Get -ContentType "application/json").value
$Owners = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$($team.id)/Owners?`$top=999" -Headers $Headers -Method Get -ContentType "application/json").value
$FlexAssetBody =
@{
type = 'flexible-assets'
attributes = @{
traits = @{
'team-name' = $settings.displayname
'team-url' = $settings.webUrl
'team-message-settings' = ($settings.messagingSettings | convertto-html -Fragment -as list | out-string) -replace $TableStyling
'team-member-settings' = ($Settings.memberSettings | convertto-html -Fragment -as list | out-string) -replace $TableStyling
"team-guest-settings" = ($Settings.guestSettings | convertto-html -Fragment -as list | out-string) -replace $TableStyling
"team-fun-settings" = ($settings.funSettings | convertto-html -Fragment -as list | out-string) -replace $TableStyling
'team-owners' = ($Owners | Select-Object Displayname, UserPrincipalname | convertto-html -Fragment | out-string) -replace $TableStyling
'team-members' = ($Members | Where-Object { $_.UserType -eq "Member" } | Select-Object Displayname, UserPrincipalname | convertto-html -Fragment | out-string) -replace $TableStyling
'team-guests' = ($Members | Where-Object { $_.UserType -eq "Guest" } | Select-Object Displayname, UserPrincipalname | convertto-html -Fragment | out-string) -replace $TableStyling
}
}
}
write-host " Uploading $($Settings.displayName) into IT-Glue" -foregroundColor green
foreach ($org in $orgID | Select-Object -Unique) {
$ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $FilterID.id -filter_organization_id $org).data | Where-Object { $_.attributes.traits.'team-name' -eq $Settings.displayName }
#If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
if (!$ExistingFlexAsset) {
if ($FlexAssetBody.attributes.'organization-id') {
$FlexAssetBody.attributes.'organization-id' = $org
}
else {
$FlexAssetBody.attributes.add('organization-id', $org)
$FlexAssetBody.attributes.add('flexible-asset-type-id', $FilterID.id)
}
write-output " Creating new Team: $($Settings.displayName) into IT-Glue organisation $org"
New-ITGlueFlexibleAssets -data $FlexAssetBody
}
else {
write-output " Updating Team: $($Settings.displayName)into IT-Glue organisation $org"
$ExistingFlexAsset = $ExistingFlexAsset | select-object -Last 1
Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id -data $FlexAssetBody
}
}
}
}
And that’s it! As always, Happy PowerShelling!