Microsoft Launches Preview PowerShell Module for Graph

Microsoft Graph PowerShell module

Accessing Data that PowerShell Can’t Reach

In a September 2019 article, I discuss how to use Microsoft Graph API calls with PowerShell to expose information that can’t be accessed with PowerShell cmdlets. In the example given, we use the PowerShell Invoke-WebRequest cmdlet to make Graph calls to fetch the email addresses of Teams channels because this is a property that you can’t get with any of the cmdlets in any Office 365 PowerShell module.

A PowerShell Module for the Graph

Time goes by time technology changes. Recently, Microsoft released a beta version (V0.11) of the Microsoft Graph PowerShell module (available in the PowerShell gallery). The module was announced at the Microsoft Ignite 2019 conference. Its task is simple: deliver a set of PowerShell cmdlets to work with Graph data to make it as easy to work with the Graph as it is to interact with Exchange, Azure Active Directory, and other sources.

Installing the Graph Module

To install the module, run the following command in an administrator PowerShell session:

Install-Module Microsoft.Graph -Repository PSGallery -Force

The installation downloads and installs a set of submodules for Graph endpoints like Groups, Teams, Sites, and so on. One issue that I met was a permissions problem with the file UserTokenCache.bin3 in C:\Program Files\WindowsPowerShell\Modules\Microsoft.Graph.Authentication\0.1.5\bin. Allowing user write access to the file made the problem disappear.

Connecting to the Graph

Once installed, you can connect to the Graph with the Connect-Graph cmdlet. The important thing is to specify the permissions you want to use to work with Graph data. This is done by passing the sources and access you want to use in the Scopes parameter. For instance, this command requests for permissions for several Graph sources:

Connect-Graph -Scopes "User.Read","User.ReadWrite.All", "Directory.ReadWrite.All", `
             "Group.Read.All", "Directory.AccessAsUser.All", "Tasks.ReadWrite", `

See this page for details of the Graph permissions you can request.

Depending on the permissions requested, the response to Connect-Graph is either a connection (“Welcome to Microsoft Graph”) or an instruction to input a supplied code to to seek consent. After the consent process completes, you’ll be connected to the Graph. The current connectivity method isn’t appropriate for production scripts which need to run in the background (a certificate-based example is available), but it’s enough to connect to the Graph to explore its cmdlets.

Learning How to Use the Graph Cmdlets

Several examples of using cmdlets from the module are posted along with the rest of the module on GitHub. These examples give an interesting starting point to understand how to use the cmdlets to fetch information and the format of the returned data. Given the understandable lack of documentation at this stage in the module’s development, some trial and error (and perhaps inspired guesswork) is needed to get anything done.

To create a working example of the Graph module in action, I updated my script that uses the Invoke-WebRequest cmdlet to fetch data about mail-enabled Teams channels and replaced those calls with Graph cmdlets like Get-MgGroup, Get-MgGroupOwner, and Get-MgTeamChannel. The process wasn’t that difficult, and the resulting code is posted below.

# Example of using Microsoft Graph PowerShell cmdlets to fetch and report information about Teams-enabled groups with
# email addresses assigned to channels

# Connect to Graph. This shows how to request a large set of permissions
Connect-Graph -Scopes "User.Read","User.ReadWrite.All", "Directory.ReadWrite.All", `
"Group.Read.All", "Directory.AccessAsUser.All", "Tasks.ReadWrite", "Sites.Manage.All" 
# Fetch Groups
Write-Host "Fetching Groups..."
$Groups = Get-MgGroup -Top 999 -OrderBy DisplayName | Select id, DisplayName, GroupTypes, ResourceProvisioningOptions, VisibilityOptions, Mail 
$Teams = $Groups | ? {$_.ResourceProvisioningOptions -Contains "Team"} 
$i = 0; $EmailAddresses = 0; $Report = [System.Collections.Generic.List[Object]]::new() # Create output file for report; $ReportLine = $Null
Write-Host $Teams.Count "teams found - checking channels for email"
ForEach ($Team in $Teams) {
      $ProgressBar = "Processing Team " + $Team.DisplayName + " (" + $i + " of " + $Teams.Count + ")"
      Write-Progress -Activity "Checking Teams Information" -Status $ProgressBar -PercentComplete ($i/$Teams.Value.Count*100)
      Try { # Get owners of the team 
        $TeamOwners = Get-MgGroupOwner -GroupId $Team.Id | Select -ExpandProperty Id
        If ($TeamOwners.Count -eq 1) { $TeamOwner = Get-MgUser -UserId $Owners | Select -ExpandProperty 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) {
          If ($Count -eq 1) {  # First owner in the list
              $TeamOwner = (Get-MgUser -UserId $Owner | Select -ExpandProperty DisplayName)
               $Count++ }
            Else { $TeamOwner = $TeamOwner + "; " + (Get-MgUser -UserId $Owner | Select -ExpandProperty DisplayName)}
      Catch {Write-Host "Unable to get owner information for team" $Team.DisplayName }  
      # Process Channels
      Try {
         $Channels = Get-MgTeamChannel -TeamId $Team.Id
         ForEach ($Channel in $Channels) {
           If (-Not [string]::IsNullOrEmpty($Channel.Email)) {
               $ReportLine = [PSCustomObject][Ordered]@{
                 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) }}
       Catch { Write-Host "Unable to fetch channels for" $Team.DisplayName } 
$Report | Sort Team | Export-CSV C:\Temp\TeamsChannelsWithEmailAddress.Csv -NoTypeInformation
Write-Host $EmailAddresses "mail-enabled channels found. Details are in C:\Temp\TeamsChannelsWithEmailAddress.Csv"

Lack of Filters

I’m certain the code can be improved as we learn more about how the cmdlets work. For instance, Get-MgGroup returns all groups in a tenant. Team-enabled groups can be detected by the presence of “Team” in the ResourceProvisioningOptions property. But Get-MgGroup doesn’t support a filter parameter (the Graph API does), so I applied the filter with PowerShell after fetching all groups. Or rather, 999 groups (the maximum supported by the cmdlet). I don’t know to handle the situation if more than 999 groups exist in a tenant because the current cmdlets don’t handle pagination of Graph results (it’s on the list of to-do items).

Accessing data via the Graph is fast. In fact, even though this code fetches groups and then filters for teams, it’s still much faster than the standard Get-Team cmdlet from the Teams PowerShell module. The Get-Team cmdlet does include some basic filtering capabilities (but not a filter parameter), but even so, it’s surprising that its performance is so poor compared to the basic Graph cmdlets.

Much the same approach as used in the script can be used to extract Yammer groups by examining the ResourceBehaviorOptions property to find instances of “YammerProvisioning” or to find Office 365 Groups by checking the GroupTypes property for “unified.” Adding filters for common conditions is an example of what I would expect to see happen as the cmdlets mature.

Very Much a Preview

It’s important to remember that the Graph module is an early-days preview. Lots of work will be done to improve, expand, and refine the cmdlet set over the coming months. The vision of a single module with cmdlets to deal with all sorts of Office 365 data is compelling. We await developments with interest.