Although Planner supports a Graph API, the API focuses on management of plans, tasks, buckets, categories, and other objects used in the application rather than plan settings like notifications or backgrounds. It’s good at reporting plans and tasks or populating tasks in a plan, but the API also doesn’t include any support for tenant-wide application settings. In most cases, these gaps don’t matter. The Planner UI has the necessary elements to deal with notification and background settings, neither of which are likely changed all that often. But tenant-wide settings are a dirty secret of Planner. Let me explain why.
In 2018, Microsoft produced the Planner Tenant Admin PowerShell module. With such a name, you’d expect this module to manage important settings for Planner. That is, until you read the instructions about how to use the module, which document the odd method chosen by the Planner development group distribute and install the software.
Even the Microsoft Commerce team, who probably have the reputation for the worst PowerShell module in Microsoft 365, manage to publish their module through the PowerShell Gallery. But Planner forces tenant administrators to download a ZIP file, “unblock” two files, and manually load the module. The experience is enough to turn off many administrators from interacting with Planner PowerShell.
But buried in this unusual module is the ability to block users from being able to delete tasks created by other people. Remember that most plans are associated with Microsoft 365 Groups. The membership model for groups allows members to have the same level of access to group resources, including tasks in a plan. Anyone can delete tasks in a plan, and that’s not good when Planner doesn’t support a recycle bin or another recovery mechanism.
The Set-PlannerUserPolicy cmdlet from the Planner Tenant Admin PowerShell module allows tenant administrators to block users from deleting tasks created by other people. It’s the type of function that you’d imagine should be in plan settings where a block might apply to plan members. Or it might be a setting associated with a sensitivity label that applied to all plans in groups assigned the label. Alternatively, a setting in the Microsoft 365 admin center could impose a tenant-wide block.
In any case, none of those implementations are available. Instead, tenant administrators must run the Set-PlannerUserPolicy cmdlet to block individual users with a command like:
Set-PlannerUserPolicy -UserAadIdOrPrincipalName Kim.Akers@office365itpros.com -BlockDeleteTasksNotCreatedBySelf $True
The point of this story is that assigning the policy to a user account also blocks the ability of the account to delete plans, even if the account is a group owner. This important fact is not mentioned in any Microsoft documentation.
I discovered the problem when investigating how to delete a plan using PowerShell. It seemed a simple process. The Remove-MgPlannerPlan cmdlet from the Microsoft Graph PowerShell SDK requires the planner identifier and its “etag” to delete a plan. This example deletes the second plan in a set returned by the Get-MgPlannerPlan cmdlet:
[array]$Plans = Get-MgPlannerPlan -GroupId $GroupId $Plan = $Plans[1] $Tag = $Plan.additionalProperties.'@odata.etag' Remove-MgPlannerPlan -PlannerPlanId $Plan.Id -IfMatch $Tag
The same problem occurred when running the equivalent Graph API request:
$Headers = @{} $Headers.Add("If-Match", $plan.additionalproperties['@odata.etag']) $Uri = ("https://graph.microsoft.com/v1.0/planner/plans/{0}" -f $Plan.Id) Invoke-MgGraphRequest -uri $Uri -Method Delete -Headers $Headers
In both cases, the error was 403 forbidden with explanatory text like:
{"error":{"code":"","message":"You do not have the required permissions to access this item, or the item may not exist.","innerError":{"date":"2024-06-13T17:10:10","request-id":"d5bf922c-ea9b-48c6-9629-d9749ab7ec51","client-request-id":"6a533cf8-4396-4743-acf1-a40c32dd11bc"}}}
Even more bafflingly, the Planner browser client refused to let me delete a plan too. At least, the client accepted the request but then failed with a very odd error (Figure 1). After dismissing the error, my access to the undeleted plan continued without an issue.
Fortunately, I have some contacts inside Microsoft that were able to check why my attempts to delete plans failed and report back that the deletion policy set on my account blocked the removal of both tasks created by other users and plans. The first block was expected, the second was not. I’m glad that the mystery is solved but underimpressed that Microsoft does not document this behavior. They might now…
The moral of the story is not to run PowerShell cmdlets unless you know what their effect would be. I wish someone told me that a long time ago.
]]>The Microsoft 365 ecosystem is so large that it’s hard to keep track of everything that changes that show up in different workloads. We’ve always known about the difficulties of tracking new features, deprecations, and other issues, but sometimes it takes a user to report something to focus on a specific problem.
An example is when a reader noted that the Graph-based script to report the storage quota used by SharePoint sites no longer included site URLs in the output (Figure 1). The original script (from 2020) used a registered Entra ID app to authenticate and use the Graph getSharePointSiteUsageDetail API to fetch site detail data.
When I investigated the problem, I decided to update the script code to use the Microsoft Graph PowerShell SDK instead. The update did nothing to retrieve the missing data. This isn’t surprising because the problem lies in the Graph API rather than the way the API is called.
The Microsoft 365 admin center uses the same Graph API for its SharePoint site usage report and the same problem of no site URL data is seen there (Figure 2).
Even worse, the SharePoint site activity report in the Microsoft 365 admin center displays no data (Figure 3).
This problem is because the getSharePointActivityUserDetail API returns no data whatsoever. Here’s an example of using the API in PowerShell in an attempt to retrieve SharePoint Online user activity for the last 180 days. The retrieved data should end up in the SPOUserDetail.CSV file.
$Uri = "https://graph.microsoft.com/v1.0/reports/getSharePointActivityUserDetail(period='D180')" Invoke-MgGraphRequest -Uri $Uri -Method GET -OutputFilePath SPOUserDatail.CSV
However, the output file is perfectly empty apart from the column headers (Figure 4).
The same approach works perfectly with other usage data. For instance, this query works nicely to fetch Exchange Online usage data:
$Uri = "https://graph.microsoft.com/v1.0/reports/getEmailActivityUserDetail(period='D180')" Invoke-MgGraphRequest -Uri $Uri -Method GET -OutputFilePath $EmailUsage.CSV
It’s not surprising that an API should have a problem. The APIs haven’t changed recently, so the root cause is more likely due to a change in the SharePoint Online back end. This feeling is reinforced by service health report SP676147 filed on 21 September 2023 (last updated 9 February 2024) that blithely says that “SharePoint and OneDrive URLs may not be displayed in some usage reports.”
Microsoft goes on to note that:
“We’re continuing our work through the validation of multiple potential mitigation strategies to display the URLs of the affected site usage reports. Due to the complexity of the scenarios involved we anticipate this may take additional time.”
The next update for the service health announcement is due on 1 March 2024. What I’m struggling with is that the usage reports included site URLs without any difficulty for years. Why it should suddenly become an issue is inexplicable. And taking over six months to find a solution is even more so.
Microsoft suggests that developers use the Graph Sites API to retrieve the site URL. For example:
$Uri = ("https://graph.microsoft.com/v1.0/sites/{0}" -f $Site.'Site Id') $SiteData = Invoke-MgGraphRequest -Uri $Uri -Method GET
This works, but only when using an application permission. Using delegated permissions restricts access to sites that the signed-in user is a member of.
Fortunately, it’s possible to get the site storage quota information using the SharePoint Online management PowerShell module. The Graph APIs read from a usage data warehouse that’s populated using background processes. The data is always at least two days old, but it’s much faster to access than using PowerShell to check the storage for each site. But needs must, and at least the old method still works.
I admit forgetting about the service health announcement, perhaps because it’s been ongoing for so long. I’m genuinely surprised that Microsoft is still working on something that seems so innocuous. And I’m even more surprised that customers aren’t making more of a fuss because the URL is the fundamental way to identify a SharePoint site.
Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.
]]>Back in 2020, I took the first opportunity to apply corporate branding to a Microsoft 365 tenant and added custom images to the Entra ID web sign-in process. Things have moved on and company branding has its own section in the Entra ID admin center with accompanying documentation. Figure 1 shows some custom branding elements (background screen, banner logo, and sign-in page text) in action.
Entra ID displays the custom elements after the initial generic sign-in screen when a user enters their user principal name (UPN). The UPN allows Entra ID to identify which tenant the account comes from and if any custom branding should be displayed.
Company branding is available to any tenant with Entra ID P1 or P2 licenses. The documentation mentions that Office 365 licenses are needed to customize branding for the Office apps. This mention is very non-specific. I assume it means Office 365 E3 and above enterprise tenants can customize branding to appear in the web Office apps. Certainly, no branding I have attempted has ever affected the desktop Office apps.
Every year, I like to refresh the custom branding elements, if only to update the sign-in text to display the correct year. It’s certainly easy to make the changes through the Entra ID admin center (Figure 2), but I like to do it with PowerShell because I can schedule an Azure Automation job to run at midnight on January 1 and have the site customized for the year.
The Graph APIs include the organizational branding resource type to hold details of a tenant’s branding (either default or custom). Updating the properties of the organizational branding resource type requires the Organization.Rewrite.All permission. Properties are divided into string types (like the sign-in text) and stream types (like the background image).
The script/runbook executes the following steps:
Connect-MgGraph -Scopes Organization.ReadWrite.All -NoWelcome # If running in Azure Automation, use Connect-MgGraph -Scopes Organization.ReadWrite.All -NoWelcome -Identity $TenantId = (Get-MgOrganization).Id # Get current sign-in text [string]$SignInText = (Get-MgOrganizationBranding -OrganizationId $TenantId -ErrorAction SilentlyContinue).SignInPageText If ($SignInText.Length -eq 0) { Write-Host "No branding information found - exiting" ; break } [string]$CurrentYear = Get-Date -format yyyy $DefaultYearImage = "c:\temp\DefaultYearImage.jpg" $YearPresent = $SignInText.IndexOf($CurrentYear) If ($YearPresent -gt 0) { Write-Output ("Year found in sign in text is {0}. No update necessary" -f $CurrentYear) } Else { Write-Output ("Updating copyright date for tenant to {0}" -f $CurrentYear ) $YearPosition = $SignInText.IndexOf('202') $NewSIT = $SignInText.SubString(0, ($YearPosition)) + $CurrentYear # Create hash table for updated parameters $BrandingParams = @{} $BrandingParams.Add("signInPageText",$NewSIT) Update-MgOrganizationBranding -OrganizationId $TenantId -BodyParameter $BrandingParams If (Test-Path $DefaultYearImage) { Write-Output "Updating background image..." $Uri = ("https://graph.microsoft.com/v1.0/organization/{0}/branding/localizations/0/backgroundImage" -f $TenantId) Invoke-MgGraphRequest -Method PUT -Uri $Uri -InputFilePath $DefaultYearImage -ContentType "image/jpg" } Else { Write-Output "No new background image available to update" } }
The script is available in GitHub.
Figure 2 shows the updated sign-in screen (I deliberately updated the year to 2025).
If you run the code in Azure Automation, the account must have the Microsoft.Graph.Authentication and Microsoft.Graph.Identity.DirectoryManagement modules loaded as resources in the automation account to use the cmdlets in the script.
The documentation describes a bunch of other settings that can be tweaked to apply full custom branding to a tenant. Generally, I prefer to keep customization light to reduce ongoing maintenance, but I know that many organizations are strongly attached to corporate logos, colors, and so on.
Applying customizations to the Entra ID sign-in screens is not complicated. Assuming you have some appropriate images to use, updating takes just a few minutes with the Entra ID admin center. I only resorted to PowerShell to process the annual update, but you could adopt it to have different sign-in screens for various holidays, company celebrations, and so on.
Learn about using Entra ID and the rest of the Microsoft 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. Use our experience to understand what’s important and how best to protect your tenant.
]]>The recent Midnight Blizzard attack on Microsoft corporate email accounts emphasized the world of threat that exists today. If attackers can compromise the world’s biggest software company, repeat with thousands of skilled security professionals, what hope have the rest of us? Even if attackers might consider your tenant to be unworthy of their attention, the answer lies in paying attention to what happens in the tenant.
In the past, I’ve written about checking consent grants to apps for high-priority permissions. The script used in that article posts the results to a team channel in the hope that administrators read and respond to anything untoward. Recent events make it useful to discuss how to retrieve the set of permissions held by apps.
To find consents for high-priority permissions (like Mail.Send or Exchange.ManageAsApp), the script builds a set of hash tables to hold the role identifiers and their display names. For instance, this code builds a hash table of all Microsoft Graph roles (also called permissions or scopes):
$GraphApp = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'" $GraphRoles = @{} ForEach ($Role in $GraphApp.AppRoles) { $GraphRoles.Add([string]$Role.Id, [string]$Role.Value) } $GraphRoles Name Value ---- ----- b0c13be0-8e20-4bc5-8c55-963c2… TeamsAppInstallation.ReadWriteAndConsentForTeam.All 926a6798-b100-4a20-a22f-a4918… ThreatSubmissionPolicy.ReadWrite.All c2667967-7050-4e7e-b059-4cbbb… CustomAuthenticationExtension.ReadWrite.All (etc.)
The script uses separate hash tables for Graph API permissions, Teams permissions. Entra ID permissions, Exchange Online permissions, and so on.
Having hash tables containing role identifiers and names makes it very easy to resolve the roles assigned to registered apps or the service principals used for enterprise apps. As you’ll recall, an enterprise app is an app created by Microsoft or an ISV that is preconfigured to authenticate against Entra ID and is capable of being used in any tenant. When an enterprise app is installed in a tenant, its service principal becomes the tenant-specific instantiation of the app and holds the permissions assigned to the app.
To find the permissions assigned to an app, we find the service principal identifier for the app and use it to fetch the set of role assignments. Each role assignment allows the use of a Graph API permission or a administrative role (like Exchange.ManageAsApp) allowing the app to behave as if it was an administrator account holding the role:
$App = Get-MgApplication | Where DisplayName -match 'My registered app' $SP = Get-MgServicePrincipal -All | Where-Object AppId -match $App.Appid [array]$AppRoleAssignments = Get-MgServicePrincipalAppRoleAssignment -All -ServicePrincipalId $SP.id
A role assignment looks like this:
AppRoleId : 5b567255-7703-4780-807c-7be8301ae99b CreatedDateTime : 05/04/2023 16:55:55 DeletedDateTime : Id : HkiavhziPkSBG_2Lh0ibAx1voO6aUiJCm3NQwDYWeu8 PrincipalDisplayName : My registered app PrincipalId : be9a481e-e21c-443e-811b-fd8b87489b03 PrincipalType : ServicePrincipal ResourceDisplayName : Microsoft Graph ResourceId : 14a3c489-ed6c-4005-96d1-be9c5770f7a3 AdditionalProperties : {}
We’re interested in the resource identifier (ResourceId) and AppRoleId properties. Together, these tell us the resource (like the Microsoft Graph or SharePoint Online) and role that the permission relates to. Because an app can hold many permissions, we loop through the array to retrieve each permission:
ForEach ($AppRoleAssignment in $AppRoleAssignments) { $Permission = (Get-MgServicePrincipal -ServicePrincipalId ` $AppRoleAssignment.resourceId).appRoles | ` Where-Object id -match $AppRoleAssignment.AppRoleId | Select-Object -ExpandProperty Value Write-Host ("The app {0} has the permission {1}" -f ` $AppRoleAssignment.PrincipalDisplayName, $Permission) }
The advantage of the hash tables is that the script doesn’t need to keep running the Get-MgServicePrincipal cmdlet to fetch the set of app roles owned by a resource. Thus, we can simply say:
$Permission = $GraphRoles[$AppRoleAssignment.AppRoleId]
As is often the case with PowerShell, other methods exist to resolve the names of app permissions from role identifiers. To explore the possibilities, I suggest you look at Sean Avinue’s article about how to generate a risk report or Vasil Michev’s application service principal inventory script.
One of the great things about PowerShell is the ease of repurposing code written by other people to expand and enhance functionality. In this context, “ease” means that it is easier to reuse code than write it from scratch. Some effort is often necessary to shoehorn the code into your scripts.
Vasil’s inventory script handles both delegate and application permissions. As an example of what I mean about code reuse, I took some of his code and fitted it into my script to generate a report about expiring app credentials (secrets and certificates). Figure 1 shows the output email with the high-priority permissions asterisked. Perhaps having highly-permissioned apps brought to the attention of administrators on a regular basis will force them to review what’s happening with apps.
You can download the updated script from GitHub. Happy coding!
Learn more about how the Microsoft 365 applications and the Graph really work on an ongoing basis by subscribing to the Office 365 for IT Pros eBook. Our monthly updates keep subscribers informed about what’s important across the Office 365 ecosystem.
]]>The first report generated by Exchange administrators as they learn PowerShell is often a list of mailboxes. The second is usually a list of mailboxes and their sizes. A modern version of the code used to generate such a report is shown below.
Get-ExoMailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited | Sort-Object DisplayName | Get-ExoMailboxStatistics | Format-Table DisplayName, ItemCount, TotalItemSize -AutoSize
I call the code “modern” because it used the REST-based cmdlets introduced in 2019. Many examples persist across the internet that use the older Get-Mailbox and Get-MailboxStatistics cmdlets.
Instead of piping the results of Get-ExoMailbox to Get-ExoMailboxStatistics, a variation creates an array of mailboxes and loops through the array to generate statistics for each mailbox.
[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited Write-Host ("Processing {0} mailboxes..." -f $Mbx.count) $OutputReport = [System.Collections.Generic.List[Object]]::new() ForEach ($M in $Mbx) { $MbxStats = Get-ExoMailboxStatistics -Identity $M.ExternalDirectoryObjectId -Properties LastUserActionTime $DaysSinceActivity = (New-TimeSpan $MbxStats.LastUserActionTime).Days $ReportLine = [PSCustomObject]@{ UPN = $M.UserPrincipalName Name = $M.DisplayName Items = $MbxStats.ItemCount Size = $MbxStats.TotalItemSize.Value.toString().Split("(")[0] LastActivity = $MbxStats.LastUserActionTime DaysSinceActivity = $DaysSinceActivity } $OutputReport.Add($ReportLine) } $OutputReport | Format-Table Name, UPN, Items, Size, LastActivity
In both cases, the Get-ExoMailboxStatistics cmdlet fetches information about the number of items in a mailbox, their size, and the last recorded user interaction. There’s nothing wrong with this approach. It works (as it has since 2007) and generates the requested information. The only downside is that it’s slow to run Get-ExoMailboxStatistics for each mailbox. You won’t notice the problem in small tenants where a script only needs to process a couple of hundred mailboxes, but the performance penalty mounts as the number of mailboxes increases.
Microsoft 365 administrators are probably familiar with the Reports section of the Microsoft 365 admin center. A set of usage reports are available to help organizations understand how active their users are in different workloads, including email (Figure 1).
The basis of the usage reports is the Graph Reports API, including the email activity reports and mailbox usage reports through Graph API requests and Microsoft Graph PowerShell SDK cmdlets. Here are examples of fetching email activity and mailbox usage data with the SDK cmdlets. The specified period is 180 days, which is the maximum:
Get-MgReportEmailActivityUserDetail -Period 'D180' -Outfile EmailActivity.CSV [array]$EmailActivityData = Import-CSV EmailActivity.CSV Get-MgReportMailboxUsageDetail -Period 'D180' -Outfile MailboxUsage.CSV [array]$MailboxUsage = Import-CSV MailboxUsage.CSV
I cover how to use Graph API requests in the Microsoft 365 user activity report. This is a script that builds up a composite picture of user activity across different workloads, including Exchange Online, SharePoint Online, OneDrive for Business, and Teams. One difference between the Graph API requests and the SDK cmdlets is that the cmdlets download data to a CSV file that must then be imported into an array before it can be used. The raw API requests can fetch data and populate an array in a single call. It’s just another of the little foibles of the Graph SDK.
The combination of email activity and mailbox usage allows us to replace calls to Get-ExoMailboxStatistics (or Get-MailboxStatistics, if you insist on using the older cmdlet). The basic idea is that the script fetches the usage data (as above) and references the arrays that hold the data to fetch the information about item count, mailbox size, etc.
You can download a full script demonstrating how to use the Graph usage data for mailbox statistics from GitHub.
To preserve user privacy, organizations can choose to obfuscate the data returned by the Graph and replace user-identifiable data with MD5 hashes. We obviously need non-obfuscated user data, so the script checks if the privacy setting is in force. If this is true, the script switches the setting to allow the retrieval of user data for the report.
$ObfuscatedReset = $False If ((Get-MgBetaAdminReportSetting).DisplayConcealedNames -eq $True) { $Parameters = @{ displayConcealedNames = $False } Update-MgBetaAdminReportSetting -BodyParameter $Parameters $ObfuscatedReset = $True }
At the end of the script, the setting is switched back to privacy mode.
My tests (based on the Measure-Command cmdlet) indicate that it’s much faster to retrieve and use the email usage data instead of running Get-ExoMailboxStatistics. At times, it was four times faster to process a set of mailboxes. Your mileage might vary, but I suspect that replacing cmdlets that need to interact with mailboxes with lookups against arrays will always be faster. Unfortunately the technique is not available for Exchange Server because the Graph doesn’t store usage data for on-premises servers.
One downside is that the Graph usage data is always at least two days behind the current time. However, I don’t think that this will make much practical difference because it’s unlikely that there will be much variation in mailbox size over a couple of days.
The point is that old techniques developed to answer questions in the earliest days of PowerShell might not necessarily still be the best way to do something. New sources of information and different ways of accessing and using that data might deliver a better and faster outcome. Always stay curious!
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>A recent discussion revealed that Graph API requests against the Groups endpoint for groups with display names longer than 120 characters generate an error. As you might know, The Groups Graph API supports group display names up to a maximum of 256 characters, so an error occurring after 120 seems bizarre. Then again, having extraordinarily long group names is also bizarre (Figure 1).
Group display names longer than 30 or so characters make it difficult for clients to list groups or teams, which is why Microsoft’s recommendation for Teams clients says that between 30 and 36 characters is a good limit for a team name. Using very long group names also creates formatting and layout issues when generating output like the Teams and Groups activity report, especially if only one or two groups have very long names.
In any case, here’s an example. After creating a group with a very long name, I populated a variable with the group’s display name (146 characters). I then created a URI to request the Groups endpoint to return any Microsoft 365 group that has the display name. Finally, I executed the Invoke-MgGraphRequest cmdlet to issue the request, which promptly failed with a “400 bad request” error:
$Name = "O365Grp-Team with an extraordinary long name that makes it much more than 120 characters so that we can have some fun with it with Graph requests" $Uri = "https://graph.microsoft.com/v1.0/groups?`$filter= displayName eq '${name}'" $Data = Invoke-MgGraphRequest -Uri $Uri -Method Get Invoke-MgGraphRequest: GET Invoke-MgGraphRequest: GET https://graph.microsoft.com/v1.0/groups?$filter=%20displayName%20eq%20'O365Grp-Team%20with%20an%20extraordinary%20long%20name%20that%20makes%20it%20much%20more%20than%20120%20characters%20so%20that%20we%20can%20have%20some%20fun%20with%20it%20%20with%20Graph%20requests' HTTP/1.1 400 Bad Request Cache-Control: no-cache
The Get-MgGroup cmdlet also fails. This isn’t at all surprising because the Graph SDK cmdlets run the underlying Graph API requests, so if those requests fail, the cmdlets can’t apply magic to make everything work again:
Get-MgGroup -Filter "displayName eq '$Name'" Get-MgGroup_List: Unsupported or invalid query filter clause specified for property 'displayName' of resource 'Group'.
The same happens if you try to use the Get-MgTeam cmdlet from the Microsoft Graph PowerShell SDK.
Get-MgTeam -Filter "displayName eq '$Name'" Get-MgTeam_List: Unsupported or invalid query filter clause specified for property 'displayName' of resource 'Group'. Status: 400 (BadRequest) ErrorCode: BadRequest Date: 2023-10-06T04:53:39
But here’s the thing. The Get-MgGroup cmdlet (and the underlying Graph API request) work if you add the ConsistencyLevel header and an output variable to accept the count of returned items. The presence of the header makes the request into an advanced query against Entra ID.
Get-MgGroup -ConsistencyLevel Eventual -Filter "displayName eq '$Name'" -CountVariable X | Format-Table DisplayName DisplayName ----------- O365Grp-Team with an extraordinary long name that makes it much more than 120 characters so that we can have some fun …
Oddly, the Get-MgTeam cmdlet doesn’t support the ConsistencyLevel header so this workaround isn’t possible using this cmdlet. Given that Teams (the app) finds its teams through Graph requests, this inconsistency is maddening, and it’s probably due to a flaw in the metadata read by the ‘AutoREST’ process Microsoft runs regularly to generate the SDK cmdlets and build new versions of the SDK modules.
None of the Teams clients that I’ve tested have any problem displaying team names longer than 120 characters, so I suspect that the clients do the necessary magic when fetching lists of teams.
The developers of the Entra ID admin center must know about the 120 character limit (and not about the workaround) because they restrict group names (Figure 2).
A StackOverflow thread from 2017 reported that attempts to use the Graph to create new groups with display names longer than 120 characters resulted in errors. However, it’s possible to now use cmdlets like New-MgGroup to create groups with much longer names.
Given that the Groups Graph API allows for 256 characters, it’s yet another oddity that the Entra ID admin center focuses on a lower limit – unless the developers chose to emphasize to administrators that it’s a really bad idea to use overly long group names.
I shall have to add this issue to my list of Microsoft Graph PowerShell SDK foibles (aka, things developers should know before they try coding PowerShell scripts using the SDK). The fortunate thing is that you’re unlikely to meet this problem in real life. At least, I hope that you are. And if you do, you’ll know what to do now.
Make sure that you’re not surprised about changes that appear inside Microsoft 365 applications or the Graph by subscribing to the Office 365 for IT Pros eBook. Our monthly updates make sure that our subscribers stay informed.
]]>Now that we’re in June 2023, the need to migrate PowerShell scripts from using the old and soon-to-be-deprecated Azure AD, AzureADPreview, and Microsoft Online Services (MSOL) modules becomes intense. Microsoft is already throttling cmdlets like New-MsOlUser that perform license assignments. These cmdlets will stop working after June 30, 2023. The other cmdlets in the affected modules will continue to work but lose support after that date. Basing operational automation on unsupported modules isn’t a great strategy, which is why it’s time to replace the cmdlets with cmdlets from the Microsoft Graph PowerShell SDK or Graph API requests.
Graph permissions are an element that people often struggle with during the conversion. After you get to know how the Graph works and how Microsoft documentation is laid out, figuring out what permissions a script needs to run is straightforward.
Understanding the difference between delegated and application permissions is a further complication that can lead developers to make incorrect assumptions. Essentially, if a script uses delegated permissions, it can only access data available to the signed-in user. Application permissions are more powerful because they allow access to data across the tenant. For example, the Planner Graph API was limited to delegated permissions for about four years. Microsoft recently upgraded the API to introduce application permission support, which now means that developers can do things like report the details about every plan in an organization.
PowerShell scripts that need to process data drawn from all mailboxes, all sites, all teams, or other sets of Microsoft 365 objects should use application permissions. RBAC for applications is available to limit script access to mailboxes, but it doesn’t extend past mailboxes.
All of which brings me to the topic of how to define Graph permissions (scopes) in scripts that use the Microsoft Graph PowerShell SDK. Two choices exist:
I do not recommend the second option. It is preferable to be precise about the permissions needed for a script and to state those permissions when connecting to the Graph.
My script to report the user accounts accessing Teams shared channels in other tenants depends on the CrossTenantUserProfileSharing.Read.All permission. Thus, the script connects with this command:
Connect-MgGraph -Scopes CrossTenantUserProfileSharing.Read.All
If multiple permissions are needed, pass them in a comma-separated list.
If the service principal used by the Graph SDK doesn’t already hold the permission, the SDK prompts the user to grant access. They can grant user access or consent on behalf of the organization (which is needed to get to other users’ data).
The alternative is to check the required permissions against the set of permissions already possessed by the service principal for the Graph SDK. For example:
Connect-MgGraph [array]$CurrentPermissions = (Get-MgContext).Scopes [array]$RequiredPermissions = "CrossTenantUserProfileSharing.Read.All" ForEach ($Permission in $RequiredPermissions) { If ($Permission -notin $CurrentPermissions) { Write-Host ("This script needs the {0} permission to run. Please have an administrator consent to the permission and try again" -f $Permission) Break } }
After connecting, the first command fetches the set of current permissions. After stating the set of required permissions in an array, we loop through the set of current permissions to check that each of the required permissions are present. It’s a lot of bother and extra code, which is why I think the simplicity of stating required permissions when connecting to the Microsoft Graph PowerShell SDK is the only way to proceed. Either way works – it’s up to you to decide what you prefer.
Good luck with converting those scripts!
Support the work of the Office 365 for IT Pros team by subscribing to the Office 365 for IT Pros eBook. Your support pays for the time we need to track, analyze, and document the changing world of Microsoft 365 and Office 365.
]]>The longer you work with a technology, the more you come to know about its strengths and weaknesses. I’ve been working with the Microsoft Graph PowerShell SDK for about two years now. I like the way that the SDK makes Graph APIs more accessible to people accustomed to developing PowerShell scripts, but I hate some of the SDK’s foibles.
This article describes the Microsoft Graph PowerShell SDK idiosyncrasies that cause me most heartburn. All are things to look out for when converting scripts from the Azure AD and MSOL modules before their deprecation (speaking of which, here’s an interesting tool that might help with this work).
Sometimes you just don’t want to write something into a property and that’s what PowerShell’s $Null variable is for. But the Microsoft Graph PowerShell SDK cmdlets don’t like it when you use $Null. For example, let’s assume you want to create a new Azure AD user account. This code creates a hash table with the properties of the new account and then runs the New-MgUser cmdlet.
$NewUserProperties = @{ GivenName = $FirstName Surname = $LastName DisplayName = $DisplayName JobTitle = $JobTitle Department = $Null MailNickname = $NickName Mail = $PrimarySmtpAddress UserPrincipalName = $UPN Country = $Country PasswordProfile = $NewPasswordProfile AccountEnabled = $true } $NewGuestAccount = New-MgUser @NewUserProperties
New-MgUser fails because of an invalid value for the department property, even though $Null is a valid PowerShell value.
New-MgUser : Invalid value specified for property 'department' of resource 'User'. At line:1 char:2 + $NewGuestAccount = New-MgUser @NewUserProperties + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: ({ body = Micros...oftGraphUser1 }:<>f__AnonymousType64`1) [New-MgUser _CreateExpanded], RestException`1 + FullyQualifiedErrorId : Request_BadRequest,Microsoft.Graph.PowerShell.Cmdlets.NewMgUser_CreateExpanded
One solution is to use a variable that holds a single space. Another is to pass $Null by running the equivalent Graph request using the Invoke-MgGraphRequest cmdlet. Neither are good answers to what should not happen (and we haven’t even mentioned the inability to filter on null values).
The pipeline is a fundamental building block of PowerShell. It allows objects retrieve by a cmdlet to pass to another cmdlet for processing. But despite the usefulness of the pipeline, the SDK cmdlets don’t support it and the pipeline stops stone dead whenever an SDK cmdlet is asked to process incoming objects. For example:
Get-MgUser -Filter "userType eq 'Guest'" -All | Update-MgUser -Department "Guest Accounts" Update-MgUser : The pipeline has been stopped
Why does this happen? The cmdlet that receives objects must be able to distinguish between the different objects before it can work on them. In this instance, Get-MgUser delivers a set of guest accounts, but the Update-MgUser cmdlet does not know how to process each object because it identifies an object is through the UserId parameter whereas the inbound objects offer an identity in the Id property.
The workaround is to store the set of objects in an array and then process the objects with a ForEach loop.
I’ve used DisplayName to refer to the display name of objects since I started to use PowerShell with Exchange Server 2007. I never had a problem with uppercasing the D and N in the property name until the Microsoft Graph PowerShell SDK came along only to find that sometimes SDK cmdlets insist on a specific form of casing for property names. Fail to comply, and you don’t get your data.
What’s irritating is that the restriction is inconsistent. For instance, both these commands work:
Get-MgGroup -Filter "DisplayName eq 'Ultra Fans'" Get-MgGroup -Filter "displayName eq 'Ultra Fans'"
But let’s say that I want to find the group members with the Get-MgGroupMember cmdlet:
[array]$GroupMembers = Get-MgGroupMember -GroupId (Get-MgGroup -Filter "DisplayName eq 'Ultra Fans'" | Select-Object -ExpandProperty Id)
This works, but I end up with a set of identifiers pointing to individual group members. Then I remember from experience gained from building scripts to report group membership that Get-MgGroupMember (like other cmdlets dealing with membership like Get-MgAdministrationUnitMember) returns a property called AdditionalProperties holding extra information about members. So I try:
$GroupMembers.AdditionalProperties.DisplayName
Nope! But if I change the formatting to displayName, I get the member names:
$GroupMembers.AdditionalProperties.displayName Tony Redmond Kim Akers James Ryan Ben James John C. Adams Chris Bishop
Talk about frustrating confusion! It’s not just display names. Reference to any property in AdditionalProperties must use the same casing as used the output, like userPrincipalName and assignedLicenses.
Another example is when looking for sign-in logs. This command works because the format of the user principal name is the same way as stored in the sign-in log data:
[array]$Logs = Get-MgAuditLogSignIn -Filter "UserPrincipalName eq 'james.ryan@office365itpros.com'" -All
Uppercasing part of the user principal name causes the command to return zero hits:
[array]$Logs = Get-MgAuditLogSignIn -Filter "UserPrincipalName eq 'James.Ryan@office365itpros.com'" -All
Two SDK foibles are on show here. First, the way that cmdlets return sets of identifiers and stuff information into AdditionalProperties (something often overlooked by developers who don’t expect this to be the case). Second, the inconsistent insistence by cmdlets on exact matching for property casing.
I’m told that this is all due to the way Graph APIs work. My response is that it’s not beyond the ability of software engineering to hide complexities from end users by ironing out these kinds of issues.
Object identification for Graph requests depends on globally unique identifiers (GUIDs). Everything has a GUID. Both Graph requests and SDK cmdlets use GUIDs to find information. But some SDK cmdlets can pass user principal names instead of GUIDs when looking for user accounts. For instance, this works:
Get-MgUser -UserId Tony.Redmond@office365itpros.com
Unless you want to include the latest sign-in activity date for the account.
Get-MgUser -UserId Tony.Redmond@office365itpros.com -Property signInActivity Get-MgUser : {"@odata.context":"http://reportingservice.activedirectory.windowsazure.com/$metadata#Edm.String","value":"Get By Key only supports UserId and the key has to be a valid Guid"}
The reason is that the sign-in data comes from a different source which requires a GUID to lookup the sign-in activity for the account, so we must pass the object identifier for the account for the command to work:
Get-MgUser -UserId "eff4cd58-1bb8-4899-94de-795f656b4a18" -Property signInActivity
It’s safer to use GUIDs everywhere. Don’t depend on user principal names because a cmdlet might object – and user principal names can change.
V2.0 of the Microsoft Graph PowerShell SDK is now in preview. The good news is that V2.0 delivers some nice advances. The bad news is that it does nothing to cure the weaknesses outlined here. I’ve expressed a strong opinion that Microsoft should fix the fundamental problems in the SDK before doing anything else.
I’m told that the root cause of many of the issues is the AutoRest process Microsoft uses to generate the Microsoft Graph PowerShell SDK cmdlets from Graph API metadata. It looks like we’re stuck between a rock and a hard place. We benefit enormously by having the SDK cmdlets but the process that makes the cmdlets available introduces its own issues. Let’s hope that Microsoft gets to fix (or replace) AutoRest and deliver an SDK that’s better aligned with PowerShell standards before our remaining hair falls out due to the frustration of dealing with unpredictable cmdlet behavior.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>After reading an article about populating extension attributes for registered devices, a reader asked me how easy it would be to create a report about the operating systems used for registered devices. Microsoft puts a lot of effort into encouraging customers to upgrade to Windows 11 and it’s a good idea to know what’s the device inventory. Of course, products like Intune have the ability to report this kind of information, but it’s more fun (and often more flexible) when you can extract the information yourself.
As it turns out, reporting the operating systems used by registered devices is very easy because the Microsoft Graph reports this information in the set of properties retrieved by the Get-MgDevice cmdlet from the Microsoft Graph PowerShell SDK.
The script described below creates a report of all registered devices and sorts the output by the last sign in date. Microsoft calls this property ApproximateLastSignInDateTime. As the name indicates, the property stores the approximate date for the last sign in. Entra ID doesn’t update the property every time someone uses the device to connect. I don’t have a good rule for when property updates occur. It’s enough (and approximate) that the date is somewhat accurate for the purpose of identifying if a device is in use, which is why the script sorts devices by that date.
Any Windows device that hasn’t been used to sign into Entra ID in the last six months is likely not active. This isn’t true for mobile phones because they seem to sign in once and never appear again. The report generated for my tenant still has a record for a Windows Phone which last signed in on 2 December 2015. I think I can conclude that it’s safe to remove this device from my inventory.
In the last script I wrote using the Get-MgDevice cmdlet, I figured out the owner of the device by extracting the user identifier from the PhysicalIds property. While this approach works, it’s complicated. A much better approach is to use the Get-MgDeviceRegisteredOwner cmdlet which returns the user identifier for the user account of the registered owner. With this identifier, we can retrieve any account property that makes sense, such as the display name, user principal name, department, city, and country. You could easily add other properties that make sense to your organization. See this article for more information about using the Get-MgUser cmdlet to interact with user accounts.
The problem that exists in using registered devices to report operating system information is that it’s not accurate. The operating system details noted for a device are accurate at the point of registration but degrade over time. If you want to generate accurate reports, you need to use the Microsoft Graph API for Intune.
With that caveat in mind, here’s the code to report the operating system information for Entra ID registered devices:
Connect-MgGraph -Scope User.Read.All, Directory.Read.All Write-Host "Finding registered devices" [array]$Devices = Get-MgDevice -All -PageSize 999 If (!($Devices)) { Write-Host "No registered devices found - exiting" ; break } Write-Host ("Processing details for {0} devices" -f $Devices.count) $Report = [System.Collections.Generic.List[Object]]::new() $i = 0 ForEach ($Device in $Devices) { $i++ Write-Host ("Reporting device {0} ({1}/{2})" -f $Device.DisplayName, $i, $Devices.count) $DeviceOwner = $Null Try { [array]$OwnerIds = Get-MgDeviceRegisteredOwner -DeviceId $Device.Id $DeviceOwner = Get-MgUser -UserId $OwnerIds[0].Id -Property Id, displayName, Department, OfficeLocation, City, Country, UserPrincipalName} } Catch {} $ReportLine = [PSCustomObject][Ordered]@{ Device = $Device.DisplayName Id = $Device.Id LastSignIn = $Device.ApproximateLastSignInDateTime Owner = $DeviceOwner.DisplayName OwnerUPN = $DeviceOwner.UserPrincipalName Department = $DeviceOwner.Department Office = $DeviceOwner.OfficeLocation City = $DeviceOwner.City Country = $DeviceOwner.Country "Operating System" = $Device.OperatingSystem "O/S Version" = $Device.OperatingSystemVersion Registered = $Device.RegistrationDateTime "Account Enabled" = $Device.AccountEnabled DeviceId = $Device.DeviceId TrustType = $Device.TrustType } $Report.Add($ReportLine) } #End Foreach Device # Sort in order of last signed in date $Report = $Report | Sort-Object {$_.LastSignIn -as [datetime]} -Descending $Report | Out-GridView
Figure 1 is an example of the report as viewed through the Out-GridView cmdlet.
You can download the latest version of the script from GitHub.
I’ve no idea whether this script will help anyone. It’s an incomplete answer to a question. However, even an incomplete answer can be useful in the right circumstances. After all, it’s just PowerShell, so use the code as you like.
Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.
]]>I’ve updated some scripts recently to remove dependencies on the Azure AD and Microsoft Online Services (MSOL) modules, which are due for deprecation on June 30, 2023 (retirement happens at the end of March for the license management cmdlets). In most cases, the natural replacement is cmdlets from the Microsoft Graph PowerShell SDK.
One example is when retrieving the groups a user account belongs to. This is an easy task when dealing with the membership of individual groups using cmdlets like:
Things are a little more complex when answering a question like “find all the groups that Sean Landy belongs to.” Let’s see how we can answer the request.
One method of attacking the problem often found in Exchange scripts is to use the Get-Recipient cmdlet with a filter based on the distinguished name of the mailbox belonging to an account: For example, this code reports a user’s membership of Microsoft 365 groups:
$User = Get-EXOMailbox -Identity Sean.Landy $DN = $User.DistinguishedName $Groups = (Get-Recipient -ResultSize Unlimited -RecipientTypeDetails GroupMailbox -Filter "Members -eq '$DN'" ) Write-Host (“User is a member of {0} groups” -f $Groups.count)
The method works if the distinguished name doesn’t include special characters like apostrophes for users with names like Linda O’Shea. In these cases, extra escaping is required to make PowerShell handle the name correctly. This problem will reduce when Microsoft switches the naming mechanism for Exchange Online objects to be based on the object identifier instead of mailbox display name. However, there’s still many objects out there with distinguished names based on display names.
As I go through scripts, I check if I can remove cmdlets from other modules to make future maintenance easier. Using Get-Recipient means that a script must connect to the Exchange Online management module, so let’s remove that need by using a Graph API request. Here’s what we can do, using the Invoke-MgGraphRequest cmdlet to run the request:
$UserId = $User.ExternalDirectoryObjectId $Uri = ("https://graph.microsoft.com/V1.0/users/{0}/memberOf/microsoft.graph.group?`$filter=groupTypes/any(a:a eq 'unified')&`$top=200&$`orderby=displayName&`$count=true" -f $UserId) [array]$Data = Invoke-MgGraphRequest -Uri $Uri [array]$Groups = $Data.Value Write-Host (“User is a member of {0} groups” -f $Groups.count)
We get the same result (always good) and the Graph request runs about twice as fast as Get-Recipient does.
Because the call is limited to Microsoft 365 groups, I don’t have to worry about transitive membership. If I did, then I’d use the group transitive memberOf API.
The Microsoft Graph PowerShell SDK contains cmdlets based on Graph requests. The equivalent cmdlet is Get-MgUserMemberOf. This returns memberships of all group types known to Entra ID, so it includes distribution lists and security groups. To return the set of Microsoft 365 groups, apply a filter after retrieving the group information from the Graph.
[array]$Groups = Get-MgUserMemberOf -UserId $UserId -All | Where-Object {$_.AdditionalProperties["groupTypes"] -eq "Unified"} Write-Host (“User is a member of {0} groups” -f $Groups.count)
Notice that the filter looks for a specific type of group in a value in the AdditionalProperties property of each group. If you run Get-MgUserMemberOf without any other processing. the cmdlet appears to return a simple list of group identifiers. For example:
$Groups Id DeletedDateTime -- --------------- b62b4985-bcc3-42a6-98b6-8205279a0383 64d314bb-ea0c-46de-9044-ae8a61612a6a 87b6079d-ddd4-496f-bff6-28c8d02e9f8e 82ae842d-61a6-4776-b60d-e131e2d5749c
However, the AdditionalProperties property is also available for each group. This property contains a hash table holding other group properties that can be interrogated. For instance, here’s how to find out whether the group supports private or public access:
$Groups[0].AdditionalProperties['visibility'] Private
When looking up a property in the hash table, remember to use the exact form of the key. For instance, this works to find the display name of a group:
$Groups[0].AdditionalProperties['displayName']
But this doesn’t because the uppercase D creates a value not found in the hash table:
$Groups[0].AdditionalProperties['DisplayName']
People starting with the Microsoft Graph PowerShell SDK are often confused when they see just the group identifiers apparently returned by cmdlets like Get-MgUserMemberOf, Get-MgGroup, and Get-MgGroupMember because they don’t see or grasp the importance of the AdditionalProperties property. It literally contains the additional properties for the group excepting the group identifier.
Here’s another example of using information from AdditionalProperties. The details provided for a group don’t include its owners. To fetch the owner information for a group, run the Get-MgGroupOwner cmdlet like this:
$Group = $Groups[15] [array]$Owners = Get-MgGroupOwner -GroupId $Group.Id | Select-Object -ExpandProperty AdditionalProperties $OwnersOutput = $Owners.displayName -join ", " Write-Host (“The owners of the {0} group are {1}” -f $Group.AdditionalProperties[‘displayName’], $OwnersOutput)
If necessary, use the Get-MgGroupTransitiveMember cmdlet to fetch transitive memberships of groups.
It would be nice if the Microsoft Graph PowerShell SDK didn’t hide so much valuable information in AdditionalProperties and wasn’t quite so picky about the exact format of property names. Apparently, the SDK cmdlets behave in this manner because it’s how Graph API requests work when they return sets of objects. That assertion might well be true, but it would be nice if the SDK applied some extra intelligence in the way it handles data.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>The article explaining how to report old guest accounts and their membership of Microsoft 365 Groups (and teams) in a tenant is very popular and many people use its accompanying script. The idea is to find guest accounts above a certain age (365 days – configurable in the script) and report the groups these guests are members of. Any old guest accounts that aren’t in any groups are candidates for removal.
The script uses an old technique featuring the distinguished name of guest accounts to scan for group memberships using the Get-Recipient cmdlet. The approach works, but the variation of values that can exist in distinguished names due to the inclusion of characters like apostrophes and vertical lines means that some special processing is needed to make sure that lookups work. Achieving consistency in distinguished names might be one of the reasons for Microsoft’s plan to make Exchange Online mailbox identification more effective.
In any case, time moves on and code degrades. I wanted to investigate how to use the Microsoft Graph PowerShell SDK to replace Get-Recipient. The script already uses the SDK to find Azure AD guest accounts with the Get-MgUser cmdlet.
Graph APIs provide the foundation for all SDK cmdlets. Graph APIs provide the foundation for all SDK cmdlets. The first thing to find is an appropriate API to find group membership. I started off with getMemberGroups. The PowerShell example for the API suggests that the Get-MgDirectoryObjectMemberGroup cmdlet is the one to use. For example:
$UserId = (Get-MgUser -UserId Terry.Hegarty@Office365itpros.com).id [array]$Groups = Get-MgDirectoryObjectMemberGroup -DirectoryObjectId $UserId -SecurityEnabledOnly:$False
The cmdlet works and returns a list of group identifiers that can be used to retrieve information about the groups that the user belongs to. For example:
Get-MgGroup -GroupId $Groups[0] | Format-Table DisplayName, Id, GroupTypes DisplayName Id GroupTypes ----------- -- ---------- All Tenant Member User Accounts 05ecf033-b39a-422c-8d30-0605965e29da {DynamicMembership, Unified}
However, because Get-MgDirectoryObjectMemberGroup returns a simple list of group identifiers, the developer must do extra work to call Get-MgGroup for each group to retrieve group properties. Not only is this extra work, calling Get-MgGroup repeatedly becomes very inefficient as the number of guests and their membership in groups increase.
The Azure AD admin center (and the Entra admin center) both list the groups that user accounts (tenant and guests) belong to. Performance is snappy and it seemed unlikely that the code used was making multiple calls to retrieve the properties for each group. Many of the sections in these admin centers use Graph API requests to fetch information, and the Graph X-Ray tool reveals those requests. Looking at the output, it’s interesting to see that the admin center uses the beta Graph endpoint with the groups memberOf API (Figure 1).
We can reuse the call used by the Azure AD center to create the query (containing the object identifier for the user account) and run the query using the SDK Invoke-MgGraphRequest cmdlet. One change made to the command is to include a filter to select only Microsoft 365 groups. If you omit the filter, the Graph returns all the groups a user belongs to, including security groups and distribution lists. The group information is in an array that’s in the Value property returned by the Graph request. For convenience, we put the data into a separate array.
$Uri = ("https://graph.microsoft.com/beta/users/{0}/memberOf/microsoft.graph.group?`$filter=groupTypes/any(a:a eq 'unified')&`$top=200&$`orderby=displayName&`$count=true" -f $Guest.Id) [array]$Data = Invoke-MgGraphRequest -Uri $Uri [array]$GuestGroups = $Data.Value
The equivalent SDK cmdlet is Get-MgUserMemberOf. To return the set of groups an account belongs to, the command is:
[array]$Data = Get-MgUserMemberOf -UserId $Guest.Id -All [array]$GuestGroups = $Data.AdditionalProperties
The format of returned data marks a big difference between the SDK cmdlet and the Graph API request. The cmdlet returns group information in a hash table in the AdditionalProperties array while the Graph API request returns a simple array called Value. To retrieve group properties from the hash table, we must enumerate through its values. For instance, to return the names of the Microsoft 365 groups in the hash table, we do something like this:
[Array]$GroupNames = $Null ForEach ($Item in $GuestGroups.GetEnumerator() ) { If ($Item.groupTypes -eq "unified") { $GroupNames+= $Item.displayName } } $GroupNames= $GroupNames -join ", "
SDK cmdlets can be inconsistent in how they return data. It’s just one of the charms of working with cmdlets that are automatically generated from code. Hopefully, Microsoft will do a better job of ironing out inconsistencies when they release V2.0 of the SDK sometime later in 2023.
A Get-MgUserTransitiveMemberOf cmdlet is also available to return the membership of nested groups. We don’t need to do this because we’re only interested in Microsoft 365 groups, which don’t support nesting. The cmdlet works in much the same way:
[array]$TransitiveData = Get-MgUserTransitiveMemberOf -UserId Kim.Akers@office365itpros.com -All
Because of the extra complexity in accessing group properties, I decided to use a modified version of the Graph API request from the Azure AD admin center. It’s executed using the Invoke-MgGraphRequest cmdlet, so I think the decision is justified.
When revising the script, I made some other improvements, including adding a basic assessment of whether a guest account is stale or very stale. The assessment is intended to highlight if I should consider removing these accounts because they’re obviously not being used. Figure 2 shows the output of the report.
You can download a copy of the script from GitHub.
Reporting obsolete Azure AD guest accounts is nice. Cleaning up old junk from Azure AD is even better. The script generates a PowerShell list with details of all guests over a certain age and the groups they belong to. To generate a list of the very stale guest accounts, filter the list:
[array]$DeleteAccounts = $Report | Where-Object {$_.StaleNess -eq "Very Stale"}
To complete the job and remove the obsolete guest accounts, a simple loop to call Remove-MgUser to process each account:
ForEach ($Account in $DeleteAccounts) { Write-Host ("Removing guest account for {0} with UPN {1}" -f $Account.Name, $Account.UPN) Remove-MgUser -UserId $Account.Id }
Obsolete or stale guest accounts are not harmful, but their presence slows down processing like PowerShell scripts. For that reason, it’s a good idea to clean out unwanted guests periodically.
Learn about mastering the Microsoft Graph PowerShell SDK and the Microsoft 365 PowerShell modules by subscribing to the Office 365 for IT Pros eBook. Use our experience to understand what’s important and how best to protect your tenant.
]]>About fifteen months ago, Microsoft introduced the notion of metered APIs where those who consumed the APIs would pay for the resources they consume. The pay-as-you-go (PAYG) model evolved further in July 2022 when Microsoft started to push ISVs to use the new Teams export API instead of Exchange Web Services (EWS) for their backup products. The Teams export API is a metered API and is likely to the test case to measure customer acceptance of the PAYG model.
So far, I haven’t heard many positive reactions to the development. Some wonder how Microsoft can force ISVs to use an API when they don’t know how high the charges metering will rack up. Others ask how Microsoft can introduce an export API for backup when they don’t have an equivalent import API to allow tenants to restore data to Teams. I don’t understand this either as it seems logical to introduce export and import capabilities at the same time. We live in interesting times!
To be fair to Microsoft, they plan to go down the same PAYG route with the new backup service they plan to introduce in 2023 as part of the Syntex content management suite. Customers will have to use an Azure subscription to pay for backups of SharePoint Online, OneDrive for Business, and Exchange Online (so far, Microsoft is leaving Teams backup to ISVs).
All of which brings me to the December 2 post from the Microsoft Graph development team where Microsoft attempts to describe what they’re doing with different Microsoft 365 APIs. Like many Microsoft texts, too many words disguise the essential facts of the matter.
Essentially, Microsoft plans to operate three Microsoft 365 API tiers:
My reading of the situation is that Microsoft won’t charge for standard APIs because this would interfere with customer access to their data. Microsoft says that standard APIs will remain the default endpoint.
However, Microsoft very much wants to charge for high-capacity APIs used by “business-critical applications with high usage patterns.” The logic here is that these APIs strain the resources available within the service. To ensure that Microsoft can meet customer expectations, they need to deploy more resources to meet the demand and someone’s got to pay for those resources. By using a PAYG model, Microsoft will charge for actual usage of resources.
Microsoft also wants customers to pay for advanced APIs. In effect, this is like an add-on license such as Teams Premium. If you want to use the bells and whistles enabled by an advanced API, you must pay for the privilege. It’s a reasonable stance.
I don’t have a problem with applying a tiered model for APIs, especially if the default tier continues with free access. The first problem here is in communications, where Microsoft has failed to sell their approach to ISVs and tenants. The lack of clarity and obfuscation is staggering for an organization that employs masses of marketing and PR staff.
The second issue is the lack of data about how much PAYG is likely to cost. Few want to write an open-ended check to Microsoft for API usage. Microsoft is developing the model and understands how the APIs work, so it should be able to give indicative pricing for different scenarios. For instance, if I have 100 teams generating 35,000 new channel conversations and 70,000 chats monthly, how much will a backup cost? Or if my tenant generates new and updated documents at the typical rate observed by Microsoft across all tenants of a certain size, how much will a Syntex backup cost?
The last issue is the heavy-handed approach Microsoft has taken with backup ISVs. Being told that you must move from a working, well-sorted, and totally understood API to a new, untested, and metered API is not a recipe for good ISV relationships. Microsoft needs its ISVs to support its API tiered model. It would be so much better if a little less arrogance and a little more humility was obvious in communication. Just because you’re the big dog who owns the API bone doesn’t mean that you need to fight with anyone who wants a lick.
Make sure that you’re not surprised about changes that appear inside Office 365 applications by subscribing to the Office 365 for IT Pros eBook. Our monthly updates make sure that our subscribers stay informed.
]]>Last month, I discussed a new version of the Microsoft 365 user activity report based on the 180-day lookback period now supported by the Graph usage reports API. This provoked some questions about the API that are worth clarifying.
Microsoft 365 administrative interfaces (like the Teams admin center) generate their usage reports from the data retrieved via the API. The API is a single point of contact for any usage report found inside Microsoft 365. This used not to be the case, but it is now. The Teams admin center currently limits reports to the previous 90 days while the Microsoft 365 admin center supports 180 days.
Because the API is used everywhere, the setting to conceal user, group, and site display names available in the Reports section of Org Settings in the Microsoft 365 admin center controls all data retrieved with the API, including any reports that you create. If you want to see display names in usage data, the setting to obfuscate this information must be turned off (Figure 1).
Teams was the last workload to apply concealment to usage data. All workloads that generate usage data now support this feature.
In a script, you can check if concealment is active and disable it temporarily to allow display names to be fetched using the Graph Usage Reports API. For example:
# first, find if the data is obscured $Display = Invoke-MgGraphRequest -Method Get -Uri 'https://graph.microsoft.com/beta/admin/reportSettings' If ($Display['displayConcealedNames'] -eq $True) { # data is obscured, so let's reset it to allow the report to run $ObscureFlag = $True Write-Host "Setting tenant data concealment for reports to False" -foregroundcolor red Invoke-MgGraphRequest -Method PATCH -Uri 'https://graph.microsoft.com/beta/admin/reportSettings' -Body (@{"displayConcealedNames"= $false} | ConvertTo-Json) }
To reverse the process, update the setting to True:
# And reset obscured data if necessary If ($ObscureFlag -eq $True) { Write-Host "Resetting tenant data concealment for reports to True" -foregroundcolor red Invoke-MgGraphRequest -Method PATCH -Uri 'https://graph.microsoft.com/beta/admin/reportSettings' -Body (@{"displayConcealedNames"= $true} | ConvertTo-Json) }
If you look at the information returned by the Graph Usage Reports API for Teams usage, the activity level reported for teams needs some interpretation. For instance, here’s some information I extracted for the team we use to manage the Office 365 for IT Pros eBook. I used a 180-day lookback to extract the data with this script.
Name : Ultimate Guide to Office 365 LastActivity : 2022-09-06 AccessType : Private Id : 33b07753-efc6-47f5-90b5-13bef01e25a6 IsDeleted : False ActiveUsers : 17 ActiveExtUsers : 7 Guests : 8 ActiveChannels : 11 SharedChannels : 1 Posts : 51 Replies : 511 Channelmessages : 674 Reactions : 241 Mentions : 185 UrgentMessages : 0 Meetings : 0
The report data shows that:
The data reported for message volume within a team is where things get interesting. The numbers of reactions and mentions are easy to understand (and you can validate the reactions number through audit records). Things are less clear with posts, replies, and channel messages. The Microsoft 365 admin center avoids confusion by only reporting the count of channel messages (674). The Teams admin center reports posts (51), replies (511), and channel messages.
According to Microsoft documentation, “Channel messages is the number of unique messages that the user posted in a team channel during the specified time period.” I think the reference to “the user” should be “users” as this makes more sense. However, adding posts and replies only gets me to 562, which is 122 less than the channel message count. Reactions could be considered as a form of reply, but 241 reactions is more than the 122 gap, so there’s a mystery as to how Microsoft calculates the number of channel messages.
The counted messages include only those posted by users. They don’t include messages posted by applications or those that come through the inbound webhook connector.
The Groups and Teams activity report script reads the usage data for a team to know if it is active or potentially obsolete. In that instance, a precise measurement of message activity isn’t a real problem because we accept that if a team has some messaging activity it’s not obsolete. However, if you’re interested in tracking the exact number of messages generated per team, it’s best to use the total of posts and replies.
The old rule applies of not accepting data until you understand its meaning. It’s nice that Teams usage data is available for tenants to browse and download. It would be even nicer if the meaning of the data was clearer.
Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.
]]>Despite being the two basic Microsoft 365 workloads, one of the notable gaps in Microsoft Graph API coverage has been administrative interfaces for SharePoint Online and Exchange Online. A small but valuable step in the right direction happened with the appearance of the settings resource type in the TenantAdmin namespace. For now, the coverage for tenant settings is sparse and only deals with some of the settings that administrators can manage using the Set-SPOTenant PowerShell cmdlet, but it’s a start, and you can see how Microsoft might develop the namespace to handle programmatic access to settings that currently can only be managed through an admin portal.
SharePoint Online tenant-wide settings apply to SharePoint Online sites and OneDrive for Business accounts. Like all Graph APIs, apps must have permissions to be able to make requests. The read-only permission is SharePointTenantSettings.Read.All while you’ll need the SharePointTenantSettings.ReadWrite.All permission to update settings.
Three methods are available to use the new API:
The Graph Explorer is acceptable for testing or one-off commands. However, given that the Set-SPOTenant cmdlet is available, it’s unlikely that you’d use the Graph Explorer as your preferred method to update settings.
Creating a dedicated app just to manage SharePoint Online settings is unlikely too unless you use the same app to manage multiple tenants. This points to the most likely use of the TenantAdmin API, which is to allow MSPs to create apps to manage multiple tenants on behalf of customers.
The Microsoft Graph PowerShell SDK could be used to replace the SharePoint Online management module. An organization might want to do this to rationalize the number of PowerShell modules its developers work with and maintain. I can see this happening in the future when Microsoft has developed the TenantAdmin API to match the capabilities available today through the Set-SPOTenant cmdlet. For now, I’d stay with the SharePoint module and keep a close eye on what happens with the API.
As an example of using the new API, let’s update the setting controlling Loop components in Microsoft 365 apps. This seems appropriate given the recent appearance of Loop components in OWA. The setting controlling the availability of Loop components is IsLoopEnabled, which is True by default. Here’s the code to retrieve the current setting:
Connect-MgGraph -Scopes SharePointTenantSettings.ReadWrite.All $Uri = "https://graph.microsoft.com/V1.0/admin/sharepoint/settings" $SPOSettings = Invoke-MgGraphRequest -Uri $Uri -Method Get $SPOSettings['IsLoopEnabled'] True
To change the setting to False (and disable Loop components), we use the same URI and run a Patch request. To make the command slightly more interesting, we’ll also update the SharePoint News feed setting at the same time and set a new default time zone for new sites created in the tenant. The time zone for new sites is an example of a setting that cannot be set using the Set-SPOTenant cmdlet. Currently, the time zone can only be set in the SharePoint admin center, so this is an example of how the Graph API will expose new settings.
First, we create a payload object.
$NewSettings = @{ "isLoopEnabled" = "false" "isSharePointNewsFeedEnabled" = "true" "tenantDefaultTimezone" = "(UTC) Dublin, Edinburgh, Lisbon, London" }
Then, we patch the settings.
Invoke-MgGraphRequest -Uri $Uri -Method Patch -Body $NewSettings
SharePoint responds by listing all the settings available to the API: You can see that the two settings have the values contained in the payload.
Name Value ---- ----- isFileActivityNotificationE... True isCommentingOnSitePagesEnabled True sharingBlockedDomainList {Gmail.com} sharingAllowedDomainList {hotmail.com, live.com, locklan.com.au, Microsoft.com...} siteCreationDefaultManagedPath /sites/ deletedUserPersonalSiteRete... 60 isSiteCreationUIEnabled True isSyncButtonHiddenOnPersona... False isSitePagesCreationEnabled False tenantDefaultTimezone (UTC) Dublin, Edinburgh, Lisbon, London isLoopEnabled False personalSiteDefaultStorageL... 5242880 allowedDomainGuidsForSyncApp {} isSiteCreationEnabled True availableManagedPathsForSit... {/sites/, /teams/, /containers/} isResharingByExternalUsersE... False isSharePointMobileNotificat... True sharingDomainRestrictionMode none sharingCapability externalUserAndGuestSharing isMacSyncAppEnabled True imageTaggingOption basic isUnmanagedSyncAppForTenant... False isSitesStorageLimitAutomatic True isSharePointNewsfeedEnabled False excludedFileExtensionsForSy... {*.exe, *.zip, *.rar, *.pst...} @odata.context https://graph.microsoft.com/beta/$metadata#admin/sharepoint/settings/$entity siteCreationDefaultStorageL... 26214400
I suspect that the new API will not be heavily used for now and won’t until it attains feature comparability with the Set-SPOTenant cmdlet. But that’s not the important thing to take away. This is the start of the development of Graph API support for tenant administrative settings, and that’s certainly something to welcome.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>A comment for my article about recent enhancements for the Microsoft Graph Explorer noted that while it was great to see the Graph Explorer generate PowerShell code snippets for requests it executes, it would be even nicer if the Graph Explorer supported “round tripping.” In this instance, that means users could send PowerShell commands to the Graph Explorer, which would then interpret the commands and generate the appropriate Graph API requests. It sounds like a great idea.
I contacted some of the folks working on the Microsoft Graph to see if this is possible. Although they couldn’t commit on such an implementation appearing in the future, I was told about a nice feature in the Microsoft Graph PowerShell SDK cmdlets. It doesn’t address the round-tripping request, but it’s a good thing to know none the less, especially if you’re grappling to understand how the SDK cmdlets work for tasks like user, group, or license management.
In a nutshell, if you add the –Debug parameter to any Microsoft Graph PowerShell SDK cmdlet, you’ll see exactly what the cmdlet does, including the Graph API request it runs to execute the command. This is a great way to gain insight into how these cmdlets work and also understand how to leverage Graph API requests.
Because many of its cmdlets are built on Graph APIs, the Debug parameter also works in the same manner for cmdlets from the Microsoft Teams PowerShell module. However, the Teams cmdlets do not output details about permissions and the older policy cmdlets that originated from the Skype for Business Online connector do not display Graph API URIs when they run.
Let’s take a basic example and run the Get-MgUser cmdlet to fetch details of all user accounts in a tenant:
Get-MgUser -All -Debug -Filter "userType eq 'Member'"
When the cmdlet starts, it shows the context it will run under, including whether it’s an interactive session and the scopes (permissions) available to the command. You can get the same information by running the Get-MgContext cmdlet, but this is useful up-front knowledge.
In Figure 1 you can see that the service principal used by the Microsoft Graph PowerShell SDK has many permissions. This is the result of permission creep, the tendency of the service principal to accrue permissions over time due to testing different cmdlets. The existence of so many permissions makes it a bad idea to use the Microsoft Graph PowerShell SDK cmdlets interactively unless you know what you’re doing. In production, it’s best to use certificate-based authentication and a registered Azure AD app to limit the permissions available.
The Graph API request is now displayed. We can see that it looks for the top 100 matching items that satisfy the filter. In other words, return the first 100 Azure AD member accounts (Figure 2).
As you can see, running with Debug set, the cmdlet halts frequently to allow you to read what’s happened and understand if the command has any problems. If you want to see the cmdlet run as normal but with the diagnostic information, set the SDebugPreference variable from its default (SilentlyContinue) to Continue.
$DebugPreference="Continue"
To revert to normal operation, set $DebugPreference back to SilentlyContinue.
Pagination is a concept that doesn’t really exist in PowerShell. Some cmdlets have a ResultSize parameter to control the number of items retrieved by a command, and some have an All parameter to tell the command to fetch everything. The Get-MgUser and Get-MgGroup cmdlets are examples of cmdlets that support an -All parameter.
Graph API requests limit the retrieval of data (usually to 100 or 200 items) to avoid issues caused by requests that might mistakenly look for tens of thousands of items. If more items exist, the application must make additional requests to fetch more pages of data until it has fetched all available items. Applications do this by following a nextlink (or skiptoken) link.
In Figure 3, we see a nextlink for the cmdlet to run to retrieve the next page of data. In this instance, I ran the Get-MgUser cmdlet with no filter, so more than 100 accounts are available, and this is what caused the Graph to respond with the first 100 accounts and the nextlink. In debug mode, you can pause after each page to see the results retrieved from the Graph.
Facilities like the Debug parameter and the Graph X-ray tool help people to understand how the Graph APIs work. Knowing how the Graph functions is invaluable. Having an insight into how cmdlets work helps people develop better code and hopefully avoid bugs. At least, that’s the theory. Try out the Debug parameter with some Microsoft Graph PowerShell SDK cmdlets and see what you think.
Learn how to exploit the data available to Microsoft 365 tenant administrators like how to debug Microsoft Graph PowerShell SDK cmdlets through the Office 365 for IT Pros eBook. We love figuring out how things work.
]]>Although I understand the tactical necessity for Microsoft to enable OAuth 2.0 authorization for programs wishing to use the POP3 and IMAP4 protocols to retrieve emails from Exchange Online, I don’t think it is the correct strategy for Exchange Online IMAP4 and POP3 access. Essentially, Microsoft is facilitating the continued use of antique messaging protocols instead of forcing developers to change to the Microsoft Graph APIs. I think that’s wrong, but I know why Microsoft is doing it.
The big basic authentication turnoff for Exchange Online is now just 88 days away. October 1 marks the point when Microsoft begins to disable seven email connectivity protocols for Exchange Online. It will take time for Microsoft to process all tenants, but eventually those wishing to use protocols like POP3, IMAP4, and Exchange ActiveSync will have no choice but to use modern authentication. In other words, a username and password won’t be enough.
Microsoft has been preparing to remove basic authentication from Exchange for over two years. The scale of Exchange Online, the product’s history, and the number of protocols and devices combine to create a myriad of complexities. Automatically upgrading the profiles for Apple’s device Mail app on iOS and iPadOS devices is a good example of the kind of detailed planning and technical execution involved in this project.
I don’t know how many companies have applications that use POP3 or IMAP4 to programmatically retrieve messages from mailboxes. Enough must exist for Microsoft’s fabled telemetry to detect a potential customer satisfaction problem should the turnoff proceed without an answer released with sufficient time for customers to prepare.
Microsoft’s solution is to allow customers to create Azure AD registered apps and assign the necessary permissions to allow the apps to use IMAP4 or POP3 to interact with mailboxes. Figure 1 shows the assignment of the IMAP.AccessAsApp permission to an app. The equivalent permission for POP3 access is POP.AccessAsApp.
After assigning the permissions, an administrator must grant consent to allow the app to use the permissions to access user mailboxes.
There’s nothing strange here. Apps follow the same process to allow them to use Graph API permissions to access other kinds of information from user accounts to Microsoft 365 groups. The only difference is that two specific permissions exist to control access via the IMAP4 and POP3 protocols.
Registered apps have service principals, which are, in the words of a Graph API architect, “convenient holders for permissions.” Exchange Online boasts a new PowerShell cmdlet called New-ServicePrincipal to make the service principal of an app holding IMAP4 or POP3 permissions known. In this example, the $ClientId variable holds the application or client identifier for the app, the $ServiceId variable holds the object identifier for the service principal, and the $TenantId variable holds the tenant identifier.
$ServiceId = (Get-MgServicePrincipal -All | ? {$_.displayname -eq "POP3 and IMAP4 OAuth 2.0 Authorization"} | Select -ExpandProperty Id) $TenantId = (Get-MgOrganization).Id $ClientId = (Get-MgServicePrincipal -All | ? {$_.displayname -eq "POP3 and IMAP4 OAuth 2.0 Authorization"} | Select -ExpandProperty AppId) New-ServicePrincipal -AppId $ClientId -ServiceId $ServiceId -Organization $TenantId -DisplayName "OAuth for POP3 and IMAP4"
Once a service principal is registered with Exchange Online, administrators can run the Add-MailboxPermission cmdlet to assign receive permissions to the service principal, just like the granting of regular delegate access to mailboxes.
Add-MailboxPermission -Identity "Kim.Akers@office365itpros.com" -User $ServiceId -AccessRights FullAccess
In passing, I should note that this is all theoretical on my part because the New-ServicePrincipal cmdlet is not available yet in any tenant that I have access to. In any case, the theory is clear:
I have no need to use IMAP4 or POP3 to access Exchange Online mailboxes, but I did want to test that I could get an OAuth 2.0 access token containing the necessary permissions. In production use, an app should use a certificate for authentication. To test, I used a client secret and ran this PowerShell code:
$Uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" $Body = @{ client_id = $ClientId scope = "https://ps.outlook.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
I then checked the access token and found that the expected permissions were present (Figure 2). All is well and the app has authorization to access the mailboxes.
Developers will probably welcome Microsoft’s approach because it means minimal change for their code. All they need to do is replace the code to sign into a mailbox using basic authentication with code to get an access token. Afterward, the rest of the app code to access messages in a mailbox should work.
Pragmatic as it is, I think Microsoft’s approach is a short-term tactical win. The long-term solution is to move to the Outlook Graph API to access mailboxes. This uses the same registered app approach with different permissions, but it’s more functional. And anyway, app developers will have to embrace the Graph sooner or later to send email via SMTP. The SMTP AUTH protocol is a current exception to Microsoft’s effort to remove basic authentication for email connectivity, but that exception won’t last forever.
I guess that October 1 date is just too close to ask developers to recode their applications. But if you’re in the position where your tenant has some apps that exploit Exchange Online IMAP4 or POP3 mailbox access , consider dumping these old protocols and laying a better foundation for the future. If you have the time…
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>As time goes by, the importance of the Microsoft Graph APIs increases. Administrators who once were happy to know how to run PowerShell cmdlets to retrieve information about Microsoft 365 workloads now need to understand how to access data through the Graph. First, Graph requests are faster to retrieve data. Second, some workloads (like Planner) don’t have PowerShell modules, meaning that the Graph is the only way to interact with data programmatically.
The Microsoft Graph PowerShell SDK is helpful in terms of bridging the gap between PowerShell and the Graph. The SDK cmdlets are essentially wrappers around Graph requests generated automatically using a process called AutoRest. The upside of the process is that literally thousands of cmdlets are available. The downside is that the documentation is poor unless humans get involved to improve the automatic text by correcting errors, adding examples, and so on. Sometimes the automatic documentation is so obscure and convoluted that it’s easier to read the documentation for the underlying Graph API to get an idea of how a cmdlet works.
Having a variety of solid code examples for each cmdlet is the big difference between traditional PowerShell cmdlet documentation and the automatic documentation. The lack of good working examples in what people regard as official documentation is a barrier to adoption for those who’d like to use the Graph but just can’t make head or tail about how the Graph works.
Which is where the Microsoft Graph Explorer comes in. The Graph Explorer is an invaluable tool when it comes to understanding how to put together Graph API requests and the responses (and errors) that come back. When researching articles like those covering basic Azure AD group management with the Graph SDK or Basic Azure AD User management with the Graph SDK, I’ve often resorted to the Graph Explorer to make sure of syntax or to check the data returned from a call. The Graph X-Ray add-in for the Azure AD admin center is also useful for figuring out requests for user and group data.
The basis of the Graph Explorer web application has remained the same since its inception about four years ago.
In Figure 1, I’ve used the request to fetch the items in a drive for a Microsoft 365 group. Drives is the Graph term for SharePoint Online sites, so the query is for items in the SharePoint site belonging to the group. The response preview in the response pane shows the data returned for the request, and the different items are listed, including a document library.
The form of the request (https://graph.microsoft.com/v1.0/groups/33b07753-efc6-47f5-90b5-13bef01e25a6/drive/items/root/children) shows that the request is against the Groups API for drive information for the group with the identifier 33b07753-efc6-47f5-90b5-13bef01e25a6.
Becoming acquainted with the syntax for Graph requests is one big reason why the tool is so useful. The syntax is consistent but is harder to understand at first than most PowerShell cmdlets are, but time and repetition makes the Graph syntax more familiar.
Some recent changes have made the Graph Explorer even more useful. First, PowerShell has joined the set of supported languages for code snippets. These are segments of code showing the query run by the Explorer in the selected language. Figure 2 shows the PowerShell code generated for the query https://graph.microsoft.com/v1.0/users?$count=true&$search=”displayName:room”&$filter=endsWith(mail,’microsoft.com’)&$orderBy=displayName&$select=id,displayName,mail.
PowerShell code snippets are not available for every Graph API. However, good coverage exists for users and groups, which are the two APIs that might be of most interest to administrators.
The second recent change is that Microsoft has added a Resources tab to the Graph Explorer. These aren’t fully-baked requests ready to run. Instead, they’re more like starting points for Graph requests to make you aware of the APIs that exist and how to begin using the APIs. For example, there’s no sample query for the SKUs (products) licensed within a tenant, but a resource exists, and you can use it without altering the query to return the set of licensed products. In Figure 3, you can see the details of the EnterprisePack SKU (Office 365 E3) and some of its service plans. It’s possible to navigate from this point to access an individual SKU and so on.
Given that Microsoft 365 will soon (August 26) move to a new licensing platform that will stop the Azure AD licensing cmdlets from working, it’s a good idea to check any scripts that perform functions like licensing reports and update the code to use Microsoft Graph SDK cmdlets (or direct Graph queries to the licensing API).
The Graph Explorer is getting better and better. It’s a tool I now use every day. That might be just because I know how to use the Graph Explorer better than I did in the past, but I suspect it’s also an indication of just how important it is for Microsoft 365 tenant administrators to understand how to use the Graph APIs to get work done.
Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.
]]>When Microsoft decided to build the administrative tools for Exchange Server 2007 around PowerShell, they realized that it would take time for administrators to become accustomed to PowerShell. Sensibly, Microsoft included a cmdlet logging facility in the Exchange Management Console (EMC) to allow administrators to see the PowerShell code used to execute different actions, such as creating a new mailbox. Cmdlet logging gave administrators prototype code to build scripts around and it’s still the best learning tool I have encountered in a Microsoft server product.
Roll on sixteen years and cmdlet logging doesn’t exist in the modern Exchange Admin Center (EAC). Many, including me, have moaned at Microsoft about this deficiency. One response is that EAC is no longer built on PowerShell, which makes it very difficult to generate and display PowerShell code for administrators to copy and reuse. It’s a great pity.
All of this brings me to a new browser extension called Graph X-Ray. Created by Microsoft employees but not a formal product, Graph X-Ray displays the Graph API commands run to execute actions in consoles like the Azure AD admin center and the Intune admin center. Not every action in these consoles depends on Graph APIs, but enough do in important areas like users, groups, and device management to make this an interesting facility.
Anyone developing code for Microsoft 365 can get value from Graph X-ray, whether you’re using compiled languages like C# or JavaScript or writing PowerShell scripts. Using Graph APIs in PowerShell normally means that scripts run faster, especially if the code must process more than a few objects. Scripters have the choice to include “raw API calls” or use cmdlets from the Microsoft Graph PowerShell SDK. The script to create a tenant configuration report is a good example of using raw API calls while the script to generate an Office 365 licensing report uses the SDK cmdlets. In either case, you need to understand how Graph API queries are formed and executed, and that’s where the Graph X-Ray extension proves its worth.
Take the example of restoring a deleted Microsoft 365 group. Before you can restore a group, you need to know what groups are in a soft-deleted state. Groups remain in the soft-deleted state for 30 days after deletion to allow administrators to restore groups using options in the Microsoft 365 and Azure AD admin centers. After the 30-day retention period lapses, Azure AD removes the groups permanently and they become irrecoverable.
In a large tenant, many groups might be waiting for permanent deletion, including inactive groups removed by the Microsoft 365 Groups Expiration policy. The Get-UnifiedGroup cmdlet can generate a list of soft-deleted groups using a command like this:
Get-UnifiedGroup -ResultSize Unlimited -IncludeSoftDeletedGroups:$True | ? {$_.WhenSoftDeleted -ne $Null} | Sort WhenSoftDeleted | Format-Table DisplayName, PrimarySmtpAddress, WhenSoftDeleted
The cmdlet works, but it’s slow. To speed things up, I tried using the Get-MgDirectoryDeletedItem SDK cmdlet. The cmdlet works when listing deleted user accounts, but no matter what I did, I couldn’t find a way to make it return a list of deleted groups.
I downloaded the Graph X-Ray extension for the Edge browser add-on (other versions are available for Chrome and a Microsoft Store app). To load the add-on, I opened the Developer Tools option in Edge and selected Graph X-Ray. A new blade opened in the browser to display the Graph API commands used for actions performed in the Azure AD admin center (Figure 1).
It’s important to emphasize that this is very much an MVP release. Things are by no means perfect, but enough value is present to allow Graph X-Ray to be very helpful. For example, the command reported when the Azure AD admin center lists deleted groups is:
Get-MgDirectoryDeletedItem -DirectoryObjectId $directoryObjectId -Property "id,displayName,mailEnabled,securityEnabled,groupTypes,onPremisesSyncEnabled,deletedDateTime,isAssignableToRole" -Sort "displayName%20asc" -Top 20
This is fine, but nowhere does it tell you how to populate the $directoryObjectId variable. On a more positive note, the raw Graph API query showed the structure needed to return deleted groups, and I was able to use that information to submit the query with the Invoke-MgGraphRequest SDK cmdlet, which worked. It’s worth noting that the Invoke-MgGraphRequest cmdlet exists to allow scripts to execute raw Graph API queries when an SDK cmdlet isn’t available (or doesn’t work).
Equipped with new-found knowledge about how to find deleted groups, I coded this script to report the set of soft-deleted groups including when each group is due for permanent deletion.
Connect-MgGraph Select-MgProfile Beta $uri = "https://graph.microsoft.com/beta/directory/deleteditems/microsoft.graph.group?`$select=id,displayName,groupTypes,deletedDateTime&`$orderBy=displayName%20asc&`$top=100" [array]$Groups = (Invoke-MgGraphRequest -Uri $Uri).Value If (!($Groups)) { write-Host "No deleted groups available for recovery" ; break } $Report = [System.Collections.Generic.List[Object]]::new() # Create output file for report $Now = Get-Date ForEach ($Group in $Groups) { $PermanentRemovalDue = Get-Date($Group.deletedDateTime).AddDays(+30) $TimeTillRemoval = $PermanentRemovalDue - $Now $ReportLine = [PSCustomObject]@{ Group = $Group.DisplayName Id = $Group.Id Deleted = $Group.deletedDateTime PermanentDeleteOn = Get-Date($PermanentRemovalDue) -format g DaysRemaining = $TimeTillRemoval.Days } $Report.Add($ReportLine) } $Report | Sort {$_.PermanentDeleteOn -as [datetime]} | Out-GridView
The add-on includes the facility to download the commands it captures in a script (GraphXRaySession.PS1). There’s likely to be some duplication of commands in the downloaded script, but it’s great to have such an easy method to copy the commands for later use.
Moving on to restoring a soft-deleted group, Microsoft’s documentation for the Restore-MgDirectoryObject cmdlet is woefully deficient in terms of useful examples. An attempt to pass the identifier of a deleted group to the cmdlet failed:
Restore-MgDirectoryObject -DirectoryObjectId $GroupId Restore-MgDirectoryObject : Resource '2eea84f2-eda3-4a72-8054-5b52c063ee3a' does not exist or one of its queried reference-property objects are not present.
Once again, I turned to Graph X-Ray to find out what command powered the restore deleted group option in the Azure AD admin center. The raw API reported by Graph X-Ray is a POST (update) query like this:
POST /directory/deleteditems/2eea84f2-eda3-4a72-8054-5b52c063ee3a/restore
It’s easy to take this command and repurpose it for use with the Invoke-MgGraphRequest cmdlet:
$uri = “https://graph.microsoft.com/beta/directory/deleteditems/8783e3dd-66fc-4841-861d-49976f0617c0/restore” Invoke-MgGraphRequest -Method Post -Uri $Uri
I wish Microsoft would provide similar insight across all the Microsoft 365 admin consoles. Being able to see the Graph API commands used to perform real-life actions is a powerful learning aid. If Microsoft is serious about driving the adoption of the Graph and the Graph SDK, they could do worse than invest in this kind of tooling. I hope that they do.
Keep up to date with developments like the Graph API commands 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.
]]>Microsoft has recently been beating the drum about the retirement of the Azure AD PowerShell module and its older Microsoft Online Services (MSOL) counterpart. On March 3, the Azure AD team posted in the Microsoft Technical Community to say that they had listened to customer feedback and pushed the termination of support out from the end of June to the end of 2022. On September 30, Microsoft set a new retirement date for the Azure AD and MSOL modules for June 30, 2023. Things tend to happen around the end of June to align with the end of Microsoft’s financial year and allow everyone to start the new year afresh.
The salient points in message center notification MC281145 are:
The deprecation date for the Azure AD and MSOL modules is shifting. Originally, this was June 2022, then the end of 2022, and now it’s June 2023. Clearly, customer feedback has told Microsoft that it’s going to be difficult to update PowerShell scripts before Microsoft wants to retire these modules. ISV products which use the modules or the Azure AD Graph API must also be updated before the axe descends. See Microsoft’s FAQ for help in identifying other applications which use the Azure AD Graph API.
Update (July 29): Microsoft has pushed out the retirement of the Azure AD and MSOL license management cmdlets to 31 March 2023.
No matter which way you turn, the basic fact is that Microsoft will eventually retire the Azure AD and MSOL modules. It’s time to update scripts now, with the priority order being:
To help, Microsoft has created some documentation for steps to migrate scripts. The most important statement is “There is currently no tool to automatically converts scripts in Azure AD PowerShell to Microsoft Graph PowerShell.” I doubt that any automatic script migration tool will appear. There are just too many variations in how people code with PowerShell to guarantee that a tool could handle even moderately complex scripts. The potential to create a support nightmare is one reason why I think Microsoft won’t produce a migration tool.
Which leaves us with Microsoft’s simple three-step approach to script migration:
Testing might be a good fourth step to add. And before you start, you need to create an inventory of scripts which use Azure AD or MSOL cmdlets.
At first glance, the process seems straightforward. In many cases, it is, and you won’t have huge difficulty in converting Get-AzureADUser with Get-MgUser. Microsoft notes some limitations, to which I add:
The Office 365 for IT Pros eBook writers are busy converting script examples to use the Microsoft Graph PowerShell SDK. We plan to have everything done over the next few months. On one level, it’s a pain to be forced to find and upgrade scripts. On another, it’s an opportunity to revamp scripts to make them work better. Perhaps you might even consider moving some of your long-running scripts to Azure Automation?
So much change, all the time. It’s a challenge to stay abreast of all the updates Microsoft makes across Office 365. Subscribe to the Office 365 for IT Pros eBook to receive monthly insights into what happens, why it happens, and what new features and capabilities mean for your tenant.
]]>By now, all Microsoft 365 tenant administrators should be aware that Microsoft is removing support for basic authentication for many Exchange Online connectivity protocols. The aim is to complete the process by October 2022. SMTP AUTH is an exception, but Microsoft will deal with it in time.
What you might not be aware of is that access to Microsoft 365 data using modern authentication requires that the developers must register their app with Azure AD. This applies to any Microsoft API, including the Outlook add-in model and the Graph APIs. If you’ve written PowerShell scripts which make Graph queries, you know that you must register an app to receive consent for the Graph permissions necessary to access the target data. This is a basic registration. Registrations for more sophisticated apps like those sourced from ISVs contain more information about the app, such as a redirect URL for the app. Registration for ISV apps usually happens during the app installation, including the creation of a service principal to allow the app to run with API permissions consented to by tenant administrators.
Previously, I’ve written about the need for tenants to clean out application crud from Azure AD. The crud is composed of unwanted apps and their service principals accumulated over time in Azure AD. Being able to fetch sign-in data for service principals via Graph queries makes it easier to add context to this exercise by knowing what service principals are active.
After cleaning out obsolete applications, Azure AD might be tidy, but do you know much about the apps which remain? The application governance add-on for Microsoft Defender for Cloud Apps might help, but only if your tenant has the necessary licenses.
Fortunately, Microsoft has an App Compliance Program, part of their Zero Trust initiative to help customers verify apps they might want to run in their tenant. App developers go through the process to achieve app certification by providing information about the app and the data it accesses. The program has three phrases or levels:
Publisher verification: The app developer has a Microsoft developer network identity. The app supports modern authentication and is capable of multi-tenant activity. This is the entry-level participation in certification.
Publisher attestation: The app developer completes a questionnaire covering security, data handling, and compliance.
Microsoft 365 certification: Instead of the app developer reporting details of their app, third-party assessors audit the assertions to validate that the app meets Microsoft standards for security and compliance. The process occurs annually, and details gathered during the audit is available online. Figure 1 shows details of a Microsoft certified app in AppSource. The audit information is available through the Microsoft 365 certification link for the app.
The app certification information available online (Figure 2) includes detail of the app permissions, including the reason why the app developers need administrator consent to use the permission.
Obviously, app developers must invest time and effort to satisfy Microsoft criteria for app certification. However, once completed, they should reap the benefits gained by increased customer confidence in their product. At least, that’s the theory.
In April 2020, I reviewed the new Manage Apps section in the Teams admin center and commented on the Microsoft 365 certified status of the Wrike app. The number of apps available for Teams continues to expand (from 462 in April 2020 to 1,402 as I write this in February 2022, or roughly 44 new apps monthly). Checking the online list of Teams apps, it looks like very few apps are Microsoft 365 certified. This begs the question why app developers feel it unnecessary to go through Microsoft’s audit process – or why publishers of apps like Wrike downgraded their apps from certified to publisher attestation.
I’m sure cost has something to do with it, along with a feeling that customers don’t go looking for apps which are Microsoft 365 certified. If a developer gains no business advantage by completing the full certification process for their apps, why bother? It’s a reasonable perspective. Microsoft would obviously like developers to go the whole hog, but this might be an uphill battle.
One way that customers might help persuade developers that app certification is worthwhile is to allow users to grant consent for apps from verified publishers when apps require only “low-impact” permissions. The idea is that if less friction exists to deploy and use an app, it will be more popular and profitable.
The consent settings for a tenant are available in the Azure AD admin center (Figure 3) and include the ability to define what you consider to be low-impact permissions. In this case, the selected option allows users to grant consent, but only for three low-impact permissions such as the ability to read a user’s profile. Tenants can define what they consider to be low-impact permissions through the Permissions Classifications option shown in Figure 3.
Some will be uneasy about the prospect of users granting consents to apps. The safeguard is that consent is only possible for verified publishers; the counterargument is that developers can attain verification too easily to make this status truly valuable. If Microsoft 365 certified apps were the threshold, a different story might ensue. Microsoft recommends that it’s OK to allow users to grant consent to apps, but without stronger controls, this might be a stretch for many organizations.
The situation is complex. Microsoft wants everyone to use modern authentication to access Microsoft 365. Getting to that position means a great deal of change for clients, apps, users, and organizations. Certification helps customers understand and control the access apps have to data in their tenant. That’s goodness, but only if ISVs co-operate and certify their products. Time enables change. While that happens, keep your app repository clean and tidy. You know it makes sense.
Learn more about how Office 365 really works on an ongoing basis by subscribing to the Office 365 for IT Pros eBook. Our monthly updates keep subscribers informed about what’s important across the Office 365 ecosystem.
]]>On February 14, Microsoft updated the Teams profile card to display the profile owner’s local time and the time difference between you and them. To see a profile card, hover over a user’s thumbnail photo anywhere they appear in Teams, like a chat or channel conversation. As you can see in Figure 1, the local time for the chosen person and the difference to your time appear.
Obviously, if you’re trying to contact someone or arrange a meeting with them, knowing when they work is valuable information. It’s also a feature requested several times in the Teams feedback forum (here’s one example).
Administrators don’t have to do anything to make local time appear in profile cards. Like other information shown in the profile card, the local time depends on information already known about users. In this case, the working hours defined for user calendars. Apart from tenant accounts, local time information is also displayed for guest accounts, if an Exchange Online organization relationship exists to share calendar information between the tenant and the guest’s home domain. Local time information is not available for federated chat including chats with Teams consumer users.
All of this is wonderful until I saw some puzzling results, like people showing up as being in odd time zones or with time differences that just didn’t work. Take the local time shown for Vasil Michev’s guest account (Figure 2). I am in Dublin, Ireland and Vasil is in Sofia, Bulgaria. At 9:51am, Teams told me that Vasil’s local time was 07:51am and that he is two hours behind me. In other words, his time zone is somewhere in the mid-Atlantic. This is upsetting, because the technical editor for the Office 365 for IT Pros eBook shouldn’t be stuck in mid-ocean.
Why would someone’s time zone be four hours off? To answer the question, we need to know where Teams obtains its time zone information.
It’s logical that Teams would use someone’s location to determine their time zone. Looking at the data, it seems OK.
Get-User -Identity Vasil.Michev | fl city, stateorprovince, countryorregion City : Sofia StateOrProvince : Sofia CountryOrRegion : Bulgaria
However, when you think about things a little more, mailboxes have a regional configuration. Perhaps this is where Teams fetches the time zone from. The problem with this theory is that guest accounts have only special cloud-only mailboxes used to store compliance records and other system data. You can’t query these mailboxes using the Get-MailboxRegionalConfiguration cmdlet, like you can for tenant mailboxes:
Get-MailboxRegionalConfiguration -Identity Ken.Bowers | fl DateFormat : dd/MM/yyyy Language : en-GB DefaultFolderNameMatchingUserLanguage : False TimeFormat : HH:mm TimeZone : Eastern Standard Time
The answer therefore must be data that remote tenants can access, such as the calendar configuration. Each calendar has a time zone for working hours, which is used when scheduling meetings and to publish free/busy information. Organizations can share free/busy data with other Microsoft 365 tenants, and as it turns out, this is the data used by Teams.
Users can set the time zone for their calendar through Outlook or OWA settings. OWA is more intelligent about time zone settings than Outlook desktop is and detects if a difference exists between the regional time zone configured in the General tab and the calendar time zone. In Figure 3, we see that OWA offers to fix a mismatch detected between a user’s regional time zone (Eastern Standard Time or UTC -5) and the time zone for their calendar meeting hours (UTC).
Although the two time zones are usually the same, they don’t need to be. By default, the time zone for the calendar is set to the tenant time zone during the creation of a new mailbox. Afterwards, the user can update the time zone to match their location, or an administrator can update the time zone using the Set-MailboxCalendarConfiguration cmdlet. For example:
Set-MailboxCalendarConfiguration -Identity Nikki.Patia -WorkingHoursTimeZone "FLE Standard Time"
Like any change to a mailbox or account setting, it can take some time before Teams clients refresh their cache to pick up the change. In testing, I found it could take several days before Teams reflected calendar adjustments in profile cards.
Apart from running the Get-MailboxCalendarConfiguration cmdlet, administrators can use the Graph Explorer to check calendar settings for a user by running a Calendar API query, which is how Teams fetches user time zone information.
To run the query, open the Graph Explorer and sign into your account. In the request body, enter the request you want the Graph to process. In this case, we want to know about the calendar settings for two users defined in the schedules section. The start and end time can be any date.
{ "schedules": [ "jane.smith@office365itpros.com", "john.hopper@office365itpros.com" ], "startTime": { "dateTime": "2023-03-15T09:00:00", "timeZone": "GMT Standard Time" }, "endTime": { "dateTime": "2023-03-15T18:00:00", "timeZone": "GMT Standard Time" }, "availabilityViewInterval": 60 }
After populating the request body, run this POST query:
https://graph.microsoft.com/v1.0/me/calendar/getSchedule
The response gives the availability of the users for the requested time slot. However, we’re interested only in the time zone included in the response for each user. In this instance, we see that the user’s calendar time zone is Eastern Standard Time.
], "startTime": "08:00:00.0000000", "endTime": "17:00:00.0000000", "timeZone": { "name": "Eastern Standard Time" }
We started off by reporting problems with the profile card for Vasil Michev’s guest account. My tenant has an Exchange Online federated relationship with Vasil’s tenant, so we can use the Calendar API to perform a free/busy lookup. This is how the Outlook scheduling assistant finds free time slots for meetings.
The lookup returned the following result shows that Vasil’s calendar uses a custom time zone to apply a four-hour time offset from UTC. This is why his profile card reports such an odd local time.
"startTime": "09:00:00.0000000", "endTime": "18:00:00.0000000", "timeZone": { "@odata.type": "#microsoft.graph.customTimeZone", "bias": -120, "name": "Customized Time Zone", "standardOffset": { "time": "04:00:00.0000000", "dayOccurrence": 5, "dayOfWeek": "sunday", "month": 10, "year": 0
You might decide that it’s up to users to make sure that their calendars have the correct time zones set and administrators have no part to play to ensure no mismatches exist. However, if you decide that you’d like to know if any mailboxes have mismatched time zones, we can detect the condition with some PowerShell code. The script below:
$ModulesLoaded = Get-Module | Select Name If (!($ModulesLoaded -match "ExchangeOnlineManagement")) {Write-Host "Please connect to the Exchange Online Management module and then restart the script"; break} Write-Host "Finding user mailboxes..." # OK, we seem to be fully connected and ready to go... [array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited If (!($Mbx)) { Write-Host "Something happened and we found no user mailboxes - exiting" ; break } $Report = [System.Collections.Generic.List[Object]]::new() ForEach ($M in $Mbx) { Write-Host "Processing" $M.DisplayName $RegionalConfiguration = Get-MailboxRegionalConfiguration -Identity $M.UserPrincipalName $CalendarConfiguration = Get-MailboxCalendarConfiguration -Identity $M.UserPrincipalName $UserInfo = Get-User -Identity $M.UserPrincipalName $Status = $Null If ($CalendarConfiguration.WorkingHoursTimeZone -ne $RegionalConfiguration.TimeZone) {$Status = "Time zone mismatch"} $DataLine= [PSCustomObject][Ordered]@{ Status = $Status User = $M.DisplayName UPN = $M.UserPrincipalName CalendarZone = $CalendarConfiguration.WorkingHoursTimeZone RegionalZone = $RegionalConfiguration.TimeZone Language = $RegionalConfiguration.Language DateFormat = $RegionalConfiguration.DateFormat TimeFormat = $RegionalConfiguration.TimeFormat Office = $UserInfo.Office StreetAddress = $UserInfo.StreetAddress City = $UserInfo.City StateOrProvince = $UserInfo.StateOrProvince CountryOrRegion = $UserInfo.CountryOrRegion PostalCode = $UserInfo.PostalCode } $Report.Add($DataLine) } $Report | Out-GridView
Figure 4 shows example output from the script.
The cmdlets to fetch regional and calendar configurations are not quick and the script can take between ten and fifteen seconds to process a mailbox. In fact, this is a great example of an Exchange Online script suitable for processing by Azure Automation.
Although it would be easy to insert an extra line of PowerShell to fix time zone mismatches by adjusting the calendar time zone to match the regional time zone, that’s a call better left to users. They know their calendar and they know how they work. But with the information to hand, you can advise users with mismatched time zones to update their settings as necessary to make sure that Teams profile cards reflect accurate data. After all, you wouldn’t want people to see bad local times, would you?
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>By now, most people who write PowerShell code to interact with Microsoft 365 workloads understand that sometimes it’s necessary to use Microsoft Graph API queries instead of “pure” PowerShell cmdlets. The Graph queries are usually faster and more reliable when retrieving large quantities of data, such as thousands of Microsoft 365 Groups. Over the last few years, as people have become more familiar with the Microsoft Graph, an increased number of scripts have replaced cmdlets with Graph queries. All these scripts use Entra ID (Azure AD) access tokens, as does any utility which interacts with the Microsoft Graph, like the Graph Explorer (Figure 1).
In the remainder of this article, I explore what an Entra ID access token contains.
Graph queries need authentication before they can run and the Graph API uses modern authentication. Entra ID registered applications bridge the gap between PowerShell and the Graph. The apps hold details used during authentication such as the app name, its identifier, the tenant identifier, and some credentials (app secret or certificate. The app also holds permissions granted to access data through Graph APIs and other APIs. When the time comes to authenticate, the service principal belonging to an app uses this information to request an access token from Entra ID. Once Entra ID issues the access token, requests issued to the Invoke-RestMethod or Invoke-WebRequest cmdlets can include the access token to prove that the app has permission to access information.
At first glance, an access token is a confused mass of text. Here’s how PowerShell reports the content of an access token:
eyJ0eXAiOiJKV1QiLCJub25jZSI6IlFQaVN1ck1VX3gtT2YzdzA1YV9XZzZzNFBZRFUwU2NneHlOeDE0eVctRWciLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1yNS1BVWliZkJpaTdOZDFqQmViYXhib1hXMCIsImtpZCI6Ik1yNS1BVWliZkJpaTdOZDFqQmViYXhib1hXMCJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9iNjYyMzEzZi0xNGZjLTQzYTItOWE3YS1kMmUyN2Y0ZjM0NzgvIiwiaWF0IjoxNjQ0ODQ1MDc3LCJuYmYiOjE2NDQ4NDUwNzcsImV4cCI6MTY0NDg0ODk3NywiYWlvIjoiRTJaZ1lEaW1McEgwTSt5QTk5NmczbWZUUXlYN0FBPT0iLCJhcHBfZGlzcGxheW5hbWUiOiJHZXRUZWFtc0xpc3QiLCJhcHBpZCI6IjgyYTIzMzFhLTExYjItNDY3MC1iMDYxLTg3YTg2MDgxMjhhNiIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2I2NjIzMTNmLTE0ZmMtNDNhMi05YTdhLWQyZTI3ZjRmMzQ3OC8iLCJpZHR5cCI6ImFwcCIsIm9pZCI6IjM4NTRiYjA4LTNjMmMtNGI1Ny05NWZjLTI0ZTA3OGQzODY4NSIsInJoIjoiMC5BVndBUHpGaXR2d1Vva09hZXRMaWYwODBlQU1BQUFBQUFBQUF3QUFBQUFBQUFBQmNBQUEuIiwicm9sZXMiOlsiVGVhbVNldHRpbmdzLlJlYWRXcml0ZS5BbGwiLCJUZWFtTWVtYmVyLlJlYWQuQWxsIiwiR3JvdXAuUmVhZC5BbGwiLCJEaXJlY3RvcnkuUmVhZC5BbGwiLCJUZWFtLlJlYWRCYXNpYy5BbGwiLCJUZWFtU2V0dGluZ3MuUmVhZC5BbGwiLCJPcmdhbml6YXRpb24uUmVhZC5BbGwiLCJBdWRpdExvZy5SZWFkLkFsbCJdLCJzdWIiOiIzODU0YmIwOC0zYzJjLTRiNTctOTVmYy0yNGUwNzhkMzg2ODUiLCJ0ZW5hbnRfcmVnaW9uX3Njb3BlIjoiRVUiLCJ0aWQiOiJiNjYyMzEzZi0xNGZjLTQzYTItOWE3YS1kMmUyN2Y0ZjM0NzgiLCJ1dGkiOiI3RVkyWnVXV2JFYVF0T3piVVlwOUFBIiwidmVyIjoiMS4wIiwid2lkcyI6WyIwOTk3YTFkMC0wZDFkLTRhY2ItYjQwOC1kNWNhNzMxMjFlOTAiXSwieG1zX3RjZHQiOjEzMDI1NDMzMTB9.N9yvmkCedti2fzT44VfBkN7GvuCInrIgiMgNxdyZeAyxnbdZjEhxHmNdU6HLLHQ3J-GonpPdt28dKwYxgLcrSibGzSPVHddh6MDPYutSwfIxh2oRanxhgFOWVJADfbFoCxsRFDhKJNT39bsauIUiRNzGzbb6dvWuZQ8LrgWjZzjae2qxVxj9jvYgjXEypeYZgLvPOzJiBCuluAMH3TjPuS-CuglFK_edn4CS-ztCwM0hmDFD5BLNZqng5P2KqGTEgjkMKoyIJ8yTGBJpASfdqqEFqWzQwcQ9ese924qNC3hJR_5TWHp2Fl73bpdhwBHRL5UwGTPi9_ysYdndKhXwgA
Access tokens issued by Entra ID comply with the OAuth 2.0 bearer token standard (RFC6750) and are structured as JSON-formatted Web Tokens. We can’t see the JSON content because it is base64Url encoded and signed. However, if you paste the token into a site like https://jwt.ms/, the site will decrypt the list of claims included in the token and we’ll see something like the details shown below for the access token featured above:
{ "typ": "JWT", "nonce": "gq3zmJhybfXGDGqt6RO2PX9s0cimmRpSRrTO90sQ4w4", "alg": "RS256", "x5t": "Mr5-AUibfBii7Nd1jBebaxboXW0", "kid": "Mr5-AUibfBii7Nd1jBebaxboXW0" }. { "aud": "https://graph.microsoft.com", "iss": "https://sts.windows.net/a662313f-14fc-43a2-9a7a-d2e27f4f3478/", "iat": 1644833772, "nbf": 1644833772, "exp": 1644837672, "aio": "E2ZgYJif1+eocevtzqRIrgDGA2V3AQ==", "app_displayname": "ReportDLs", "appid": "76c31534-ca1f-4d46-959a-6159fcb2f77a", "appidacr": "1", "idp": "https://sts.windows.net/a662313f-14fc-43a2-9a7a-d2e27f4f3478/", "idtyp": "app", "oid": "4449ce36-3d83-46fb-9045-2d1721e8f032", "rh": "0.AVwAPzFitvwUokOaetLif080eAMAAAAAAAAAwAAAAAAAAABcAAA.", "roles": [ "Group.Read.All", "Directory.Read.All", "User.Read.All" ], "sub": "4449ce36-3d83-46fb-9045-2d1721e8f032", "tenant_region_scope": "EU", "tid": "a662313f-14fc-43a2-9a7a-d2e27f4f3478", "uti": "BU1RVc7mHkmBq2FMcZdTAA", "ver": "1.0", "wids": [ "0997a1d0-0d1d-4acb-b408-d5ca73121e90" ], "xms_tcdt": 1302543310 } .[Signature]
The deciphered token divides into three parts: header, payload, and signature. The aim of a token is not to hide information, so the signature is not protected by encryption. Instead, it’s signed using a private key by the issuer of the token. Details of the algorithm and private key used to sign an access token are in its header. An application can validate the signature of an access token if necessary, but this is not usually done when running a PowerShell script. The payload is the location for the claims made by the token and is the most interesting place to check.
Another way to check what’s in an access token is to use the JWTDetails PowerShell module, which is available in the PowerShell Gallery. To install this (very small) module, run:
Install-Module -Name JWTDetails -RequiredVersion 1.0.0 -Scope AllUsers
Afterward, you can examine a token with the Get-JWTDetails cmdlet. Here’s an example revealing that the access token issued to an app allows it to access Exchange Online using the IMAP4 or POP3 protocols:
Get-JWTDetails -Token $Token aud : https://outlook.office.com iss : https://sts.windows.net/b662313f-14fc-43a2-9a7a-d2e27f4f3478/ iat : 1671891468 nbf : 1671891468 exp : 1671895368 aio : E2ZgYDAQS/prW6b0Zsah6KMXtnTEAQA= app_displayname : POP3 and IMAP4 OAuth 2.0 Authorization appid : 6a90af02-6ac1-405a-85e6-fb6ede844d92 appidacr : 1 idp : https://sts.windows.net/a662313f-14fc-43a2-9a7a-d2e27f4f3478/ oid : b7483867-51b6-4fdf-8882-0c43aede8dd5 rh : 0.AVwAPzFitvwUokOaetLif080eAIAAAAAAPEPzgAAAAAAAABcAAA. roles : {POP.AccessAsApp, IMAP.AccessAsApp} sid : 1475d8e7-2671-47e9-b538-0ea7b1d43d0c sub : b7483867-51b6-4fdf-8882-0c43aede8dd5 tid : a662313f-14fc-43a2-9a7a-d2e27f4f3478 uti : COCw22GGpESVXvfdhmEVAQ ver : 1.0 wids : {0997a1d0-0d1d-4acb-b408-d5ca73121e90} sig : PdScMpYqwA25qJL1z8q589sz/Ma5CGQ4ea9Bi0lnO2yByrIs530emYPnFPfQNN9EPBIvv4EaAoTLomrw4RMBWYoQSAgkBUXVrYGnC jzAU6a2ZNZgo7+AORHk4iyLO0FpbLEaMJvCvI5vWhP9PHOxnGLcIsCbOmyrCK6lxxIKtBx851EpLrhpyvJ3p05NSw0D/mKzXPRKtc rzQcUwECxOUugbm1zdq8JaE/PmSggBb87VZy7p1S2BXhxQZ5QU17JeIADyhCGm1Ml+avuIHsVS2iat/LPEi/nktbrXMcOzROpUKyZ /7uVhxQ0cscJ6WGxbd+zJm36s25Yp1vMzSHaRxQ== expiryDateTime : 24/10/2022 15:22:48 timeToExpiry : 00:59:34.7611307
The list of claims in the access token includes simple claims and scopes (groups of claims). A claim is an assertion about something related to the token. In this case, the claims tell us details like:
Get-MgServicePrincipal -Filter "Id eq '4449ce36-3d83-46fb-9045-2d1721e8f032'" DisplayName Id AppId SignInAudience ServicePrincipalTy pe ----------- -- ----- -------------- ------------------ ReportDLs 4449ce36-3d83-46fb-9045-2d1721e8f032 77c31534-ca1f-4d46-959a-6159fcb2f77a AzureADMyOrg Application
Scopes are a logical grouping of claims, and they can serve as a mechanism to limit access to resources. The roles claim contains a scope of Graph API permissions starting with Group.Read.All and ending with User.Read.All. We therefore know that this app has consent from the organization to use the permissions stated in the scope when it executes Graph API queries. The list of permissions is enough to allow the PowerShell script (in this case, one to generate a report of distribution list memberships) to query the Graph for a list of all groups and read the membership of each group.
From bitter experience, I know how easy it is to get Graph permissions wrong. One way to check is sign into the Graph Explorer and run the query (here’s an example) to check what permissions the Explorer uses to execute the query. However, you can also dump the access token to check that the set of permissions in the access token matches what you expect. It’s possible that you might have requested some application permissions for the app and failed to gain administrator consent for the request, meaning that the access token issued to the app by Entra ID won’t include the requested permissions.
Once we’re happy that we have a good access token, we can use it with Graph queries. Here’s how to fetch the list of distribution groups in a tenant. The access token is included in the $Headers variable passed to the Invoke-RestMethod cmdlet.
$Headers = @{Authorization = "Bearer $token"} $Uri = "https://graph.microsoft.com/V1.0/groups?`$filter=Mailenabled eq true and not groupTypes/any(c:c+eq+'Unified')&`$count=true" [array]$DLs = (Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Get -ContentType "application/json") $DLs = $DLs.Value
And if everything goes to plan, we should have a set of distribution lists to process. If not, it’s bound to be a problem with your access token, so it’s time to return to square one and restart the acquisition process.
Learn more about how Office 365 really works on an ongoing basis by subscribing to the Office 365 for IT Pros eBook. Our monthly updates keep subscribers informed about what’s important across the Office 365 ecosystem.
]]>Message center notification MC315739 (January 18, roadmap item 83946) brings news of a big change for the information included in notifications. Soon, along with the text describing new features or changes to existing Microsoft 365 features, notifications will include service usage data relevant to the change. Deployment starts for targeted release tenants in mid-January and should be complete worldwide for all tenants by mid-February.
Let’s take the change announced in MC302456 as an example. This notification describes how users can maintain their guest accounts in other tenants from Teams. To help administrators understand how many people will be affected by the change, the service communications API queries the Microsoft Graph reports API to retrieve the monthly active user data for Teams and reports this information in the notification.
Figure 1 shows a mock-up included in MC315739 to illustrate how Microsoft 365 notifications highlight user data. On the left, you see a notification for a change affecting multiple workloads together with the usage data for each workload (Outlook is really Exchange Online, but obviously non-Outlook clients can connect to Exchange Online mailboxes). On the right, you see a notification for Kaizala, which doesn’t store its usage data in the Microsoft Graph, so it’s impossible to display this information.
Editorial comment: The need for Kaizala is possibly now much reduced by the general availability of the Teams Walkie-Talkie feature.
The Microsoft Graph reports API allows access to usage data about some Microsoft 365 services. Coverage is good for base workloads (SharePoint Online, Exchange Online, Teams, and OneDrive for Business) and not so good elsewhere (Planner, Stream, Forms, Whiteboard, etc.). Nevertheless, the usage data is detailed enough to build a picture of user activity over the last ninety days. If you’d like to know how to use the API with PowerShell, consider running the User Activity Analysis script to see how to make calls against the reports API and the kind of data the API returns. For example, this code creates a query to retrieve Teams activity data for users over the last 30 days. Data returned by the reports API is always a few days behind the actual date.
$TeamsUserReportsURI = "https://graph.microsoft.com/v1.0/reports/getTeamsUserActivityUserDetail(period='D30')" [array]$TeamsUserData = (Invoke-RestMethod -Uri $TeamsUserReportsURI -Headers $Headers -Method Get -ContentType "application/json") -Replace "...Report Refresh Date", "Report Refresh Date" | ConvertFrom-Csv
The data returned by the API is in an array. Here’s the item in the area for an account:
Report Refresh Date : 2022-01-16 User Principal Name : Jane.Smith@office365itpros.org Last Activity Date : 2022-01-15 Is Deleted : False Deleted Date : Assigned Products : POWER BI (FREE)+ENTERPRISE MOBILITY + SECURITY E5+BUSINESS APPS (FREE)+MICROSOFT POWER AUTOMATE FREE+MICROSOFT VIVA TOPICS+MICROSOFT DEFENDER FOR CLOUD APPS – APP GOVERNANCE+OFFICE 365 E5 WITHOUT AUDIO CONFERENCING Team Chat Message Count : 58 Private Chat Message Count : 14 Call Count : 1 Meeting Count : 5 Has Other Action : No Report Period : 30
The data looks good and is useful. However, some workloads (like Teams) return data for both tenant and guest accounts, so the numbers reported in message center notifications will reflect that data. You might be concerned about how a change will affect guest users, but I hazard a guess that most tenant administrators will focus on the effect on tenant users.
Another issue (acknowledged in MC315739) is the non-specific nature of the report. Usage across all clients and all features is included into one workload figure. For instance, a change affecting Microsoft Lists in SharePoint Online and OneDrive for Business might affect just the five people who create and manage Lists, but the notification will say that the change affects everyone who has used SharePoint Online or OneDrive for Business in the last month. You won’t know either if a change is specific to a client platform, like Android or iOS.
Counting all and sundry who use a workload isn’t such a big problem for new features. It is more important for updated features and becomes even more critical when Microsoft deprecates some functionality. You then want to know precisely who is affected, or at least, how many are affected.
Another aspect of an all-up number is that it doesn’t take account of multi-geo deployments. You’ll know that some people in the organization might need to be informed about a change, but not their location.
Even with the caveats listed above, including user data in Microsoft 365 notifications is still a good change. If you see a notification where a low number of users will experience an impact, you can probably spend less time preparing for that change and more on changes affecting large user populations. The availability of data through Graph APIs limit what the developers can do to slice and dice usage data to make it more precise and informative. This will probably happen over time. In the interim, take the user information presented in Microsoft 365 notifications as a starting point to help you understand the likely impact of individual changes on users. Use this data in conjunction with your knowledge of the tenant and how people work within the organization, and the monthly active user data for affected workloads will be helpful. Taken as an exact guide, it won’t be.
I guess I might have to update my script to extract and report information from message center notifications to accommodate this change…
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>Vasil Michev, the Technical Editor of the Office 365 for IT Pros eBook, comes up with all sorts of weird and wonderful insights into Microsoft 365. A recent question he discussed on his blog was how to find the creation date for a tenant. It’s a good question because it forces respondents to know where to look for this information and is exactly the kind of poser we like to tease out as we write content for the book.
As Vasil points out, the obvious answer is to fire up the Teams admin center because the tenant creation date appears on a card displayed on its home screen (Figure 1). The Teams admin center is the only Microsoft 365 portal which shows this information. Why the Teams developers thought that it was useful to highlight the tenant creation date is unknown. After all, the date won’t change over time and static information is not usually featured by workload dashboards.
Opening an administrative portal is no challenge. Vasil suggests several alternate methods to retrieve the tenant creation date. It seemed like fun to try some of these methods against my tenant. Here’s what I found.
If you’ve used Exchange Online from the start, you can check the creation date of the Exchange organization configuration object, created when an administrator enables Exchange Online for the first time.
(Get-OrganizationConfig).WhenCreated Monday 27 January 2014 20:28:45
It’s an interesting result. Exchange Online reports its initiation in January 2014 while Teams is quite sure that the tenant existed in April 2011. I’ve used Exchange Online for email ever since I had a tenant, so the disconnect between Exchange Online and the tenant creation date is interesting.
Another way of checking Exchange data is to look at the creation dates for mailboxes. This PowerShell snippet finds all user mailboxes and sorts them by creation date. The first mailbox in the sorted array is the oldest, so we can report its creation date:
[array]$Mbx = Get-ExoMailbox -ResultSize Unlimited -Properties WhenCreated -RecipientTypeDetail UserMailbox | Sort {$_.WhenCreated -as [datetime]} Write-Host ("The oldest mailbox found in this tenant is {0} created on {1}" -f $Mbx[0].DisplayName, $Mbx[0].WhenCreated) The oldest mailbox found in this tenant is Tony Redmond created on 27/01/2014 20:36:38
(Dates shown are in Ireland local format. The equivalent U.S. format date is 01/27/2014).
Grabbing all mailboxes to check their creation date will not be a fast operation. Even using the REST-based Get-ExoMailbox cmdlet from the Exchange Online management module, it will take time to retrieve all the user mailboxes in even a medium size tenant.
As it turns out, the oldest mailbox is my own, created about eight minutes after the initiation of Exchange Online. However, we’re still in 2014 when the tenant proclaims its creation in 2011, so what happened?
A search through old notes revealed that Microsoft upgraded my original Office 365 tenant created in 2011 to an enterprise version in 2014. It seems that during the tenant upgrade, Microsoft recreated the instance of Exchange Online. That explanation seems plausible.
Another method is to examine the creation dates of administrator accounts to find the oldest account. This is usually the administrator account created during tenant setup. In other words, when you create a new tenant, you’re asked to provide the name for an account which becomes the first global administrator. If we look at the administrator accounts in the tenant and find the oldest, it should be close to the tenant creation date shown in the Teams admin center. That is, unless someone deleted the original administrator account.
Azure AD is the directory of record for every Microsoft 365 tenant, so we should check Azure AD for this information. The steps are:
Here’s the code I used:
# Find the identifier for the Azure AD Global Administrator role $TenantAdminRole = Get-AzureADDirectoryRole | Where-Object {$_.DisplayName -eq ‘Global Administrator’} | Select ObjectId # Get the set of accounts holding the global admin role. We omit the account used by # the Microsoft Rights Management Service $TenantAdmins = Get-AzureADDirectoryRoleMember -ObjectId $TenantAdminRole.ObjectId | ? {$_.ObjectId -ne "25cbf210-02e5-4a82-9f5c-f41befd2681a"} | Select-Object ObjectId, UserPrincipalName # Get the creation date for each of the accounts $TenantAdmins | ForEach-Object { $_ | Add-Member -MemberType NoteProperty -Name "Creation Date" -Value (Get-AzureADUserExtension -ObjectId $_.ObjectId ).Get_Item("createdDateTime") } # Find the oldest account $FirstAdmin = ($TenantAdmins | Sort-Object {$_."Creation Date" -as [datetime]} | Select -First 1) Write-Host ("First administrative account created on {0}" -f $FirstAdmin."Creation Date")
The older Microsoft Online PowerShell module doesn’t require such a complicated approach to retrieve account creation data. Taking the code shown above and replacing the Get-AzureADUserExtension cmdlet with Get-MsOlUser, we get:
$TenantAdmins | ForEach-Object { $_ | Add-Member -MemberType NoteProperty -Name "Creation Date" -Value ((Get-MsOlUser -ObjectId $_.ObjectId ).WhenCreated) }
Using either cmdlet, the result is:
First administrative account created on 11/04/2011 17:35:11
The Teams admin center also reports April 11, 2011, so using administrator accounts might be a viable way to determine tenant age.
Microsoft 365 stores information for each tenant in the Microsoft Graph, and it’s the Graph which is the source for the Teams admin center. We can retrieve the same information by running the https://graph.microsoft.com/V1.0/organization Graph query. The createdDateTime property returned in the organization settings is what we need.
Here’s the PowerShell code to run after obtaining the necessary access token for a registered app, which must have consent to use the Organization.Read.All Graph permission. Vasil used the beta endpoint when he showed how to fetch tenant organization settings using the Graph Explorer (which saves the need to write any code), but the V1.0 endpoint works too.
$Uri = "https://graph.microsoft.com/V1.0/organization" $OrgData = Invoke-RESTMethod -Method GET -Uri $Uri -ContentType "application/json" -Headers $Headers If ($OrgData) { Write-Host ("The {0} tenant was created on {1}" -f $Orgdata.Value.DisplayName, (Get-Date($Orgdata.Value.createdDateTime) -format g)) } The Redmond & Associates tenant was created on 11/04/2011 18:35
The first administrator account appears to date from 17:35 while the tenant creation time is an hour later. This is easily explained because all dates stored in the Graph are in UTC whereas the dates extracted from Azure AD and reported by PowerShell reflect local time. In April 2011, local time in Ireland was an hour ahead of UTC.
After all the checks, it’s clear that I created my tenant in the early evening of April 11, 2011. Given that this was ahead of Microsoft’s formal launch of Office 365 in July 2011, I can claim to use an old tenant, for what that’s worth.
]]>Message center notification MC297438 arrived in the Microsoft 365 admin center on November 10 to inform me that Microsoft was about to enforce version 1.2 of the Transport Layer Security (TLS) for Direct Routing SIP interfaces. I have no problem with this proposal. It seems perfectly splendid to enforce TLS 1.2 for all manner of communications.
The note then said: “You are receiving this message because our reporting indicates that your organization is still connecting using SMTP Auth client submission via smtp.office365.com with TLS1.0 or TLS1.1 to connect to Exchange Online.”
The problem here is that PowerShell uses the system default for TLS unless you specify otherwise. Although Microsoft is excluding SMTP AUTH from the set of connection protocols they will block for basic authentication in all tenants in October 2022, this doesn’t mean that SMTP AUTH is immune from other efforts within Microsoft 365 to remove older, less secure protocols. As Microsoft notes in MC297438, they communicated their intention to remove TLS 1.0 and 1.1 from Microsoft 365 as far back as December 2017, so this development shouldn’t come as a shock to anyone.
I covered this topic in January 2021 and noted that script developers who use the Send-MailMessage cmdlet to send email via Exchange Online should include a line in their scripts to force PowerShell to use TLS 1.2. If you don’t, the deprecation of TLS 1.0 and 1.1 in Exchange Online will prevent scripts being able to send messages.
For the record, the command to force TLS 1.2 connections from PowerShell is:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Hopefully, components like multi-function devices which use basic authentication with SMTP AUTH today can use TLS 1.2 connections. If they can’t, those connections will stop working even while basic authentication for SMTP AUTH persists.
Forcing PowerShell to use TLS 1.2 is effective, but it’s a short-term fix. Microsoft will come back to the topic of SMTP AUTH once the dust settles after the removal of basic authentication for the other connection protocols next year. The time will come when Exchange Online ceases support for basic authentication with SMTP AUTH connections.
Microsoft’s preferred method for sending secure email with Exchange Online is to use the Graph APIs. You can do this in two ways by upgrading scripts to replace calls to the Send-MailMessage cmdlet with:
Graph APIs use modern authentication, so the basic authentication issue doesn’t arise.
It’s time to inventory the scripts in your tenant which send email via Exchange Online to know what needs to be done, make sure that TLS 1.2 is used by all scripts, and consider the best option for future upgrades.
Insight like this doesn’t come with hard work and experience. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>I recently published a Practical365.com article explaining how to create a licensing report for an Office 365 tenant using cmdlets from the Microsoft Graph SDK for PowerShell.
A reader asked: “I am trying to determine when a specific license, in this case an E3 Security and Mobility license, was added for all users.”
It’s an interesting question. As written, my script generates a report based on the licenses and service plans assigned to user accounts. However, it doesn’t do anything to tell you when a license for a product like Enterprise Security and Mobility E3 (EMS E3) is assigned to a user. This is because Azure AD does not record assignment dates for the product license information held for user accounts. Instead, license information for an account is presented as a table of SKU identifiers with any disabled service plans in a SKU noted:
$User,AssignedLicenses DisabledPlans SkuId ------------- ----- {bea4c11e-220a-4e6d-8eb8-8ea15d019f90} b05e124f-c7cc-45a0-a6aa-8cf78c946968 {} 8c4ce438-32a7-4ac5-91a6-e22ae08d9c8b {} 4016f256-b063-4864-816e-d818aad600c9 {a23b959c-7ce8-4e57-9140-b90eb88a9e97} 6fd2c87f-b296-42f0-b197-1e91e994b900
However, date information is included in the service plan information for an account:
$User.AssignedPlans AssignedDateTime CapabilityStatus Service ServicePlanId ---------------- ---------------- ------- ------------- 09/11/2021 10:33:37 Enabled AzureAdvancedThreatAnalytics 14ab5db5-e6c4-4b20-b4bc-13e36fd2227f 09/11/2021 10:33:37 Enabled AADPremiumService eec0eb4f-6444-4f95-aba0-50c24d67f998 09/11/2021 10:33:37 Enabled RMSOnline 5689bec4-755d-4753-8b61-40975025187c 09/11/2021 10:33:37 Enabled SCO c1ec4a95-1f05-45b3-a911-aa3fa01094f5 09/11/2021 10:33:37 Enabled AADPremiumService 41781fb2-bc02-4b7c-bd55-b576c07bb09d 09/11/2021 10:33:37 Enabled MultiFactorService 8a256a2b-b617-496d-b51b-e76466e88db0 09/11/2021 10:33:37 Enabled RMSOnline bea4c11e-220a-4e6d-8eb8-8ea15d019f90 09/11/2021 10:33:37 Enabled RMSOnline 6c57d4b6-3b23-47a5-9bc9-69f17b4947b3 09/11/2021 10:33:37 Enabled Adallom 2e2ddb96-6af9-4b1d-a3f0-d6ecfd22edb2
Given that date information is available for service plans, it should therefore be possible to check against the service plan information for user accounts to find assignments of a service plan belonging to a product (SKU). Looking at the Product names and service plan identifiers for licensing page , we find the list of service plans included in EMS E3 (SKU identifier efccb6f7-5641-4e0e-bd10-b4976e1bf68e). The set of service plans are:
The theory is that you should be able to check accounts assigned EMS E3 to retrieve information about one of the service plans in the SKU and retrieve and report the assigned date. I don’t have EMS E3 in my tenant, but I do have EMS E5. I therefore checked the theory by running this PowerShell code:
# Check the date when a service plan belonging to a product like EMS E3 is assigned to an account $EMSE3 = "efccb6f7-5641-4e0e-bd10-b4976e1bf68e" # Product SKU identifier for Enterprise Mobility and Security E3 $EMSE5 = "b05e124f-c7cc-45a0-a6aa-8cf78c946968" # Product SKU identifier for Enterprise Mobility and Security E5 $TestSP = "41781fb2-bc02-4b7c-bd55-b576c07bb09d" # Azure Active Directory Premium P1 $Report = [System.Collections.Generic.List[Object]]::new() # Find tenant accounts Write-Host "Finding Azure AD accounts..." [Array]$Users = Get-MgUser -Filter "UserType eq 'Member'" -All | Sort DisplayName ForEach ($User in $Users) { ForEach ($SP in $User.AssignedPlans) { If (($User.AssignedLicenses.SkuId -contains $EMSE5) -and ($SP.ServicePlanId -eq $TestSP -and $SP.CapabilityStatus -eq "Enabled")) { $ReportLine = [PSCustomObject][Ordered]@{ User = $User.DisplayName UPN = $User.UserPrincipalName ServicePlan = $SP.Service ServicePlanId = $SP.ServicePlanId Assigned = Get-Date($SP.AssignedDateTime) -format g } $Report.Add($ReportLine) } #End if } #End ForEach Service plans } #End ForEach Users
After defining some variables, the code calls the Get-MgUser cmdlet to find the Azure AD accounts in the tenant (I used the script described in this article as the basis; see this article for more information about the Microsoft Graph SDK for PowerShell). Make sure that you connect to the beta endpoint as license information is not available with the V1.0 endpoint (run Select-MgProfile beta after connecting to the Graph).
Next, the code checks the assigned plans and if the desired plan belongs to the right product and is enabled, we report it. Each line in the report is like this:
User : Kim Akers UPN : Kim.Akers@office365itpros.com ServicePlan : AADPremiumService ServicePlanId : 41781fb2-bc02-4b7c-bd55-b576c07bb09d Assigned : 11/11/2017 16:52
This is a quick and dirty answer to the problem of discovering when a product license is assigned to user accounts. It might serve to fill in while Microsoft improves matters.
As reported by Vasil Michev, Microsoft recently added a licenseAssignmentState resource to the Graph API. This isn’t yet available for PowerShell, but the date information can be retrieved using the Graph. In this snippet, we find user accounts and examine their assignment state for EMS E5 to discover when the license was assigned. The code assumes that you’ve already used a registered app to authenticate and fetch an access token to interact the Graph APIs. Remember that you might need to use pagination to fetch all the pages of user data available in the tenant. Anyway, here’s my quick and dirty code to prove the point:
# Use the Graph API to check license assignment states Write-Host "Fetching user information from Azure AD..." $Uri = "https://graph.microsoft.com/v1.0/users?&`$filter=userType eq 'Member'" [Array]$Users = (Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Get -ContentType "application/json") $Users = $Users.Value Write-Host “Processing users…” ForEach ($User in $Users) { $Uri = "https://graph.microsoft.com/beta/users/" + $User.UserPrincipalName + "?`$select=licenseAssignmentStates" [Array]$Assignments = Get-GraphData -Uri $Uri -AccessToken $Token ForEach ($License in $Assignments.LicenseAssignmentStates) { $LicenseUpdateDate = $Null If ($License.SkuId -eq $EMSE5 -and $License.State -eq "Active") { If ([string]::IsNullOrWhiteSpace(($License.lastUpdatedDateTime)) -eq $False ) { $LicenseUpdateDate = Get-Date($License.lastUpdatedDateTime) -format g } Else { $LicenseUpdateDate = "Not set" } Write-Host ("Last update for EMS for {0} on {1}" -f $User.DisplayName, $LicenseUpdateDate) } } # End ForEach License } # End ForEach User Last update for EMS for Tony Redmond on 15/07/2021 15:28 Last update for EMS for Andy Ruth (Director) on Not set Last update for EMS for Kim Akers on 26/10/2021 16:58 Last update for EMS for Jack Hones on Not set Last update for EMS for Oisin Johnston on 03/10/2020 13:18
The dates retrieved using this method differ to the values you get from service plans because Microsoft is populating these values using the last licensing change made to the account. However, in the future, the dates will be more accurate and usable because they will capture changes, hopefully when PowerShell access is possible.
In passing, I note that the Office 365 audit log captures a “Change user license” audit record when an administrator updates the licenses for an account. However, the audit record doesn’t include details of what licenses were added, changed, or removed. The Azure AD team could do a better job of capturing audit information about license updates. I’m sure they’ll be happy to hear that.
Keep up with the changing world of the Microsoft 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. Monthly updates mean that our subscribers learn about new development as they happen.
]]>Microsoft’s October 5 announcement that they will decommission 25 Exchange Web Services (EWS) APIs by March 31, 2022, contains some clear signals that EWS is on a short runway to oblivion. Microsoft says that the set of 25 APIs selected to go first (Figure 1) are those least used with Exchange Online.
Microsoft also says that they’ll remove the ability to create new EWS apps starting on September 30, 2022. Both steps are part of the sunset process to ease EWS out of Microsoft 365, with Microsoft noting that “EWS is a legacy API surface that has served us well, but no longer meets the security and manageability needs of modern app development.”
Microsoft created EWS as a SOAP-based protocol to allow ISVs access to Exchange Server content. In fact, when it appeared in Exchange 2007, EWS was the first successful API for Exchange that Microsoft delivered since MAPI. Since then, EWS has been used by Microsoft in the Entourage and Outlook for Mac clients and by ISVs for many different purposes, including migration of mailbox and PST data to Exchange Online and as the foundation for backup products.
The introduction of the Microsoft Graph APIs as the preferred access method for data across Microsoft 365 marked the beginning of the end for EWS. The first sign was Microsoft’s July 2018 announcement that EWS would receive no further feature updates. In many cases, a static API is a dead API, especially when its owner’s attention is directed firmly at another API.
It’s hard to deny that Microsoft is wrong to force people towards the Microsoft Graph APIs at this point. The Graph APIs are newer, used across Microsoft 365, support standards like OAuth and OData, and its REST-based model is familiar to many programmers. The wide range of tools like the Graph Explorer and SDKs in different programming languages make it easier for developers. It’s also true that the Graph APIs have more granular security policies. But the biggest thing is that the Graph is where the puck is going.
The second sign was the inclusion of EWS in the set of connectivity protocols which Microsoft plans to disable for basic authentication. The latest turn-off date is October 1, 2022.
All of which brings us to the need for organizations to figure out how EWS is used inside their tenants and draw up plans for its replacement. The focus areas to look for EWS include:
Every organization is different and EWS has been around for a long time. The two factors make it difficult to give a definitive set of guidelines for what to look. Three obvious areas are migration, connectors, and backup. Any product which streams information into or out of Exchange Online should be questioned to establish what API is in use. Most backup products for Exchange Online use EWS. The protocol was never intended as the foundation for backup, but nothing else is available so ISVs have used what they can.
As they digest the news that EWS is in terminal decline, ISVs will naturally look for a replacement. Right now, the Microsoft Graph APIs cover Outlook (mail) and other objects stored inside mailboxes like tasks, notes, and contacts. However, there isn’t any obvious Graph API to handle large-scale streaming of mailbox content of the type usually experienced in backup and restore operations.
Microsoft has just released a Graph export API for Teams which is deemed suitable for backup and restore scenarios. However, that API comes with consumption meters and a per-message charging model which creates a whole new economic model for pricing for backup data. The volume of Teams messages is lower than Exchange Online email and the size of Exchange Online mailboxes is larger (even with the new 1.5 TB limit for auto-expanding archives). If Microsoft decides to impose the same consumption pricing model on an export/import API for Exchange Online, it could create many headaches for customers and ISVs alike to deal with the costs associated with scenarios like:
Although all the signs are that EWS will slip away soon, Microsoft hasn’t set a final (public) date for the removal of EWS from Exchange Online. You could put your head into the sand and hope that the protests of customers and ISVs will be sufficient to force Microsoft to keep EWS going for longer. I don’t think that’s a great strategy. It’s better to accept that the days of EWS are numbered (in the low hundreds) and start working on replacement components for your IT infrastructure. You know it makes sense.
Learn about Exchange Online and the rest of the Office 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. Use our experience to understand what’s importance and how changes affect your tenant.
]]>A reader asked about the meaning of x:x in a Graph API query included in the article about upgrading Office 365 PowerShell scripts to use the Graph. You see this construct (a Lambda operator) in queries like those necessary to find the set of accounts assigned a certain license. For example, to search for accounts assigned Office 365 E3 (its SKU or product identifier is always 6fd2c87f-b296-42f0-b197-1e91e994b900):
https://graph.microsoft.com/beta/users?$filter=assignedLicenses/any(s:s/skuId eq 6fd2c87f-b296-42f0-b197-1e91e994b900)
Find the set of Microsoft 365 Groups in the tenant:
https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(a:a eq 'unified')
Find the set of Teams in the tenant:
https://graph.microsoft.com/beta/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')
As you might expect, because the cmdlets in the Microsoft Graph SDK for PowerShell essentially are wrappers around Graph API calls, these cmdlets use the same kind of filters. For example, here’s how to find accounts with the Office 365 licenses using the Get-MgUser cmdlet:
[array]$Users = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq 6fd2c87f-b296-42f0-b197-1e91e994b900)" -all
All these queries use lambda operators to filter objects using values applied to multi-valued properties. For example, the query to find users based on an assigned license depends on the data held in the assignedLicenses property of Azure AD accounts, while discovering the set of Teams in a tenant relies on checking the resourceProvisioningOptions property for Microsoft 365 groups. These properties hold multiple values or multiple sets of values rather than simple strings or numbers. Because this is a query against a multivalue property for an Entra ID directory object, it’s called an advanced query.
Accessing license information is a good example to discuss because Microsoft is deprecating the Azure AD cmdlets for license management at the end of 2022, forcing tenants to upgrade scripts which include these cmdlets to replace them with cmdlets from the Microsoft Graph SDK for PowerShell or Graph API calls. This Practical365.com article explains an example of upgrading a script to use the SDK cmdlets.
If we look at the value of assignedLicenses property for an account, we might see something like this, showing that the account holds three licenses, one of which has a disabled service plan.
disabledPlans skuId ------------- ----- {33c4f319-9bdd-48d6-9c4d-410b750a4a5a} 6fd2c87f-b296-42f0-b197-1e91e994b900 {} 1f2f344a-700d-42c9-9427-5cea1d5d7ba6 {} 8c4ce438-32a7-4ac5-91a6-e22ae08d9c8b
It’s obvious that assignedLicenses is a more complex property than a single-value property like an account’s display name, which can be retrieved in several ways. For instance, here’s the query with a filter to find users whose display name starts with Tony.
https://graph.microsoft.com/v1.0/users?$filter=startswith(displayName,'Tony')
As we’re discussing PowerShell here, remember that you must escape the dollar character in filters. Taking the example above, here’s how it is passed in PowerShell:
$Uri = "https://graph.microsoft.com/v1.0/users?`$filter=startswith(displayName,'Tony')" [array]$Users = Invoke-WebRequest -Method GET -Uri -ContentType "application/json" -Headers $Headers | ConvertFrom-Json
The data returned by the query is in the $Users array and can be processed like other PowerShell objects.
Getting back to the lambda operators, while OData defines two (any and all), it seems like the all operator, which “applies a Boolean expression to each member of a collection and returns true if the expression is true for all members of the collection (otherwise it returns false)” is not used. At least, Microsoft’s documentation says it “is not supported by any property.”
As we’ve seen from the examples cited above, the any operator is used often. This operator “iteratively applies a Boolean expression to each member of a collection and returns true
if the expression is true
for any member of the collection, otherwise it returns false
.”
If we look at the filter used to find accounts assigned a specific license:
filter=assignedLicenses/any(s:s/skuId eq 6fd2c87f-b296-42f0-b197-1e91e994b900)
My interpretation of the component parts (based on Microsoft documentation) of the filter is:
All of this is second nature to professional developers but not so much to tenant administrators who want to develop some PowerShell scripts to automate operations. This then poses the question about how to discover when lambda qualifiers are needed. I don’t have a great answer except to look for examples in:
And when you find something which might seem like it could work, remember that the Graph Explorer is a great way to test queries against live data in your organization. Figure 1 shows the results of a query for license information.
One complaint often extended about Microsoft’s documentation for the Graph APIs is that it pays little attention to suitable PowerShell examples. The Graph SDK developers say that they understand this situation must change and they plan to improve their documentation for PowerShell over the next year. Although understandable that languages like Java and C# have been priorities up to now, Microsoft can’t expect the PowerShell community to embrace the Graph and learn its mysteries (like lambda qualifiers) without help. Let’s hope that the Graph SDK developers live up to their promise!
Learn how to exploit the Office 365 data available to tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.
]]>MC275344 (published August 3, updated August 31, Microsoft 365 roadmap item 81959) deals with the topic of anonymization of user information in Microsoft 365 usage reports. Until now, the situation has been that the usage reports show full usage data, including details of user principal names and group names with an option for the tenant to choose pseudonymized information. In this situation, anonymized values like A6968D016DB2256910FD3B85B4B0457B replace user or group identifiable information in the reports. You can still understand the overall context of the report and what it tells you about the usage pattern for a workload like SharePoint or Teams, but you can’t dive down into the detail at user level.
Microsoft says that de-identifying user data will help tenants support local privacy laws. The changeover to use anonymized data by default came into effect on September 1, 2021. Users with access to report data now see values like those shown in Figure 1.
If you want to revert to see real user information in usage reports, a global administrator can switch through the Reports section of Org-wide settings by clearing the checkbox shown in Figure 2.
Updating the setting captures an UpdatedCFRPrivacySettings audit record. For instance, here’s an edited version of the audit record captured when I enabled identifiable user information in usage reports.
RecordType : CoreReportingSettings CreationDate : 06/09/2021 19:37:55 UserIds : Tony.Redmond@office365itpros.com Operations : UpdatedCFRPrivacySettings AuditData : { "ModifiedProperties": [ { "Name": "PrivacyEnabled", "OldValue": "True", "NewValue": "False" } ], "Id": "639e2bcc-eba9-4146-8885-333622ffb4b0", "RecordType": "CoreReportingSettings", "CreationTime": "2021-09-06T19:37:55", "Operation": "UpdatedCFRPrivacySettings", }
In the past, this would have been sufficient to let any account holding an administrative role with access to usage data to see user information. This is not now the case as Microsoft has made a further change to confine the ability to see user information to “administrative and report reader roles.”
In effect, this means that roles like:
Can see user information (anonymized or real as selected by the tenant setting), but other administrative roles such as Usage summary reports reader or Global reader, which used to be able to see user information, no longer have access. Users with these roles see only summary graphs (Figure 3).
The change affects usage reports in the Microsoft 365 admin center and the Teams admin center. It also affects programmatic access to usage data through the Microsoft Graph usage reports API, including SharePoint site detail. This is because the usage reports API is the basis for reporting across Microsoft 365.
As noted when Microsoft originally introduced anonymized user data for reports, if the organization generates its own version of usage reports like my Office 365 User Activity Report, you’ll need to make sure to generate the report using an account with a suitable administrative role. Identifiable user data makes these kinds of reports much more valuable, especially if you use the reports to analyze usage patterns based on departments, locations, and workloads, and if you want the reports to contain this information, the org-wide setting to allow identifiable user data must be enabled when the report runs. Arranging for this to be done if the organization decides to use anonymized user information for reporting could be a challenge!
There’s no doubt that this is a good step from the perspective of privacy advocates. However, I wonder if obscuring information about how people use technology at the level of detail available in the Graph (like the number of emails sent and read, or Yammer conversations created) will make it harder for administrators to do their job. I agree with the move to restrict access to detailed information to the more highly privileged administrative roles, but wonder how many organizations will try to use anonymized user information before reverting because good reason exists to access detailed data.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
]]>The Experts Conference (TEC) 2021 took place using a mixture of Teams Live Events and online meetings over September 1-2. The TEC organizers have posted videos and slides of the presentations online for all to access.
Naturally, you’ll check out my session on Leveraging the Graph to Manage Microsoft 365 (video and slides). While many of the topics covered in the session have also appeared in articles, there’s nothing like making a pitch on important topics like this to force you to think through the value of the subject.
Many other useful sessions are also available – certainly enough for anyone to find some nuggets of information. I’ve pointed to a set of sessions that I like here, including interesting talks about the Microsoft 365 substrate, Azure AD futures, and defending your company against sophisticated cyberattacks.
The Experts Conference 2022 takes place September 20-21, 2022, in the Loews Atlanta Hotel. Although Teams has served as a worthy platform for TEC 2020 and TEC 2021, it will be great to get back to an in-person conference. Many great speakers have already been lined up, and the combination of talent and in-person interaction should make TEC 2022 a fantastic event to attend. Super early bird registration for TEC 2022 ends Feb 28, 2022, and costs $350. The registration gradually increases to $799, so this is a good example of getting in early to save money.
I look forward to meeting many Office 365 for IT Pros subscribers at TEC 2021 in Atlanta.
]]>The Microsoft Graph Insights API proves different views of users and documents:
Insights are consumed by many apps and Microsoft 365 components such as MyAnalytics, Workplace Analytics, Viva Insights for Teams, and the Office 365 profile card. Figure 1 is a Microsoft graphic to explain the use of the Insights API and its value to “drive productivity and creativity in businesses.”
Delve was the first app to surface insights, but used Office Graph settings to allow users to decide if they wanted to reveal information about their document-centric activities. Some users never want details of their work exposed, even to people who have access to documents, because they either don’t see the need or because they wish to preserve the confidential nature of the information they work with. They can protect content by assigning a sensitivity label with encryption to confidential documents, but this won’t stop document metadata like titles showing up in insights. The feature settings for Delve therefore have a slider to control showing documents in Delve (trending, used, and shared). When the slider is Off, Delve blocks insights based on documents (Figure 2).
In April, I wrote about how Microsoft is replacing Office Graph controls over Item Insights with Microsoft Graph controls. The change is now effective in Microsoft 365 tenants and mean that instead of user-driven control over how the Insights API reveals information, a tenant has:
Access to these settings is available through the Search & Intelligence section of the Microsoft 365 admin center (Figure 3).
The question arises how to find the current set of accounts with the option disabled in Delve so that you can add the accounts to the Azure AD group. As it happens, I was asked this question by a Microsoft customer engineer who wanted to help their customer move to the new Microsoft Graph controls.
The first step is to find the set of accounts with Delve insights disabled. This cannot be done with PowerShell only because no cmdlet exists to retrieve the value of the Delve setting. Instead, we can combine PowerShell with a call to the Graph Users API. Here are the steps:
You can download the script I used to report users with Delve insights disabled from GitHub.
The next step is to review the report and decide which accounts to add to the Azure AD group used to control item insights. To review the data, open the CSV file generated by the script (Figure 4), and remove any accounts which should not be added to the control group.
We can then use the updated CSV file as the input for a script which:
The essential code to fetch the settings from the Graph and update the membership of the control group looks like this:
$InputCSV = "c:\temp\DelveDisabledAccounts.csv" $TenantDetails = Get-AzureADTenantDetail $TenantId = $TenantDetails.ObjectId $TenantName = $TenantDetails.DisplayName $Uri = "https://graph.microsoft.com/beta/organization/" + $TenantId + "/settings/iteminsights" $Settings = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/JSON" -Headers $Headers -UseBasicParsing If ($Settings.isEnabledInOrganization -ne $True) { Write-Host "Insights control setting not set for" $TenantName ; break } Else { $DisabledGraphInsightsGroup = $Settings.disabledForGroup } [array]$CurrentMembers = Get-AzureADGroupMember -ObjectId $DisabledGraphInsightsGroup | Select -ExpandProperty ObjectId Write-Host "Adding users to the Disabled Graph Insights Group" $Users = Import-CSV $InputCSV ForEach ($User in $Users) { If ($User.ObjectId -notin $CurrentMembers) { Write-Host "Adding" $User.Name Add-AzureADGroupMember -ObjectId $DisabledGraphInsightsGroup -RefObjectId $User.ObjectId } }
I haven’t published a script to GitHub for this purpose because the code is straightforward and simple to plug into an existing script (or add to the bottom of the script mentioned above). Happy Insights!
Learn more about how Office 365 really works on an ongoing basis by subscribing to the Office 365 for IT Pros eBook. Our monthly updates keep subscribers informed about what’s important across the Office 365 ecosystem.
]]>Microsoft posts notifications to the message center in the Microsoft 365 admin center to inform tenant administrators about a variety of different updates made to its service. MC272885 posted on Jul 24, 2021, has the title Attachments for messages with Data Privacy Tag, which might leave you scratching your head to understand what Microsoft means. At first glance, the combination of attachments and messages points to email and tag could mean a sensitivity or retention label. But that’s not what it means.
Reading the detail reveals that Microsoft is introducing a new tag for service update messages. Let’s explore what this means.
When Microsoft publishes a service update message, it applies tags to help tenant administrators understand the importance and potential impact of the change (Figure 1).
The tags shown in the message center include:
Many updates have multiple tags. For instance, MC264095 has the major update, feature update, and user impact tags.
Using the Graph API for Service Communications, we can fetch the messages currently available in the Microsoft 365 admin center to see what tags are in use. As you’ll recall, this API spans both incidents (outages) reported in the admin center and service updates. I took the example script I created for service updates and used some of the code to pull all update messages into an array.
$Uri = "https://graph.microsoft.com/beta/admin/serviceAnnouncement/messages" [array]$Messages = Get-GraphData -AccessToken $Token -Uri $uri
I then used some simple code to analyze the tags placed on each message.
$TagAdmin = 0; $TagUpdate = 0; $TagMajor = 0; $TagNew = 0; $TagRetirement = 0; $TagUser = 0; $TagUpdatedMessage = 0; $TagDataPrivacy = 0 ForEach ($Message in $Messages) { ForEach ($Tag in $Message.Tags) { Switch ($Tag) { "Admin impact" {$TagAdmin++} "Feature update" {$TagUpdate++} "New feature" {$TagNew++} "Retirement" {$TagRetirement++} "User impact" {$TagUser++} "Updated message" {$TagUpdatedMessage++} "Data privacy" {$TagDataPrivacy++} } # End Switch } # End Foreach tag If ($Message.IsMajorChange -eq $True) {$TagMajor++} } # End ForEach message Write-Host "Admin impact messages: " $TagAdmin Write-Host "Feature update messages:" $TagUpdate Write-Host "Major update messages: " $TagMajor Write-Host "New feature messages: " $TagNew Write-Host "Retirement messages: " $TagRetirement Write-Host "User impact messages: " $TagUser Write-Host "Updated messages: " $TagUpdatedMessage Write-Host "Data privacy messages: " $TagDataPrivacy Admin impact messages: 165 Feature update messages: 65 Major update messages: 76 New feature messages: 119 Retirement messages: 31 User impact messages: 191 Updated messages: 96 Data privacy messages 0
The total count of messages was 266. You can see that:
Your mileage might vary because Microsoft issues service updates to tenants based on the feature set licensed by the tenant.
Microsoft is introducing a new Data Privacy tag to indicate messages which need administrator attention because they potentially impact sensitive data. The change is due to roll out by the end of July.
Microsoft says that messages might also contain one or more downloadable attachments (if multiple, the attachments are in a zip file) to help administrators “gain additional insight into the described scenario.” For instance, an attachment might be a PowerShell script to report data or users affected by a service update.
Only accounts holding the Global administrator and Privacy reader roles can access the downloadable attachments.
It’s hard to be certain about how Microsoft will use the new Data Privacy tag and what kind of service update messages they will tag. I guess we will see when some messages appear with the tag (none are found in the messages in my tenant) and the kind of attachments available for the messages.
So much change, all the time. It’s a challenge to stay abreast of all the updates Microsoft makes across Office 365. Subscribe to the Office 365 for IT Pros eBook to receive monthly insights into what’s happening.
]]>Over the last few months, I’ve written many times about using Microsoft Graph API calls in PowerShell scripts to get real work done. Among the many examples are:
In addition, Microsoft has stirred the pot by announcing that they won’t support the Azure AD Graph from June 2022. This affects the Azure AD PowerShell module, one of the most heavily used modules for Office 365 tenant management. And we have examples where Microsoft introduces new features, like tenant privacy controls, which can be controlled only through Graph API calls.
As a result of this activity, I’ve received several questions about how to decide when to use Graph API calls in scripts. And as I due to speak about combining PowerShell and the Graph at the (free) TEC 2021 event in September, it seemed like a good idea to formulate some thoughts about how I approach the issue.
I use a simple four-step process when writing scripts to automate some aspect of Office 365:
Sketch out the solution: Understand what source data is available and how to access it. Define the expected output and the processing needed to achieve the result. Make an initial selection of PowerShell modules and Graph APIs which might be useful, understanding that some data is only accessible to the Graph (and might need a beta API). Do an internet search to see if anyone has already written code to do what you want or something similar. Never reinvent the wheel if someone else has one to use.
Code in PowerShell first: It’s often wise to write the initial code in PowerShell before introducing any Graph APIs. The code you write might work well enough to be the solution you need without doing any further work. This is often the case when a small amount of data is involved, in which case you don’t need the additional overhead necessary to introduce Graph API calls.
Speed Things Up: Usually, the biggest advantage gained through using Graph APIs is speed, especially when fetching large numbers of objects like user accounts or groups. The next step is to find places in your code where large delays occur to run calls like Get-UnifiedGroup and replace those cmdlets with Graph APIs.
Adjust for Production: Every tenant has their own idea of how to run PowerShell scripts in production. After developing a script which can run interactively, you might need to change it to run as a background process and deal with issues like certificate-based authentication (never store passwords in scripts). Because of the need to adjust scripts for production usage, the code I write for books and articles is to illustrate principles rather than being fully worked-out answers.
The most important point in the checklist is the internet search for code. If you don’t find a suitable script to remove the need to create anything new, you’ll probably find the basis or starting point for what you want to do. It astounds me that people post questions in forums when it is perfectly obvious that they haven’t done the basic research to uncover details which can help solve their problem. Unfortunately, too many people expect answers to be handed to them on a plate and aren’t prepared to learn through experimentation and failure. I spend most of my time in the latter state.
The Microsoft Graph isn’t scary. It’s there to be used and like any other tool, it should be used at the right time. PowerShell gets most things done really well when it comes to tenant management. It has its limitations, some of which the Graph can fill in. Starting with simple tasks and moving forward to more complex issues is a great way to learn how to use the Graph with PowerShell. Your task is to provide the brainpower to combine the two to get things done most effectively.
Learn how to exploit the Office 365 data available to tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.
]]>Sometimes I hate PowerShell. Not the language itself, just my ineptitude, or my inability to remember how to do things, or the speed of some cmdlets which deal with objects like mailboxes and groups. It’s not that the cmdlets are inefficient. They do a lot of work to retrieve information about objects, so they are slow.
This is fine for ad-hoc queries or where you only need to process a couple of hundred mailboxes or groups. The problem is accentuated as numbers grow, and once the need exists to process thousands of objects, some significant time is spent waiting for cmdlets to complete, meaning that scripts can take hours to run.
Microsoft has made significant progress in the Exchange Online PowerShell module to introduce faster cmdlets like Get-ExoMailbox and Get-ExoMailboxStatistics. These REST-based cmdlets are faster and more robust than their remote PowerShell cousins and these improvements are ample justification for the work needed to revisit and upgrade scripts. The module also supports automatic renewal of sessions to Exchange Online and the Security and Compliance endpoints, so it’s all good.
Things aren’t so impressive with Get-UnifiedGroup, which retrieves details about Microsoft 365 Groups. Reflecting the use of Microsoft 365 groups, Get-UnifiedGroup is a complex cmdlet which assembles details from Azure AD, Exchange Online, and SharePoint Online to give a full picture of group settings. Running Get-UnifiedGroup to fetch details of 200 groups is a slow business; running the cmdlet to fetch details of 10,000 groups is a day-long task. The Get-Team cmdlet is no speedster either. In their defense, Microsoft designed these cmdlets for general-purpose interaction with Groups and Teams and not to be the foundation for reporting thousands of objects over a short period.
If you only need a list of Microsoft 365 Groups, it’s also possible to create the list using the Get-Recipient cmdlet.
Get-Recipient -RecipientTypeDetails GroupMailbox -ResultSize Unlimited
Creating a list of groups with Get-Recipient is usually much faster than creating it with Get-UnifiedGroup. However, although you end up with a list of groups, Get-Recipient doesn’t return any group-related properties, so you usually end up running Get-UnifiedGroup to retrieve settings for an individual group before you can process it. Still, that overhead can be spread out over the processing of a script and might only be needed for some but not all groups.
Which brings me to the Microsoft Graph API for Groups. As I’ve pointed out for some years, using Graph APIs with PowerShell is a nice way to leverage the approachability of PowerShell and the power of the Graph. The script to create a user activity report from Graph data covering Exchange, SharePoint, OneDrive, Teams, and Yammer is a good example of how accessible the Graph is when you get over the initial learning curve.
Three years ago, I wrote about a script to find obsolete Teams and Groups based on the amount of activity observed in a group across Exchange Online, SharePoint Online, and Teams. In turn, that script was based on an earlier script which processed only Office 365 Groups. Since then, I have tweaked the script in response to comments and feedback and everything worked well. Except that is, once the script ran in large environments supporting thousands of groups. The code worked, but it was slow, and prone to time-outs and failures.
The solution was to dump as many PowerShell cmdlets as possible and replace them with Graph calls. The script (downloadable from GitHub) now uses the Graph to retrieve:
The result is that the script is much faster than before and can deal with thousands of groups in a reasonable period. Fetching the group list still takes time as does fetching all the bits that Get-UnifiedGroup returns automatically. On a good day when the service is lightly loaded, the script takes about six seconds per group. On a bad day, it could be eight seconds. Even so, the report (Figure 1) is generated about three times faster.
Results - Teams and Microsoft 365 Groups Activity Report V5.1 -------------------------------------------------------------- Number of Microsoft 365 Groups scanned : 199 Potentially obsolete groups (based on document library activity): 121 Potentially obsolete groups (based on conversation activity) : 130 Number of Teams-enabled groups : 72 Percentage of Teams-enabled groups : 36.18% Total Elapsed time: 1257.03 seconds Summary report in c:\temp\GroupsActivityReport.html and CSV in c:\temp\GroupsActivityReport.csv
The only remaining use of an “expensive” cmdlet in the script is when Get-ExoMailboxFolderStatistics fetches information about compliance items for Teams stored in Exchange Online mailboxes. The need for this call might disappear soon when Microsoft eventually ships the Teams usage report described in message center notification MC234381 (no sign so far despite a promised delivery of late February). Hopefully, that report will include an update to the Teams usage report API to allow fetching of team activity data like the number of conversations over a period. If this happens, I can eliminate calling Get-ExoMailboxFolderStatistics and gain a further speed boost.
The downsides of using the Graph with PowerShell are that you need to register an app in Azure Active Directory and make sure that the app has the required permissions to access the data. This soon becomes second nature, and once done, being able to process data faster than is possible using the general-purpose Get-UnifiedGroup and Get-Team cmdlets is a big benefit when the time comes to process more than a few groups at one time.
]]>In my article about how to decrypt SharePoint Online documents with PowerShell, I explained how to use the Unlock-SPOSensitivityLabelEncryptedFile cmdlet to decrypt protected SharePoint files by removing the sensitivity labels protecting the files. The example script uses cmdlets from the SharePoint PnP module to return a set of files from a folder in a document library for processing, and the unlock cmdlet then removes protection from any file with a sensitivity label.
The script works, but it’s not as flexible as I would like. For instance, because PnP can’t distinguish files with labels, every document in the folder is processed whether it is labelled or not. This does no harm, but it’s not something that you might want to do in the case of something like a tenant-to-tenant migration where thousands of protected documents might need to be processed.
Update May 10, 2021: The latest version of the SharePoint Online PowerShell module contains the Get-FileSensitivityLabelInfo cmdlet. This can be run to return the label status of a file, including if the label assigned to the file encrypts the file. The existence of this cmdlet removes some of the need to use the Graph to find and remove labels from protected files, but the Graph is still the fastest way to get the job done.
Which brings me to an updated version of the script (available from GitHub), which uses the Sites API from the Microsoft Graph to navigate through SharePoint Online and find labelled documents to process. Apart from being able to search for documents with sensitivity labels, a Graph API is usually the fastest way to deal with large numbers of objects.
Because we’re making Graph calls from PowerShell, we need to create a registered app in Azure AD to use as the entry point to the Graph (the same steps as outlined in this post are used). The app needs to be able to read site data, so I assigned it Sites.Read.All and Sites.ReadWrite.All permissions (Figure 1).
The script accepts two parameters: the name of the site to search (not the URL) and an optional folder. If multiple matching sites are found, the user is asked to choose which one to search (Figure 2).
Once a target site is confirmed, the script figures out if a folder is specified and if that folder exists in the chosen site. In Graph terms, we’re now dealing with drive objects. The default drive is the root folder of a document library and each folder is a different drive. To find folders, we need to find the child objects in the root, identify the right folder, find its drive identifier, and use that to find the files in the folder. All good, clean Graph fun.
The Drive API returns a maximum of 200 items at a time, so some Nextlink processing is needed to fetch the complete set of files in a folder. Each file is examined to figure out if it has a sensitivity label with protection, and if so, the display name of the label. After processing all the files, we tell the user what we’ve found and ask permission to go ahead and decrypt the files (Figure 3). If the user chooses not to proceed, the script writes details of the protected files out to a CSV file.
Files are decrypted by calling the Unlock-SPOSensitivityLabelEncryptedFile cmdlet. There’s no native Graph API call to decrypt SharePoint documents. In any case, we’re running a PowerShell script so it’s easy to call the cmdlet.
The script is an example of what’s possible with a combination of PowerShell and Graph API calls. I’m sure that the code and the functionality can be improved (feel free to suggest changes and improvements via GitHub). I’m just happy to demonstrate how things work and how including the Graph enables some extra flexibility.
Read the Office 365 for IT Pros eBook to find much more information about how sensitivity labels work – and many PowerShell examples too!
]]>