Microsoft Teams|Office|Office 365

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

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.

Sponsored Content

Passwords Haven’t Disappeared Yet

123456. Qwerty. Iloveyou. No, these are not exercises for people who are brand new to typing. Shockingly, they are among the most common passwords that end users choose in 2021. Research has found that the average business user must manually type out, or copy/paste, the credentials to 154 websites per month. We repeatedly got one question that surprised us: “Why would I ever trust a third party with control of my network?

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).

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.

BECOME A PETRI MEMBER:

Don't have a login but want to join the conversation? Sign up for a Petri Account

Register
Comments (8)

8 responses to “Exploiting the Graph When PowerShell Can’t Do Enough for Teams”

  1. <p>I'd love to be able to update my Teams status (message and free/busy) via powershell but I've not been able to find a way to do this. Used to be able to do it in Skype to include what I was currently working on, set away for lunch etc.</p>

    • <blockquote><a href="#16455"><em>In reply to absoblogginlutely:</em></a><em> Unfortunately this isn't possible (yet) with the Teams PowerShell module. We live in hope.</em></blockquote><p><br></p>

  2. <p>VERY new to Graph.</p><p>I've found two samples like yours on the internet. Both have the same issue the query stopping at 100. We have Hundreds of teams, each with multiple channels. The script is unable to pull all the information. I have tried $top=999, this has not worked.</p><p>Any ideas?</p>

    • <blockquote><a href="#16459"><em>In reply to dwordin:</em></a><em> What happens when you try and fetch all Office 365 Groups (not Teams)? Does the problem still exist at &gt; 100 groups? But I don't understand why the code doesn't work for more than 100 teams so I shall have to go and check.</em></blockquote><p><br></p>

      • <blockquote><a href="#16490"><em>In reply to Tony-Redmond:</em></a><em> To answer myself, the solution is to use the odata.nextlink link returned when more results exist to be retrieved. You loop through the set until the nextlink is null.</em></blockquote><p><br></p>

  3. <p>Thanks for providing this, but I'm having some problems. Would love to get it working, seems really useful.</p><p><br></p><p>I've granted the Group.ReadAll permission to my app, and authorised on behalf of the organisation, but continue to get "Authorization_RequestDenied", "message": "Insufficient privileges to complete the operation."</p><p><br></p><p>This article says that the idea is that an "app is assigned enough permissions to work with the data you need to access", but doesn't say what permissions this particular app needs.</p><p><br></p><p>Can anyone help?</p>

Leave a Reply

Tony Redmond has written thousands of articles about Microsoft technology since 1996. He covers Office 365 and associated technologies for Petri.com and is also the lead author for the Office 365 for IT Pros eBook, updated monthly to keep pace with change in the cloud.