I’m often asked why the Microsoft 365 admin consoles omit the print option. For instance, go to the Active users section of the Microsoft 365 admin center and look for a way to print a list of users. There isn’t one. What usually exists is an option to export data (users in this case) to a CSV file.
I think several reasons exist why Microsoft takes this approach with admin consoles. First, the consoles page information for display. As you move to the bottom of the list, more data appears until the complete set is present (I’ve never tried this with a tenant with 50,000 users, but that’s the theory). This implementation is consistent with the way the Graph APIs fetch data. In larger tenants, paging works better than if you were forced to wait for all data to be available. Second, the engineering effort to implement and support print options across all the admin consoles might be a cost Microsoft wants to avoid. Third, the export option allows tenants to download the information and format it according to their own requirements (all organizations have their own formats). Last, programmatic access to the data is often available through PowerShell or Graph API. Overall, it’s hard to complain too much about the lack of printing support in Microsoft 365 admin consoles.
Teams Policies
Which brings me neatly to Teams and a request to generate a report of the policies assigned to user accounts. As you know, Teams is extraordinary fond of policies. A recent check revealed 40 separate Teams policies which can be assigned to an account (sixteen policies are available for editing through the Teams admin center). Unless they’re involved with the Teams Phone system (which consumes many policies), the average Teams administrator might interact with the following set:
- Meeting Policy: Controls capabilities available in Teams meetings.
- Messaging Policy: Controls capabilities in Teams chat and channel messages.
- App Setup Policy: Controls the apps pinned to the app navigation bar and the apps users can install.
- App Permission Policy: Controls the set of apps available to Teams users.
- Enhanced Encryption Policy: Controls the availability of Teams end to end encryption in 1:1 calls.
- Update Management Policy: Controls if users can access preview features.
- Channels Policy: Controls if users can create new private and shared channels.
- Feedback Policy: Controls if users are prompted to send feedback surveys to Microsoft.
- Live Events Policy: Controls how the user can create live events.
With this set of policies in mind, we can write some PowerShell to generate a report of Teams policy assignments.
Coding the Report
The report script is very straightforward.
- Connect to the Microsoft Teams PowerShell module to fetch information about the policies assigned to users.
- Connect to the Exchange Online management PowerShell module. This is an optional connection that I use to fetch the tenant name for the report using the Get-OrganizationConfig cmdlet. You could also use the Get-AzureADTenantDetail cmdlet from the Azure AD module.
- For each user, extract the policy assignments and update a PowerShell list object. It’s easy to add or substract policy assignments to customize the output. If the default policy is used, we output “Tenant Default” (you can chose a different name if you like), otherwise the script inserts the name of the assigned policy.
- When all users are processed, use the list data and some HTML code to create a HTML file.
- Create a CSV file using the report data to make it easy to analyze the assignments.
- Finish up by reporting success and the names of the created files.
Figure 1 shows an example of the report. As you can see, the report lists the assignment for each of the nine targeted polices for each user.
You can download the script from GitHub. Feel free to amend the code to suit the requirements of your tenant. The basics will remain the same, but you might want to add some extra policies or spruce up the formatting of the report.
The Power of the Shell
The script didn’t take long to write (admittingly, I had the HTML bits to hand). It’s yet another proof of how useful PowerShell is to Microsoft 365 tenant administrators in terms of filling the gaps left by Microsoft. Or, put another way, going where Microsoft choses not to go. Enjoy!
Hi Tony,
I would like to only extract list of users who has been assigned to the “Global’ app permission policy
Get-CsOnlineUser -Filter {TeamsAppPermissionPolicy -eq ”} | Export-CSV
Not sure how to mentioned to extract “Global”, I have tried with $null, blank and global.. no success
Any help plz
Try: Get-CsOnlineuser | where-Object {$Null -eq $_.TeamsAppPermissionPolicy} | Format-Table DisplayName
Thanks Tony for the prompt response, I only have 3 user fields in the csv that I’m using SIP, UPN and Email, so I still need to get their policy values with get-csonline user. If I run as is I cant retrieve the users policies
[array]$Users = import-csv ./masterlist_summary1.csv
$Report = [System.Collections.Generic.List[Object]]::new()
# Process each user to fetch their policy assignments
ForEach ($user in $Users) {
$TenantDefaultString = “Tenant Default”
$TenantDialPlan = $TenantDefaultString
Of course you’ll need to retrieve the policy information for each user with Get-CsOnlineUser. I meant that you wouldn’t use the cmdlet to fetch the set of users for processing.
Because your CSV file only contains three properties with different names to those returned by Get-CsOnlineUser, the command to fetch details for each user will be something like Get-CsOnlineUser -Identity $User.UPN instead of what’s in the script now.
Thanks for the reply. Unfortunately I’m not very good at powershell but I can understand how your script is creating the array from up to 5000 users in the tenant.
What I’m confused by is your script has 1 array that is used to get-csonlineuser, retrieves the results for up to 5000 users and then can optionally filter on the array. If I change that $users array to be “[array]$Users = Import-CSV ./Users.csv” then it removes the get-csonlineuser. My question is how do I create an array with my csv AND run get-csonlineuser at the same line of code? I’m not sure how that looks. Really appreciate your help.
There are two lines in the script that fetch users and prepare them for processing:
[array]$Users = Get-CsOnlineUser -ResultSize 5000
# Filter the set to get Teams users – this will filter out all but cloud-only Teams users. If you don’t want to use the filter, comment it out.
$Users = $Users | Where-Object {$_.InterpretedUserType -eq “PureOnlineTeamsOnlyUser” -or $_.InterpretedUserType -eq “PureOnlineTeamsOnlyUserFailedPublishingToAAD”} | Sort-Object DisplayName
If (!($Users)) {Write-Host “No users found – exiting”; break }
Replace these lines with:
[array]$Users = Import-CSV filename…
You don’t need to run Get-CsOnlineUser if you’re providing user details another way.
Sorry meant to say how can I run the script against users in A CSV. We are migrating users in batches so I only want to report on that rather than running the script against the whole organisation and doing matches in Excel against the migration group.
The script uses these lines of code to find the set of users to process:
[array]$Users = Get-CsOnlineUser -ResultSize 5000
# Filter the set to get Teams users – this will filter out all but cloud-only Teams users. If you don’t want to use the filter, comment it out.
$Users = $Users | Where-Object {$_.InterpretedUserType -eq “PureOnlineTeamsOnlyUser” -or $_.InterpretedUserType -eq “PureOnlineTeamsOnlyUserFailedPublishingToAAD”} | Sort-Object DisplayName
Instead, to build the set of users to process from a CSV, you’d use a command like:
[array]$Users = Import-CSV Users.csv
Great script. Can I ask how can I run the script only against users in the CSV (a group of users not the whole organisation)