I’ve written several articles describing how to write PowerShell scripts to create membership reports for groups and teams in the past (here’s a recent example). Usually, the scripts involve interrogating groups about their membership or finding sets of users and checking what groups they are members of. For this kind of processing, Graph API requests are the fastest way to generate results because of the number of user accounts or groups to be processed.
This brings me to the Get-AssociatedTeam cmdlet, a new cmdlet that makes its debut in version 4.6 of the MicrosoftTeams PowerShell module. Microsoft hasn’t published documentation for the cmdlet yet, but its purpose is obvious: it reports all the teams a user is a member of. As such, the Get-AssociatedTeam cmdlet could be the basis for a script to report the membership of all Teams.
Before plunging into the details of how to write such a script, the interesting thing about the Get-AssociatedTeam cmdlet is that it reports membership of shared channels. Or rather, direct membership of shared channels, which is when a user is explicitly invited to share a channel hosted in their tenant or in an external tenant. The information returned by the cmdlet includes all instances where a user is a regular member of a team and where they are a direct member of a shared channel. Unfortunately, the information returned for a team doesn’t tell you if the membership is of a regular team or shared channel. Here’s an example of the information for a team:
GroupId : a53141d5-54ef-4a6d-877d-63b0cbda409f
DisplayName : Marketing Department
TenantId : a662313f-14fc-43a2-9a7a-d2e27f4f3478
Being able to report membership of shared channels is the unique added value of the Get-AssociatedTeam cmdlet. There doesn’t seem to be a way to extract this information using a Graph API request, nor is there a way to report the membership of private channels without checking each of these channels.
Writing a Report Script
The script I wrote to explore the possibilities of Get-AssociatedTeam is available from GitHub. It:
Uses Get-MgUser to find the set of licensed Azure AD member accounts in the tenant. The Get-AssociatedTeam cmdlet doesn’t process guest accounts.
For each account, run Get-AssociatedTeam to return the set of teams for the user.
For each team, use the findTenantInformationByTenantId Graph API to resolve the tenant identifier to find the tenant name. This helps to highlight access to external tenants.
Report the information.
The code for the main loop is:
[int]$i = 0
$UserTeamInfo = [System.Collections.Generic.List[Object]]::new()
ForEach ($User in $Users) {
$i++
Write-Host ("Processing team membership for {0} ({1}/{2})..." -f $User.DisplayName, $i, $Users.Count)
[array]$TeamInfo = Get-AssociatedTeam -User $User.UserPrincipalName
ForEach ($Team in $TeamInfo) {
If ($Team.TenantId -eq $TenantId) { # Resolve the tenant identifier to a name
$Name = $TenantName }
Else {
$LookUpId = $Team.TenantId.toString()
$Uri = "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$LookUpId')"
$ExternalTenantData = Invoke-MgGraphRequest -Uri $Uri -Method Get
$Name = $ExternalTenantData.DisplayName
}
$TeamData = [PSCustomObject][Ordered]@{ # Write out details of the team
Id = $User.Id
DisplayName = $User.DisplayName
UPN = $User.UserPrincipalName
Team = $Team.DisplayName
TeamId = $Team.GroupId
Tenant = $Name
TenantId = $Team.TenantId}
$UserTeamInfo.Add($TeamData)
} #End ForEach Team
} # End ForEach User
Analyzing the Data
Figure 1 shows the kind of information generated by the script.
Figure 1: Report of Teams membership generated by the Get-AssociatedTeam cmdlet
Once the membership data is available, we can slice and dice it in different ways. For example, some simple analysis reveals nuggets like the average number of teams a user belongs to, how many external teams people are members of, and so on.
[array]$ExternalTeams = $UserTeamInfo | Where-Object {$_.TenantId -ne $TenantId} | Sort-Object TeamId -Unique
$ExternalPeople = $UserTeamInfo | Where-Object {$_.TenantId -ne $TenantId} | Sort-Object UPN -Unique
$ExternalPeople = $ExternalPeople.DisplayName -Join ", "
$ExternalTenants = $ExternalTeams.Tenant | Sort-Object -Unique
$AvgTeams = [math]::round(($UserTeamInfo.Count/$Users.Count),2)
Write-Host ""
Write-Host ("Each of the {0} users belongs to an average of {1} teams" -f $Users.Count, $AvgTeams)
Write-Host ("Membership of {0} teams found in {1} external tenant(s)" -f $ExternalTeams.Count, $ExternalTenants.Count)
Write-Host ("These accounts have membership of external teams: {0}" -f $ExternalPeople)
Each of the 28 users belongs to an average of 15.54 teams
Membership of 2 teams found in 1 external tenant(s)
These accounts have membership of external teams: Chris Bishop, James Ryan, Ken Bowers, Sean Landy, Tony Redmond
Generating Report Files
Carefully sliced and diced data makes an excellent foundation for reports, which can be generated as CSV files, Excel spreadsheets, HTML files, and so on.
I’ve mentioned the PSWriteHTML module before as an easy way to generate good-looking reports from PowerShell. Apart from its ability to format data in various ways, PSWriteHTML includes a very nice search builder capability to filter data. For instance, in Figure 2 I used the search builder to find the members of a specific team. When I’m happy with the data, generating a PDF, Excel, or CSV file is as easy as clicking a button.
Figure 2: Reporting Teams membership with PSWriteHTML
New Cmdlet is Useful
I’m always amazed by the number of organizations that want to report memberships of groups and teams. The Get-AssociatedTeam cmdlet certainly makes it easier to create reports for Teams. As always, the caveat for these kinds of reports is that the scripts to generate the report can take a long time to run as the number of users and teams increases into the thousands. At that point, it’s time to investigate the use of Azure Automation to create reports as scheduled jobs. Finally, if you need to generate PDF reports from PowerShell, check out the PSWriteHTML module.
Keep up to date with developments like new PowerShell cmdlets by subscribing to the Office 365 for IT Pros eBook. Our monthly updates make sure that our subscribers understand the most important changes happening across Office 365.
7 Replies to “Using the Get-AssociatedTeam Cmdlet to Report Team Memberships”
Hello!
THANKS for this great script. However, this one does not run stable for me. Out of 5 tries it runs through, all the other attempts end with some errors.
I am pretty sure that the reason is as following:
>> Get-AssociatedTeam -User xxxx is totally unreliable for me!!
Only once in around 8 attempts the response does include the TenantID! In all the other queries, only GroupID and DisplayName is returned and TenantID stays empty/null.
Obviously this does not help for the rest of the script… Do you see similar strangeness??
I haven’t seen any problems. Is the problem with the Get-AssociatedTeam cmdlet or with the resolution of the tenant identifier?
Is the problem showing up here? You say that the tenantid doesn’t show up, but this value is returned by Get-AssociatedTeam so I can’t see how it would be missing.
ForEach ($Team in $TeamInfo) {
If ($Team.TenantId -eq $TenantId) { # Resolve the tenant identifier to a name
$Name = $TenantName }
Else {
$LookUpId = $Team.TenantId.toString()
$Uri = “https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId=’$LookUpId’)”
$ExternalTenantData = Invoke-MgGraphRequest -Uri $Uri -Method Get
$Name = $ExternalTenantData.DisplayName
}
Yes, the problem is with the cmdlet itself, not with your script.
Means, I doubt your statement of “but this value is returned by Get-AssociatedTeam” as it’s not (for me). 😉
I am firing this manually so no scripting involved whatsoever. Sometimes I get a TenantID back, but mostly not. Maybe 1 success in about 8 attempts.
I have no idea what causes this….But see for yourself:
Using the account identifier is OK for the script (I just tested it) and I am curious to know if it fixes this problem.
Loading...
Thanks for the tip, but pretty much SAME result. It feels like the success rate went up from 1 out of 8 into 1 out of 5, but this is no fundamental change, right?
PS C:\Users\haral> Get-AssociatedTeam -user 21ce8fef-xxxx-xxxx-xxxx-8759a25fb40e | fl
GroupId : 13db627c-xxxx-xxxx-xxxx-9c2b44936869
DisplayName : HST Consulting Team1
TenantId :
Loading...
According to Microsoft, it’s a bug that’s caused by some components in the service not being completely updated. They are working on a fix. It should be resolved soon.
{"id":null,"mode":"button","open_style":"in_modal","currency_code":"EUR","currency_symbol":"\u20ac","currency_type":"decimal","blank_flag_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":100,"top_media_type":"featured_image","featured_image_url":"https:\/\/office365itpros.com\/wp-content\/uploads\/2022\/11\/cover-141x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Virtual Tip Jar","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"Office 365 for IT Pros","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"Office 365 for IT Pros","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for supporting the work of Office 365 for IT Pros!","payment_confirmation_title":"Office 365 for IT Pros","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
Hello!
THANKS for this great script. However, this one does not run stable for me. Out of 5 tries it runs through, all the other attempts end with some errors.
I am pretty sure that the reason is as following:
>> Get-AssociatedTeam -User xxxx is totally unreliable for me!!
Only once in around 8 attempts the response does include the TenantID! In all the other queries, only GroupID and DisplayName is returned and TenantID stays empty/null.
Obviously this does not help for the rest of the script… Do you see similar strangeness??
I haven’t seen any problems. Is the problem with the Get-AssociatedTeam cmdlet or with the resolution of the tenant identifier?
Is the problem showing up here? You say that the tenantid doesn’t show up, but this value is returned by Get-AssociatedTeam so I can’t see how it would be missing.
ForEach ($Team in $TeamInfo) {
If ($Team.TenantId -eq $TenantId) { # Resolve the tenant identifier to a name
$Name = $TenantName }
Else {
$LookUpId = $Team.TenantId.toString()
$Uri = “https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId=’$LookUpId’)”
$ExternalTenantData = Invoke-MgGraphRequest -Uri $Uri -Method Get
$Name = $ExternalTenantData.DisplayName
}
Hello Tony, thanks for your reply.
Yes, the problem is with the cmdlet itself, not with your script.
Means, I doubt your statement of “but this value is returned by Get-AssociatedTeam” as it’s not (for me). 😉
I am firing this manually so no scripting involved whatsoever. Sometimes I get a TenantID back, but mostly not. Maybe 1 success in about 8 attempts.
I have no idea what causes this….But see for yourself:
PS C:\Users\haral> Get-AssociatedTeam -User office@xxxxx.onmicrosoft.com|fl
GroupId : 13db627c-0f46-xxxx-xxxx-9c2b44936869
DisplayName : HST Consulting Team1
TenantId :
PS C:\Users\haral> Get-AssociatedTeam -User office@xxxxx.onmicrosoft.com|fl
GroupId : 13db627c-0f46-xxxx-xxxx-9c2b44936869
DisplayName : HST Consulting Team1
TenantId : 3544a441-xxxx-xxxx-aa3a-8b5a22bbf6a6
BTW: I am on MicrosoftTeams Powershell Module releaase 4.6.0 if this is of any relevance.
==> I would say there is something buggy on their side.
I know that there are some fixes being worked on for 4.6.0 (not for this cmdlet though). I’ll ask Microsoft.
As a test, change the user parameter to be the Azure AD user account identifier instead of the User Principal Name and see what happens.
[array]$TeamInfo = Get-AssociatedTeam -User $User.Id
Using the account identifier is OK for the script (I just tested it) and I am curious to know if it fixes this problem.
Thanks for the tip, but pretty much SAME result. It feels like the success rate went up from 1 out of 8 into 1 out of 5, but this is no fundamental change, right?
PS C:\Users\haral> Get-AssociatedTeam -user 21ce8fef-xxxx-xxxx-xxxx-8759a25fb40e | fl
GroupId : 13db627c-xxxx-xxxx-xxxx-9c2b44936869
DisplayName : HST Consulting Team1
TenantId :
According to Microsoft, it’s a bug that’s caused by some components in the service not being completely updated. They are working on a fix. It should be resolved soon.