Exploiting the Graph When PowerShell Can’t Do Enough for Teams

Microsoft G raph Logo

Teams PowerShell: OK but Limited at Times

After a shaky start, the Teams PowerShell module is now in a reasonable state. Valid gripes still exist that the Teams module is slow, and you must resort to the horrible Skype for Business Online module to work with Teams policies, but hope exists that Microsoft will improve performance and create an integrated module in the future.

But no matter how the Teams PowerShell module improves, its usefulness is still limited by the properties Microsoft chooses to expose through this interface. For instance, even if sometimes Microsoft backtracks on useful changes, Exchange Online, exposes a lot of information about mailboxes and Office 365 Groups. This makes it much easier for administrators to automate common operational processes for mailboxes and groups.

However, Exchange was the first major Microsoft server to adopt PowerShell way back in Exchange 2007. When Teams came along, PowerShell wasn’t its primary choice for an interface to enable automation. Instead, Teams focused on the Microsoft Graph API. The Teams PowerShell module is built on top of the Graph API, a fact that explains some oddities in filtering and other behavior.

To the Graph and Beyond

In any case, because Microsoft doesn’t expose all the properties of teams through PowerShell, sometimes you’re forced to use the Graph to get at information (the Graph Explorer helps you understand the information available for Teams). This is fine if you’re a programmer who’s used to dealing with RESTful APIs, but maybe not so good if you’re an administrator who writes some PowerShell scripts from time to time.

Fortunately, you can access the Graph API through PowerShell. And once you get your head around the concepts involved, it’s reasonably straightforward to write scripts to access and use the information exposed through the Graph.

Basic Graph Concepts

There are many blog posts available that explain how to connect to the Graph with PowerShell (here’s a good example and here’s another approach). The basic idea is that:

  • An app created in Azure Active Directory gives an entry point to the Graph. The app is tied to your tenant and can authenticate to the Graph using an application secret (analogous to a password) to access information. The app is assigned enough permissions to work with the data you need to access, like Groups.
  • PowerShell uses the Invoke-WebRequest cmdlet to send HTTP commands to the app to process against the Graph. For example, you use a GET command to request information.
  • The JSON data returned by the Graph is unpacked and used as normal.
  • If the app has write permission, data can be updated by sending a POST command to the Graph. You can also remove information by sending a DELETE command.

Working Example – Reporting Channel Email Addresses

So much for theory. As my working example, I’ve chosen to interrogate Teams to discover the set of channels that are mail-enabled and report the email addresses assigned to these channels. Any team member can enable a channel by requesting an email address. When this happens, Office 365 creates a special hidden mailbox for the channel and links the mailbox to Teams with a connector. Mail sent to the channel shows up as a new conversation and is also captured in the SharePoint document library belonging to the team.

The Teams PowerShell module includes a Get-TeamChannel cmdlet to return the set of channels in a team. However, it doesn’t return properties to show if a channel is mail-enabled or what its email address is, but the Graph knows. Here’s the code I used, broken into:

  1. Define details of the Azure Active Directory app to use to connect to the Graph.
  2. Create a token to connect.
  3. Connect to the Teams Graph endpoint and fetch a list of teams.
  4. Process each team to find its channels.
  5. Examine each channel to find if it is mail-enabled and if so, record its details.
  6. Generate CSV file.
Cls
# Define the values applicable for the application used to connect to the Graph. These values 
# are unique to an app within a tenant.
$AppId = "dx16b32c-0edb-48be-9385-30a9cfd96155"
$TenantId = "b662333f-14fc-43a2-9a7a-d2e27f4f3478"
$AppSecret = 's_rkvIn1oZ1cNceUBvJ2or1lrrIsb*:='

# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $AppId
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $AppSecret
    grant_type    = "client_credentials"
}

# Get OAuth 2.0 Token
$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing

# Unpack Access Token
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token

# Base URL
$uri = "https://graph.microsoft.com/beta/"
$headers = @{Authorization = "Bearer $token"}
$ctype = "application/json"

# Create list of Teams in the tenant
Write-Host "Fetching list of Teams in the tenant"
$Teams = Invoke-WebRequest -Method GET -Uri "$($uri)groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')" -ContentType $ctype -Headers $headers | ConvertFrom-Json

# Loop through each team to examine its channels and discover if any are email-enabled
$i = 0; $EmailAddresses = 0; $Report = [System.Collections.Generic.List[Object]]::new(); $ReportLine = $Null
ForEach ($Team in $Teams.Value) {
      $i++
      $ProgressBar = "Processing Team " + $Team.DisplayName + " (" + $i + " of " + $Teams.Value.Count + ")"
      Write-Progress -Activity "Checking Teams Information" -Status $ProgressBar -PercentComplete ($i/$Teams.Value.Count*100)
      # Get owners of the team
      $TeamOwners = Invoke-WebRequest -Method GET -Uri "$($uri)groups/$($team.id)/owners" -ContentType $ctype -Headers $headers | ConvertFrom-Json  
      If ($TeamOwners.Value.Count -eq 1) {$TeamOwner = $TeamOwners.Value.DisplayName}
      Else { # More than one team owner, so let's split them out and make the string look pretty
         $Count = 1
         ForEach ($Owner in $TeamOwners.Value) {
            If ($Count -eq 1) {  # First owner in the list
               $TeamOwner = $Owner.DisplayName
               $Count++ }
            Else { $TeamOwner = $TeamOwner + "; " + $Owner.DisplayName }
       }}                         
      # Fetch list of channels for the team
      $Channels = Invoke-WebRequest -Method GET -Uri "$($uri)teams/$($team.id)/channels" -ContentType $ctype -Headers $headers | ConvertFrom-Json
      #Loop through each channel and get its email address if set
      ForEach ($Channel in $Channels.Value) {
        If (-Not [string]::IsNullOrEmpty($Channel.Email)) {
            # Write-Host "Email address found" $Channel.Email
            $EmailAddresses++
            $ReportLine = [PSCustomObject]@{
              Team                = $Team.DisplayName
              TeamEmail           = $Team.Mail
              Owners              = $TeamOwner
              Channel             = $Channel.DisplayName
              ChannelDescription  = $Channel.Description
              ChannelEmailAddress = $Channel.Email   }
            # And store the line in the report object
            $Report.Add($ReportLine) }}
}  
$Report | Sort Team | Export-CSV C:\Temp\TeamsChannelsWithEmailAddress.Csv -NoTypeInformation
Write-Host $EmailAddresses "mail-enabled channels found. Details are in C:\Temp\TeamsChannelsWithEmailAddress.Csv"

The output is a CSV file holding details of the mail-enabled channels (Figure 1).

CSV file with mail enabled Teams Channels
Figure 1: CSV file with details of mail-enabled Teams channels (image credit: Tony Redmond)

Explore the Possibilities

The Graph is hugely important to Office 365 and Microsoft is moving away from older APIs to use the Graph as quickly as it can. There’s obviously lots more that you can do to exploit the Graph with PowerShell (improve my code for a start), but hopefully this example is enough to get some creative juices going and remove a barrier that might have stopped you going near the Graph. PowerShell is great; it’s just even better when connected to the Graph.