Microsoft Graph – Office 365 for IT Pros https://office365itpros.com Mastering Office 365 and Microsoft 365 Fri, 13 Sep 2024 09:05:56 +0000 en-US hourly 1 https://i0.wp.com/office365itpros.com/wp-content/uploads/2024/06/cropped-Office-365-for-IT-Pros-2025-Edition-500-px.jpg?fit=32%2C32&ssl=1 Microsoft Graph – Office 365 for IT Pros https://office365itpros.com 32 32 150103932 Copilot Usage Report APIs Available https://office365itpros.com/2024/09/13/copilot-usage-report-api/?utm_source=rss&utm_medium=rss&utm_campaign=copilot-usage-report-api https://office365itpros.com/2024/09/13/copilot-usage-report-api/#comments Fri, 13 Sep 2024 07:00:00 +0000 https://office365itpros.com/?p=66347

Copilot Usage Reports Weak on Detail

Announced in message center notification MC877369 (29 August 2024, Microsoft 365 roadmap item 396562), the Microsoft Graph beta usage reports API now includes support for Copilot for Microsoft 365 tenant usage data. All tenants that use Copilot for Microsoft 365 should now have access to the usage data.

Microsoft says that the availability of this information will “facilitate the creation of customized reporting and analytics,” but the fact is that the data exposed by the API is bare-bones. On the upside, the data matches what’s available in the report section of the Microsoft 365 admin center (Figure 1).

  • Tenant-level summary of Copilot-enabled (licensed) users and active users.
  • Adoption trend (tenant summary) over time.
  • Last activity date for Copilot interaction in different apps for each user.
Copilot usage reports in the Microsoft 365 admin center.
Figure 1: Copilot usage reports in the Microsoft 365 admin center

Accounts accessing the Graph data must have a Copilot for Microsoft 365 license.

User Count Summary

The user count summary report returns a count of the user accounts licensed for Copilot for Microsoft 365 (enabled users) and a count of the users with an active interaction with Copilot in each app during the reporting period (7, 30, 90, or 180 days). Unsurprisingly, when someone is enabled for Copilot in one app, they’re usually enabled for all:

  • Teams
  • Outlook (classic, new Outlook for Windows, OWA).
  • Excel.
  • PowerPoint.
  • Copilot Graph-grounded chat (aka Copilot Chat).
  • OneNote.
  • Loop.
$Uri = "https://graph.microsoft.com/beta/reports/getMicrosoft365CopilotUserCountSummary(period='D90')"
$Data = Invoke-GraphRequest -Uri $Uri -Method Get
$Data.value.adoptionByProduct

Name                           Value
----                           -----
loopEnabledUsers               100
reportPeriod                   90
oneNoteActiveUsers             3
wordEnabledUsers               100
powerPointEnabledUsers         100
microsoftTeamsActiveUsers      97
oneNoteEnabledUsers            100
excelActiveUsers               43
loopActiveUsers                2
copilotChatEnabledUsers        100
outlookEnabledUsers            100
anyAppEnabledUsers             100
anyAppActiveUsers              97
microsoftTeamsEnabledUsers     100
excelEnabledUsers              100
wordActiveUsers                61
powerPointActiveUsers          12
copilotChatActiveUsers         73
outlookActiveUsers             18

User Activity Detail

This report is the most interesting because it details the last activity date for Copilot interaction by users with each of the various Copilot-enabled apps. In addition, the last activity date for any Copilot interaction with any of the supported apps is published (lastActivityDate). An array (value) holds a separate usage report for each Copilot-enabled account.

The user principal name and display name is obfuscated if the tenant data privacy control is enabled. In the following extract, we see that the user has never used Copilot for Loop and OneNote and hasn’t used Copilot with PowerPoint since April 11, 2024:

$Uri = https://graph.microsoft.com/beta/reports/getMicrosoft365CopilotUsageUserDetail(period='D90')
$Data = Invoke-GraphRequest -Uri $Uri -Method Get
$Data.value[0]

Name                           Value
----                           -----
copilotActivityUserDetailsByP… {System.Collections.Hashtable}
microsoftTeamsCopilotLastActi… 2024-09-05
outlookCopilotLastActivityDate 2024-08-29
lastActivityDate               2024-09-05
reportRefreshDate              2024-09-08
excelCopilotLastActivityDate   2024-09-05
loopCopilotLastActivityDate
oneNoteCopilotLastActivityDate
copilotChatLastActivityDate    2024-09-05
powerPointCopilotLastActivity… 2024-04-11
userPrincipalName              Tony.Redmond@office365itpros.com
displayName                    Tony Redmond
wordCopilotLastActivityDate    2024-09-05

Adoption Trend over Time

This report returns an array called adoptionByDate with entries for each day during the reporting period (7, 30, 90, or 180 days). The purpose of the report is to track progress in Copilot adoption over time and to note if any specific action had an effect. For instance, you might run an education campaign to teach users how to generate effective results using Copilot in Excel. Over the weeks following the campaign, you’d expect to see the number of users who use Copilot in Excel to grow.

$Uri = "https://graph.microsoft.com/beta/reports/getMicrosoft365CopilotUserCountTrend(period='D90')"
$Data = Invoke-GraphRequest -Uri $Uri -Method Get
$Data.Value.copilotActivityUserDetailsByPeriod

reportDate                     2024-06-17
excelEnabledUsers              100
wordActiveUsers                51
powerPointActiveUsers          11
copilotChatActiveUsers         66
outlookActiveUsers             15
loopEnabledUsers               100
oneNoteActiveUsers             1
wordEnabledUsers               100
powerPointEnabledUsers         100
microsoftTeamsActiveUsers      86
oneNoteEnabledUsers            1
excelActiveUsers               21
loopActiveUsers                1
copilotChatEnabledUsers        100
outlookEnabledUsers            100
anyAppEnabledUsers             100
anyAppActiveUsers              86
microsoftTeamsEnabledUsers     100

Track Copilot Activity Using Audit Records instead of Copilot Usage Reports

Although it’s nice to have Copilot usage reports included in the Graph API, the information exposed isn’t very informative in terms of how people use Copilot. The data tells you that someone used Copilot in an app during a day. At least, they clicked a Copilot button. The information doesn’t reveal any more insight than that. Any enterprise who invests large sums of money in expensive Copilot for Microsoft 365 licenses will find a dearth of detail here in terms of understanding whether the investment is justified. In many cases, you will be better off analyzing the audit records captured for Copilot interactions to figure out what’s really going on.


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.

]]>
https://office365itpros.com/2024/09/13/copilot-usage-report-api/feed/ 1 66347
Transferring Reusable PowerShell Objects Between Microsoft 365 Tenants https://office365itpros.com/2024/09/03/tojsonstring-method/?utm_source=rss&utm_medium=rss&utm_campaign=tojsonstring-method https://office365itpros.com/2024/09/03/tojsonstring-method/#respond Tue, 03 Sep 2024 07:00:00 +0000 https://office365itpros.com/?p=66220

The Graph SDK’s ToJsonString Method Proves Its Worth

ToJsonString Method is valuable

One of the frustrations about using the internet is when you find some code that seems useful, copy the code to try it out in your tenant, and discover that some formatting issue prevents the code from running. Many reasons cause this to happen. Sometimes it’s as simple as an error when copying code into a web editor, and sometimes errors creep in after copying the code, perhaps when formatting it for display. I guess fixing the problems is an opportunity to learn what the code really does.

Answers created by generative AI solutions like ChatGPT, Copilot for Microsoft 365, and GitHub Copilot compound the problem by faithfully reproducing errors in its responses. This is no fault of the technology, which works by creating answers from what’s gone before. If published code includes a formatting error, generative AI is unlikely to find and fix the problem.

Dealing with JSON Payloads

All of which brings me to a variation on the problem. The documentation for Graph APIs used to create or update objects usually include an example of a JSON-formatted payload containing the parameter values for the request. The Graph API interpret the JSON content in the payload to extract the parameters to run a request. By comparison, Microsoft Graph PowerShell SDK cmdlets use hash tables and arrays to pass parameters. The hash tables and arrays mimic the elements of the JSON structure used by the underlying Graph APIs.

Composing a JSON payload is no challenge If you can write perfect JSON. Like any other rules for programming or formatting, it takes time to become fluent with JSON, and who can afford that time when other work exists to be done? Here’s a way to make things easier.

Every object generated by a Graph SDK cmdlet has a ToJsonString method to create a JSON-formatted version of the object. For example:

$User = Get-MgUser -UserId Kim.Akers@office365itpros.com
$UserJson = $User.ToJsonString()

$UserJson
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "id": "d36b323a-32c3-4ca5-a4a5-2f7b4fbef31c",
  "businessPhones": [ "+1 713 633-5141" ],
  "displayName": "Kim Akers (She/Her)",
  "givenName": "Kim",
  "jobTitle": "VP Marketing",
  "mail": "Kim.Akers@office365itpros.com",
  "mobilePhone": "+1 761 504-0011",
  "officeLocation": "NYC",
  "preferredLanguage": "en-US",
  "surname": "Akers",
  "userPrincipalName": Kim.Akers@office365itpros.com
}

The advantages of using the ToJsonString method instead of PowerShell’s ConvertTo-JSON cmdlet is that the method doesn’t output properties with empty values. This makes the resulting output easier to review and manage. For instance, the JSON content shown above is a lot easier to use as a template for adding new user accounts than the equivalent generated by ConvertTo-JSON.

Transferring a Conditional Access Policy Using ToJsonString

The output generated by ToJsonString becomes very interesting when you want to move objects between tenants. For example, let’s assume that you use a test tenant to create and fine tune a conditional access policy. The next piece of work is to transfer the conditional access policy from the test tenant to the production environment. Here’s how I make the transfer:

  • Run the Get-MgIdentityConditionalAccessPolicy cmdlet to find the target policy and export its settings to JSON. Then save the JSON content in a text file.
$Policy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId '1d4063cb-5ebf-4676-bfca-3775d7160b65'
$PolicyJson = $Policy.toJsonString()
$PolicyJson > PolicyExport.txt
  • Edit the text file to replace any tenant-specific items with equivalent values for the target tenant. For instance, conditional access policies usually include an exclusion for break glass accounts, which are listed in the policy using the account identifiers. In this case, you need to replace the account identifiers for the source tenant in the exported text file with the account identifiers for the break glass account for the target tenant.
  • Disconnect from the source tenant.
  • Connect to the target tenant with the Policy.ReadWrite.ConditionalAccess scope.
  • Create a variable ($Body in this example) containing the conditional policy settings.
  • Run the Invoke-MgGraph-Request cmdlet to import the policy definition into the target tenant.
$Uri = "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies"
Invoke-MgGraphRequest -uri $uri -method Post -Body $Body

The Other Way

Another way to create a conditional access policy with PowerShell is to run the New-MgIdentityConditionalAccessPolicy cmdlet, which takes a hash table as its payload. It’s easy to translate the JSON into the format used for parameter values stored in the hash table, but it’s even easier to run Invoke-MgGraphRequest and pass the edited version of the JSON exported from the source tenant. Why make things hard for yourself?


This tip is just one of the hundreds included the Automating Microsoft 365 with PowerShell eBook (available separately, as part of the Office 365 for IT Pros (2025 edition) bundle, or as a paperback from Amazon.com).

]]>
https://office365itpros.com/2024/09/03/tojsonstring-method/feed/ 0 66220
Switching Microsoft 365 Data Report Privacy On and Off https://office365itpros.com/2024/08/15/usage-reports-api-ga/?utm_source=rss&utm_medium=rss&utm_campaign=usage-reports-api-ga https://office365itpros.com/2024/08/15/usage-reports-api-ga/#respond Thu, 15 Aug 2024 07:00:00 +0000 https://office365itpros.com/?p=65999

Admin Settings API to Control Usage Reports Data Gets an Update

If you don’t follow the sometimes-anarchic world of the Microsoft Graph, message center notification MC859853 (13 August 2024) might have passed you by without comment. However, given the importance of reporting usage data to understand the activity level within tenants, this is a significant change.

The option to anonymize user information like display names in usage reports generated from the Microsoft Graph has existed since 2020. The control for the option is under Reports in the Org Settings section of the Microsoft 365 admin center and its purpose is to protect the privacy of users. The control affects all access to usage data via the Graph, including reports generated using PowerShell, such as the Teams and Groups Activity Report. In fact, if you choose to obfuscate user data, reports lose much of their value and can make it impossible to derive comparisons between different forms of usage data. For instance, the script to analyze use of different Microsoft 365 workloads by individual accounts to determine who could best use Copilot for Microsoft 365 licenses depends on being able to match user principal names.

Programmatic Access to Set the Privacy Control for Usage Reports Data

It’s useful for programs and scripts to be able to turn the privacy control off to fetch usage data and back on again when finished. Until now, programmatic access to control the privacy setting for usage reports existed in the beta adminReportSettings Graph API. What’s changed is that the API is now generally available and therefore available through the V1.0 Graph endpoint. In the past, a script might have done something like this to check if the privacy setting was on or off:

$Uri = "https://graph.microsoft.com/beta/admin/reportSettings"
$Data = Invoke-MgGraphRequest -Method Get -Uri $Uri
Write-Host ("The current report privacy setting is {0}" -f $Data.displayConcealedNames)
The current report privacy setting is False

Now that the API is generally available and fully supported, the URI is https://graph.microsoft.com/V1.0/admin/reportSettings. For instance, to update the privacy setting to set it on, you’d do:

$Uri = "https://graph.microsoft.com/V1.0/admin/reportSettings"
$Settings = @{}
$Settings.Add("displayConcealedNames","true")
Invoke-MgGraphRequest -Uri $Uri -Method Patch -Body $Settings

The Microsoft Graph PowerShell SDK has just had a refresh to V2.22 but the SDK cmdlets haven’t yet caught up with the change and remain using the beta endpoint. This means that you should use Get-MgBetaAdminReportSetting to fetch values and Update-MgBetaAdminReportSetting to switch the control from on to off or vice versa.

To update the privacy control, the signed-in account must hold the global administrator role and the app used must have consent for the ReportSettings.Read.All permission.

Backup Restore Module in V2.22 of the Microsoft Graph PowerShell SDK

One of the notable things about V2.22 of the Microsoft Graph PowerShell SDK is the appearance of a new beta module for Microsoft 365 Backup (backup and restore operations). To list the commands in the module, run Get-Command:

Get-Command -Module Microsoft.graph.beta.backuprestore

Use of the cmdlets requires consent for the BackupRestore-Control.Read.All permission (Figure 1).

Granting consent for permission to use Microsoft 365 Backup APIs.

Usage Reports API
Figure 1: Granting consent for permission to use Microsoft 365 Backup APIs

Despite having the permission and an active Microsoft 365 Backup schedule in place for SharePoint Online, OneDrive for Business, and Exchange Online, all attempts to use the cmdlets met with an internal error. Oh well, Microsoft 365 backup is only just generally available, and this is a beta module. Things are expected to go wrong. It’s just another opportunity for improvement within the Microsoft 365 ecosystem.

Graph Keeps On Growing

Being able to control usage report data privacy and Microsoft 365 Backup through Graph APIs are two examples of how people might not have considered using the Graph to automate common administrative scenarios. It’s proof of the growing influence of the Graph, and underlines why Microsoft 365 tenant administrators need to become Graph literate.


Stay updated with developments across the Microsoft 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. We do the research to make sure that our readers understand the technology.

]]>
https://office365itpros.com/2024/08/15/usage-reports-api-ga/feed/ 0 65999
Handling the Too Many Retries Error and Dealing with Odd Numbers of Audit Events https://office365itpros.com/2024/08/14/auditlog-query-oddities/?utm_source=rss&utm_medium=rss&utm_campaign=auditlog-query-oddities https://office365itpros.com/2024/08/14/auditlog-query-oddities/#comments Wed, 14 Aug 2024 07:00:00 +0000 https://office365itpros.com/?p=65970

AuditLog Query API Cmdlets Now Available in the Microsoft Graph PowerShell SDK

In April 2024, I wrote about the new AuditLog Query Graph API. At the time, the API exhibited the normal rough edges found in any beta API, but I managed to use it to retrieve records from the Microsoft 365 unified audit log.

Roll forward some months and cmdlets are available for the AuditLog Query Graph API in the beta version of the Microsoft Graph PowerShell SDK (I used version 2.21 to test). Microsoft uses a process called AutoRest to automatically generate SDK cmdlets from Graph API metadata and cmdlets usually turn up a month or so after an API appears. The relevant cmdlets are:

  • New-MgBetaSecurityAuditLogQuery: create and submit an audit log query. Purview processes audit log queries in the background, just like the way audit searches work in the Purview compliance portal.
  • Get-MgBetaSecurityAuditLogQuery: check the processing status of an audit log query. Because background jobs handle the queries, they take much longer to complete than searches performed with the Search-UnifiedAuditLog cmdlet do. One job took 35 minutes to complete when Search-UnifiedAuditLog required three minutes.
  • Get-MgBetaSecurityAuditLogQueryRecord: retrieve the audit records found by the query.

Running a query is a matter of constructing a hash table containing the parameters such as the start and end time and the operations to search for, checking for completion of the job, and downloading the results. You can check out the test script I used from GitHub.

The Too Many Retries Problem

Two oddities occurred during testing. First, “Too many retries performed” errors appeared when running the New-MgBetaSecurityAuditLogQuery cmdlet. A search against the SDK issues revealed that I wasn’t the only one to encounter the problem. Adding the Set-MgRequestContext cmdlet to the script seems to have solved the problem. At least, it hasn’t reappeared.

According to its documentation, the Set-MgRequestContext cmdlet “Sets request context for Microsoft Graph invocations.” This is a delightfully obscure description that means little to most people. The important point is that you can increase the retry delay (in seconds) and maximum retries to get around then “too many retries problem” that seems to afflict some Graph APIs (those dealing with devices and Intune seem to be most affected). The default for these values are 3 (retries) and 3 (seconds delay). The maximums are 10 (retries) and 180 (delay seconds). For example:

Set-MgRequestContext -MaxRetry 10 -RetryDelay 15

Some trial and error is likely required to determine the optimum values for a script.

The Incorrect Audit Record Counts

The second issue was a complete disconnect between the number of audit records returned by the audit log query (10,878) and Search-UnifiedAuditLog (10,879), and the number reported by the Purview compliance portal (2,538).

Audit search results shown in the Purview compliance portal.

AuditLog Query Graph API
Figure 1: Audit search results shown in the Purview compliance portal

The compliance portal loads pages of 150 audit records at a time. If you scroll to the bottom of the list, it loads the next page, and so on. If you’re persistent, it’s possible to advance page by page until the full set of retrieved records is exhausted (Figure 2).

 Paging through the results gets to the end of the audit events
Figure 2: Paging through the results gets to the end of the audit events

I don’t know why the Purview compliance portal shows an incorrect count of audit records found by a search. The reason might be that the actual number of audit records found by a search is not returned by the API. Instead, you must fetch the records to find out how many are found.

Microsoft might be relying on the fact that audit searches are often quite precise (for instance, focusing on Copilot interactions for a single user). These searches don’t return thousands of records. If only 100 audit records are found, it’s easy for the portal to display an accurate count.

AuditLog Query API Still Needs Work

It’s nice to see the AuditLog Query API appear in SDK cmdlets. However, the API is still in beta status and the audit records it returns are less complete than those found by the Search-UnifiedAuditLog cmdlet. I guess everything needs time to mature.


Learn more about how the Microsoft 365 applications 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.

]]>
https://office365itpros.com/2024/08/14/auditlog-query-oddities/feed/ 4 65970
Finding Managers of Users with the Microsoft Graph PowerShell SDK https://office365itpros.com/2024/07/29/find-manager-for-entra-id-account/?utm_source=rss&utm_medium=rss&utm_campaign=find-manager-for-entra-id-account https://office365itpros.com/2024/07/29/find-manager-for-entra-id-account/#comments Mon, 29 Jul 2024 07:00:00 +0000 https://office365itpros.com/?p=65761

Find Manager for Entra ID Accounts is Easy at the Individual Level

Following Friday’s discussion about needing to update the script to create the Managers and Direct Reports report, I was asked what’s the best way to find managers assigned to Entra ID user accounts (Figure 1).

The manager listed in the properties of an Entra ID account.

Find manager for Entra ID account.
Figure 1: Find manager for Entra ID account in the Entra admin center

It is simple to find and report the manager for an individual user account with PowerShell. For instance, to find Sean Landy’s manager, run the Get-MgUserManager cmdlet. The return value is the object identifier for the manager’s account, so to find details of the manager, we must fetch it from the data stored in the additionalProperties property.

Get-MgUserManager -UserId Sean.Landy@office365itpros.com | Select-Object -ExpandProperty additionalproperties

Key               Value
---               -----
@odata.context    https://graph.microsoft.com/v1.0/$metadata#directoryObjects/$entity
@odata.type       #microsoft.graph.user
businessPhones    {+353 1 8816644}
displayName       James Ryan
givenName         James
jobTitle          Chief Story Teller
mail              James.Ryan@office365itpros.com

The Manager property is in the set available to Get-MgUser, but it must be fetched to be available for processing. The property is a reference to another account, so it must be resolved by using the ExpandProperty parameter. Again, the manager’s display name is retrieved from the additionalProperties property.

$UserData = Get-MgUser -UserId Sean.Landy@office365itpros.com -Property displayname, manager -ExpandProperty Manager

$UserData | Format-Table @{n='Employee'; e={$_.displayname}}, @{n='Manager'; e={$data.manager.additionalproperties['displayName']}}

Employee   Manager
--------   -------
Sean Landy James Ryan

Find the Managers for Multiple Users

Challenges emerge when dealing with multiple user accounts. For example, it’s common to retrieve the set of licensed user accounts in a tenant with a complex query that checks for the presence of at least one license. However, adding the ExpandProperty parameter to this command stops it working:

[array]$users = Get-MgUser -Filter "userType eq 'Member' and assignedLicenses/`$count ne 0" -ConsistencyLevel eventual -CountVariable UsersFound -All -PageSize 999 -Property Id, userPrincipalName, displayName, Manager, Department, JobTitle, EmployeeId -ExpandProperty Manager

The error is not terribly helpful:

Expect simple name=value query, but observe property 'assignedLicenses' of complex type 'AssignedLicense'.

Removing the ExpandProperty parameter from the command makes it work, but the Manager property is not populated.

Any filter to find user accounts that needs to populate the Manager property is restricted to a simple query. Here’s an example of a query to find all member accounts and populate the Manager property. A client-side filter then reduces the set to accounts with an assigned manager:

[array]$EmployeesWithManager = Get-MgUser -All -PageSize 999 -Property Id, DisplayName, JobTitle, Department, City, Country, Manager -ExpandProperty Manager -Filter "UserType eq 'Member'"| Where-Object {$_.Manager.id -ne $null}

$EmployeesWithManager | Format-Table id, displayname, @{Name='Manager';expression={$_.Manager.additionalProperties.displayName}} -Wrap

Id                                   DisplayName                             Manager
--                                   -----------                             -------
a3eeaea5-409f-4b89-b039-1bb68276e97d Ben Owens                               James Ryan
d446f6d7-5728-44f8-9eac-71adb354fc89 James Abrahams                          Kim Akers 
cad05ccf-a359-4ac7-89e0-1e33bf37579e James Ryan                              René Artois

The results generated by this code are acceptable because a user account with an assigned manager is probably one used by a human. The account probably has licenses too. Obviously, any account that hasn’t got an assigned manager will be left out of the report.

Looking for User Accounts without Managers

Things get a little more difficult if we reverse the client-side filter and look for member accounts that don’t have an assigned manager:

[array]$EmployeesWithoutManager = Get-MgUser -All -PageSize 999 -Property Id, DisplayName, JobTitle, Department, City, Country, Manager, UserPrincipalName -ExpandProperty Manager -Filter "UserType eq 'Member'"| Where-Object {$_.Manager.id -eq $null}

In addition to user accounts lacking managers, the set of resulting accounts will include utility accounts created by Exchange Online, including:

  • Room and equipment accounts.
  • Shared mailbox accounts.
  • Accounts used for Microsoft Bookings.
  • Accounts synchronized from other tenants in a multi-tenant organization (MTO).
  • Accounts created for submission of messages to the Exchange Online High Volume Email (HVE) solution.
  • Accounts created for Teams meeting rooms.
  • Service accounts created by the tenant for background processing and other reasons.

In a medium to large tenant, there might be thousands of these kinds of accounts cluttering up the view. To remove the utility accounts, create an array containing the object identifiers of the owning accounts:

[array]$CheckList = Get-ExoMailbox -RecipientTypeDetails RoomMailbox, EquipmentMailbox, SharedMailbox, SchedulingMailbox -ResultSize Unlimited | Select-Object -ExpandProperty ExternalDirectoryObjectId

If the tenant uses HVE, add the account identifiers for the HVE accounts to the array.

Get-MailUser -LOBAppAccount | ForEach { $Checklist += $_.ExternalDirectoryObjectId }

Now filter the account list to find those that don’t appear in the list of utility mailboxes:

$EmployeesWithoutManager = $EmployeesWithoutManager | Where-Object {($_.Id -notin $Checklist)}

If the tenant is part of a multi-tenant organization, this filter removes the accounts synchronized from the other tenants:

$EmployeesWithOutManager = $EmployeesWithoutManager | Where-Object {$_.UserPrincipalName -notlike "*#EXT#@*"}

Eventually, you’ll end up with hopefully a very small list of employees without assigned managers and can take the necessary action to rectify the situation.

Entra ID Should Mark Utility Accounts

The problem of dealing with utility accounts that end up in Entra ID with the same status as “human” user accounts is growing. Applications create new member accounts without thinking about the consequences. No problem is apparent because no licenses are consumed, but the steps needed to cleanse the set of accounts returned by Entra ID with cmdlets like Get-MgUser are another trap waiting for the unwary administrator. Microsoft really should do better in this area, like creating a new “utility” value for the UserType property. Would that be so bad?


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.

]]>
https://office365itpros.com/2024/07/29/find-manager-for-entra-id-account/feed/ 9 65761
The Maddening Side of the Microsoft Graph PowerShell SDK https://office365itpros.com/2024/07/26/microsoft-graph-powershell-sdk-odd/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-powershell-sdk-odd https://office365itpros.com/2024/07/26/microsoft-graph-powershell-sdk-odd/#comments Fri, 26 Jul 2024 07:00:00 +0000 https://office365itpros.com/?p=65740

Counting Fetched Objects is a Hard Computer Problem

All software has its own foibles, something clearly evident in the Microsoft Graph PowerShell SDK. You become accustomed to the little oddities and workaround known issues and all is well. But when the underlying foundation of software causes problems, it can cause a different level of confusion.

Take the question posed by MVP Aleksandar Nikolić on Twitter about how many accounts a Get-MgUser command will return (Figure 1).

An apparently simple question for the Get-MgUser cmdlet

Microsoft Graph PowerShell SDK
Figure 1: An apparently simple question for the Get-MgUser cmdlet

The answer is 4 even though the command explicitly requests the return of the top 3 matching objects. Why does this happen? It’s all about Graph pagination and the way it’s implemented in Microsoft Graph PowerShell SDK cmdlets.

Pagination and Page Size

To understand what occurs, run the command with the debug switch to see the actual Graph requests posted by Get-MgUser. The first request is against the Users endpoint and requests the top 2 matching objects.

https://graph.microsoft.com/v1.0/users?$top=2

Note that the Graph request only includes a $top query parameter. This sets the page size of the query and matches the PageSize parameter in the Get-MgUser command. The Top parameter used with Get-MgUser has no significance for the Graph query because it’s purely used to tell PowerShell how many objects to show when the command completes. The use of Top in different contexts is confusing, but few people look behind the scenes to see how the cake is made.

The Graph request respects the page size and fetches 2 objects. However, we asked for 3 objects so some more work is needed to fetch the outstanding item. Microsoft’s Graph documentation says “When more than one query request is required to retrieve all the results, Microsoft Graph returns an @odata.nextLink property in the response that contains a URL to the next page of results.” The skiptoken or nextlink lets the command know that further data remains to be fetched, so it continues to fetch the next page with a request that includes the skiptoken:

https://graph.microsoft.com/v1.0/users?$top=2&$skiptoken=RFNwdAIAAQAAAFI6NWUyZWI1YWIub2ZmaWNlMzY1aXRwcm9zLmNvbV9lbWVhLnRlYW1zLm1zI0VYVCNAUmVkbW9uZEFzc29jaWF0ZXMub25taWNyb3NvZnQuY29tKVVzZXJfYmNmYTZlY2ItY2M1Yi00MzU3LTkwOWMtMWUwZTQ1MDg2NGUzuQAAAAAAAAAAAAA

The follow up request fetches the remaining page containing 2 more objects and completes processing. The person running the command sees 4 objects from the two pages. In effect, the Get-MgUser cmdlet ignores the instruction passes in the Top parameter to only show 3 objects.





The same processing happens with different combinations of page size and objects requested, and it’s the same for other cmdlets too. For instance, the command:

Get-MgGroup-Top 7 -PageSize 3

Returns 9 group objects because 3 page fetches are necessary. It seems odd, and it’s odder still that running Get-MgGroup -Top 7 without specifying a page size will return exactly what we asked for (7 objects), while using a larger page size returns all the objects that can be packed into the page:

Get-MgUser -Top 3 -PageSize 8 | Format-Table Id, DisplayName

Id                                   DisplayName
--                                   -----------
44c0ca9c-d18c-4466-9492-c60c3eb78423 12 Knocksinna (Gmail)
bcfa6ecb-cc5b-4357-909c-1e0e450864e3 Email Channel for Exchange GOMs
da1288d5-e63c-4118-af62-3280823e04e1 GOM Email List for Teams
de67dc4a-4a51-4d86-9ee5-a3400d2c12ff Guest member - Project Condor
3e5a8c92-b9b6-4a45-a174-84ee97e5693f Arthur Smith
63699f2f-a46a-4e99-a068-47a773f9af11 Annie Jopes
7a611306-17d0-4ea0-922e-2924616d54d8 Andy David 
d8afc094-9c9b-4f32-86ee-fadd63b112b2 Aaron Jakes

Frustrating Paging and Display

The typical page size for a Graph request is 100 objects (but this can differ across resources), so it’s unusual to use the Top parameter to request a limited set of objects that’s larger than the default page size. Usually, I bump the page size up to 999 (the maximum) to reduce the number of requests made to fetch large quantities of user or group objects. Using a large page size can significantly affect the performance of queries retrieving large numbers of objects.

The conclusion is that changing the default page size for a Microsoft Graph PowerShell SDK cmdlet overrides the Top parameter. This kind of thing is commonly known as a bug and it’s very frustrating. The Graph requests work perfectly but then something gets in the way of restricting the output to the required number of objects.

Selecting Properties to Use

The same kind of problem arises when Microsoft changes the way Graph requests respond. For instance, this week I was asked why a script I included in an article about reporting Entra ID Managers and their Direct Reports didn’t work. The article dates from April 2023, so neither the text nor the script code is ancient.

Sometime in the intervening period, Microsoft made a change that affected the set of default properties returned by the Get-MgUser cmdlet (probably in the transition to V2.0 of the Microsoft Graph PowerShell SDK). The result meant that some of the properties returned when the script was written are not returned today. The fix is simple: use the Property parameter to specify the properties you expect to use in the script:

[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable Records -All -PageSize 999 -Property Id, displayName, userprincipalName, department, city, country

I believe Microsoft made the change to reduce the strain on Graph resources. It’s annoying to be forced to update scripts because of external factors, especially when cmdlets appear to run smoothly and generate unexpected output.

More Handcrafting Required for the Microsoft Graph PowerShell SDK

The issues discussed here make me think that Microsoft should dedicate more engineering resources to perfecting the Microsoft Graph PowerShell SDK instead of creating a new Entra PowerShell module that duplicates Microsoft Graph PowerShell SDK cmdlets. The statement’s been made that the Entra cmdlets are better because they’re “handcrafted,” which I understand means that humans write the code for cmdlets. T

It’s nice that the Entra module gets such attention, but it would be nicer if the Graph PowerShell SDK received more human handcrafting and love to make it more predictable and understandable. Even Entra ID would benefit from that work.


Stay updated with developments across the Microsoft 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. We do the research to make sure that our readers understand the technology.

]]>
https://office365itpros.com/2024/07/26/microsoft-graph-powershell-sdk-odd/feed/ 3 65740
Upgrading the Teams and Groups Activity Report to 6.0 https://office365itpros.com/2024/07/15/teams-and-groups-activity-6/?utm_source=rss&utm_medium=rss&utm_campaign=teams-and-groups-activity-6 https://office365itpros.com/2024/07/15/teams-and-groups-activity-6/#comments Mon, 15 Jul 2024 06:00:00 +0000 https://office365itpros.com/?p=65597

Updating Old Code to Use the Microsoft Graph PowerShell SDK

Teams and Groups activity report

The Teams and Groups Activity Report is a reasonably popular script which attempts to measure whether teams and groups are in active use based on criteria like the number of messages sent in a team. Processes like this are important because it’s all too easy for a Microsoft 365 tenant to fall into a state of digital rot where unused teams and groups mask where useful work is done.

But like many scripts, the code has evolved over years (since 2016 in this case). The current version uses many Graph API calls and some Exchange Online cmdlets to fetch and analyze statistics. Microsoft recently released the Entra PowerShell module, which is built on top of the Microsoft Graph PowerShell SDK. I think this is a mistake because there are many issues that Microsoft should address in the PowerShell SDK. Dividing their engineering resources and focus across two modules seems like a recipe for inadequacy instead of excellence.

To prove the usefulness of the Microsoft Graph PowerShell SDK, it seemed like a good idea to rewrite the Teams and Groups activity report and replace Graph API requests with PowerShell SDK cmdlets wherever possible. The new Entra PowerShell module is incapable of the task because it deals exclusively with Entra objects, and the script needs to access elements like usage reports to determine if a group or team is active.

Microsoft Graph PowerShell SDK Advantages

By converting to the Microsoft Graph PowerShell SDK, I wanted to take advantages of two specific features offered by the SDK cmdlets. First, you don’t need to worry about pagination. Second, you don’t need to deal with access token acquisition and renewal. Many SDK cmdlets like Get-MgGroup have an All parameter, which instructs a cmdlet to perform automatic pagination to fetch all available items. Token acquisition and renewal is handled automatically for Graph SDK interactive or app-only sessions.

The old version of the script handles pagination and token renewal, but scripts require code to handle these tasks. Extra code means extra places where things can go wrong, and that’s always a concern.

The value passed to the PageSize parameter is another important factor for performance. Cranking its value up to 999 (or whatever the maximum supported value is for a resource like groups) reduces the number of Graph requests required to fetch data, a factor that can be very important when dealing with thousands of groups and teams.

Upgrading Script Code

Like all PowerShell scripts that use Graph API requests, the previous version uses an Entra ID application (or rather, the application’s service principal) to hold the Graph permissions used by the script.

The same technique can be used with the Microsoft Graph PowerShell SDK. In fact, it’s the right way to confine apps to the limited set of permissions necessary to do whatever processing they perform. Using an Entra ID registered app to connect to the Graph means that application permissions are used rather than delegated permissions and therefore the script has access to all data consented through permissions rather than just the data available to the signed-in account, which is the case with an interactive Graph session.

Here’s the code to connect a Graph session in app-only mode. The code specifies the tenant identifier, application identifier, and a certificate thumbprint. After connection, the script can use any permission consented to for the application.

$TenantId = "a662313f-14fc-43a2-9a7a-d2e27f4f3478"
$AppId = "a28e1143-88e3-492b-bf82-24c4a47ada63"
$CertificateThumbprint = "F79286DB88C21491110109A0222348FACF694CBD"
# Connect to the Microsoft Graph
Connect-MgGraph -NoWelcome -AppId $AppId -CertificateThumbprint $CertificateThumbprint -TenantId $TenantId

In the case of the script, the application must hold consent for the Group.Read.All, Reports.Read.All, User.Read.All, GroupMember.Read.All, Sites.Read.All, Organization.Read.All, and Teams.ReadBasic.All application permissions.

Some Hiccups

Like all coding projects, some hiccups occurred.

First, the cmdlets to fetch usage report data don’t seem to be capable of saving the data to a PSObject. Instead, the data must be saved to a temporary CSV file and then imported into an array. Also in this area, the annoying bug that prevents SharePoint usage data returning site URLs persists. It’s only been present since September 2023!

Second, the Get-MgSite cmdlet returned a 423 “site locked” error for some sites when retrieving site information. As it turned out, the sites were archived by Microsoft 365 Archive. Unfortunately, the Get-MgSite cmdlet doesn’t have an IsArchived property to filter against.

Third, it’s always better for performance to have the Graph return sorted information instead of fetching data and then sorting it with the Sort-Object cmdlet. When fetching groups, the original script used Sort-Object to sort the objects by display name. I converted this code to:

[array]$Groups = Get-MgGroup -Filter "groupTypes/any(a:a eq 'unified')" -PageSize 999 -All `
-Property id, displayname, visibility, assignedlabels, description, createdDateTime, renewedDateTime, drive -Sort "displayname DESC"

Get-MgGroup_List: Sorting not supported for current query.

The command didn’t work and the error isn’t as helpful as it could be. The reason for the failure is that adding a sort converts the query from a standard to an advanced query, which means that you need to add the ConsistencyLevel and CountVar parameters. Here’s a working version of the command:

[array]$Groups = Get-MgGroup -Filter "groupTypes/any(a:a eq 'unified')" -PageSize 999 -All `
-Property id, displayname, visibility, assignedlabels, description, createdDateTime, renewedDateTime, drive -Sort "displayname DESC" -ConsistencyLevel eventual -CountVar GroupCount

Oddly, the Get-MgTeam cmdlet doesn’t support the ConsistencyLevel parameter so you cannot sort a list of teams except by sorting the objects fetched by Get-MgTeam with the Sort-Object cmdlet.

A Successful Conversion

I am happy with the migration. There are about 10% fewer lines of code in the Graph SDK version of the script, and everything works as expected. Or so I think. If you want to see the converted script, you can download it from GitHub.


Learn more about how the Office 365 applications 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.

]]>
https://office365itpros.com/2024/07/15/teams-and-groups-activity-6/feed/ 1 65597
The Curiously Unfinished Outlook Settings API https://office365itpros.com/2024/06/26/outlook-settings-api/?utm_source=rss&utm_medium=rss&utm_campaign=outlook-settings-api https://office365itpros.com/2024/06/26/outlook-settings-api/#respond Wed, 26 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65249

Many Mailbox Settings Missing from Outlook Settings API

One of the curious things about the Graph APIs is the incomplete Outlook settings API. It’s a well-known fact that Microsoft has not done a good job of supporting Exchange management operations through the Graph API. Perhaps understandably because of its long-term history with Exchange, PowerShell is the current focal point for Exchange Management automation

Perhaps the Outlook settings API is the starting point for what will become a full-fledged implementation to manage all aspects of mailbox settings. Given the scheduled retirement of Exchange Web Services (EWS) from October 2026. If so, an API covering all aspects of mailbox configuration would be a welcome development. PowerShell is great, but a Graph API is more flexible because of its support. With that thought in mind, let’s review what the current API can do.

Different Clients, Different Settings

Outlook classic (Win32) and OWA (or the new Outlook for Windows) use different client settings. Some crossover exists, such as roaming signatures, but the different history for the clients means that settings are divided into those stored in the system registry (Outlook classic) and those held in user mailboxes (OWA).

Exchange Online supports cmdlets like Get-MailboxCalendarConfiguration to manage mailbox settings, but the Outlook settings API only deals with a limited subset of the settings exposed through the OWA client (Figure 1).

OWA Language and Time mailbox settings


Outlook settings API
Figure 1: OWA Language and Time mailbox settings

Properties Returned by the Outlook Settings API

The properties returned by the Outlook Settings API are:

  • Auto-replies (automaticRepliesSetting).
  • Date format (dateFormat).
  • Delegate message delivery options (delegateMeetingMessageDeliveryOptions).
  • Locale (localeInfo).
  • Time format (timeFormat).
  • Time zone (timezone).
  • Working hours (workingHours)
  • User purpose or mailbox type (userPurpose).

The Get-MgUserMailboxSettings cmdlet returns all the properties supported by the Outlook Settings API. Here’s how to fetch the settings for the currently signed-in user:

Connect-MgGraph -Scopes MailboxSettings.ReadWrite
$User = Get-MgUser -UserId (Get-MgContext).Account
[Array]$Settings = Get-MgUserMailboxSetting -UserId $User.Id

$Settings | Format-Table

ArchiveFolder                         : AAMkADAzNzBmMzU0LTI3NTItNDQzNy04NzhkLWNmMGU1MzEwYThkNAAuAAAAAAB_7ILpFNx8TrktaK8VYWerAQA3tTkMTDKYRI6zB9VW59QNAABnZQYBAAA=
AutomaticRepliesSetting               : Microsoft.Graph.PowerShell.Models.MicrosoftGraphAutomaticRepliesSetting
DateFormat                            : d MMM yyyy
DelegateMeetingMessageDeliveryOptions : sendToDelegateAndPrincipal
Language                              : Microsoft.Graph.PowerShell.Models.MicrosoftGraphLocaleInfo
TimeFormat                            : HH:mm
TimeZone                              : GMT Standard Time
UserPurpose                           : user
WorkingHours                          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphWorkingHours

To reveal full details of a setting shown with a Graph object type rather than a value, pipe the property to the Format-List cmdlet:

$Settings.Language | Format-List

DisplayName          : English (Ireland)
Locale               : en-IE
AdditionalProperties : {}

As a practical example of using the API, here’s how to configure auto-replies. The example configures a simple HTML auto-reply message for both external and internal senders to be sent during a scheduled period extending from now to 30 days in the future. Details of the different values available to configure the autoreply settings are available online. This code uses some simple hash tables to hold the parameters (for those who care, I find this technique easier and less probe to error than composing a request body in JSON, especially when nesting values).

to error than composing a request body in JSON).
[array]$Settings = Get-MgUserMailboxSetting -UserId $User.Id
$Timezone = $Settings.TimeZone

$Start = Get-Date (Get-Date).AddHours(-2)-format s
$End = Get-Date (Get-Date).AddDays(+30) -format s

$StartDateTime = @{}
$StartDateTime.Add("dateTime", $Start)
$StartDateTime.Add("timezone", $TimeZone)

$EndDateTime = @{}
$EndDateTime.Add("dateTime", $End)
$EndDateTime.Add("timezone", $TimeZone)

$Parameters = @{}
$Parameters.Add("Status", "scheduled")
$Parameters.Add("externalAudience","all")
$Parameters.Add("internalreplymessage",$HtmlMessage)
$Parameters.Add("externalreplymessage",$HtmlMessage)
$Parameters.Add("scheduledEndDateTime",$EndDateTime)
$Parameters.Add("scheduledStartDateTime",$StartDateTime)

$AutoRepliesSetting = @{}
$AutoRepliesSetting.Add("automaticRepliesSetting", $Parameters)
Update-MgUserMailboxSetting -UserId $User.id -BodyParameter $AutoRepliesSetting

The effect of the update to mailbox settings is shown in Figure 2.

Auto-reply settings updated using the Outlook Settings API
Figure 2: Auto-reply settings updated using the Outlook Settings API

OWA and Outlook classic share most auto-reply settings. Three settings specific to OWA are shown under the scheduled period, like “block my calendar for this period.” These settings are not available in Outlook classic and unsupported by the Outlook settings API. Auto-reply settings can be set using the Exchange Online Set-MailboxAutoReplyConfiguration cmdlet, as in this example of configuring auto-replies for shared mailboxes to respond to incoming customer queries over a holiday period.

The Archive Folder

I’m not quite sure why the settings include the mailbox folder identifier for the Archive folder. The Archive folder is one of Outlook’s default mailbox folders and has nothing to do with the online archive. The folder identifier might be present to tell Outlook the target folder when executing the move to archive action.

In any case, an API exists to translate folder identifiers between different formats. The value is stored as a “RestID,” which is the default used by the Graph. Here’s how to translate the identifier to the MAPI format, which is what you’d see when browsing mailbox contents with the MFCMAPI utility.

[array]$SourceIds = $Settings.ArchiveFolder
$Body = @{}
$Body.Add("sourceIdType", "RestId")
$Body.Add("inputIds", $SourceIds)
$Body.Add("targetIdType", "entryid")

$R = Invoke-MgTranslateUserExchangeId -UserId Rene.Artois@office365itpros.com -BodyParameter $Body
Write-Host ("REST format identifier is {0}" -f $R.SourceId)
Write-Host ("MAPI format identifier is {0}" -f $R.TargetId)
REST format identifier is AAMkAGU2MDhlMDhjLTdlZGMtNDMwNC05M2Y4LTIyNzNiYzI5N2VlNwAuAAAAAAC8kIa3heviTIMxxfhY7u2KAQB7Y5w0HV7-Rou7AD9UAhLGAAAAAAE9AAA=
MAPI format identifier is AAAAALyQhreF6-JMgzHF-Fju7YoBAHtjnDQdXv9Gi7sAP1QCEsYAAAAAAT0AAA2

To see more of the gory details about item and folder identifier formats, see Vasil’s blog.

Good in Parts

The Outlook settings API is like a curate’s egg: good in parts. It seems like something Microsoft started on some time ago (look at the 2016 dates used in the update examples) and then forgot. If so, that’s a pity. It would be nice to have full Graph coverage of all Microsoft 365 workload. We’re still waiting and looks like we’ll have to wait for a while yet.


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 developments as they happen.

]]>
https://office365itpros.com/2024/06/26/outlook-settings-api/feed/ 0 65249
Planner User Policy Stops Task and Plan Deletions https://office365itpros.com/2024/06/21/set-planneruserpolicy-effects/?utm_source=rss&utm_medium=rss&utm_campaign=set-planneruserpolicy-effects https://office365itpros.com/2024/06/21/set-planneruserpolicy-effects/#respond Fri, 21 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65202

Running the Set-PlannerUserPolicy Cmdlet Has an Unexpected Effect

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.

The Planner Tenant Admin PowerShell Module

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.

What the Set-PlannerUserPolicy Cmdlet Does

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 Downside of the Set-PlannerUserPolicy Cmdlet

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.

The Planner browser app declines to delete a plan because of the effect of the Set-PlannerUserPolicycmdlet.
Figure 1: The Planner browser app declines to delete a plan

A Mystery Solved

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.

]]>
https://office365itpros.com/2024/06/21/set-planneruserpolicy-effects/feed/ 0 65202
Working with Calendar Permissions using the Microsoft Graph PowerShell SDK https://office365itpros.com/2024/06/18/set-default-calendar-permission/?utm_source=rss&utm_medium=rss&utm_campaign=set-default-calendar-permission https://office365itpros.com/2024/06/18/set-default-calendar-permission/#respond Tue, 18 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65222

Set Calendar Permission to Allow Organization Users to See Limited Details

In September 2021, I wrote about how to set the calendar permission for mailboxes to allow users within the organization to view event titles and locations. In the article, I discuss how to use the Set-MailboxFolderPermission cmdlet to update the access rights assigned to the “default user” from availability only to limited details. The permission assigned to the default user is the one used if a more specific permission is unavailable. By allowing more access to a user calendar for the default user, it means that anyone in the organization can see more information from that user’s calendar. In OWA and the new Outlook for Windows (Monarch) client, the sharing permission is called “can view titles and locations” (Figure 1).

Can view titles and locations means that users who check someone else’s calendar to see event subjects and locations. The default shows only that slots in a calendar are blocked or free.

Using OWA to set the default user calendar permission
Figure 1: Using OWA to set the default user calendar permission

Calendar Permissions and the Graph

Time passes on and today an alternative solution is available in the form of the Graph calendar permission resource and its methods, plus the associated Microsoft Graph PowerShell SDK cmdlets like Get-MgUserCalendarPermission and Update- MgUserCalendarPermission.

The Get-MailboxFolderPermission and Set-MailboxFolderPermission cmdlets have never been quick, so the question is whether the Graph-based cmdlets are faster at checking and setting calendar permissions.

Testing Performance

I decided to test by writing two scripts. Both scripts fetch user and room mailboxes which use the limited availability permission and update the mailboxes to allow access to limited details.

Both scripts use the Get-ExoMailbox cmdlet to fetch mailbox details. There isn’t a good Graph-based method to fetch mailbox-enabled accounts. Get-MgUser can apply a filter to fetch licensed accounts, but that set won’t include room mailboxes. Get-MgUser can fetch all member accounts, but this set will probably include a bunch of accounts that don’t have mailboxes. In addition, because the script loads the Exchange Online management module to use Get-ExoMailbox, it can also use Set-Mailbox to update a custom attribute with an indicator after processing a mailbox.

Maintaining an indicator in a custom attribute is important because the Get-ExoMailbox command can filter out mailboxes that have the permission set. For instance, if you run the script monthly, it will only process mailboxes created since the last run.

Here’s the Exchange Online script. The Set-MailboxFolderPermission cmdlet requires passing the name of the calendar folder, so there’s some code to figure out the value in different languages.

# Exchange Online version 
[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox, RoomMailbox -Filter {CustomAttribute10 -ne "OpenCalendar"} -ResultSize Unlimited -Properties Languages | Sort-Object DisplayName
Write-Host ("{0} mailboxes found" -f $Mbx.Count)
[int]$Updates = 0
ForEach ($M in $Mbx) {
  # Figure out the name of the Calendar folder in the user's preferred language
  [array]$Languages = $M.Languages
  Switch ($Languages[0]) {
      "en-US" { $CalendarName = "Calendar" }
      "fr-FR" { $CalendarName = "Calendrier" }
      "de-DE" { $CalendarName = "Kalender" }
      "es-ES" { $CalendarName = "Calendario" }
      "it-IT" { $CalendarName = "Calendario" }
      "nl-NL" { $CalendarName = "Agenda" }   
      Default { $CalendarName = "Calendar" }
  }
  # Build the path to the Calendar folder
  $CalendarFolder = ("{0}:\{1}" -f $M.UserPrincipalName, $CalendarName)
  [array]$Data = Get-MailboxFolderPermission -Identity $CalendarFolder | Where-Object {$_.User.usertype.value -eq "Default"} | Select-Object -ExpandProperty AccessRights
  If ([string]$Data -ne "LimitedDetails") {
      Write-Host ("Setting LimitedDetails permission for {0}" -f $M.displayName) -ForegroundColor Yellow
      Set-MailboxFolderPermission -Identity $CalendarFolder -User Default -AccessRights LimitedDetails
      Set-Mailbox -Identity $M.UserPrincipalName -CustomAttribute10 "OpenCalendar"
      $Updates++
  } Else {
      # for some reason the custom attribute is not set to reflect the calendar permission, so update it
      Write-Host "Setting custom attribute for" $M.UserPrincipalName
      Set-Mailbox -Identity $M.UserPrincipalName -CustomAttribute10 "OpenCalendar"
  }
}
Write-Host ("Calendar permission updated for {0} mailboxes" -f $Updates)

Here’s the version using a mixture of Exchange Online and Microsoft Graph PowerShell SDK cmdlet. This code doesn’t need to know anything about language values for folder names because the Graph uses different identifiers.

# Graph version
[int]$Updates = 0
[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox, RoomMailbox -Filter {CustomAttribute10 -ne "OpenCalendar"} -ResultSize Unlimited -Properties Languages | Sort-Object DisplayName
Write-Host ("{0} mailboxes found" -f $Mbx.Count)
ForEach ($M in $Mbx){
    [array]$CalendarPermissions = Get-MgUserCalendarPermission -UserId $M.ExternalDirectoryObjectId
  If ($CalendarPermissions) {  
     $OrgDefault = $null
     [array]$OrgDefault = $CalendarPermissions | Where-Object {$_.EmailAddress.Name -eq "My Organization"}  
     If ($Permission -notin $OrgDefault.Role) {
        Write-Host ("Setting Limited Read permission for {1}" -f $M.DisplayName) -ForegroundColor Yellow
        Try {
           Update-MgUserCalendarPermission -UserId $M.ExternalDirectoryObjectId `
             -Role "LimitedRead" -CalendarPermissionId $OrgDefault.id | Out-Null
           $Updates++
        } Catch {
            Write-Host ("Failed to update calendar permission for {0}" -f $M.DisplayName) -ForegroundColor Red
        }
        Set-Mailbox -Identity $M.ExternalDirectoryObjectId -CustomAttribute10 "OpenCalendar"
        } Else {
          Write-Host ("{0} already has the Limited Read permission" -f $M.DisplayName)
        }
  } 
}
Write-Host ("Calendar permission updated for {0} mailboxes" -f $Updates)

Here’s the version using a mixture of Exchange Online and Microsoft Graph PowerShell SDK cmdlet. This code doesn’t need to know anything about language values for folder names because the Graph uses different identifiers. I can’t account for why Microsoft decided to call the permission LimitedDetails in Exchange and LimitedRead in the Graph. The different roles available for the Graph are documented online.

# Graph version
[int]$Updates = 0
[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox, RoomMailbox -Filter {CustomAttribute10 -ne "OpenCalendar"} -ResultSize Unlimited -Properties Languages | Sort-Object DisplayName
Write-Host ("{0} mailboxes found" -f $Mbx.Count)
ForEach ($M in $Mbx){
    [array]$CalendarPermissions = Get-MgUserCalendarPermission -UserId $M.ExternalDirectoryObjectId
  If ($CalendarPermissions) {  
     $OrgDefault = $null
     [array]$OrgDefault = $CalendarPermissions | Where-Object {$_.EmailAddress.Name -eq "My Organization"}  
    If ("LimitedRead" -notin $OrgDefault.Role) {
       Write-Host ("Setting Limited Read permission for {0}" -f $M.DisplayName) -ForegroundColor Yellow
       Try {
          Update-MgUserCalendarPermission -UserId $M.ExternalDirectoryObjectId `
            -Role "LimitedRead" -CalendarPermissionId $OrgDefault.id | Out-Null
          $Updates++
       } Catch {
           Write-Host ("Failed to update calendar permission for {0}" -f $M.DisplayName) -ForegroundColor Red
       }
       Set-Mailbox -Identity $M.ExternalDirectoryObjectId -CustomAttribute10 "OpenCalendar"
       } Else {
         Write-Host ("{0} already has the Limited Read permission" -f $M.DisplayName)
       }
  } 
}
Write-Host ("Calendar permission updated for {0} mailboxes" -f $Updates)

The Measure-Command cmdlet generated the test results, which showed that the Exchange script required 2.84 seconds per mailbox to run. The Graph version was nearly a second faster per mailbox (1.96 seconds). Your mileage might vary.

No Need to Change Unless You Must

Using the Graph SDK cmdlets saves almost a second per mailbox. That doesn’t mean that you should update scripts to rip out and replace the Set-MailboxFolderPermission cmdlet. While it’s important to use code that runs quickly, this kind of script is not something you’re going to run daily. It’s more likely to run on a scheduled basis, such as an Azure Automation runbook, and you won’t notice the extra time.

Besides, the most important contribution to performance in this example is reducing the number of mailboxes to process by maintaining the indicator and using the indicator to filter mailboxes. One cmdlet might be faster than another, but it’s how you use cmdlets in a script that dictates overall performance.


So much change, all the time. It’s a challenge to stay abreast of all the updates Microsoft makes across the Microsoft 365 ecosystem. 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.

]]>
https://office365itpros.com/2024/06/18/set-default-calendar-permission/feed/ 0 65222
Per-User MFA State Added to Tenant Passwords and MFA Report https://office365itpros.com/2024/06/14/per-user-mfa-state/?utm_source=rss&utm_medium=rss&utm_campaign=per-user-mfa-state https://office365itpros.com/2024/06/14/per-user-mfa-state/#comments Fri, 14 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65168

Per-User MFA State Available for User Accounts Through the Graph

On June 10, 2024, the Microsoft Graph changelog included some interesting additions to the beta version of the authentication resource type to make the settings for per-user MFA retrievable for user accounts. Until now, it’s been possible to see this information through the Entra admin center but not to fetch it programmatically.

The addition of the per-user MFA state is interesting because Microsoft is doing its level best to eliminate per-user MFA from Entra ID. Today, Office 365 E3 and above licenses include the ability to use per-user MFA when connecting to Office 365 services. Per-user Entra ID MFA covers all connections processed by the Microsoft identity service.

Microsoft’s long-term plan for enforcement of multifactor authentication is to use conditional access policies. They’ve put enormous effort over the past few years to build out the capabilities of these policies. The latest update was the ability to block connections using the device code authentication flow, something that all tenants should consider unless a solid business need exists to support device code authentication.

Moving Away from Per-User MFA

To make the transition easier for tenants, some Microsoft-managed conditional access policies are available to organizations with Entra ID P1 or P2 licenses, including one to assist the migration of per-user MFA. A potential issue for those with Office 365 MFA is that moving to conditional access policies requires Entra ID P1 licenses. This isn’t a problem if the organization has purchased Entra ID P1 for other reasons, like self-service password reset, but it is a hurdle to overcome for others. Security defaults is another option for tenants who don’t want to use conditional access policies, especially in the small to medium-sized sectors.

Knowing who still uses per-user MFA is invaluable information for anyone planning to migrate. It’s possible to get details about per-user MFA through the Users section of the Entra admin center, but the user interface is antiquated and unwieldy and the list includes both member and guest accounts (Figure 1).

 Per-user MFA state viewed through the Entra admin center.
Figure 1: Per-user MFA state viewed through the Entra admin center

Being able to extract the information via the Graph allows us to do something like this to find licensed member accounts, check each account for its per-user MFA state, and report the findings. The new capability is in beta for now with no indication of when the V1.0 (production) will support it. Reading the per-user MFA state requires consent to use the Policy.ReadWrite.AuthenticationMethod application permission.

Connect-MgGraph -Scope Policy.ReadWrite.AuthenticationMethod, User.Read.All -NoWelcome

# Get licensed users
[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" 
-ConsistencyLevel eventual -CountVariable UsersFound -All -PageSize 999
    
If ($Users) {
    Write-Host ("{0} users found" -f $Users.Count)
} Else {
    Write-Host "No users found"
    Break
}

$Report = [System.Collections.Generic.List[Object]]::new()
Foreach ($User in $Users){
  $Uri = ("https://graph.microsoft.com/beta/users/{0}/authentication/requirements" -f $user.id)
  $Data = Invoke-MgGraphRequest -Uri $Uri -Method Get
  $ReportLine = [PSCustomObject][ordered]@{
    User                = $User.UserPrincipalName
    Name                = $User.displayName
    "MFA State"         = $Data.PerUserMfaState
  }
  $Report.Add($ReportLine)
}

$Report | Export-CSV -Path "C:\Temp\MFAState.csv" -NoTypeInformation -Encoding utf8

Accounts can be in one of three states for per-user MFA: disabled, enabled, or enforced. To update the per-user MFA state for an account, use the Patch method:

$Body= @{}
$Body.Add("perUserMFAState", "enabled")

$Uri = ("https://graph.microsoft.com/beta/users/{0}/authentication/requirements" -f $user.id)
Invoke-MgGraphRequest -Uri $Uri -Method Patch -Body $Body

Enhancing the User Passwords and MFA Report

Being able to generate a quick report of per-user MFA states is nice; integrating that data with other sources to create a comprehensive view of account password and MFA properties is even better. In January, I wrote about the inability to query the Graph to find which accounts use MFA. This is because when you use conditional access policies, MFA is the outcome of an assessment against policy for inbound connections rather than a fixed property of user accounts (like per-user MFA). The script I described in the article therefore uses information from several sources, including Entra ID sign-in logs, to report registration of MFA methods, and password change information. The report shows the last time when accounts successfully used MFA to connect, which is the acid test to know if an account uses MFA or not. User registration of MFA methods is one step along the path; using those methods when connecting to Entra ID is what we want to see.

Now that per-user MFA state information is available, I have updated the script (available from GitHub) to include that data. The HTML report generated by the script highlights accounts enabled or enforced for per-user MFA (Figure 2).

User Password and Authentication report with per-user MFA state.
Figure 2: User Password and Authentication report with per-user MFA state

The end of the report includes a summary of the findings (Figure 3), including the number of accounts in the enabled or enforced per-user MFA states and the display names of the users in those categories.

Summary for the User Passwords and Authentication report.
Figure 3: Summary for the User Passwords and Authentication report

The report also generates a CSV file for you to slice and dice the data as you wish.

Nice Addition to Entra ID Data

Being able to report per-user MFA states is a nice addition to the data available to Entra ID administrators. Whether it will convince organizations currently using per-user MFA to move to conditional access policies remains to be seen.


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.

]]>
https://office365itpros.com/2024/06/14/per-user-mfa-state/feed/ 15 65168
Choosing Between Graph API Requests or Graph SDK Cmdlets https://office365itpros.com/2024/06/05/microsoft-graph-powershell-sdk-api/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-powershell-sdk-api https://office365itpros.com/2024/06/05/microsoft-graph-powershell-sdk-api/#comments Wed, 05 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65033
Microsoft Graph PowerShell SDK.

Which to Choose for PowerShell Development?

I’m sometimes asked why people should bother using the Microsoft Graph PowerShell SDK to develop PowerShell scripts. The arguments against the SDK are that it’s buggy, doesn’t have great documentation, and adds an extra layer on top of Graph API requests. I can’t deny that the SDK has had recent quality problems that shook developer confidence.

I cannot advance a case that Microsoft’s documentation for the Graph PowerShell SDK cmdlets is good because it’s not. Some improvements have been made over the last year, but the examples given (copied mostly from the Graph documentation) are too simple, if they exist at all. There’s also the small fact that the Graph PowerShell SDK cmdlets share some foibles that make them less useful than they should be.

Given the problems, why continue to persist with the Graph PowerShell SDK? I guess the reason is that the SDK cmdlets are easier to work with for anyone who’s used to PowerShell development. For instance, the Graph SDK automatically performs housekeeping operations like retrieving an access token, renewing the token (only needed for long-running scripts), and pagination. None of these operations are complex. Once mastered, the same code can be copied into scripts to take care of these points.

Call me mad, I therefore persist in writing scripts using Graph PowerShell SDK cmdlets. However, times exist when it’s necessary to use a Graph API request, including when:

  • Microsoft’s AutoRest process has not processed a new API to create a cmdlet.
  • The data returned by a cmdlet is not as complete as the underlying Graph API request. This shouldn’t happen, but it does.
  • It’s necessary to retrieve properties that a cmdlet doesn’t support.

Let’s look at examples of the last two points.

Fetching Attendee Data with Microsoft Graph PowerShell SDK Cmdlets and API Requests

I’ve used the List CalendarView API in situations like reporting usage statistics for room mailboxes. Here’s an example of retrieving calendar events between two dates.

$Uri = ("https://graph.microsoft.com/V1.0/users/{0}/calendar/calendarView?startDateTime={1}&endDateTime={2}" -f $Organizer.Id, $StartDateSearch, $EndDateSearch)

The resulting URI fed to the Invoke-MgGraphRequest cmdlet looks like this:

$Uri
https://graph.microsoft.com/V1.0/users/4adf6057-95da-430a-8757-6a58c85e13d4/calendar/calendarView?startDateTime=2024-03-28T12:56:37&endDateTime=2024-05-29T12:56:37

$Items = Invoke-MgGraphRequest -Method Get -Uri $Uri | Select-Object -ExpandProperty Value

You might ask why I use Invoke-MgGraphRequest (a cmdlet from the Microsoft Graph PowerShell SDK) rather than the general-purpose Invoke-RestMethod cmdlet. It’s because I start scripts off with the Graph PowerShell SDK and only go to standard Graph API requests when necessary.

In any case, the attendees of a meeting are returned like this:

attendees                      {System.Collections.Hashtable, System.Collections.Hashtable, System.Collections.Hashtable, System.Collections.Hashtable}

The attendee data are available in individual hash tables and are easy to access:

$Items[0].attendees

Name                           Value
----                           -----
emailAddress                   {[address, Sean.Landy@office365itpros.com], [name, Sean Landy]}
status                         {[response, none], [time, 01/01/0001 00:00:00]}
type                           required
emailAddress                   {[address, Lotte.Vetler@office365itpros.com], [name, Lotte Vetler (Paris)]}
status                         {[response, none], [time, 01/01/0001 00:00:00]}

Get-MgUserCalendarView is the equivalent cmdlet in the Microsoft Graph PowerShell SDK. This command does the same job as the List CalendarView API request above.

[array]$CalendarItems = Get-MgUserCalendarView -UserId $Organizer.id -Startdatetime $StartDateSearch -Enddatetime $EndDateSearch -All

Attendees                     : {Microsoft.Graph.PowerShell.Models.MicrosoftGraphAttendee, Microsoft.Graph.PowerShell.Models.MicrosoftGraphAttendee}

$calendarItems[0].Attendees

Type
----
required
required

The attendee data is incomplete. No information is available about the attendees’ email addresses and display names. That’s why my scripts use the API rather than the cmdlet.

How the Microsoft Graph PowerShell SDK Cmdlets Return Data

When you run a Graph PowerShell SDK cmdlet, the returned data ends up in an array, which is convenient for further PowerShell processing. You’ll note that I use the -All parameter to fetch all available objects.

$AllUsers = Get-MgUser -All

$AllUsers.gettype()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

Things are a little more complicated with Graph API requests. We get an array back, but the array contains a hashtable. The actual data that we might want to process is in the record with the Value key. We also see an @odata.nextlink to use to fetch the next page of available data:

$Uri = "https://graph.microsoft.com/v1.0/users"
$Data = Invoke-MgGraphRequest -Method Get -Uri $Uri
$Data.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

$Data

Name                           Value
----                           -----
@odata.context                 https://graph.microsoft.com/v1.0/$metadata#users
value                          {44c0ca9c-d18c-4466-9492-c60c3eb78423, bcfa6ecb-cc5b-4357-909c-1e0e450864e3, da1288d5-e63c-4118-af62-3280823e04e1, de67dc4a-4a51-4d86-…
@odata.nextLink                https://graph.microsoft.com/v1.0/users?$skiptoken=RFNwdAIAAQAAABc6Z3NjYWxlc0BkYXRhcnVtYmxlLmNvbSlVc2VyX2UwNjIzZjE0LTUzM2QtNDhmYS1hODRl…

In most cases, I simply create an array of the data and then go ahead and process the information as normal for an array:

[array]$Data = $Data.Value

The Invoke-MgGraphRequest cmdlet supports output to a PowerShell object.

$Data = Invoke-MgGraphRequest -Method Get -Uri $Uri -OutputType PsObject

The output data is the same but it’s in the form of an array rather than a hash table:

$Data | Format-List

@odata.context  : https://graph.microsoft.com/v1.0/$metadata#users
@odata.nextLink : https://graph.microsoft.com/v1.0/users?$skiptoken=RFNwdAIAAQAAABc6Z3NjYWxlc0BkYXRhcnVtYmxlLmNvbSlVc2VyX2UwNjIzZjE0LTUzM2QtNDhmYS1hODRlLTljOTg0MDhkNDgxYbkAAAAAAAAAAAAA
value           : {@{businessPhones=System.Object[]; displayName=12 Knocksinna (Gmail); givenName=12; jobTitle=; mail=12knocksinna@gmail.com; mobilePhone=;
officeLocation=; preferredLanguage=; surname=Knocksinna; userPrincipalName=12knocksinna_gmail.com#EXT#@RedmondAssociates.onmicrosoft.com;
id=44c0ca9c-d18c-4466-9492-c60c3eb78423}, @{businessPhones=System.Object[]; displayName=Email Channel for Exchange GOMs; givenName=Teams;
jobTitle=; mail=5e2eb5ab.office365itpros.com@emea.teams.ms; mobilePhone=; officeLocation=; preferredLanguage=; surname=Email Channel for GOM List;

Once again, the data to process is in the Value record.

I usually don’t bother outputting to a PowerShell object, perhaps because I’m used to dealing with the hash table.

Mix and Match

The important thing to remember is that a PowerShell script can mix and match Graph API requests and Graph PowerShell cmdlets. My usual approach is to start with cmdlets and only use Graph requests when absolutely necessary. I know others will disagree with this approach, but it’s one that works for me.


Make sure that you’re not surprised about changes that appear inside Microsoft 365 applications by subscribing to the Office 365 for IT Pros eBook. Our monthly updates make sure that our subscribers stay informed.

]]>
https://office365itpros.com/2024/06/05/microsoft-graph-powershell-sdk-api/feed/ 10 65033
Sending Urgent Teams Chats with PowerShell https://office365itpros.com/2024/04/24/teams-urgent-message-ps/?utm_source=rss&utm_medium=rss&utm_campaign=teams-urgent-message-ps https://office365itpros.com/2024/04/24/teams-urgent-message-ps/#respond Wed, 24 Apr 2024 01:00:00 +0000 https://office365itpros.com/?p=64540

Scripting Teams Urgent Messages for a Set of Users

A reader asked if it was possible to write a PowerShell script to send chats to a set of people when something important happened, like a failure in an important piece of plant or a medical emergency. They explained that they have the facility to broadcast this kind of information via email, but a lot of their internal communications have moved to Teams and they’d like to move this kind of scripted communication too.

Teams supports urgent messages for one-to-one chats. Originally, these messages were called priority notifications and Microsoft planned to charge for their use. That idea disappeared in the mists of the Covid pandemic, and anyone can send urgent messages today. The nice thing about urgent messages is that Teams pings the recipient every two minutes until they read the message or twenty minutes elapses.

Compose and Send Teams Urgent Messages with PowerShell

The Teams PowerShell module is designed for administrative activities and doesn’t support access to user data like chats. To compose and send chats, you must use Graph API requests or cmdlets from the Microsoft Graph PowerShell SDK, which is what I chose to do.

The outline of the script is as follows:

  • Run the Connect-MgGraph cmdlet to connect to the Graph. Delegated permissions must be used, and I specified the Chat.ReadWrite and User.Read.All permissions.
  • Because the script works with delegated permissions, the chats are sent by the signed-in user. The script runs the Get-MgContext cmdlet to find out what that account is.
  • The script sends chats to a set of users. Any Entra ID group will do.
  • The New-MgChatMessage cmdlet eventually sends the chat message. Because I want to include an inline image and a mention in the message, work must be done to construct the payload needed to tell Teams what content to post.
  • In Graph requests, this information is transmitted in JSON format. PowerShell cmdlets don’t accept parameters in the same way. Three different parameters are involved – the body, the mention, and the hosted content (image uploaded to Teams). Each parameter is passed as a hash table or array, and if the parameter takes an array, it’s likely to include some hash tables. Internally, Teams converts these structures to JSON and submits them to the Graph request. You don’t need to care about that, but constructing the various arrays and hash tables takes some trial and error to get right. The examples included in Microsoft documentation are helpful but are static examples of JSON that are hard to work with programmatically. I use a different approach. Here’s an example of creating the hash table to hold details of the inline image:

# Create a hash table to hold the image content that's used with the HostedContents parameter
$ContentDataDetails = @{}
$ContentDataDetails.Add("@microsoft.graph.temporaryId", "1")
$ContentDataDetails.Add("contentBytes", [System.IO.File]::ReadAllBytes("$ContentFile"))
$ContentDataDetails.Add("contentType", "image/jpeg")
[array]$ContentData = $ContentDataDetails
  • After populating the hash tables and arrays, the script runs the New-MgChat cmdlet. If an existing one-on-one chat exists for the two users, Teams returns the identifier of that chat thread. If not, Teams creates a new chat thread and returns that identifier.
  • The script runs the New-MgChatMessage cmdlet to post the prepared message to the target chat thread. Setting the importance parameter to “urgent” marks this as a Teams urgent message.

$ChatMessage = New-MgChatMessage -ChatId $NewChat.Id -Body $Body -Mentions $MentionIds -HostedContents $ContentData -Importance Urgent

The Urgent Teams Message

Figure 1 shows an example of the chat message posted to threads. You can see the inline image and that an @mention exists for James Ryan. If the recipient hovers over the mention, Teams displays the profile card for James Ryan to reveal details like contact information.

Teams urgent message created with PowerShell.
Figure 1: Teams urgent message created with PowerShell

You can download the script from GitHub.

Plain Sailing After Understanding Parameter Formatting

There’s no doubt that it’s more complicated to create and send one-to-one chats than it is to send email to a group of recipients, especially if you stray away from a very simple message body. However, much of the complexity is getting your head around the formatting of parameter input. Once you understand that, it’s reasonably easy to master the rest of the code.


Learn about using the Microsoft Graph PowerShell SDK and the rest of Microsoft 365 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.

]]>
https://office365itpros.com/2024/04/24/teams-urgent-message-ps/feed/ 0 64540
Microsoft Graph Activity Logs Hit General Availability https://office365itpros.com/2024/04/18/graph-activity-logs-ga/?utm_source=rss&utm_medium=rss&utm_campaign=graph-activity-logs-ga https://office365itpros.com/2024/04/18/graph-activity-logs-ga/#respond Thu, 18 Apr 2024 08:00:00 +0000 https://office365itpros.com/?p=64465

Graph Activity Logs for Security Analysis and Threat Hunting

On April 11 2024, Microsoft announced the general availability of Microsoft Graph activity logs, explained as: “visibility into HTTP requests made to the Microsoft Graph service in your tenant. In other words, every time an app generates a HTTP request to a Graph API, the service captures a log record. This covers Microsoft, third-party, and tenant apps, including the Graph requests run by cmdlets in Graph-based PowerShell modules like Microsoft Teams or the Microsoft Graph PowerShell SDK.

Microsoft says that: “With rapidly growing security threats and an increasing number of attacks, this log data source allows you to perform security analysis, threat hunting, and monitor application activity…

Graph Activity Logs Are Another Trail to Follow

In essence, the Graph activity logs give security analysts another audit trail to follow when looking for signs of anomalous activity within a tenant either before or after an attack occurs. Some configuration is needed to use the logs. You’ll need an Entra ID P1 license to access the logs through the Monitoring & health section of the Entra admin center (Figure 1).

Checking a Microsoft Graph activity log record in the Entra admin center.
Figure 1: Checking a Microsoft Graph activity log record in the Entra admin center

The highlighted Graph request in Figure 1 uses the group delta API to check for new groups information. The fact that security analysts see Graph requests when reviewing log data creates a need for a level of familiarity with how Graph APIs work and what the expected pattern of requests is. In addition, security analysts will need to understand the context of when requests happen and what a request does. For instance, opening the Microsoft 365 admin center generates a blizzard of Graph requests to fetch information about multiple objects. Even writing this article in a Word document generated many log entries that appear to be Data Loss Prevention checks.

It’s sometimes possible to extract the request from an audit record and run it, just to see what happens. For example, I copied a request and ran it in an interactive Microsoft Graph PowerShell SDK session like this:

Uri = "https://graph.microsoft.com/beta/groups/5aabcff4-118b-40f4-a033-2fd1c8d7cf6e/?`$select=expirationDateTime,assignedLabels"
$data = invoke-MgGraphRequest -Method get -Uri $Uri
$data

Name                           Value
----                           -----
@odata.context                 https://graph.microsoft.com/beta/$metadata#groups(expirationDateTime,assignedLabels)/$e…
assignedLabels                 {Non-business use}
expirationDateTime             02/01/2026 11:20:14

The request fetches the sensitivity label and expiration date properties for a Microsoft 365 group. It’s the kind of request used to fetch group properties for display in an admin console. The only strange thing is that the request fetches just two properties where it could have retrieved many more.

If you want to retain log data for more than 30 days, you’ll need to offload the data into something like a Log Analytics workspace and pay for that through an Azure subscription. Microsoft Sentinel seems like a good place to work with this data, and it might well be the case that the sheer amount of Graph log data generated in tenants will create a case to use a tool like Security Copilot to extract and understand important events.

Good Examples of Graph Activity Logs in Action

I am no expert in the art of analyzing security logs. If you want to read about the potential insights that the Graph activity logs might uncover, read these posts (part 1 and part 2) by Security MVP Faben Bader. They helped me understand the potential of using Graph activity logs to track threat within a tenant. Another valuable post by Bert-Jan Pals includes many practical examples of using KQL to query the Graph activity logs to summarize and report data.

The Difference with Audit Logs

With Graph activity logs now available, does the need for the Microsoft 365 unified audit log diminish? The answer is no. Graph activity logs capture details about HTTP requests to Graph endpoints. The unified audit log ingests events capturing details about 1,600+ actions taken by workloads within a Microsoft 365 tenant, including Entra ID. Some of the workloads don’t use Graph APIs or partially use Graph APIs. Exchange Online management is an example as is SharePoint Online management. The initial support for Graph-based management for SharePoint Online tenant settings hasn’t progressed since its 2022 debut and Exchange Online has not embraced Graph APIs for management yet (mailbox contents are accessible through the Graph).

Eventually, Microsoft 365 might get to a point where all actions taken by all apps result in Graph requests. We’re still a while away from that point and until then, a combination of log sources and data is needed to build as close to a complete picture of what happens inside a tenant as possible. Some events are not logged: an egregious example is running audit log searches, which have never been captured and won’t be until Microsoft delivers roadmap item 392841 in June 2024.

Microsoft 365 Auditing is a Fragmented Space

The current auditing setup around Microsoft 365 is fragmented. Some audit information needs premium licenses. Entra ID audit data is kept for 30 days and then discarded while the unified audit log can keep information for up to 365 days. PowerShell can get at some data and not others, and the Kusto Query Language (KQL) is similarly handicapped. Graph APIs are available for some data but not others.

A truly unified auditing framework that ingested details from all available sources into a common database and made the information accessible through PowerShell, a Graph API, and KQL would be appreciated. But given the number of different Microsoft development groups involved in this space, I doubt that we will see any progress towards unified cloud auditing soon. This should not stop you from investigating Graph activity logs. Security analysts will welcome the extra detail, if they can understand what that detail means.


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.

]]>
https://office365itpros.com/2024/04/18/graph-activity-logs-ga/feed/ 0 64465
Report OneDrive for Business Storage Based on Usage Data https://office365itpros.com/2024/02/27/onedrive-storage-report-usage/?utm_source=rss&utm_medium=rss&utm_campaign=onedrive-storage-report-usage https://office365itpros.com/2024/02/27/onedrive-storage-report-usage/#comments Tue, 27 Feb 2024 01:00:00 +0000 https://office365itpros.com/?p=63828

Much Faster to Create OneDrive Storage Report from Usage Data

Nearly five years ago, I wrote an article about a PowerShell script to report OneDrive for Business storage consumption. The script works well but it’s slow because it uses the Get-SPOSite cmdlet from the SharePoint Online management module. Everything is fine when running in a tenant with less than five hundred accounts. Past this point and you might have plenty of time for coffee.

That’s where the Graph usage reports API comes in handy. Despite being two or so days behind in terms of absolute accuracy for storage consumption, the usage reports API is extremely fast because it reads from a data warehouse populated with information by background processes running in the Microsoft datacenters. In this case, we need the OneDrive account detail report, which can cover usage from 7 to 180 days.

Including User Data in the OneDrive Storage Report

As noted previously, an ongoing issue affects usage reports for SharePoint Online data and prevents the population of site URLs in the reports. The same issue exists for OneDrive for Business data. This is a pain, but there’s often a silver lining in a bug. In this case, I decided to incorporate some user data into the report to make it possible for tenant administrators to sort by city, country, or department.

Outline of the Script to Create the OneDrive Storage Report

Here’s what the script does:

  • Runs Connect-MgGraph to connect to the Graph. This report only needs the User.Read.All and Reports.Read.All permissions.
  • Checks if the tenant obscures user data in reports. If this is true, the script updates the setting to allow it to fetch unobscured data.
  • Runs Get-MgUser to fetch details of all licensed member accounts in the tenant.
  • Populates a hash table. The key is the user principal name and the value is an array of user properties. Looking up a hash table to find user details is quicker than running Get-MgUser for each account or reading an array.
  • Use the Invoke-MgGraphRequest cmdlet to fetch the OneDrive account detail data for the last seven days. The data is loaded into an array.
  • Loop through the array to extract storage information for a user’s OneDrive for Business account and report what’s found. Included in the report is the information found by looking up the hash table for user details.
  • Export the report data to a CSV file.
  • Reset the tenant obscured report data setting if necessary.

Figure 1 shows an example of the OneDrive storage report generated by the script. When Microsoft fixes the Site URL problem for usage reports, I’ll update the script to include that property, but for now the script does a nice job of reporting OneDrive storage consumed by user accounts. And the script runs much faster than the older version based on the SharePoint Online management cmdlets.

OneDrive for Business user storage consumption report.
Figure 1: OneDrive for Business user storage consumption report.

Two Things to Learn About Reporting Microsoft 365 Data

This script demonstrates two things about reporting Microsoft 365 data. First, don’t assume that you need 100% up-to-date information about usage. The point is that data in reports might be accurate immediately after the generation of the report but degrades thereafter. There’s no great difference between an account that’s used 91.01% of its storage quota and 91.11%. The information available through the usage reports API gives as accurate a picture about usage in 99% of cases.

Second, don’t assume that the data returned by a cmdlet limit what you can use in a report. Properties like user identifiers (GUIDs) and user principal names enable matches for data drawn from multiple sources. Using hash tables to store information fetched from different sources is an excellent and fast way to create lookup tables for reports.

You can download the script from GitHub. Normal caveats apply. Don’t assume that the script has bulletproof error handling (it doesn’t) nor that a bug isn’t lurking somewhere. Test the script and have some fun chasing bugs if there are any.


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.

]]>
https://office365itpros.com/2024/02/27/onedrive-storage-report-usage/feed/ 2 63828
Use the Graph SDK to Access Microsoft 365 Service Health Information https://office365itpros.com/2024/02/07/service-health-data-api/?utm_source=rss&utm_medium=rss&utm_campaign=service-health-data-api https://office365itpros.com/2024/02/07/service-health-data-api/#comments Wed, 07 Feb 2024 01:00:00 +0000 https://office365itpros.com/?p=63487

Graph-based Service Communications API is now the Route to Service Health Data

In January 2021, I wrote about how to use the Office 365 Service Communications API to programmatically retrieve the service health information that’s available in the Microsoft 365 admin center (Figure 1).

Service Health information viewed in the Microsoft 365 admin center.

Microsoft 365 service health data.
Figure 1: Service Health advisory messages viewed in the Microsoft 365 admin center

At the time, the API used the manage.office.com endpoint. In December 2021, Microsoft deprecated the manage.office.com endpoint and introduced the Service Communications Graph API as the replacement. In this article, I explain how to use the API with Microsoft Graph PowerShell SDK cmdlets to retrieve service health information.

Retrieving Service Health Data

As shown in Figure 1, the active items Microsoft is working on are those that impact the service in some way, usually by removing the ability of users to do something. To find these items, run the Get-MgServiceAnnouncementIssue cmdlet and filter for items classified as advisory with a status of ‘serviceDegration’:

[array]$ServiceHealthItems = Get-MgServiceAnnouncementIssue -All `
    -Filter "classification eq 'Advisory' and status eq 'serviceDegradation'" | `
    Sort-Object {$_.LastModifiedDateTime -as [datetime]} -Descending

$ServiceHealthItems | Format-Table Id, Title, FeatureGroup, LastModifiedDateTime

If you don’t filter the service health items, the Get-MgServiceAnnouncementIssue cmdlet, including those where Microsoft resolved the issue (as with many SDK cmdlets, the All switch tells the cmdlet to fetch everything). This data reveals the areas where most issues occur. In my tenant, the 346 available issues broke down as follows:

$Data = Get-MgServiceAnnouncementIssue -All
$Data | Group-Object FeatureGroup -Noelement | Sort-Object Count -Descending | Format-Table Name, Count -AutoSize

Name                                    Count
----                                    -----
Teams Components                           80
Administration                             39
E-Mail and calendar access                 27
SharePoint Features                        25
Portal                                     23
Management and Provisioning                22
Microsoft Defender for Endpoint            21
Cloud App Security                         13
Viva Engage                                10

Another interesting grouping is by service:

$Data | Group-Object Service -Noelement | Sort-Object Count -Descending | Format-Table Name, Count -AutoSize

Name                                      Count
----                                      -----
Microsoft Teams                              80
Microsoft 365 suite                          64
Exchange Online                              60
Microsoft Defender XDR                       32
SharePoint Online                            30
Microsoft Defender for Cloud Apps            25
Microsoft Viva                               12
OneDrive for Business                         8

The start date for the oldest issue was March 1, 2023. The oldest last modified date for an issue was July 31, 2023. This suggests that Microsoft might keep about six months of service issue data online. Your mileage might vary.

Fetching Overall Service Health Data

Underneath the advisory items, the Microsoft 365 admin center displays an overview showing the health for individual services like Exchange Online, Teams, SharePoint Online, and so on. This information is accessible by running the Get-MgServiceAnnouncementHealthOverview cmdlet. In my tenant, this generates a list of 32 individual services, some of which (like Sway and Microsoft Managed Desktop), I’m not interested in. I therefore amend the output by filtering the services that I consider most important:

[array]$ImportantServices = "Exchange", "Teams", "SharePoint", "OrgLiveID", "Planner", "microsoftteams", "O365Client", "OneDriveForBusiness"
[array]$ImportantServiceStatus = Get-MgServiceAnnouncementHealthOverview | Where-Object {$_.Id -in $ImportantServices}
$ImportantServiceStatus | Sort-Object Service | Format-Table Service, Status -AutoSize

Service            Status
-------            ------
Exchange Online    serviceDegradation
Microsoft 365 apps serviceOperational
Microsoft Entra    serviceOperational
Microsoft Teams    serviceDegradation
Planner            serviceOperational
SharePoint Online  serviceDegradation

Using Service Health Data to Highlight Current Advisories

Many people will be perfectly happy to access service health information via the Microsoft 365 admin center. The advantage of using an API to retrieve the same information is that you can then use it in whatever way you think appropriate. As a working example to demonstrate what’s possible, I wrote a script that can run interactively or as an Azure Automation runbook using a managed identity.

The script retrieves the open service health advisories and creates an email with an HTML-format report containing the service data that is sent to nominated recipients (any mixture of mail-enabled objects, including individual mailboxes, distribution lists, and Microsoft 365 groups). The idea is to keep the recipients updated about progress with open issues that Microsoft is working on. Figure 2 shows an example email generated using the service advisories published in my tenant.

Email detailing open service health advisories.
Figure 2: Email detailing open service health advisories

After it’s extracted, the report can be disseminated in other ways. For instance, you could publish it as a Teams channel message.

You can download the script from GitHub.

Disrupted Change

Changing the details of an API is always disruptive. It’s not just the new endpoint. It’s also the way that the API returns data. Everything must be checked and verified. At least now the Service Communications API is part of the Microsoft Graph. As such, the level of change should be minimal in the future and we have the added benefit of PowerShell cmdlets to work with.


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.

]]>
https://office365itpros.com/2024/02/07/service-health-data-api/feed/ 4 63487
Graph User.ReadBasic.All Application Permission Available https://office365itpros.com/2024/01/30/user-readbasic-all-permission/?utm_source=rss&utm_medium=rss&utm_campaign=user-readbasic-all-permission https://office365itpros.com/2024/01/30/user-readbasic-all-permission/#respond Tue, 30 Jan 2024 01:00:00 +0000 https://office365itpros.com/?p=63521

Controlling Application Access to Entra ID User Account Information

Message center notification MC704030 (5 January 2024) brings important news for developers that the User.ReadBasic.All permission is now available for both delegated and application usage. The permission restricts the ability of an app to retrieve properties of Entra ID user accounts to a basic set instead of all properties. The Microsoft Graph access model is based on least permissions, but if an app can always retrieve everything known about a user account, that hardly fits the definition of “least.”

Until now, the User.ReadBasic.All has been available for delegated permissions, meaning that it works in the context of the signed-in user and restricts the user to viewing the limited set of properties for other users. This is good, but many apps, including those run by PowerShell scripts, process user details and some of those apps don’t need access to all account properties. That’s where the User.ReadBasic.All permission comes in.

Testing the Effect of the User.ReadBasic.All Permission

Let’s examine what using the new permission means in practice. First, create a registered app in Entra ID. You can also use an existing app. The important thing is to ensure that the application permissions consented for the app include User.ReadBasic.All and no other permission that could allow the app to access full user information (like Directory.Read.All).

Fetch an access token for the app as normal and examine the access token to ensure that the permissions specified in the token are as expected. The access token shown through jwt.io in Figure 1 has two permissions: User.ReadBasic.All and Group.Read.All. The latter won’t allow the app to fetch extended user information, so it’s good to test.

An access token with the User.ReadBasic.All permission.
Figure 1: An access token with the User.ReadBasic.All permission

Running a Graph query to find properties for a specified user account returns the following:

$Uri = "https://graph.microsoft.com/v1.0/Users/21a3ce8f-6d55-4e57-9210-d85a2f1618ec?`$Select=businessPhones,displayname, givenname, jobtitle, mail,mobilephone,officelocation,preferredlanguage, surname, userprincipalname, id, department, city, stateorprovince, country, employeeid, employeetype, employeehiredate"
[array]$user = Get-GraphData -AccessToken $Token -Uri $Uri
$User | Format-List

businessPhones    : {}
displayName       : "Popeye" Doyle
givenName         : Jimmy
jobTitle          :
mail              : Popeye.Doyle@o365maestro.onmicrosoft.com
mobilePhone       :
officeLocation    :
preferredLanguage :
surname           : Doyle
userPrincipalName : Popeye.Doyle@o365maestro.onmicrosoft.com
id                : 21a3ce8f-6d55-4e57-9210-d85a2f1618ec
department        :
city              :
country           :
employeeId        :
employeeType      :
employeeHireDate  :

You can see that even though the query requested a bunch of user properties, the restricted permission limited the Graph to returning only some properties. If an app needs to retrieve all properties, it needs consent for the User.Read.All permission. The same query run by an app with User.Read.All permission returns this set:

businessPhones    : {1 404 14746141}
displayName       : "Popeye" Doyle
givenName         : Jimmy
jobTitle          : Chief Police Sleuth
mail              : Popeye.Doyle@o365maestro.onmicrosoft.com
mobilePhone       : 1 405 1461511
officeLocation    : Manhatten
preferredLanguage :
surname           : Doyle
userPrincipalName : Popeye.Doyle@o365maestro.onmicrosoft.com
id                : 21a3ce8f-6d55-4e57-9210-d85a2f1618ec
department        : Detectives
city              : NYC
country           : United States
employeeId        : 1461431
employeeType      : Permanent
employeeHireDate  : 06/06/2023 23:00:00

The difference in output is obvious!

Filtering with User.ReadBasic.All

Before rushing to limit all applications, make sure that you understand what properties each application needs to process. Also, in MC704030, Microsoft notes that they fixed a bug that allowed apps to filter on properties that should have been blocked (like the employee hire date property). Attempts to filter against unauthorized properties now generate a 403 “insufficient privileges” error. This can cause unexpected results.

For example, the code below uses a reasonably common query to find user accounts with assigned licenses that are member of the tenant rather than guest accounts. After using certificate-based authentication to sign into the Microsoft Graph PowerShell SDK (to use application rather than delegated permissions), the script runs the Get-MgUser cmdlet, and the cmdlet fails due to insufficient privileges.

Connect-MgGraph -TenantId $TenantId -AppId $AppId -CertificateThumbprint $CertificateThumbprint

(Get-MgContext).Scopes
Group.Read.All
User.ReadBasic.All

[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable Records -All -Property Id, userprincipalname, displayname, mail, city, country, usagelocation, usertype, signinactivity | Sort-Object displayName
Get-MgUser_List: Insufficient privileges to complete the operation.

The result is exactly what is expected. The Graph declines to run the query because of the filters against the assignedLicenses and UserType properties, neither of which are in the restricted set allowed by User.ReadBasic.All.

Pause Before Using User.ReadBasic.All

Like any improvement made by Microsoft, care must be exercised about taking advantage of the improvement. As in this case, implementing a change with the best intentions might have unfortunate side effects like stopping applications working. That’s always a bad thing.


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.

]]>
https://office365itpros.com/2024/01/30/user-readbasic-all-permission/feed/ 0 63521
How to Update Tenant Corporate Branding for the Entra ID Sign-in Screen with PowerShell https://office365itpros.com/2024/01/25/corporate-branding-for-entra-id/?utm_source=rss&utm_medium=rss&utm_campaign=corporate-branding-for-entra-id https://office365itpros.com/2024/01/25/corporate-branding-for-entra-id/#comments Thu, 25 Jan 2024 01:00:00 +0000 https://office365itpros.com/?p=63398

Use Graph SDK Cmdlets to Apply Annual Updates to Corporate Branding for Entra ID Sign-in Screen

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.

Corporate branding applied to the Entra ID sign-in screen.

Corporate Branding for Entra ID.
Figure 1: Corporate branding applied to the Entra ID sign-in screen

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.

Scripting the Annual Branding Refresh

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.

Editing corporate branding settings in the Entra ID admin center.
Figure 2: Editing corporate branding settings in the Entra ID admin center

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:

  • Connects to the Graph using a managed identity.
  • Retrieves details of the current sign-in text using the Get-MgOrganizationBranding cmdlet.
  • Checks if the sign-in text has the current year. If not, update the sign-in text and run the Update-MgOrganizationBranding cmdlet to refresh the setting. The maximum size of the sign-in text is 1024 characters. The new sign-in text should be displayed within 15 minutes.
  • Checks if a new background image is available. The code below uses a location on a local disk to allow the script to run interactively. To allow the Azure Automation runbook to find the image, it must be stored in a network location like a web server. The background image should be sized 1920 x 1080 pixels and must be less than 300 KB. Entra ID refuses to upload larger files.
  • If a new image is available, update the branding configuration by running the Invoke-MgGraphRequest cmdlet. I’d like to use the Set-MgOrganizationBrandingLocalizationBackgroundImage cmdlet from the SDK, but it has many woes (issue #2541), not least the lack of a content type parameter to indicate the type of image being passed. A new background image takes longer to distribute across Microsoft’s network but should be available within an hour of the update.

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

The refreshed corporate branding for the Entra ID sign-in screen.

Corporate branding Entra Id
Figure 3: The refreshed corporate branding for the Entra ID sign-in screen.

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.

Full Corporate Branding Possible

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.

Corporate Branding for Entra ID Isn’t Difficult

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.

]]>
https://office365itpros.com/2024/01/25/corporate-branding-for-entra-id/feed/ 1 63398
How to Use PowerShell to Retrieve Permissions for Entra ID Apps https://office365itpros.com/2024/01/24/app-permissions-report/?utm_source=rss&utm_medium=rss&utm_campaign=app-permissions-report https://office365itpros.com/2024/01/24/app-permissions-report/#respond Wed, 24 Jan 2024 01:00:00 +0000 https://office365itpros.com/?p=63376

Keeping an Eye on App Permissions

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.

Hash Tables for Fast Permission Lookup

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.

Finding App Permissions from an App’s Service Principal

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]

Other Methods to Report App Permissions

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.

Email containing an app permissions report.
Figure 1: Email containing an app permission report

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.

]]>
https://office365itpros.com/2024/01/24/app-permissions-report/feed/ 0 63376
How to Report Expiring Credentials for Entra ID Apps https://office365itpros.com/2024/01/16/app-credential-expiration/?utm_source=rss&utm_medium=rss&utm_campaign=app-credential-expiration https://office365itpros.com/2024/01/16/app-credential-expiration/#comments Tue, 16 Jan 2024 01:00:00 +0000 https://office365itpros.com/?p=63246

Use the Microsoft Graph to Report App Credential Expiration Dates

A reader asks if it’s possible to notify administrators when app secrets expire or are close to expiring. App secrets (also called client secrets) are a mechanism to allow Entra ID registered applications to prove their identity and secure an access token to allow them to retrieve data. App secrets last between three and twenty-four months are not intended for production use. Instead, app secrets are a good way to test an application to make sure that it can access the right data using the right permissions before moving it from development to production.

An app can use both secrets and certificates (Figure 1). Both methods work, but once an application reaches production, it should move away from app secrets and use certificate-based authentication or, if the app runs as an Azure Automation runbook, a managed identity.

An Entra ID registered app can use both secrets and certificates to authenticate.
Figure 1: An Entra ID registered app can use both secrets and certificates to authenticate

Keeping track of expiring app secrets is a good idea. Developers might not have access to the app to replace an expired secret and have to ask an administrator to create a new secret. And if an app does get through into production while using app secrets, the check might reveal its existence and prompt developers to use a more secure authentication method.

Certificates do have expiration dates and are more likely to be used for production than app secrets, so paying attention to certificate expiration can be extremely important. Finally, it’s good to remove expired credentials from apps because once a credential expires, it becomes unusable debris. Being proactive and reviewing apps periodically helps to identify issues before they occur, which is always a positive thing to do.

Creating a Script to Report App Credential Expiration

The steps involved in creating a script to find applications and report their app credential expiration dates are straightforward. Here’s an outline.

  • Connect to the Graph endpoint with the Application.Read.All permission. If you want to email the report, the script uses the Mail.Send permission.
  • Run the Get-MgApplication cmdlet to find the set of registered apps. I usually reduce the set by only processing single-organization apps (the sign-in audience is “AzureADMyOrg”) and by removing the apps used by the SharePoint Framework to make calls to Graph APIs. The resulting set are those created by developers for use within the tenant.
  • For each credential, find its expiration date and report if it is expired, active, or about to expire (within 30 days of expiration).
  • Email the report to tenant administrators.

The PowerShell is relatively simple. The only oddity I found in the data is that some app secrets have expiration dates of 31-Dec-2299, which seems just a tad long (Figure 2 shows the excessively long-lasting expiration dates in an emailed report). While I can’t remember this, apparently it was possible to set expiration for an app secret to be “never” in the past, which results in the 2299 date. The Entra ID admin center doesn’t allow a never-expiring app secret now, and certainly no secret created since March 2021 has such an expiration. Never-expiring app secrets might be convenient for development purposes, but if you don’t like them, you can easily fix the problem by removing the secret and replacing it with a secret configured with a more reasonable expiration date. Any apps that use the deleted secret must be updated to use the new secret.

Email to administrators with information about app credential expiration dates/
Figure 2: Email to administrators with information about app credential expiration dates

Points About Retrieving App Credential Expiration Dates

By default, the Get-MgApplication cmdlet does not retrieve details of app secrets and credentials when it retrieves information for a registered app. To get this data, you must include the keyCredentials (certificates) and passwordCredentials (app secrets) properties in the set requested when running the cmdlet.

Microsoft’s documentation doesn’t explain this, so working things out took a little while and some experimentation with the Graph Explorer. Even the Graph X-Ray extension, which gives an insight into the Graph commands used by the Entra ID admin center to process objects, comes up with a blank when exposing the credentials for an application, but I got there in the end.

Run Periodic Checks to Make Sure that App Credentials Don’t Expire

I like to run jobs that check settings on a periodic basis using scheduled Azure Automation runbooks. The code for the script (downloadable from GitHub) is easily adapted to access the Graph witha managed identity. One advantage of using a runbook is that the script can send email from any account, unless RBAC for Applications restricts access to certain mailboxes, which is probably how things should be configured.


Learn more about how the Microsoft 365 applications and Entra ID 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.

]]>
https://office365itpros.com/2024/01/16/app-credential-expiration/feed/ 11 63246
Mastering Microsoft Graph PowerShell SDK Foibles https://office365itpros.com/2024/01/12/user-extension-attributes-sdk/?utm_source=rss&utm_medium=rss&utm_campaign=user-extension-attributes-sdk https://office365itpros.com/2024/01/12/user-extension-attributes-sdk/#respond Fri, 12 Jan 2024 01:00:00 +0000 https://office365itpros.com/?p=63200

Microsoft 365 Groups, Entra ID, and User Extension Attributes

Last year, I wrote about some of the foibles encountered by scripters as they work with the Microsoft Graph PowerShell SDK. At the time, we were waiting for V2 of the SDK, which duly arrived in July 2023. At the time of writing, the current version of the SDK is 2.11.1, meaning that updates appear frequently.

A foible that recently came to my attention is that the fifteen custom single-value attributes available for customers to populate for Exchange Online mailboxes are synchronized to Entra ID and available through the Get-MgUser cmdlet, but the same attributes are not available for Entra ID groups even though they exist in Exchange. The five multi-value custom attributes available for mail-enabled objects in Exchange do not synchronize with Entra ID.

Custom attributes allow organizations to store whatever data they like for mailboxes, groups, and other mail-enabled objects. Mailboxes are linked to user accounts and Entra ID synchronizes the 15 custom attributes to OnPremisesExtensionAttributes for Entra ID accounts. An Entra ID account does not have to be mailbox-enabled to use these attributes, but most are.

Figure 1 shows the extension attributes for my account as viewed through the Entra ID admin center. Extension attribute 9 is used to hold details of my favorite drink, which is then exposed to those who need to know by customizing the Microsoft 365 user profile card.

User extension attributes for an account shown in the Entra ID admin center.
Figure 1: User extension attributes shown in the Entra ID admin center

Another example of using custom attributes is to set an expiration date for guest accounts that can then be actioned by processes to detect and remove expired accounts.

Entra ID groups don’t currently support custom attributes and that’s where the problem lies. Given that Microsoft 365 groups and distribution lists support these attributes and show up as Entra ID groups, it seems like a gap exists in the connection between Exchange Online and Entra ID. The Graph APIs can’t make data appear where it not present.

I’ve asked Microsoft why groups don’t support custom attributes and discussions continue. Hopefully, Microsoft will close the gap in the future. In the meantime, if you need to work with custom attributes for groups, use the Exchange Online cmdlets.

Reporting Graph SDK Problems

I reported the problem with custom attributes for groups to Microsoft via the PowerShell GitHub repro. This is the right place to report issues and suggestions for the Microsoft Graph PowerShell SDK. The SDK development team monitors the issues that come in and will respond. Before you add a new issue, it’s worthwhile scanning the set of existing issues to see if someone else reported the same problem. Reading problems can also be a good way to learn how SDK cmdlets work and how people are using them to solve problems.

Understanding Graph Permissions

Apps, including the Microsoft Graph PowerShell SDK, need permissions (scopes) to access data via Graph APIs. It’s sometimes difficult to understand what permission is needed to do something, especially when contemplating interactive sessions (delegate permissions and administrative roles assigned to the signed-in account) versus other forms of use like certificate-based authentication, Azure Automation runbooks, and registered apps, all of which use application permissions. Administrative roles can also come into the frame too. The bottom line is that picking the right permissions – and the least-permissioned of those permissions -can take some effort.

This article covers how to use Graph SDK cmdlets like Find MgGraphPermission to find the right permissions. Christian Rittler used Find-MgGraphPermission to create a useful function called Get-GraphScriptPermission that accepts a script block as input and parses the cmdlets in the script block to find the required permissions. The idea is that instead of checking individual cmdlets, you can check what permissions are needed for an entire script. For example, this code creates a script block containing SDK cmdlets to retrieve user accounts and check each account to find if a manager exists.

$Script = {
[array]$Users = Get-MgUser -All -Filter "userType eq 'Member'"
ForEach ($User in $Users) {
   $Manager = Get-MgUser -UserId $User.Id | Select-Object userPrincipalName, @{n="Manager";e={(Get-MgUserManager -UserId $_.Id).AdditionalProperties.userPrincipalName}}
   If ($Manager) {
      Write-Host ("User {0}'s manager is {1}" -f $User.displayName, $Manager.Manager)
   }
 }
}

To use the function, call it and pass the variable containing the script block. The output lists the cmdlets found and the permissions needed.

Get-GraphScriptPermission -Script $Script

Cmdlet : Get-MgUser
Source : Microsoft.Graph.Users
Verb   : Get
Type   : MgUser
Scopes : DeviceManagementApps.Read.All (admin: True), DeviceManagementApps.ReadWrite.All (admin: True),DeviceManagementConfiguration.Read.All (admin: False), DeviceManagementConfiguration.Read.All (admin: True), DeviceManagementConfiguration.ReadWrite.All (admin: True), DeviceManagementManagedDevices.Read.All (admin:False), DeviceManagementManagedDevices.ReadWrite.All (admin: False),DeviceManagementManagedDevices.ReadWrite.All (admin: True), DeviceManagementServiceConfig.Read.All (admin: False), DeviceManagementServiceConfig.Read.All (admin: True), DeviceManagementServiceConfig.ReadWrite.All (admin: False), DeviceManagementServiceConfig.ReadWrite.All (admin: True), Directory.Read.All (admin: False), Directory.ReadWrite.All (admin: False), User.Read (admin: False), User.Read.All (admin: False), User.Read.All (admin: True), User.ReadBasic.All (admin: False), User.ReadWrite (admin: False), User.ReadWrite.All (admin:False), User.ReadWrite.All (admin: True)

Cmdlet : Get-MgUserManager
Source : Microsoft.Graph.Users
Verb   : Get
Type   : MgUserManager
Scopes : Directory.Read.All (admin: True), Directory.ReadWrite.All (admin: True), User.Read.All (admin: True), User.ReadWrite.All (admin: True)

When a permission has admin: True, it means that the account running the code must hold a suitable administrative role to use the cmdlet. Many of the scopes listed for Get-MgUser can be used without an administrative role to allow users to retrieve details of their account, but an administrative role is needed to run Get-MgUserManager.

I amended the original function to generate scopes as strings rather than an array along with some other minor changes. You can download my version from GitHub, use the original, or create your own.

In the past, developers had to consult the documentation for the underlying Graph APIs to find details of required permissions. Microsoft has started to include this information in the documentation for the Graph SDK cmdlets, and that’s a welcome step forward.

SDK Improving Slowly

There’s no doubt that the Graph SDK is improving all the time, albeit slowly, especially with the retirement of the MSOL and Azure AD modules fast approaching (March 30, 2024). Perhaps this is familiarity talking and someone will less experience of dealing with SDK foibles, permissions, and missing features might not be quite so positive. But nothing is perfect (especially software). Upwards and onwards.

]]>
https://office365itpros.com/2024/01/12/user-extension-attributes-sdk/feed/ 0 63200
A New Approach to Reporting Exchange Mailbox Statistics https://office365itpros.com/2023/11/21/graph-usage-data-mailboxes/?utm_source=rss&utm_medium=rss&utm_campaign=graph-usage-data-mailboxes https://office365itpros.com/2023/11/21/graph-usage-data-mailboxes/#respond Tue, 21 Nov 2023 01:00:00 +0000 https://office365itpros.com/?p=62520

Exploit Graph Usage Data Instead of PowerShell Cmdlets

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.

Graph Usage Data and Microsoft 365 Admin Center Reports

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

Email usage reports in the Microsoft 365 admin center

Graph usage data
Figure 1: Email usage reports in the Microsoft 365 admin center

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.

User Data Obfuscation

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.

Faster but Slightly Outdated

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.

]]>
https://office365itpros.com/2023/11/21/graph-usage-data-mailboxes/feed/ 0 62520
Using Microsoft Graph SDK Cmdlets to Create a SharePoint Online List https://office365itpros.com/2023/10/30/create-sharepoint-list-graph/?utm_source=rss&utm_medium=rss&utm_campaign=create-sharepoint-list-graph https://office365itpros.com/2023/10/30/create-sharepoint-list-graph/#comments Mon, 30 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=62117

Easier to Create SharePoint Lists with PnP.PowerShell

Updated 14 August 2024

Last week, I wrote about how to use cmdlets from the PnP.PowerShell module to create and populate a list in a SharePoint Online site using data generated by the Teams Directory script. As benefits a module deeply rooted in SharePoint history, the cmdlets worked well and the script wasn’t too difficult to write.

The Microsoft Graph is supposed to be “the gateway to data and intelligence in Microsoft 365. It provides a unified programmability model that you can use to access the tremendous amount of data in Microsoft 365…” I don’t have much argument with this assertion because in most cases, it’s true. That is, until you come to SharePoint Online where the coverage of SharePoint objects and data is not as good as for other workloads.

This article describes some of the challenges involved in writing a script based on Microsoft Graph PowerShell SDK cmdlets and Graph API requests to create SharePoint lists similar to what I did using PnP.PowerShell. Let’s see how I got on.

How to Create SharePoint List Script in a Nutshell

The script is simple in concept. The data comes from a CSV file generated by the Teams Directory script. The script creates a list in a SharePoint Online site and populates the list with items imported from the CSV file. The plan was to use cmdlets from the Microsoft Graph PowerShell SDK (V2.8) because there appears to be cmdlets available for everything the script needs to do.

Connecting to the Graph and the Target Site

The first steps connect to the Graph with the relevant permissions and retrieve details of the site holding the list. The script then checks if the list already exists and if found, removes the list. Rebuilding a list from scratch is easier than attempting to synchronize changes.

Connect-MgGraph -Scopes Sites.ReadWrite.All, Sites.Manage.All -NoWelcome
$ListName = "Teams Directory - Graph"
# Get target site 
Write-Host "Fetching details of the target site and list..."
$Site =  Get-MgSite -Search 'Office 365 for IT Pros Communications'
# Get List
$List = Get-MgSiteList -SiteId $Site.Id -Filter "displayName eq 'Teams Directory - Graph'"
If ($List) {
    # Delete the list
    Write-Host ("Removing previous version of list {0}" -f $List.DisplayName)
    Remove-MgSiteList -SiteId $Site.Id -ListId $List.Id
}

Removing a list like this won’t work if a retention label applies to the list.

Create SharePoint List with New-MgSiteList

The next step creates the list. The Graph SDK includes the New-MgSiteList cmdlet, but no matter what I did with the cmdlet, it refused to co-operate. Even the example from the Microsoft documentation failed with the following error:

New-MgSiteList_Create: Unable to determine type of provided column definition
 
Status: 400 (BadRequest)
ErrorCode: invalidRequest
Date: 2023-10-20T16:44:06

As described in this SDK bug report, the problem is that the columns shown in the example define the data type for each column but not what’s acceptable in the column (see this page for more detail about the supported types for list columns). For instance, the text data type can be plain text or rich text or both. If you don’t want to be this specific when creating a list (because you want to customize the list through the GUI afterwards), you can run the Invoke-MgGraphRequest cmdlet to create the list as shown below:

Write-Host "Defining the new list"
$Uri = ("https://graph.microsoft.com/v1.0/sites/{0}/Lists" -f $Site.Id)
$ListDetails = '{
    "displayName": "Teams Directory - Graph",
    "description": "Discover teams to join in Office 365 for IT Pros",
    "columns": [
      {
        "name": "Deeplink",
        "description": "Link to access the team",
        "text": { }
      },{
        "name": "Description",
        "description": "Purpose of the team",
        "text": { }
      },
      {
        "name": "Owner",
        "description": "Team owner",
        "text": { }
      },      
      {
        "name": "OwnerSMTP",
        "description": "Primary SMTP address for owner",
        "text": { }
      },
      {
        "name": "Members",
        "description": "Number of tenant menbers",
        "number": { }
      },
      {
        "name": "ExternalGuests",
        "description": "Number of external guest menbers",
        "number": { }
      },
      {
        "name": "Access",
        "description": "Public or Private access",
        "text": { }
      },
    ],
  }'
Invoke-MgGraphRequest -Uri $Uri -Method POST -Body $ListDetails | Out-Null

The Graph request creates a blank list. The new list includes the specified columns and a single column called Title inherited from the template. If you want to use a column called Title, you can leave it as is. If not, you can rename the column, which is what the script does to make the Title column to be TeamName. The internal name of the column remains Title, which is important to remember when updating records.

$List = Get-MgSiteList -SiteId $Site.Id -Filter "displayName eq 'Teams Directory - Graph'"
$ColumnId = (Get-MgSiteListColumn -SiteId  $Site.Id -ListId $List.Id | `
    Where-Object {$_.Name -eq 'Title'}).Id
Update-MgSiteListColumn -ColumnDefinitionId $ColumnId -SiteId $Site.Id -ListId $List.Id `
  -Description 'Name of the team' -DisplayName 'Team Name' -Name 'TeamName' | Out-Null

Adding Records to the List

After preparing the list, the script populates it with data imported from the Teams Directory. I ran into issues with the New-MgSiteListItem cmdlet. This could be a documentation issue, but some internet forums (like this example) indicate that this cmdlet has not had a happy history. I ended up creating each item as a custom object, wrapping the item data inside another custom object, converting it to JSON, and using the JSON content as a payload to post to the items endpoint:

$Uri = ("https://graph.microsoft.com/v1.0/sites/{0}/lists/{1}/items" -f $Site.Id, $List.Id)
ForEach ($Team in $TeamsData) {
  Write-Host ("Adding directory record for team {0} {1}/{2}" -f $Team.Team, $i, $TeamsData.Count)
  $i++
  $FieldsDataObject  = [PSCustomObject] @{
        Title          = $Team.Team
        Deeplink       = $Team.Deeplink
        Description    = $Team.Description
        Owner          = $Team.Owner
        OwnerSMTP      = $Team.OwnerSMTP
        Members        = $Team.Members
        ExternalGuests = $Team.ExternalGuests
        Access         = $Team.Access
  }
  $NewItem = [PSCustomObject] @{
        fields         = $FieldsDataObject
  } 
  $NewItem = $NewItem | ConvertTo-Json
  $Status = Invoke-MgGraphRequest -Method POST -Uri $Uri -Body $NewItem
  If ($Status.Id) {
     Write-Host ("Record added to list with id {0}" -f $Status.Id)
  }
}   

This approach works, but I could never write to a hyperlink field (something that the Add-PnPListItem cmdlet can do). Apparently, the Graph doesn’t currently support list hyperlink fields, so I ended up writing the deeplink to a team to a text field. The result is the list shown in Figure 1 where users see deeplinks that are not clickable. Users can copy the link to a browser tab and navigate to Teams that way, but that’s not very user-friendly. For small lists, you can create a hyperlink field in the list and copy deeplinks to that field. Users can then click on the link in the hyperlink field. Such a solution is unacceptable at any scale.

Teams directory data written to a SharePoint list using the Graph

Create sharepoint list
Figure 1: Teams directory data written to a SharePoint list using the Graph

You can download the full script from GitHub.

Choose PnP.PowerShell to Create SharePoint Lists

What I learned from the exercise is that the PnP.PowerShell module is a more robust and reliable tool to use when working with SharePoint Online lists. PnP has its own quirks, but it works. I spent far too long chasing Graph SDK cmdlets that didn’t work as documented or couldn’t do what I wanted, so I recommend that you use PnP until Microsoft sorts out the SDK cmdlets and documentation.

In closing, I asked Bing Chat Enterprise to write a script to create and populate a list in a SharePoint site Online based on the Microsoft Graph PowerShell SDK. The results were impressive (Figure 2).

Bing Chat Enterprise script to create and populate a SharePoint Online list
Figure 2: Bing Chat Enterprise script to create and populate a SharePoint Online list

After this experience, I might use Bing Chat Enterprise more often in the future to sketch out the basics of scripts. In this case, Bing Chat Enterprise was helpful. In others, it’s been awful. But that’s the nature of generative AI in respect of its ability to regurgitate errors couched in what seems to be impressive terms.


Keep up to date with developments like how to create SharePoint lists with the Microsoft Graph PowerShell SDK 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.

]]>
https://office365itpros.com/2023/10/30/create-sharepoint-list-graph/feed/ 1 62117
Microsoft 365 Groups with Long Names Cause Graph Errors https://office365itpros.com/2023/10/16/group-display-name-error-120/?utm_source=rss&utm_medium=rss&utm_campaign=group-display-name-error-120 https://office365itpros.com/2023/10/16/group-display-name-error-120/#respond Mon, 16 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61883

Keep Group Display Names Short to Avoid Problems

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

Teams settings for a group with a very long name
Figure 1: Teams settings for a group with a very long name

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.

Replicating the Problem

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
 

The Workaround for Group Display Name Errors

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.

Inconsistency in Entra ID Admin Center

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

The Entra ID admin center wants to restrict group names to 120 characters

Group display name error
Figure 2: The Entra ID admin center wants to restrict group names to 120 characters

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.

Time to Update SDK Foibles

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.

]]>
https://office365itpros.com/2023/10/16/group-display-name-error-120/feed/ 0 61883
Microsoft Graph PowerShell SDK V2.0 Reaches General Availability https://office365itpros.com/2023/07/10/graph-powershell-sdk-v2/?utm_source=rss&utm_medium=rss&utm_campaign=graph-powershell-sdk-v2 https://office365itpros.com/2023/07/10/graph-powershell-sdk-v2/#comments Mon, 10 Jul 2023 01:00:00 +0000 https://office365itpros.com/?p=60781

Release of Microsoft Graph PowerShell SDK V2.0 an Example of Poor Communications

Updated: 14 August 2023

On July 4, 2023 when the Microsoft world was very quiet due to the U.S. holiday, the Graph SDK development team made V2.0 of the Microsoft Graph PowerShell SDK generally available through the PowerShell Gallery. Despite the ramifications of moving to V2.0 of the SDK, I can find no public announcement of this fact in a Microsoft Technical Community blog or Microsoft 365 message center notification (both mandatory for announcements of this nature that affect Microsoft 365 tenants). Vasil told me of a Devblogs post dated July 7, but that’s not a place where tenant admins pick up their news.

I only started to look for evidence of general availability after I noticed that some documentation featured 2.0 examples and the Graph X-Ray tool began to output V2.0 code.

Microsoft Graph PowerShell SDK V2.0 (but only the V1.0 endpoint cmdlets)
Figure 2: Microsoft Graph PowerShell SDK V2.0 (but only the V1.0 endpoint cmdlets)

Update: Since the original release of the SDK, Microsoft has pushed out several updates to fix issues that have become apparent since the transition to the new module. The current version is V2.3.

Microsoft announced the preview of the Microsoft Graph PowerShell SDK V2 in December, 2022. At the time, I expressed some doubts, not least the proposal to split the Microsoft Graph PowerShell SDK V2.0 into V1.0 (production) and beta modules. The intention to speed up load time and reduce the overall size of the module was good. My doubt centered on the potential impact on customer scripts, especially as organizations had to figure out how to move from the AzureAD and MSOL modules. Microsoft’s decision to push out the final deprecation of those modules until March 30, 2024 eased the pressure to update, but the fact remains that customers migrated and wrote scripts from scratch using SDK V1.0. The changes Microsoft proposed implied further work to check, modify, and test any script created using SDK V1.0.

The moment of truth arrived. Microsoft documentation now centers on SDK 2.0 and the development group met their target to shop SDK 2.0. However, no guidance exists about how to check scripts written using SDK 1.0 for SDK 2.0. The foibles we learned to live with in SDK 1.0 have not gone away and some new issues have appeared. Did anyone say that life was good?

Updating to Microsoft Graph PowerShell SDK 2.0

Upgrading to SDK 2.0 is easy. I recommend that you reboot your PC to make sure that PowerShell has no loaded module and then create a new Administrator session. Run the Update-Module cmdlet to fetch the Microsoft.Graph module from the PowerShell gallery and you’ll soon have SDK 2.0. Alternatively, use the script to upgrade all Office 365 modules (including the SDK) including the essential step of removing old versions (I’ve updated the script for SDK 2.0).

The problem is that updating the Microsoft.Graph module only updates the V1.0 module. If you want to use the beta cmdlets to interact with the beta Graph endpoint, you must install the beta module separately:

Install-Module Microsoft.Graph.Beta -AllowClobber

Upgrading to Microsoft Graph PowerShell SDK V2.0 Cmdlets

Microsoft has a migration tool in the shape of a PowerShell module. The tool is a cmdlet to scan a script and update cmdlet names with beta cmdlet names where appropriate. Having a tool is nice, but it’s not enough to verify that every cmdlet will work as before after upgrading to SDK V2.

Chapter 23 of the Office 365 for IT Pros eBook (2024 edition) covers PowerShell and the Graph. It includes hundreds of code examples, including many based on SDK 1.0. I spent the weekend going through every example to see what works and what doesn’t. We’ll release the results of that work (and reviews of SDK examples in other chapters) in our August 2023 update. In the interim, here are some first impressions:

When you run Connect-MgGraph to connect to the Graph, you use the V1.0 cmdlets. When checking scripts, the first thing to look for is the presence of the Select-MgProfile cmdlet, used by the V1.0 SDK to switch between V1.0 and beta endpoints. As the V1.0 endpoint is the default, the usual command is:

Select-MgProfile beta

Once you see this command in a script, you know that you’ll need to remove the command and update the SDK cmdlets to use beta cmdlets. For example:

Get-MgUser becomes Get-MgBetaUser.

Get-MgGroup becomes Get-MgBetaGroup.

Get-MgSubscribedSku becomes Get-MgBetaSubscribedSku, and so on.

Rushing to use the beta cmdlets is a simplistic upgrade strategy. Before deciding to use a beta cmdlet, check the V1.0 cmdlet to see if it does what you need. If it does, use the V1.0 cmdlet. For example, in SDK V1.0, I always used the beta cmdlets whenever fetching details of user licenses. For example, this code finds user accounts with at least one assigned license:

[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable Records -All

The command works, but the V1.0 cmdlet doesn’t retrieve the assignedLicenses property that holds the license information. The beta cmdlet does retrieve the license data, which is why it seems like a good thing to use. However, you can instruct Get-MgUser to retrieve assignedLicenses by including the Property parameter, meaning that scripts can use the V1.0 cmdlet:

[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable Records -All -Property id, displayName, userPrincipalName, assignedLicenses

On the other hand, if a V1.0 cmdlet doesn’t work, it’s worth trying its beta counterpart to see if it succeeds. As an example, the Update-MgUser cmdlet can’t update values for Azure AD custom security attributes but the Update-MgBetaUser cmdlet can.

Problems Noted with Microsoft Graph PowerShell SDK V2.0 Cmdlets

Apart from the expected work to replace beta cmdlets with new cmdlet names, I found a couple of other issues. The strong caveat is that I only reviewed cmdlets used in examples. There might be other problems lurking in SDK 2.0. Here are my notes:

The cmdlets that update properties like group owners and user managers operate by reference, passing an identifier to point to the account of the new owner or manager. With SDK V1.0, I could pass the identifier of a new group owner like this:

New-MgGroupOwnerByRef -GroupId $GroupId -AdditionalProperties @{"@odata.id"="https://graph.microsoft.com/v1.0/users/2a3b60f2-b36b-4758-8533-77180031f3d4"}

Running the code in V2.0 signals an error:

cmdlet New-MgGroupOwnerByRef at command pipeline position 1
Supply values for the following parameters:
OdataId:

However, passing the identifier for the new owner through the BodyParameter parameter works:

$UserId = (Get-MgUser -UserId Rene.Artois@office365itpros.com).id
$OwnerId = ("https://graph.microsoft.com/v1.0/users/{0}" -f $UserId)
$NewOwner = @{"@odata.id"=$OwnerId}
New-MgGroupOwnerByRef -GroupId $GroupId -BodyParameter $NewOwner

SDK V2.0 cmdlets are more precise about specifying what properties cmdlets should retrieve. For instance, using the V1.0 code, I could do this to find the last sign-in activity for guest accounts using the beta endpoint:

[array]$Guests = Get-MgUser -Search $Search -ConsistencyLevel Eventual -Property SignInActivity -All
If (!($Guests)) { Write-Host "No matching account can be found"; break }
ForEach ($G in $Guests) {
   If ($G.UserType -eq "Guest") {
      If (!([string]::IsNullOrWhiteSpace($G.SignInActivity.LastSignInDateTime))) {
          $Days = New-TimeSpan($G.SignInActivity.LastSignInDateTime)    
          Write-Host ("Guest member {0} last signed in on {1} or {2} days ago" -f $G.DisplayName, $G.SignInActivity.LastSignInDateTime, $Days.Days ) }
      Else { Write-Host ("No recent sign-in data available for {0} ({1})" -f $G.DisplayName, $G.Mail)  }
     }
}

With SDK 2.0, if you use the V1.0 endpoint, make sure that you request the properties you want to use in your script. Returning a minimal set of object properties is a quirk of the SDK cmdlets. It’s done for performance reasons but it’s different to the way other workload modules work.

[array]$Guests = Get-MgUser -Search $Search -ConsistencyLevel Eventual -Property SignInActivity, id, DisplayName, userPrincipalName, userType -All

My initial attempts to fetch the Microsoft 365 Groups policy template resulted in an error because a failure to load version 1.28 of the Microsoft.Graph.Authentication sub-module:

$GroupTemplate = (Get-MgDirectorySettingTemplate | Where-Object {$_.DisplayName -eq "Group.Unified.Guest"})
WARNING: Unable to find type [Microsoft.Graph.PowerShell.Authentication.Utilities.DependencyAssemblyResolver].
Get-MgDirectorySettingTemplate : Could not load file or assembly 'Microsoft.Graph.Authentication, Version=1.28.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. The system cannot find the file
specified.

When I checked the loaded modules, I found two references to V1.28.0. I have no idea why these versions of the modules still existed on the PC (the script to update modules should have removed the older files). I assume that the files were loaded when the script attempted to remove them.

Get-Module

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     2.0.0      Microsoft.Graph.Applications        {Add-MgApplicationKey, Add-MgApplicationPassword, Add-MgSe...
Script     2.0.0      Microsoft.Graph.Authentication      {Add-MgEnvironment, Connect-MgGraph, Disconnect-MgGraph, G...
Script     1.28.0     Microsoft.Graph.Authentication      {Add-MgEnvironment, Connect-MgGraph, Disconnect-MgGraph, G...
Script     2.0.0      Microsoft.Graph.Beta.Identity.Di... {Complete-MgBetaDirectoryImpactedResource, Complete-MgBeta...
Script     2.0.0      Microsoft.Graph.Beta.Reports        {Confirm-MgBetaAuditLogSignInCompromised, Confirm-MgBetaAu...
Script     2.0.0      Microsoft.Graph.Groups              {Add-MgGroupDriveListContentTypeCopy, Add-MgGroupDriveList...
Script     1.28.0     Microsoft.Graph.Identity.Directo... {Complete-MgDirectoryImpactedResource, Complete-MgDirector...

After exiting all PowerShell sessions, I deleted the on-disk directories for V1.28.0 of Microsoft.Graph.Authentication and Microsoft.Graph.Identity.DirectoryManagement (for instance, C:\Program Files\WindowsPowerShell\Modules\Microsoft.Graph.Authentication\1.28.0). After deleting the files, I updated the script to use the Get-MgBetaDirectorySettingTemplate to make everything work. This experience points to the need to clear out old V1.0 modules from PCs to make sure that they are not loaded.

Installing the Microsoft Graph PowerShell SDK 2.0 on a new PC won’t run into this problem.

Time Needed to Move to Microsoft Graph PowerShell SDK V2.0

While I don’t like Microsoft’s communications around making SDK V2.0 generally available, moving to the new version isn’t horrible. It’s just another thing that must be done, another time drain on PowerShell developers. Now I have to go and review my SDK-based scripts to upgrade them to V2.0. Joy!


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.

]]>
https://office365itpros.com/2023/07/10/graph-powershell-sdk-v2/feed/ 6 60781
The Right Way for Scripts to Request Permissions with the Microsoft Graph PowerShell SDK https://office365itpros.com/2023/06/02/connect-mggraph-scopes/?utm_source=rss&utm_medium=rss&utm_campaign=connect-mggraph-scopes https://office365itpros.com/2023/06/02/connect-mggraph-scopes/#respond Fri, 02 Jun 2023 01:00:00 +0000 https://office365itpros.com/?p=60314

Pass Permissions with the Connect-MgGraph Scopes

Connect-MgGraph Scopes

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

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.

Defining Permissions for a Script with Connect-MgGraph Scopes

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.

Examples for Connect-MgGraph Scopes

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.

]]>
https://office365itpros.com/2023/06/02/connect-mggraph-scopes/feed/ 0 60314
Microsoft Graph Early Adopter Badges and Other Stuff https://office365itpros.com/2023/05/26/microsoft-graph-early-adopter/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-early-adopter https://office365itpros.com/2023/05/26/microsoft-graph-early-adopter/#comments Fri, 26 May 2023 01:00:00 +0000 https://office365itpros.com/?p=60213

Giving Microsoft Feedback, Azure AD Cmdlet Throttling, and Microsoft Graph Early Adopter Badges

I’m the proud possessor of a badge awarded through the Microsoft Graph Early Adopter Recognition program, something that I never knew about nor realized that badges were on offer. Notification about the badge arrived in a surprise email. Apparently, the badge (Figure 1) recognizes people who provide Microsoft with valuable (meaningful) feedback about Microsoft Graph tools and SDKs. According to the program blurb, you should submit feedback by creating an issue in the GitHub repository of a Microsoft Graph product to allow the program managers to know about the issue and recognize the feedback.

The Microsoft Graph Early Adopter badge
Figure 1: The Microsoft Graph Early Adopter badge

Feedback About the Microsoft Graph PowerShell SDK

As far as I know, the only time I left feedback like this was to note some concerns about the direction Microsoft was heading for V2.0 of the Microsoft Graph PowerShell SDK. Microsoft has still not addressed some of the concerns, especially around the proposal to have sets of differently-named cmdlets for the V1.0 and beta endpoints.

If Microsoft’s proposal proceeds, anyone who’s upgrading PowerShell code to replace cmdlets from the soon-to-retire Azure AD and Microsoft Online Services modules with Microsoft Graph PowerShell SDK will have to revisit their code anywhere they use the beta endpoint. For example, the Get-MgUser cmdlet doesn’t return license details for an account via the V1.0 endpoint but it does via the beta endpoint. In any case, spirited discussions continue about that point.

Azure AD and MSOL Cmdlet Throttling

Speaking of the PowerShell retirements, I’ve noticed many examples where people report problems with throttling of their scripts. Figure 2 shows an example reported in the Facebook Office 365 Technical Discussions group where the New-MsolUser cmdlet halted after 20 transactions because it “exceeded the maximum number of allowable transactions.” The advice to “try again later” is because this is a temporary throttle imposed by Microsoft to advise people that they need to move off the deprecated modules.

Figure 2: The woes of throttling hit the New-MsolUser cmdlet
Figure 2: The woes of throttling hit the New-MsolUser cmdlet

It would be nice if Microsoft issued a more explicit and understandable error message. Something along the lines of “this cmdlet will stop working on 30 June 2023. Time to update your code! For now, we’re just throttling, but we will get serious soon…” It’s obvious that the message about the module retirements has not landed in some places, even though Microsoft has been banging the drum for about two years.

Note that cmdlet throttling only happens for cmdlets that interact with license management. The other Azure AD and MSOL cmdlets will continue working after the retirement date, but assigning and updating licenses to Azure AD accounts through PowerShell should now use Graph API requests or Graph SDK cmdlets.

Getting Back to Badges

Going back to the original topic, I don’t quite know how to feel about badges awarded for giving feedback. People like recognition, which is why Teams has the Praise app and Viva Insights includes a version of that app. The joy went out of those apps when Microsoft removed support for the creation and use of custom badges.

I never put myself in the category of those motivated by badges or awards, so I am ambivalent about the Microsoft Graph Early Adopter badge. I understand why the Microsoft team has gone down the path of creating a digital badge to recognize external contributions, but it doesn’t move my needle. But if it floats your boat, enjoy the opportunity to share the recognition with your nearest and dearest.

On the point of feedback, maybe the best way to let Microsoft know exactly what you think about their products, at least in the Microsoft 365 space, is to use the feedback portal. Spend a little time thinking about the message you want to send. Write it down in Word or another text editor. Leave it for an hour or so and then check the text again. And finally, copy your feedback into the appropriate space. You know it makes sense.


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

]]>
https://office365itpros.com/2023/05/26/microsoft-graph-early-adopter/feed/ 1 60213
New Graph API for SharePoint Online Administration https://office365itpros.com/2023/05/16/sharepoint-admin-api-graph/?utm_source=rss&utm_medium=rss&utm_campaign=sharepoint-admin-api-graph https://office365itpros.com/2023/05/16/sharepoint-admin-api-graph/#comments Tue, 16 May 2023 01:00:00 +0000 https://office365itpros.com/?p=60087

SharePoint Admin API Supports Graph-Based Administration

On May 8, Microsoft announced the availability of the SharePoint Admin Graph API to give developers and administrators access to tenant-wide settings for SharePoint Online and OneDrive for Business. The settings accessible through the API include a small subset of the controls that can be set using the Set-SPOTenant cmdlet from the SharePoint Online administration module along with some other settings that aren’t available to the cmdlet. For instance, the imageTaggingOption setting controls the Syntex image tagging option for the tenant (see message center notification MC551018, Microsoft 365 roadmap item 88715).

Because it only covers tenant-wide settings, this API is emphatically not a replacement for Microsoft’s SharePoint PowerShell module or the PnP PowerShell module. Nevertheless, it is a start on the process of moving SharePoint Online to the common Graph platform. With that thought in mind, let’s see how to use the API.

API Requirements

Two basic requirements must be met to run the SharePoint Admin API:

  • The app must have consent for the SharePointTenantSettings.ReadWrite.All permission. For instance, to run the API with the Graph Explorer, you must obtain administrator consent to use the SharePointTenantSettings.ReadWrite.All permission (Figure 1).
  • The signed-in user must belong to either the Global Administrator or SharePoint Administrator roles.

 Assigning the permission to use the SharePoint Admin API to the Graph Explorer
Figure 1: Assigning the permission to use the SharePoint Admin API to the Graph Explorer

Here’s an example of signing into the Microsoft Graph PowerShell SDK with the required permission and running the Invoke-MgGraphRequest cmdlet to fetch the set of tenant-wide settings supported by the API:

Connect-MgGraph -Scopes SharePointTenantSettings.ReadWrite.All
$Uri = "https://graph.microsoft.com/v1.0/admin/sharepoint/settings"
Invoke-MgGraphRequest -Uri $Uri

Name                           Value
----                           -----
imageTaggingOption             basic
isUnmanagedSyncAppForTenant... False
isMacSyncAppEnabled            True
isRequireAcceptingUserToMat... True
isSharePointMobileNotificat... True
idleSessionSignOut             {isEnabled, warnAfterInSeconds, signOutAfterInSeconds}
sharingBlockedDomainList       {Gmail.com}
personalSiteDefaultStorageL... 5242880
isCommentingOnSitePagesEnabled True
isSiteCreationEnabled          True
excludedFileExtensionsForSy... {*.exe, *.zip, *.rar, *.pst...}
@odata.context                 https://graph.microsoft.com/v1.0/$metadata#admin/sharepoint/settings/$entity
isFileActivityNotificationE... True
isSyncButtonHiddenOnPersona... False
isSharePointNewsfeedEnabled    False
sharingCapability              externalUserAndGuestSharing
sharingAllowedDomainList       {hotmail.com, live.com, locklan.com.au, Microsoft.com...}
availableManagedPathsForSit... {/sites/, /teams/}
isResharingByExternalUsersE... False
siteCreationDefaultManagedPath /sites/
deletedUserPersonalSiteRete... 60
allowedDomainGuidsForSyncApp   {}
isSiteCreationUIEnabled        True
isLegacyAuthProtocolsEnabled   True
siteCreationDefaultStorageL... 26214400
isSitesStorageLimitAutomatic   True
isSitePagesCreationEnabled     False
sharingDomainRestrictionMode   none
tenantDefaultTimezone          (UTC) Dublin, Edinburgh, Lisbon, London
isLoopEnabled                  True

The other option for SharePoint tenant settings is to update one of the supported settings. For example, here’s how to update the isLoopEnabled setting (required to allow Teams to use Loop components) by specifying the new value in a hash table and running a PATCH update against the API:

Invoke-mggraphrequest -uri $uri
$Settings = @{
   "isLoopEnabled" = $false }
Invoke-mggraphrequest -uri $uri -Method PATCH -Body $Settings

The API responds by listing the new settings.

Next Step Exchange?

Now that SharePoint Online has started to support a Graph API for tenant-wide settings, perhaps the next step is for Microsoft to create a Graph API for Exchange Online tenant-wide settings. The focus for automation in the Exchange development group has been twofold:

  • Move away from Exchange Web Services (EWS). Microsoft deprecated 25 EWS APIs on March 31, 2022. Given the strategic direction to embrace Graph APIs whenever possible and move away from “legacy” APIs, it seems logical that Microsoft will terminate support for EWS in the future.
  • Move PowerShell away from basic authentication and Remote PowerShell sessions. The process is well underway and will complete later this year.

The big issue for developers is that the Graph/PowerShell combination can still not do everything that’s possible in EWS. To move, developers need comparable functionality, especially in terms of an API to perform management operations. These aren’t as important in the cloud because Microsoft takes care of server management, but configuration of Exchange Online tenant settings is definitely desirable.


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.

]]>
https://office365itpros.com/2023/05/16/sharepoint-admin-api-graph/feed/ 2 60087
Removing Permissions from the Graph Explorer https://office365itpros.com/2023/05/04/graph-explorer-permissions/?utm_source=rss&utm_medium=rss&utm_campaign=graph-explorer-permissions https://office365itpros.com/2023/05/04/graph-explorer-permissions/#respond Thu, 04 May 2023 01:00:00 +0000 https://office365itpros.com/?p=60017

Too Many Graph Explorer Permissions Can Get in the Way

The Microsoft Graph Explorer is an excellent web-based tool for debugging Graph API requests. I can’t tell you how many times I have used the Graph Explorer to figure out the syntax needed to make a request work, or even to do real work, such as customizing the Microsoft 365 user profile card. Microsoft continues to add new features to the Graph Explorer to make it even better. The example queries are especially useful as they can be run against data from your tenant (if you sign in) or sample data.

One problem that the Graph Explorer shares with interactive sessions using the Microsoft Graph PowerShell SDK is permission accumulation. Over time, different permissions are necessary to run Graph requests. The consents granted to use those permissions accrue until the service principal (enterprise app) for the Graph Explorer (application identifier de8bc8b5-d9f9-48b1-a8ad-b748da725064) become excessive. Figure 1 shows the current permission status for the Graph Explorer in my tenant. My only explanation is that I do a lot of testing.

Permissions for the Graph Explorer enterprise app
Figure 1: Graph Explorer permissions assigned to the enterprise app

One quick way to clear all the permissions is to close the Graph Explorer window and then delete the Graph Explorer app. Wait a few minutes afterwards and then launch the Graph Explorer and sign-in again. The Explorer detects that its app isn’t available in the tenant and will recreate it with no consented permissions. The same technique works for the Microsoft Graph PowerShell SDK app.

The Problem of Too Many Permissions

Although it might seem good that the Graph Explorer should have the permissions necessary to run requests against many different types of Microsoft 365 data, too many permissions can get in the way. Take the situation where you use the Graph Explorer to test a request and are delighted when the request runs and returns data. Assuming that all is well, you go ahead and use the request in a PowerShell script only to discover that the script fails when it runs because the app it uses to authenticate for Graph requests doesn’t have the right permissions. The Graph Explorer had the necessary permission but you never knew (or checked) that an app must have consent for a specific permission to run the request you tested.

The least permission model used by the Graph can sometimes make it difficult to figure out exactly what permission an individual request requires. A further complexity is that the Graph Explorer uses delegated permissions rather than application permissions, so a lot of what the Explorer can do depends on the roles held by the signed-in account.

Clean Up Graph Explorer Permissions

In any case, to save bruised egos and ensure that testing reveals exactly what permissions are necessary, you can clean up the permissions held by the Graph Explorer. One way to do this is to remove permissions through the Microsoft Entra admin center, just like you’d do for any other registered app. The other is to use the Consent to permissions option in the Graph Explorer, revealed by clicking the avatar for the signed in user (Figure 2).

The consent to permissions option in the Graph Explorer
Figure 2: The consent to permissions option in the Graph Explorer

The option opens a page to browse Graph permissions. You can consent to permissions that the Graph Explorer does not have or unconsent to permissions that it does. In Figure 3, we see the set of Mail Graph permissions, three of which are held by the Graph Explorer (probably used when debugging my mailbox clean up script). When you see AllPrincipal listed against a permission, it means that an administrator granted consent. Principal means that a user granted consent.

Browsing Graph Explorer permissions
Figure 3: Browsing Graph Explorer permissions

When testing with the Graph Explorer, my first step now is to check the permissions already in place and remove any that I think might interfere with testing. I can then gradually add permissions back to establish the exact set of permissions required to allow a Graph request to run. After making changes, you must sign-out and sign back in again to pick up the refreshed permission set (in an access token).

One point to remember is that you can’t remove the User.Read permission because that’s needed to sign into the Graph Explorer.

Permission Accrual is Bad

It’s easier to leave permissions in place but this practice will lead to problems. Treat Graph Explorer as a tool to help you understand how Graph requests work, including the permissions needed to run, and clean up between projects. (And yes, the same advice applies to the app used by the Microsoft Graph PowerShell SDK).


Stay updated with developments across the Microsoft 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. We do the research to make sure that our readers understand the technology.

]]>
https://office365itpros.com/2023/05/04/graph-explorer-permissions/feed/ 0 60017
Microsoft Limits Graph API Requests for User Account Data https://office365itpros.com/2023/04/05/signinactivity-limit-graph-api/?utm_source=rss&utm_medium=rss&utm_campaign=signinactivity-limit-graph-api https://office365itpros.com/2023/04/05/signinactivity-limit-graph-api/#respond Wed, 05 Apr 2023 01:00:00 +0000 https://office365itpros.com/?p=59723

Old Limit with SignInActivity was 999 – New Limit for Azure AD Accounts is 120

Because it retrieves details of Azure AD accounts, the List Users API is one of the most heavily used of the Microsoft Graph APIs. It also underpins the Get-MgUser cmdlet from the Microsoft Graph PowerShell SDK. Microsoft generates the cmdlet from the API using a process called AutoRest, which means that changes made to the API show up soon afterward in the cmdlet.

I’ve documented some of the issues that developers must deal with when coding with the cmdlets from the Microsoft Graph PowerShell SDK. The cmdlets have been stable recently, which is a relief because tenants are migrating scripts from the Azure AD and MSOL modules. However, last week an issue erupted in a GitHub discussion that caused a lot of disruption.

In a nutshell, if you use List Users to fetch Azure AD accounts and include the SignInActivity property, the API limits the page size for results to 120 items. Calls made without specifying SignInActivity can set the page size to be anything up to 999 items.

An Unannounced Change

To help manage demand on the service, all Graph API requests limit the number of items that they return. To retrieve all matching items for a request, developers must fetch pages of results until nothing remains. When a developer knows that large numbers of items must be fetched, they often increase the page size to reduce the number of requests.

Microsoft didn’t say anything about the new restriction on requests that fetch Azure AD account data with sign-in activity. Developers only discovered the problem when programs and scripts failed. I first learned of the issue when some of the users of the Office 365 for IT Pros GitHub repository reported that a Graph request which included a $top query parameter to increase the page size to 999 items failed. For example:

$uri = "https://graph.microsoft.com/beta/users?`$select=displayName,userPrincipalName,mail,id,CreatedDateTime,signInActivity,UserType&`$top=999"
[array]$Data = Invoke-RestMethod -Method GET -Uri $Uri -ContentType "application/json" -Headers $Headers
Invoke-RestMethod : The remote server returned an error: (400) Bad Request.
At line:1 char:16
+ ... ray]$Data = Invoke-RestMethod -Method GET -Uri $Uri -ContentType "app ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest)
   [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.I

As shown in Figure 2, testing with the Get-MgUser cmdlet revealed some more information in the error (“Cannot query data for more than 120 users at a time”). This was the first time I learned about a query limit:

Get-MgUser reports more useful error information

Cannot query data for more than 120 users at a time (SignInActivity)
Figure 2: Get-MgUser reports more useful error information

According to a response reported in the GitHub discussion, Microsoft support reported

The PG have confirmed that this endpoint will be transitioning from beta to General Availability (GA).

As part of this transition, changes to its behavior has been made, this includes not requesting more than 120 results per call. They recommend requesting less than 120 results per call, which can be done by setting the top parameter to, say 100.”

It’s likely that Microsoft made the change because retrieving sign-in activity data for Azure AD accounts is an expensive operation. Reducing the page size to 120 possibly makes it easier to process a request than if it asked for 999 items.

Beta Version of List Users Moving to Production

When the product group (PG) says that the endpoint is transitioning from beta to GA, it means that instead of needing to use https://graph.microsoft.com/beta/users to access sign-in activity, the data will be available through https://graph.microsoft.com/V1.0/users. If you use the Microsoft Graph PowerShell SDK, you won’t have to run the Select-MgProfile cmdlet to choose the beta endpoint. Moving the beta version of the API to the production endpoint is a good thing because there are many other account properties now only available through the beta endpoint (like license assignments).

If you use the Microsoft Graph PowerShell SDK, the Get-MgUser cmdlet is unaffected by the change if you specify the All parameter. This is because the cmdlet handles pagination internally and fetches all pages automatically without the need to specify a page size. For instance, this works:

$AccountProperties = @( ‘Id’, ‘DisplayName’, ‘SignInActivity’)
[array]$Users = Get-MgUser -All -Property $AccountProperties | Select-Object $AccountProperties

Moving to Production

Although it’s good that Microsoft is (slowly) moving the beta versions of the List Users API towards production, it’s a pity that they introduced a change that broke so many scripts and programs without any warning. At worse, this so exhibits a certain contempt for the developer community. At best, it’s a bad sign when communication with the developer community is not a priority. That’s just sad.


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.

]]>
https://office365itpros.com/2023/04/05/signinactivity-limit-graph-api/feed/ 0 59723
Flaws in the Plan for Microsoft Graph PowerShell SDK V2 https://office365itpros.com/2022/12/20/microsoft-graph-powershell-sdk-v2/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-powershell-sdk-v2 https://office365itpros.com/2022/12/20/microsoft-graph-powershell-sdk-v2/#comments Tue, 20 Dec 2022 01:00:00 +0000 https://office365itpros.com/?p=58426

Work Ongoing on Other Projects – and Now the Microsoft Graph PowerShell SDK V2 Appears

Due to the deprecation of the Azure AD and Microsoft Online Services (MSOL) PowerShell modules (still scheduled for June 30, 2023), there’s been a lot of activity around upgrading scripts to use cmdlets from the Microsoft Graph PowerShell SDK. This is especially true for any script that performs license management activities as these cmdlets will stop working on March 31, 2023.

Microsoft’s documentation says, “Scripts written in Azure AD PowerShell won’t automatically work with Microsoft Graph PowerShell.” This is incorrect. The scripts won’t work at all because the cmdlets differ. Because the modules are based on very different technologies, no one-to-one translation from Azure AD cmdlets to SDK cmdlets either. Moving to a new module isn’t therefore not a matter of a quick edit to swap cmdlets over. Parameters and outputs differ. The effort needed to upgrade and test even a relatively simple script might extend to half a day or more.

The experience of using the SDK is growing within the technical community, but a knowledge gap still exists at times, especially when searching for good examples of how to accomplish a task. Microsoft’s documentation for the SDK cmdlets has improved recently, but it’s still not at the level that it should be.

Microsoft PowerShell Graph SDK V2

The current situation with the transition from Azure AD to SDK makes me think that Microsoft’s plan for changes in version two of the Microsoft PowerShell Graph SDK are badly flawed. The new version is still in the preview stage so things will probably change before general availability. At least, I hope that they do.

There’s some good changes lined up that I’ll cover first.

Although it’s possible to use V1 of the SDK with an Azure Automation managed identity, the method requires getting an access token from Azure and isn’t as clean as other implementations, such as those for Microsoft Teams and V3.0 of the Exchange Online management module. V2 of the SDK will allow you to connect using:

Connect-MgGraph -Identity

Support for managed identities will extend to user-created managed identities. Another change for authentication is support a credentials prompt when signing into the Graph. Finally, V2 supports certificate-based authentication.

Other changes include support for HTTP/2 and better handling by cmdlets for HTTP status codes.

Breaking Up is Hard to Do

V1 of the SDK is a giant module with 40 sub-modules (like Microsoft.Graph.Authentication). The size and unwieldly nature of the SDK means that it’s more difficult to manage than it should be. For instance, when Microsoft updates the SDK, the sub-modules used by developers on local PCs and in Azure Automation accounts require updating.

One reason why the SDK is so large is that it includes both V1.0 and beta version of cmdlets. This is because the Graph APIs that Microsoft generates the cmdlets from come in V1.0 and beta versions. Microsoft’s solution for the V2 SDK is to deliver separate modules: one for V1.0 (production) and another for beta.

Practical Side-Effects of Breaking the Microsoft Graph PowerShell SDK V2 into Two Modules

Conceptually, I don’t have any issue with the idea of splitting up the SDK into two modules. It’s on a practical level where my concerns kick in.

Today, a script can switch between V1.0 and beta by running the Select-MgProfile cmdlet. I do this all the time because the beta version of many cmdlets deliver more information than their V1.0 counterparts do. For example, Get-MgUser is a basic cmdlet to fetch details of an Azure AD user. The V1.0 cmdlet does not return license assignment data while the beta cmdlet does.

Select-MgProfile v1.0
Get-MgUser -UserId Tony.Redmond@office365itpros.com | fl assign*

AssignedLicenses :
AssignedPlans    :

Select-MgProfile beta
Get-MgUser -UserId Tony.Redmond@office365itpros.com | fl assign*

AssignedLicenses : {f61d4aba-134f-44e9-a2a0-f81a5adb26e4, 61902246-d7cb-453e-85cd-53ee28eec138, 26d45bd9-adf1-46cd-a9e1-51e9a5524128, 4016f256-b063-4864-816e-d818aad600c9...}
AssignedPlans    : {b44c6eaf-5c9f-478c-8f16-8cea26353bfb, fd2e7f90-1010-487e-a11b-d2b1ae9651fc,f00bd55e-1633-416e-97c0-03684e42bc42, 3069d530-e41b-421c-ad59-fb1001a23e11...}

Basic functionality issues afflict V1.0 cmdlets that operate against user accounts, groups, and other Azure AD objects. It would be nice if Microsoft fixed these problems and delivered a solid V1.0 module that allowed developers to focus on V1.0. Instead, the need exists to use the beta cmdlets.

Instead of making sure that many important cmdlets work like they should, Microsoft plans to drop the Select-MgProfile cmdlet. They say that “the profile design made the module bulky and error prone as it combined Microsoft Graph v1.0 and beta commands into a single module.” I accept that combining the two cmdlet sets in a single module is bulky, but is that a reason to remove a useful piece of functionality that allows developers to switch between V1.0 and beta cmdlets as needed? I don’t think it would take a lot of software engineering to figure out how to make the Select-MgProfile cmdlet load and unload modules as needed.

Even worse, Microsoft plans to introduce different names for the cmdlets in the two modules. Cmdlets in the V1.0 module will have the original names like Get-MgUser and Get-MgGroup. The beta cmdlets will have names like Get-MgBetaUser and Get-MgBetaGroup. Microsoft says that an advantage of their approach is that customers will be able to run V1.0 and beta cmdlets in the same script. In my experience, this never happens. Developers use Select-MgProfile to decide what cmdlets to use and then use cmdlets from that set. Mixing and matching cmdlets from different modules overcomplicates things.

Will this command be Get-MgBetaUser in the Microsoft Graph PowerShell SDK V2
Figure 1: Will this command be Get-MgBetaUser in the Microsoft Graph PowerShell SDK V2

The suggestion of using different names for cmdlets is just silly. It means that a developer must decide what module they want to use for a script up front to know what cmdlet names to use. Developers must check every existing script to identify if the correct cmdlet names are in place (and to deal with the Select-MgProfile issue). All the work done to upgrade scripts from the Azure AD and MSOL modules will need revalidation. That’s work Microsoft is forcing on tenants at a time when the Exchange development group wants tenants to upgrade their Exchange scripts to remove dependencies on Remote PowerShell. Forcing tenants to upgrade scripts for Exchange and Azure AD at the same time is an example of a lack of joined-up thinking within Microsoft.

I hear that Microsoft might generate a tool to help developers move to V2 by updating references to the beta cmdlets to use the new names. That might help, but work still needs to be done to review scripts before and after the tool runs and test to make sure that the updated script works. And what happens if Microsoft updates the V1.0 cmdlets and a decision is made to revert to that version? You’ll still have to update scripts manually.

A Way Forward for the Microsoft Graph PowerShell SDK V2

What I would like to see done in the Microsoft Graph PowerShell SDK V2 is:

  • Repurpose the Select-MgProfile cmdlet so that it switches between the two modules as transparently as possible.
  • Keep the same cmdlet names in both modules. It then becomes a choice for the developer as to which cmdlets to use.
  • Fix the V1.0 of basic user and group cmdlets like Get-MgUser and Get-MgGroup so that they return the information necessary to get real work done. If the V1.0 cmdlets delivered that functionality, the need to switch to beta wouldn’t be as pressing. The problems must be fixed in the Graph API rather than the SDK (which simply replicates what the Graph API does).

The precedent for having cmdlets with the same name in production and development modules exists. We’ve used the AzureAD and AzureADPreview modules in this manner for years. Why Microsoft can’t do the same with V2 of the Microsoft Graph PowerShell SDK is beyond me.

In any case, the first preview version of the Microsoft Graph PowerShell SDK V2 is available to download from the PowerShell Gallery. Test it and see what you think. The important thing is to give feedback to Microsoft (you can comment in GitHub). If you don’t, then the current plan is what will flow through to the Generally Available release of the Microsoft Graph PowerShell SDK V2 sometime in 2023.


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.

]]>
https://office365itpros.com/2022/12/20/microsoft-graph-powershell-sdk-v2/feed/ 3 58426
Microsoft Clarifies How It Plans to Charge for APIs https://office365itpros.com/2022/12/14/microsoft-365-api-tiers/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-api-tiers https://office365itpros.com/2022/12/14/microsoft-365-api-tiers/#comments Wed, 14 Dec 2022 01:00:00 +0000 https://office365itpros.com/?p=58245

Pay as You Go Model for Microsoft 365 APIs

Microsoft 365 APIs

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!

PAYG with Syntex Backup

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.

Three Microsoft 365 API Tiers

Essentially, Microsoft plans to operate three Microsoft 365 API tiers:

  • Standard: The regular Graph-based and other APIs that allow Microsoft 365 tenants to access and work with their data.
  • High-capacity: Metered APIs that deal with high-volume operations like the streaming of data out of Microsoft 365 for backups or the import of data into Microsoft 365.
  • Advanced: APIs developed by Microsoft to deliver new functionality. Microsoft points to Azure Communications Services as an example. These APIs allow developers to add the kind of communication options that are available in Teams to their applications.

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.

Problem Areas for Microsoft 365 APIs

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.

]]>
https://office365itpros.com/2022/12/14/microsoft-365-api-tiers/feed/ 3 58245
Tips for Working with the Graph Usage Reports API https://office365itpros.com/2022/09/09/graph-usage-report-tips/?utm_source=rss&utm_medium=rss&utm_campaign=graph-usage-report-tips https://office365itpros.com/2022/09/09/graph-usage-report-tips/#respond Fri, 09 Sep 2022 01:00:00 +0000 https://office365itpros.com/?p=56909

Understanding What the Graph Usage Reports API Generates

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.

Single Point of Reference for Reports

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.

Concealing Display Names

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

Setting the concealed display names option for the Graph usage report API
Figure 1: Setting the concealed display names option for the Graph usage reports API

Teams was the last workload to apply concealment to usage data. All workloads that generate usage data now support this feature.

Switching Concealment On and Off

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

Teams Data Needs Interpretation

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 last activity noted for the team was September 6, 2022. The usage report data is always a couple of days behind.
  • The team has private membership and is not deleted. Usage data includes deleted teams that have logged some activity within the lookback period.
  • The number of active users is 17. However, the team has 2 owners and 9 members, so this figure is odd.
  • The number of active external users is 7. This matches the 7 guest members in the team.
  • The number of guest accounts is 8. Currently, the team has only 7 guests, but a team owner or administrator could have removed a guest account during the lookback period. The shortest lookback period is 7 days. When I use this to query the usage data, it reports 7 guests.
  • The number of active channels is 11. There are currently 10 channels in the team plus one deleted channel. Deleted channels remain available for restore for a 21-day period. In this case, the deleted channel is a shared channel that I deleted many months ago, so that’s worth investigating. The Graph API for channels doesn’t list deleted channels, so I can’t check its deletion status or deletion date. What’s also weird is that no audit records exist for the deletion of this channel…
  • There is one other shared channel in the team.

Message Counts are Even More Confusing

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.

Seeking Understanding from the Graph Usage Reports API

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.

]]>
https://office365itpros.com/2022/09/09/graph-usage-report-tips/feed/ 0 56909
Reporting Teams Channel Email Addresses https://office365itpros.com/2022/08/24/teams-channel-email-addresses/?utm_source=rss&utm_medium=rss&utm_campaign=teams-channel-email-addresses https://office365itpros.com/2022/08/24/teams-channel-email-addresses/#respond Wed, 24 Aug 2022 01:00:00 +0000 https://office365itpros.com/?p=56655

Connecting Those Obscure Teams Channel Email Addresses

Updated 10 July 2023

A colleague posed the question of how to find what team a channel email address belonged to. This is more difficult than you might imagine because when Teams creates an email address for a channel (like 24482dde.office365itpros.com@na.teams.ms), the address doesn’t contain any hint that would allow you to connect the address to its channel. Teams can generate email addresses for any type of channel – regular, private, and shared, and the same kind of obfuscated addresses are used for all types. The Teams admin center doesn’t offer any way of reporting channel email addresses – the only out-of-the-box method to see a channel’s email address is with the Get Email Address option in the Teams client.

 Viewing a Teams channel email address
Figure 1: Viewing a Teams channel email address

No matter, I said, PowerShell will do the trick. In fact, I’d been down this path before when I wrote about how to use audit records to know when channels get or lose email addresses. Part of that article looked at reporting the email addresses that exist for channels using a mixture of PowerShell and Graph API requests (the script is available from GitHub). The Graph requests are required because the Get-TeamChannel cmdlet doesn’t include the email address in the set of properties it returns.

Use the Graph to fetch Teams Channels

The script was one of my earliest efforts at using the Graph with PowerShell and like all code, a fresh eye sometimes creates better results. At least, that’s the eternal hope. I decided to use the Microsoft Graph PowerShell SDK this time around. I used version 1.10 of the Graph SDK for this project. You need the Graph Channel.ReadBasic.All permission to access channel settings. (update: I’ve checked the code against Microsoft Graph PowerShell SDK V2).

Three years ago, the Microsoft Graph PowerShell SDK wasn’t in great shape. Some would argue that it’s still not where it should be. I think the SDK has improved greatly, even if its documentation still sucks dirty canal water. In one respect, the biggest benefit of the SDK is that you don’t need to create an Entra ID registered app to hold the permissions needed to run Graph requests. Some of its cmdlets take care of pagination too.

For many people, the SDK is easier to use than running Graph requests. Once you’ve gotten the hang of API request formats, you’re able to switch between SDK cmdlets and Graph requests, much like people can switch between left-hand and right-hand drive cars. In any case, here’s the code I wrote:

Connect-MgGraph -Scopes Group.Read.All, Directory.Read.All, Channel.ReadBasic.All
[array]$Teams = Get-MgGroup -Filter "resourceProvisioningOptions/Any(x:x eq 'Team')" -All
If (!($Teams)) {Write-Host "Can't find any teams - exiting"; break} 
$Teams = $Teams | Sort-Object DisplayName
$ChannelsList = [System.Collections.Generic.List[Object]]::new()
[int]$i = 0
ForEach ($Team in $Teams) {
   $i++
   $ProgressBar = "Processing Team " + $Team.DisplayName + " (" + $i + " of " + $Teams.Count + ")"
   Write-Progress -Activity "Checking Teams Information" -Status $ProgressBar -PercentComplete ($i/$Teams.Count*100)
   [array]$Channels = Get-MgTeamChannel -Teamid $Team.Id 
   ForEach ($Channel in $Channels) {
     If ($Channel.Email) {
       $ChannelLine = [PSCustomObject][Ordered]@{  # Write out details of the private channel and its members
          Team           = $Team.DisplayName
          Channel        = $Channel.DisplayName
          Description    = $Channel.Description
          EMail          = $Channel.Email
          Id             = $Channel.Id }
       $ChannelsList.Add($ChannelLine) } 
  } #End Foreach Channel
} # End ForEach Team

Refining for Teams-Generated Email Addresses

So far, so good. The only complication is that the Get-MgTeamChannel cmdlet returns the primary SMTP address for the underlying Microsoft 365 group for a team’s General channel even if Teams generates an email address for the channel. In some respects, this is expected because the General channel is the heart of a team. However, you can’t use the group primary SMTP address to send email to the General channel. because Exchange Online delivers any messages sent to this address to the group mailbox.

To find the set of teams-generated addresses, we therefore need to apply an additional filter. This is straightforward because all teams-generated addresses end in “teams.ms,” the special domain used for this purpose. The command is:

$TeamsEmailAddresses = $ChannelsList | Where-Object {$_.Email -Like "*.teams.ms"}

Figure 2 shows the set of email addresses in my tenant. Most of them are used for testing, which accounts for some of the obscure channel names.

Reporting Teams channel email addresses
Figure 2: Reporting Teams channel email addresses

PowerShell Invaluable to Administrators

Situations like this underline the truth of the assertion that PowerShell is an invaluable tool for Microsoft 365 tenant administrators. No set of administration tools created by a vendor can be all-encompassing and PowerShell’s ability to fill in the gaps is what makes it so useful. Add in the ability to make Graph calls from PowerShell, and you have a very powerful tool indeed.


Learn more about how the Office 365 applications 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.

]]>
https://office365itpros.com/2022/08/24/teams-channel-email-addresses/feed/ 0 56655
Creating a Composite Microsoft 365 User Activity Report https://office365itpros.com/2022/08/08/microsoft-365-user-activity-2022/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-user-activity-2022 https://office365itpros.com/2022/08/08/microsoft-365-user-activity-2022/#comments Mon, 08 Aug 2022 01:00:00 +0000 https://office365itpros.com/?p=56371

Bringing Microsoft 365 User Activity Data Together from Multiple Workloads

I’ve been dabbling with the Microsoft Graph usage report API for a couple of years. This is the API that powers the activity reports available in the Microsoft 365 admin center, Teams admin center, and SharePoint Online admin center, so it’s a good source of hard information.

The output of my labor is the Microsoft 365 user activity report, a PowerShell script that assembles data from SharePoint Online, Exchange Online, Teams, OneDrive for Business, and Yammer activity to build a picture of how active a user account is, with the intention of removing underused or unused accounts to save on licensing costs. The 2020 version of the script introduced a bunch of performance fixes to make it possible to retrieve data quickly and efficiently.

New Version Extends the Usage History to 180 Days

Recently, a reader pointed out that the usage report API now supports a lookback period of 180 days, doubling the previous 90 days. In other words, you can fetch information about the activities performed by an account inside Microsoft 365 for the last 180 days. I don’t know when Microsoft made this change, but it’s a good one.

The usage report API doesn’t capture data about every possible user activity, nor does it cover all workloads. For instance, there’s no usage API covering Stream and Planner activity. However, in the case of Stream, once the transition to OneDrive for Business and SharePoint Online, video activities will show up in the data for those workloads.

In any case, the usage data is sufficient to make a good assessment of just how active an account is. After all, if little or no trace of activity exists over 180 days, the account probably isn’t too active and is a candidate for removal. Measuring usage over 90 days is also a good yardstick of activity but doubling the measurement period makes the assessment even more accurate because it accommodates long absences such as sabbaticals and parental leave. This underlines the need to assess data in a wider context when deciding whether accounts really are inactive.

Example Microsoft 365 User Activity Data

The script works by extracting usage data for the supported workloads and combining them into an overall record per user. Here’s an example of a combined record. Note that usage data is always a couple of days behind real time.

UPN                     : Tony.Redmond@office365itpros.com
DisplayName             : Tony Redmond
Status                  : Account in use
LastSignIn              : 03/08/2022 18:08
DaysSinceSignIn         : 0
EXOLastActive           : 31-Jul-2022
EXODaysSinceActive      : 3
EXOQuotaUsed            : 5.91
EXOItems                : 34324
EXOSendCount            : 2572
EXOReadCount            : 4661
EXOReceiveCount         : 11158
TeamsLastActive         : 01-Aug-2022
TeamsDaysSinceActive    : 2
TeamsChannelChat        : 362
TeamsPrivateChat        : 493
TeamsMeetings           : 22
TeamsCalls              : 2
SPOLastActive           : 31-Jul-2022
SPODaysSinceActive      : 3
SPOViewedEditedFiles    : 798
SPOSyncedFiles          : 575
SPOSharedExtFiles       : 17
SPOSharedIntFiles       : 34
SPOVisitedPages         : 92
OneDriveLastActive      : 31-Jul-2022
OneDriveDaysSinceActive : 3
OneDriveFiles           : 6183
OneDriveStorage         : 27.2147
OneDriveQuota           : 1024
YammerLastActive        : 19-Jul-2022
YammerDaysSinceActive   : 15
YammerPosts             : 104
YammerReads             : 238
YammerLikes             : 1
License                 : POWER BI (FREE)+ENTERPRISE MOBILITY + SECURITY E5+BUSINESS APPS
 (FREE)+MICROSOFT POWER AUTOMATE FREE+MICROSOFT VIVA TOPICS+OFFICE 365 E5
OneDriveSite            : https://redmondassociates-my.sharepoint.com/personal/tony_redmond_office365itpros_com
IsDeleted               : False
EXOReportDate           : 31-Jul-2022
TeamsReportDate         : 01-Aug-2022
UsageFigure             : 5.2

Very importantly, if your organization chooses to obfuscate usage data (Figure 1), it isn’t possible to generate the report because user principal names provide the match for usage data from the workloads, and the routine that generates the obscured data creates different values for the user principal name in each workload.

Concealed data setting for reports in the Microsoft 365 admin center
Figure 1: Concealed data setting for reports in the Microsoft 365 admin center

Microsoft 365 User Activity Report Output

The output generated by the script is a PowerShell list which can be exported in different formats. Figure 2 shows the output as viewed through the Out-GridView cmdlet. The script also generates a CSV file, but you could also use the ImportExcel module to create a nicely-formatted Excel worksheet or the PSWriteHTML module to generate a HTML report.

Example of Microsoft 365 user activity report data
Figure 2: Example of Microsoft 365 user activity report data

You can download the updated Microsoft 365 user activity report script from GitHub. If you find an enhancement (aka a bug fix), please suggest it in GitHub. It’s always good to have extra eyes review and improve code.


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 ultimate eBook covering Office 365 and the wider Microsoft 365 ecosystem.

]]>
https://office365itpros.com/2022/08/08/microsoft-365-user-activity-2022/feed/ 13 56371
Microsoft Forces Backup Vendors and Customers Toward Teams Export API https://office365itpros.com/2022/07/26/teams-backup-export-api/?utm_source=rss&utm_medium=rss&utm_campaign=teams-backup-export-api https://office365itpros.com/2022/07/26/teams-backup-export-api/#comments Tue, 26 Jul 2022 01:00:00 +0000 https://office365itpros.com/?p=56224

Warns Customers Not to Use Exchange Web Services for Teams Backup

On July 12, right in the middle of that delicious sleepy time in mid-summer for the IT industry, Microsoft dropped a little bomb on ISVs and customers by declaring that they would start to restrict access to Teams message data via Exchange Web Services (EWS) from September 30, 2022. Microsoft recommends that organizations that need to export Teams message data use the Teams Export Graph API instead.

Update: Microsoft has quietly (and without updating the date of the article) pushed out the date when they will restrict access via EWS to Teams message data in Exchange mailboxes to January 31, 2023.

Microsoft explained their decision by saying they “observed over time that a number of 3rd party apps are accessing Teams messages through EWS” and that the “approach is both undocumented and unsupported by Microsoft and poses the risk of code failure.” In fact, ISVs do not use EWS to access Teams message data. They use EWS to copy the Teams compliance records stored in user and group mailboxes for their Teams backup products.

Teams Compliance Records Stored in Exchange Online

The compliance records created for Teams messages and chats by the Microsoft 365 substrate are incomplete copies of the real data (which remains in Azure Cosmos DB). The substrate creates compliance records as users send messages in chats and channel conversations, including the capture of records for messages sent by hybrid, guest, and federated users.

The items are mail messages with a limited set of properties tailored for eDiscovery. Nevertheless, this doesn’t stop vendors using compliance records as the source for backups, conveniently ignoring the salient fact that there’s no way to restore compliance records into Teams chats or channel conversations.

Third-Party Applications Can’t Access Teams Data in Mailboxes

Microsoft’s supportability statement says that “Third-party applications aren’t allowed to access or use Teams data in mailboxes” and points out that “Teams can change its location and use of data at any time.” Microsoft’s statement seems highly revisionary (or hopeful). I had never heard about a prohibition against accessing Teams compliance records stored in mailboxes. Given the number of ISV that use EWS to read compliance items from mailboxes for Teams backup, it doesn’t seem that many ISVs had either.

Teams backup
Figure 1: Teams backup (Source: Keepit)

The second point about Teams moving its location for compliance records is accurate as this happened in 2020 and broke scripts (and I assume programs) that depended on looking for data in a specific mailbox folder. However, how often the folder location might change in the future is an open question. My bet is not often.

Charging for the Teams Export API

In June, Microsoft announced that they would start charging for the Teams Export Graph API in July. I have not heard of many companies complaining about charges levied against their Azure subscription for using these APIs, but that might be because ISVs haven’t switched their Teams backup products over to use the Graph APIs yet.

By imposing restrictions on EWS, the natural suspicion is that Microsoft is giving ISVs and their customers a big steer to start using products based on the Graph APIs. One perspective is that this is a win for customers because the Graph APIs real actual Teams data instead of the limited copies stored in Exchange Online mailboxes. The downside is the charges incurred to read items out of the Teams store. No one knows quite how much those charges will be but given the increasing use of Teams across all Microsoft 365 segments, it’s reasonable to predict that the bills can only go up.

ISVs and Customers Shouldn’t be Penalized by Progress

The only reason why ISVs use EWS to access Teams data in Exchange Online is that Microsoft is five years late in delivering an API to export information from the Teams message store. As soon as Teams become generally available in early 2017, customers started to ask about backups. What were ISVs supposed to do? If they played dumb and said that they couldn’t do anything until Microsoft delivered a supported API, they ran the risk that their customer would go elsewhere, and that might risk losing the business for backup of supported workloads like Exchange Online and SharePoint Online.

ISVs did the logical and rational thing by using tools available to them to get the job done. Once Microsoft decided to use the substrate to capture and store mail items in Exchange Online for Teams messages, ISVs had a solution. It was imperfect, and some ISVs overplayed their hands and made claims about their solution capabilities that were simply wrong. But what was delivered made customers happy.

I’m glad that Microsoft now has a supported API to export Teams message data. That’s a good thing. It would be much better if Microsoft dropped the charges for a grace period (perhaps two years) to help ISVs and customers to transition to the new platform and become accustomed to the idea of charging. The time would also allow Microsoft to observe the volumes involved in customer usage and fine-tune the charging rates.

Alas, that’s not happening. Instead. Microsoft is forcing ISVs and customers down a route to extra unknown cost for Teams backup. I’ve always said that Teams is the most difficult Microsoft 365 workload to backup. This is just another reason to justify that feeling.


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.

]]>
https://office365itpros.com/2022/07/26/teams-backup-export-api/feed/ 5 56224
Microsoft Graph Support for SharePoint Online Tenant Settings https://office365itpros.com/2022/07/19/sharepoint-graph-settings/?utm_source=rss&utm_medium=rss&utm_campaign=sharepoint-graph-settings https://office365itpros.com/2022/07/19/sharepoint-graph-settings/#comments Tue, 19 Jul 2022 01:00:00 +0000 https://office365itpros.com/?p=56125

Introducing the Tenant Admin Namespace for SharePoint Graph Settings

SharePoint Graph settings

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.

Options to Manage SharePoint Online Settings

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.
  • A dedicated app registered in Entra ID
  • The Microsoft Graph PowerShell SDK.

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.

Updating SharePoint Online Settings with the Microsoft Graph PowerShell SDK

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

Something to Monitor

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.

]]>
https://office365itpros.com/2022/07/19/sharepoint-graph-settings/feed/ 2 56125
Use the Debug Parameter for Microsoft Graph PowerShell SDK Cmdlets to Expose Graph API Requests https://office365itpros.com/2022/07/11/debug-microsoft-graph-powershell-sdk/?utm_source=rss&utm_medium=rss&utm_campaign=debug-microsoft-graph-powershell-sdk https://office365itpros.com/2022/07/11/debug-microsoft-graph-powershell-sdk/#comments Mon, 11 Jul 2022 01:00:00 +0000 https://office365itpros.com/?p=56010

Debug Microsoft Graph PowerShell SDK Cmdlets to Gain Insights into What They Do

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.

Running with the Debug Parameter

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.

Debug Microsoft Graph PowerShell SDK cmdlets - the execution context
Figure 1: Debug Microsoft Graph PowerShell SDK cmdlets – the execution context

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

Debug Microsoft Graph PowerShell SDK cmdlets - the HTTP GET request
Figure 2: Debug Microsoft Graph PowerShell SDK cmdlets – the HTTP GET request

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

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.

Debug Microsoft Graph PowerShell SDK cmdlets - a nextlink to more data
Figure 3: Debug Microsoft Graph PowerShell SDK cmdlets – a nextlink to more data

Another Thing for the Administrator Toolbox

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.

]]>
https://office365itpros.com/2022/07/11/debug-microsoft-graph-powershell-sdk/feed/ 2 56010
Microsoft Graph Explorer Boosted by New Features https://office365itpros.com/2022/06/27/graph-explorer-resources/?utm_source=rss&utm_medium=rss&utm_campaign=graph-explorer-resources https://office365itpros.com/2022/06/27/graph-explorer-resources/#comments Mon, 27 Jun 2022 01:00:00 +0000 https://office365itpros.com/?p=55692

The Growing Importance of the Graph and the Graph Explorer

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.

Enter the Explorer

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.

  • Open the application.
  • Sign into your organization account to work with tenant data or use sample queries against test data.
  • Perform GET, POST, PATCH, PUT, and DELETE requests against the V1.0 (production) or beta endpoints. You can input your own query or select from the set of sample queries available in the Explorer. This set grows over time to add coverage of Graph APIs and the requests supported by the APIs. After selecting a sample query, the Graph Explorer copies it into the query box. You can amend the request before running it.
  • The Explorer takes care of fetching the necessary access tokens to authorize requests. You’ll probably need to give consent for permissions to be used for either your data or at a tenant level. It’s important to understand that the Explorer can only use delegated Graph permissions (this article explains how the Graph calculates the set of effective permissions from those consented to the app and those held by the signed-in account).

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.

 Running a Graph API request with the Microsoft Graph Explorer
Figure 1: Running a Graph API request with the Microsoft Graph Explorer

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.

PowerShell Code Snippets

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 snippet generated by the Microsoft Graph Explorer
Figure 2: PowerShell code snippet generated by the Microsoft Graph Explorer

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 Resources Tab

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.

Viewing tenant SKUs (products) in the Microsoft Graph Explorer
Figure 3: Viewing tenant SKUs (products) in the Microsoft Graph Explorer

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

Embrace the Graph

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.

]]>
https://office365itpros.com/2022/06/27/graph-explorer-resources/feed/ 3 55692
Guest Accounts Can’t Update Their Photos with the Microsoft Graph PowerShell SDK https://office365itpros.com/2022/06/07/azure-ad-guest-account-photo/?utm_source=rss&utm_medium=rss&utm_campaign=azure-ad-guest-account-photo https://office365itpros.com/2022/06/07/azure-ad-guest-account-photo/#respond Tue, 07 Jun 2022 01:00:00 +0000 https://office365itpros.com/?p=55373

Upgrading Scripts to Use New Cmdlets

Microsoft plans to deprecate the Azure AD and Microsoft Online Services (MSOL) PowerShell modules in late 2022 or early 2023. Apart from the license management cmdlets, the other cmdlets in these modules will continue to work but will be unsupported. Eventually, the cmdlets will stop working, so the time is ripe for upgrading scripts and seeking new ways of getting things done, like how to manage Azure AD guest accounts.

In most cases, it’s possible to upgrade scripts by replacing Azure AD cmdlets with cmdlets from the Microsoft Graph PowerShell SDK. As we prepare for the launch of the Office 365 for IT Pros (2023 Edition) eBook, we’re going through all the Azure AD and MSOL code examples in the book to replace as many as possible with either Microsoft Graph API queries or SDK cmdlets. Microsoft publishes a useful cmdlet map to help developers match old cmdlets with suitable replacements.

Sometimes, it’s not yet possible to replace a cmdlet because Microsoft doesn’t yet provide a direct equivalent in the Graph. In these instances, we’re leaving the older code in place because it will continue to work. When a suitable replacement is available, we’ll update the book with new Graph-based code.

It’s Good to Use Photos with Azure AD Accounts

We recommend that tenant administrators add photos for all Azure AD accounts, both regular user accounts and guest accounts. Having a face to recognize makes it easier to appreciate who’s involved in sharing a document or participating in a channel conversation in Teams. Tenant administrators are busy people, and even if they devote time to “guest hygiene” (cleaning up unwanted or obsolete guest accounts), they might not get around to adding photos for guest accounts via the Azure AD admin center or PowerShell. This is why it’s good if guest accounts can update their own photos.

In April 2021, I wrote about how to use cmdlets from the Azure AD PowerShell module to update the photo for your Azure AD guest account in another Microsoft 365 tenant. It’s a relatively straightforward procedure that’s facilitated by the way the Azure AD module works. Once you have a connection to a tenant, you can work with accounts and other objects the signed-in account can access. In this instance, your guest account.

Time moves on and it was time to upgrade the example showing how guests can upload their own photos. Unhappily, although the Set-MgUserPhotoContent cmdlet is available to replace the Set-AzureADUserThumbNailPhoto cmdlet, the technique of connecting to a target tenant with a guest account to update the account photo doesn’t work. At least, it doesn’t work unless the target tenant meets specific criteria.

Experimenting with the Microsoft Graph PowerShell SDK

I experimented with several target tenants where I have guest accounts to see what’s possible. The Connect-MgGraph cmdlet is happy to connect to a tenant and you can sign in with your guest account. At this point, things go wrong. Once you connect with the Azure AD module, you can update the guest account as described in the post referenced above.

However, the Microsoft Graph takes a more restrictive approach to permissions (or scope). Administrators must grant consent to the service principal used for interactive sessions with the Microsoft Graph PowerShell SDK for the permissions to interact with user accounts. In this case, consent must be in place for the User.ReadWrite.All permission before it’s possible to update a user account (or guest account) with a photo. Interactive sessions with the PowerShell SDK use delegated permissions, so even though the permission is User.ReadWrite.All (implying access to all mailboxes), the Graph constrains the scope of the permission to the signed-in user.

Our renowned technical editor, Vasil Michev, thought that he had solved the problem and published a note to that effect. Unhappily, further investigation proved that using the Microsoft Graph PowerShell SDK to connect to a target tenant to update the photo for a guest account only works if the service principal for the Microsoft Graph PowerShell enterprise app (application id 14d82eec-204b-4c2f-b7e8-296a70dab67e) has consent for the User.ReadWrite.All permission.

The Service Principal Question

Meeting these requirements means that someone has run Connect-MgGraph at some time in the past in the target tenant. This action creates the service principal if it’s not already known to Azure AD. If the service principal doesn’t exist in the target tenant, Connect-MgGraph cannot proceed until an administrator signs in to create the service principal (Figure 1). After the creation of the service principal, it can receive consent to use permissions, including User.ReadWrite.All.

Azure AD prompts to create the service principal for the Microsoft Graph PowerShell SDK

Azure AD Guest Account
Figure 1: Azure AD prompts to create the service principal for the Microsoft Graph PowerShell SDK

Although these conditions might exist for some tenants, there’s no guarantee that the service principal exists and holds the permission. For example, connecting to the Microsoft tenant with a guest account reveals that the User.Read permission is available, but the User.ReadWrite.All permission is not.

Disconnect-MgGraph
Connect-MgGraph -TenantId 72f988bf-86f1-41af-91ab-2d7cd011db47
Welcome To Microsoft Graph!
Get-Mgcontext

ClientId              : 14d82eec-204b-4c2f-b7e8-296a70dab67e
TenantId              : 72f988bf-86f1-41af-91ab-2d7cd011db47
CertificateThumbprint :
Scopes                : {openid, profile, User.Read, email}
AuthType              : Delegated
AuthProviderType      : InteractiveAuthenticationProvider
CertificateName       :
Account               :
AppName               : Microsoft Graph PowerShell
ContextScope          : CurrentUser
Certificate           :
PSHostVersion         : 5.1.22000.653
ClientTimeout         : 00:05:00

In a nutshell, it’s possible to use the Microsoft Graph PowerShell SDK to update photos for guest accounts in other tenants, but only when the conditions are exactly right.

That Permissioned Service Principal

In closing, let me note once again the cumulative nature of the service principal used by the Microsoft Graph PowerShell SDK. If an administrator consents to a permission for use with the Graph SDK, that permission remains assigned to the service principal unless an administrator removes it. Over time, permissions accrue, and the service principal becomes highly permissioned. This makes it easy for tenant administrators to run SDK cmdlets in interactive sessions, but it’s possibly not what you want to happen. On the upside, interactive sessions use delegated permissions rather than application permissions, so the Graph won’t allow the signed-in user to access data that they couldn’t otherwise open. Which is nice to know.

]]>
https://office365itpros.com/2022/06/07/azure-ad-guest-account-photo/feed/ 0 55373
Graph X-Ray Tool Helps PowerShell Developers Master the Graph https://office365itpros.com/2022/05/23/graph-x-ray-powershell/?utm_source=rss&utm_medium=rss&utm_campaign=graph-x-ray-powershell https://office365itpros.com/2022/05/23/graph-x-ray-powershell/#comments Mon, 23 May 2022 01:00:00 +0000 https://office365itpros.com/?p=55170

Even in First Release, Graph X-Ray Proves Its Worth

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.

Raw Graph or Graph SDK

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.

Restoring Deleted Microsoft 365 Groups

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.

Using the Graph X-Ray

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

Viewing Graph commands with the Graph X-Ray extension
Figure 1: Viewing Graph commands with the Graph X-Ray extension

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.

More Insight from Graph X-Ray

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

More Please!

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.

]]>
https://office365itpros.com/2022/05/23/graph-x-ray-powershell/feed/ 1 55170
Using the Graph API to Generate Mailbox Folder Statistics https://office365itpros.com/2022/05/19/mailbox-folder-statistics/?utm_source=rss&utm_medium=rss&utm_campaign=mailbox-folder-statistics https://office365itpros.com/2022/05/19/mailbox-folder-statistics/#comments Thu, 19 May 2022 01:00:00 +0000 https://office365itpros.com/?p=55098

Sometimes PowerShell Needs a Little Help

Retrieving information about mailbox folder statistics is a common administrative activity. Exchange Online administrators usually reach for the Get-ExoMailboxStatistics and Get-ExoMailboxFolderStatistics cmdlets and build scripts around their output. This approach works, but it has some downsides:

  • The cmdlets are “heavy” – they take a long time to run.
  • Some of the output (like folder sizes) need manipulation before they can be used in computations.

Running Get-ExoMailboxStatistics to process a batch of mailboxes fetched using Get-ExoMailbox is not a quick operation. However, it gets the job done in terms of fetching mailbox folder statistics and that’s why you see this approach taken in scripts so often.

The history of these cmdlets goes back to the original implementation of PowerShell in Exchange Server 2007. At the time, the Exchange developers made the brave decision to build all of the Exchange 2007 administrative functionality around PowerShell, including the Exchange Management Console (EMC). The design focus for the cmdlets was on the retrieval and manipulation of data required by the console. No consideration was given to being able to access more than raw data, such as the size of the mailbox or the size of an individual folder and the number of items it contained.

The Role of EWS and the Graph

At the time, Exchange Web Services (EWS) was the public API available to developers who needed to go deeper into mailbox contents. For example, if you want to return both the number of items in the Inbox together with the unread count (here’s a StackOverflow discussion on the topic)

Today, the Microsoft Graph is the preferred option, which is where I headed when a reader noted their frustration at not being able to report the unread count for mailboxes. One reason why you might want to report unread counts is to monitor activity for shared mailboxes used to process customer requests. In any case, some business reason exists, so let’s explore how to respond.

Using the Mail API to Fetch Mailbox Folders

The Microsoft Graph Mail API contains the List Mail Folders call to return a collection of folders from a user’s mailbox. Some properties of interest to calculate mailbox folder statistics are included for each folder. Here’s an example of the data returned for a folder. As you can see, this folder is the Inbox, and the Graph returns an unread count.

id               : AQMkADY5NmJhYzk4LTA2ZGYtNGZkMy05NDJlLThlNDA1ZTMxZTU1AGMALgAAA4-72A-LRNdArLgWql46nuMBAEulavbT-91Jpzh8YFiJOcwAAAIBDAAAAA==
displayName      : Inbox
parentFolderId   : AQMkADY5NmJhYzk4LTA2ZGYtNGZkMy05NDJlLThlNDA1ZTMxZTU1AGMALgAAA4-72A-LRNdArLgWql46nuMBAEulavbT-91Jpzh8YFiJOcwAAAIBCAAAAA==
childFolderCount : 0
unreadItemCount  : 1715
totalItemCount   : 2818
sizeInBytes      : 721593032
isHidden         : False

Equipped with this knowledge, it’s easy to create a PowerShell script to fetch mailbox folder statistics by:

  • Use an Azure AD registered app and an app secret to acquire an access token to interact with the Graph. The registered app must have consent to use the application Mail.Read permission (to access user mailboxes).
  • Run Get-ExoMailbox to return the set of user mailboxes in the tenant.
  • Loop through each mailbox to fetch the set of mail folders. This call doesn’t return folders like Contacts, Calendar, Tasks, and so on.
  • Extract information about the Inbox.
  • Capture the information in a report.
  • Output the report (as a CSV file, Excel file, or other format – I decided to use Excel as described in this post).

The main loop is shown below:

ForEach ($M in $Mbx) {
    Write-Host ("Processing mailbox {0} of {1}: {2}" -f $i, $Mbx.Count, $M.DisplayName); $i++
    $Uri = "https://graph.microsoft.com/v1.0/users/" + $M.ExternalDirectoryObjectId + "/mailFolders?`$top=250"    
    $FolderData = Invoke-RestMethod -Headers $Headers -Uri $Uri -UseBasicParsing -Method "GET" -ContentType "application/json"
    $InboxData = $FolderData.Value | ? {$_.displayname -eq "Inbox"}
    $TotalMbxItems = ($FolderData.Value.totalitemcount | Measure-Object -Sum | Select -ExpandProperty Sum)
    $TotalMbxSize = ($FolderData.Value.SizeInBytes | Measure-Object -Sum | Select -ExpandProperty Sum)
    $ReportLine = [PSCustomObject][Ordered]@{  # Write out details of the mailbox
       "User"              = $M.DisplayName
       UPN                 = $M.UserPrincipalName
       InboxCount          = $InboxData.totalItemCount
       UnreadCount         = $InboxData.unreadItemCount
       TotalMbxFolders     = $FolderData.Value.Count
       TotalMbxItems       = $TotalMbxItems
       TotalMbxFolderSize  = [math]::Round($TotalMbxsize/1Mb,2)  }
      $Report.Add($ReportLine) 
}

By default, a call to retrieve mailbox folders returns the first 10 folders matching the query. Microsoft Graph queries limit the amount of data they return to minimize demand on services. A process called pagination allows developers to fetch successive pages of data until they exhaust all available data. In this case, we can either instruct the Graph to return more than the default amount (done here by using the Top parameter to specify that the script will accept up to mail 250 folders), or use a nextlink to fetch the next page of data until a nextlink is no longer available.

Figure 1 shows the output generated from the mailbox folder data returned by the Graph.

Mailbox folder statistics retrieved by the Microsoft Graph shown in Excel
Figure 1: Mailbox folder statistics retrieved by the Microsoft Graph shown in Excel

Graph is No Panacea

Because it includes only mail folders, the total mailbox data reported by the Graph API is not the same as returned by the Get-ExoMailboxStatistics cmdlet. The difference is accounted for by folders like Calendar, Contacts, and Tasks.

The Graph is not a universal panacea for access to mailbox data. It’s a tool that adds to the capabilities available to tenant administrators. In this instance, the combination of PowerShell and the Graph allowed us to find the unread count for the Inbox folder in mailboxes. It’s nice to have an additional method to get at data.

You can download the script I used from GitHub.


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.

]]>
https://office365itpros.com/2022/05/19/mailbox-folder-statistics/feed/ 5 55098
Basic Entra ID Group Management with the Microsoft Graph PowerShell SDK https://office365itpros.com/2022/03/29/create-entra-id-group/?utm_source=rss&utm_medium=rss&utm_campaign=create-entra-id-group https://office365itpros.com/2022/03/29/create-entra-id-group/#comments Tue, 29 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=54278

Create Entra ID Groups with SDK Cmdlets

Updated 28 December 2023

Last week, I discussed how to perform basic Entra ID user account management operations using cmdlets from the Microsoft Graph PowerShell SDK. Now it’s time to discuss the management of group objects.

Use the Entra ID admin center to create Entra ID groups and manage them afterward.
Figure 1: Use the Entra ID admin center to create Entra ID groups and manage them afterward

To work with the cmdlets discussed here, you should connect to the Microsoft Graph with the Group.ReadWrite.All and GroupMember.ReadWrite.All permissions:

$RequiredScopes = @("Group.ReadWrite.All", "GroupMember.ReadWrite.All", "User.ReadWrite.All")
Connect-MgGraph -Scopes $RequiredScopes -NoWelcome

See this post for more information about connecting to the Graph and permissions.

Creating Different Kinds of Entra ID Groups

Entra ID groups include:

  • Microsoft 365 groups/Teams (including groups used by Yammer).
  • Security groups.
  • Dynamic groups (including those used by Microsoft 365 groups/Teams).
  • Distribution lists.
  • Mail-enabled security groups.

The New-MgGroup cmdlet can create Microsoft 365 groups, dynamic groups, and security groups. It can’t create distribution lists or mail-enabled security groups., nor can Graph API requests or Graph SDK cmdlets update the membership of distribution lists or mail-enabled security groups because these group types are essentially Exchange Online rather than Entra ID objects.

Although New-MgGroup can create groups of different types, it is often better to use the dedicated cmdlet for a particular type of group to ensure that Microsoft 365 performs all the necessary provisioning, like New-UnifiedGroup or New-Team.

Here’s an example of using New-MgGroup to create a Microsoft 365 group (the key point is to set the GroupTypes parameter to be “Unified”):

New-MgGroup -DisplayName "Sales Operations Team" -GroupTypes Unified -MailNickName Sales.Operations -MailEnabled:$True -SecurityEnabled:$False -Description "A group for Sales Operation management"

It’s a good idea to capture the result of the cmdlet in a variable. If the command is successful, you’ll then have a variable containing properties of the new group including its identifier. As we’ll see, you’ll need the identifier to interact with the group using other SDK cmdlets.

The downside of creating a Microsoft 365 group using the New-MgGroup cmdlet is that you will probably end up fixing up some of the group’s properties afterwards. For instance, New-MgGroup adds the signed-in account as the group owner, which you might not want. In addition, you can’t update properties like group privacy or assign a sensitivity label, so these must be set afterwards.

Creating a Dynamic Microsoft 365 Group

One scenario where New-MgGroup scores is where you want to create a dynamic Microsoft 365 Group, as this cannot be done using the New-UnifiedGroup cmdlet. This command creates a group using a membership rule to find people whose usage location (for Microsoft 365 services) is the U.S:

$Group = New-MgGroup -DisplayName "U.S. Based Employees" -Description "Dynamic group containing U.S. Based Employees" -MailEnabled:$True -SecurityEnabled:$False -MailNickname US.Employees -GroupTypes "DynamicMembership", "Unified" -MembershipRule "(User.usagelocation -eq ""US"")" -MembershipRuleProcessingState "On"

Update Group Properties

PowerShell modules like Exchange Online and Azure AD usually include Set- cmdlets to update the properties of objects. The SDK uses Update- cmdlets, so to update a group, you run the Update-MgGroup cmdlet. For example, this command updates a group’s description:

Update-MgGroup -GroupId dc9e6f8b-6734-4180-af25-aa40fae79280 -Description "People lucky enough to have Office 365 E5 licenses"

Currently, the Microsoft Graph Groups API treats a group’s proxyAddresses property as read-only, which means that you can’t add or remove a proxy address using the Update-MgGroup cmdlet. Use an Exchange Online cmdlet like Set-UnifiedGroup instead.

Updating Group Membership

The New-MgGroupMember cmdlet populates the group membership. In this example, we get the group identifier, use Get-MgUser to find a set of suitable group members, and finally add them to the group:

$GroupId = (Get-MgGroup -Filter "displayName eq 'Sales Operations Team'").Id
[array]$Users = Get-MgUser -Filter "department eq 'Sales'"
ForEach ($User in $Users) {
  New-MgGroupMember -GroupId $GroupId -DirectoryObjectId $User.Id }

In the past, checking that the right members are present afterwards is not as simple as it should be. Instead of Get-MgGroupMember returning a list of group members, you must pipe the output to the Get-MgUser cmdlet:

Get-MgGroupMember -GroupId $GroupId -All | ForEach {Get-MgUser -UserId $_.Id}

You can look at the AdditionalProperties property retuned by Get-MgGroup to find information about the group members. For example, this command returns a list of display names for group members:

$GroupData = Get-MgGroupmember -GroupId $GroupId
$GroupData.AdditionalProperties.displayName

Adding a group owner is a little complicated because the owner is stored by reference to its object rather than as a simple property. The New-MgGroupOwnerByRef cmdlet requires the identifier for the owner’s account to be passed in a hash table:

New-MgGroupOwnerByRef -GroupId $GroupId -AdditionalProperties @{"@odata.id"="https://graph.microsoft.com/v1.0/users/2a3b60f2-b36b-4758-8533-77180031f3d4"}

To remove a member from a Microsoft 365 group, use the Remove-MgGroupMemberByRef cmdlet. This cmdlet doesn’t work with distribution lists or mail-enabled security groups. This command removes the user object pointed to by the GUID from a target group identified by the $GroupId variable.

Remove-MgGroupMemberByRef -DirectoryObjectId 08dda855-5dc3-4fdc-8458-cbc494a5a774 -GroupId $GroupId

Removing Groups

The Remove-MgGroup cmdlet removes a Microsoft 365 group or security group. For example:

Remove-MgGroup -GroupId f6dd8a3e-d50c-4af2-a9cf-f4adf71ec82b

The cmdlet can’t remove distribution lists or mail-enabled security groups. You must do this with the Exchange Online cmdlets.

Restore Deleted Groups

Deleted groups remain in a soft-deleted state for 30 days following their deletion. During this time, it’s possible to restore the group using the Restore-MgGroup cmdlet. To find the set of soft-deleted groups awaiting permanent removal, take the code to find soft-deleted users in this article and amend the Get-MgDirectoryDeletedItem cmdlet to look for microsoft.graph.group objects instead of microsoft.graph.user.

The report lists the set of soft-deleted groups, including their identifiers. To restore a group, run the Restore-MgDirectoryDeletedItem-MgGroup cmdlet and pass the identifier of the group:

Restore-MgDirectoryDeletedItem -DirectoryObjectId 4e9393c3-67e9-4f95-a0df-70103a667c0a

Finding Group Objects

The Get-MgGroup cmdlet fetches details of Entra ID groups. To retrieve a single group, use its display name as a filter:

Get-MgGroup -Filter "DisplayName eq 'Leadership Team'"

You can also search using the StartsWith filter:

 Get-MgGroup -Filter "startsWith(displayname, 'Leadership')"

If you add the All parameter, you’ll get all the groups in the tenant.

[array]$Groups = Get-MgGroup -All

The command returns groups of all types. To filter out the various types of groups, we can check different properties to identify each type of group. Table 1 lists useful properties to check.

PropertyUsed by
MailEnabled = TrueDistribution lists
Microsoft 365 groups
Mail-enabled security groups
SecurityEnabled = TrueSecurity groups
Mail-enabled security groups
GroupTypes = UnifiedMicrosoft 365 groups
GroupTypes = DynamicMembershipDynamic groups
GroupTypes = Unified, DynamicMembershipDynamic Microsoft 365 groups
ResourceProvisioningOptions = TeamTeam-enabled Microsoft 365 groups
Table 1: Filters for different types of Entra ID groups

The simplest filters are those which find groups based on a property. For example, to find all security-enabled groups:

Get-MgGroup -Filter “securityEnabled eq true” -All

Find all mail-enabled groups:

Get-MgGroup -Filter “mailEnabled eq true” -All

The GroupTypes and ResourceProvisioningOptions properties require complex filters with Lambda operators. For example, to find the set of Microsoft 365 groups in the tenant:

[array]$M365Groups = Get-MgGroup -Filter "groupTypes/any(c:c eq 'unified')" -All 

To find the set of dynamic Microsoft 365 Groups:

Get-MgGroup -Filter "groupTypes/any(c:c eq 'dynamicmembership') and groupTypes/any(x:x eq 'unified')" -All

To find the set of Microsoft 365 groups enabled for Teams:

[array]$Teams = Get-MgGroup -Filter "resourceProvisioningOptions/Any(x:x eq 'Team')" -All

In addition, client-side filter can refine the results returned by the server. For instance, after fetching all security-enabled groups, we use a client-side filter to find the set with dynamic membership:

[array]$SecurityGroups = Get-MgGroup -Filter “securityEnabled eq true” -All 
[array]$DynamicSecurityGroups = $SecurityGroups | ? {$_.GroupTypes -eq “DynamicMembership”}

Filter Speed

The filters used by Get-MgGroup are server-side, meaning that the data is filtered when the server returns it to PowerShell. Because they’re Graph-based and return fewer properties than cmdlets like Get-UnifiedGroup, these commands are very fast, which makes them worth considering if you have scripts which fetch subsets of groups for processing.

As an example, I replaced calls to the Get-UnifiedGroup and Get-AzureADMSGroup cmdlets in a script to report Microsoft 365 groups under the control of the Groups expiration policy with Get-MgGroup and processing time fell from 30 seconds to 1.9 seconds.

Complex Queries Against Entra ID Groups

Get-MgGroup supports complex queries against Entra ID. Essentially, a complex query is one that the Microsoft Graph resolves against a separate property store. It’s not always obvious when a complex query is necessary. Microsoft could hide this need in code instead of forcing PowerShell coders to remember when they must add the ConsistencyLevel parameter to mark a query as complex. Searching the display name of groups for a term is an example of a complex query.

[array]$Groups = Get-MgGroup -Search '"displayname:Office 365"' -ConsistencyLevel Eventual

Another example is to use the Filter parameter to find groups which start with a value. For instance, we might want to find groups whose display name starts with Office:

[array]$Groups = Get-MgGroup -Filter "startsWith(DisplayName, 'Office')" -ConsistencyLevel Eventual

An Evolving Story

Microsoft’s documentation for migration of Azure AD cmdlets admits “There is currently no tool to automatically converts scripts in Azure AD PowerShell to Microsoft Graph PowerShell.” I don’t anticipate that such a tool will appear. As described here, its Graph foundation mean that the ways of performing actions against Entra ID groups with the Microsoft Graph PowerShell SDK differ from how things work with the Azure AD module. It will take time and effort to master the details. Hopefully, the advice contained here helps that evolution.


Keep up to date with developments like the migration from the cmdlets in the Azure AD module to the Microsoft Graph SDK for PowerShell 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.

]]>
https://office365itpros.com/2022/03/29/create-entra-id-group/feed/ 4 54278
Basic User Account Management with the Microsoft Graph PowerShell SDK https://office365itpros.com/2022/03/24/entra-id-user-accounts-powershell/?utm_source=rss&utm_medium=rss&utm_campaign=entra-id-user-accounts-powershell https://office365itpros.com/2022/03/24/entra-id-user-accounts-powershell/#comments Thu, 24 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=54188

Preparing to Migrate Away from Old AzureAD cmdlets

Updated: 15 March, 2023

Manage Entra ID user accounts

I received a lot of reaction when I described Microsoft’s new deprecation schedule for the AzureAD and MSOL modules. In summary, you have until 30 March 2024 to update scripts which assign licenses to user accounts. After this, Microsoft will disable the cmdlets. The other cmdlets will continue working after Microsoft deprecates the modules. However, they’ll be out of support, which is not a good foundation for PowerShell scripts used to automate administrative processes, like managing Entra ID user accounts.

With time running out, it’s obvious that tenants need to inventory and upgrade scripts. One reaction I received was that there’s a dearth of information to help people who are less familiar with PowerShell and might have inherited ownership of some scripts. My response is that the community will publish examples over time, just like they did when Microsoft launched the AzureAD module in 2016 and the Exchange Online management REST-based cmdlets at Ignite 2019. Let’s hope this is true.

Over on Practical365.com, I compare creating a new Entra ID user account and assigning licenses to the account using both the old AzureAD module and the Microsoft Graph PowerShell SDK. In this post, I consider some additional basic user account management actions.

Connections

The basics of using the Microsoft Graph PowerShell SDK (the SDK) is to connect. You can connect interactively (delegated access) or with certificate-based authentication (application access). You can also run SDK cmdlets in Azure Automation runbooks. The simplest approach is to run Connect-MgGraph interactively, which signs into the Graph using the account you signed into PowerShell with.

Scopes

SDK cmdlets interact with Microsoft Graph APIs. A big difference between the SDK and AzureAD modules is that the SDK forces you to request the set of Graph permissions you want to use. The SDK uses a service principal to hold the permissions, and over time, that service principal might become overly permissioned. It’s a thing to keep an eye on.

In this example, we define an array of Graph permissions we wish to use, and then connect. If you request a permission that the SDK service principal doesn’t already hold, you’ll see an administrator prompt for consent.

$RequiredScopes = @("Directory.AccessAsUser.All", "Directory.ReadWrite.All", "User.ReadWrite.All", “User.Read.All”)
Connect-MgGraph -Scopes $RequiredScopes -NoWelcome

Welcome To Microsoft Graph!

Updating Properties for Entra ID User Accounts

Let’s assume that you’ve created the Sue.Ricketts@Office365itpros.com account using the New-MgUser cmdlet as described in this article and stored the user identifier for the account in the $UserId variable.

$UserId = (Get-MgUser -UserId Sue.Ricketts@office365itpros.com).Id

To update the properties of a user account, run the Update-MgUser cmdlet.

Update-MgUser -UserId $UserId -JobTitle "Senior Editor" -State NY

Updating Email Properties for an Account

You can’t update the proxyAddresses property of a user account because the Graph treats it as read-only, possibly because Exchange Online takes care of email proxy address management. However, if you change the UserPrincipalName property of an account, Update-MgUser sets the primary SMTP address of the account to match the new user principal name. The logic here is likely that it is best practice to match the user principal name and primary SMTP address. In most cases, this is true and it’s a good idea to have the cmdlet behave like it does. However, in some circumstances, you might decide to have different values in these properties.

In both situations, you should use the Exchange Online Set-Mailbox cmdlet to update proxy addresses. For example, this command adds a new SMTP proxy address to the mailbox identified by the $UserId variable:

Set-Mailbox -Identity $UserId -EmailAddresses @{Add="Johnnie.West@Office365itpros.com"}

This command updates the primary SMTP address for the mailbox without changing the user principal name:

Set-Mailbox -Identity $UserId -WindowsEmailAddress Johnnie.West@Office365itpros.com

Exchange Online uses a dual-write mechanism to make sure that any change made to mailboxes happens simultaneously to the underlying user account.

Updating a User’s Manager

The manager of a user account is updated by reference (to their account) rather than simply updating a property. To update the manager of a user account, run the Set-MgUserManagerByRef cmdlet after storing the identifier of the manager’s account in a variable:

$ManagerId = (Get-MgUser -UserId Terry.Hegarty@office365itpros.com).Id
Set-MgUserManagerByRef -UserId $UserId `
   -AdditionalProperties @{
     "@odata.id" = "https://graph.microsoft.com/v1.0/users/$ManagerId" }

To check that the manager update was successful, we need to fetch the manager’s details (expanded into a dictionary object) and retrieve the property we want.

$ManagerData = Get-Mguser -UserId $UserId -ExpandProperty Manager
$ManagerData.Manager.AdditionalProperties['displayName']
Terry Hegarty

You can also use the Get-MgUserManager cmdlet to return the manager of an account.

Get-MgUserManager -UserId Chris.Bishop@Office365itpros.com | Select-Object @{n="DisplayName";e={$_.AdditionalProperties.displayName}},@{n="UserPrincipalName";e={$_.AdditionalProperties.userPrincipalName}}

DisplayName UserPrincipalName
----------- -----------------
James Ryan  James.Ryan@office365itpros.com

Obviously, Microsoft has made defining and retrieving the manager of an account more complex than it needs to be. It would be nice if they would hide the complexity in code and deliver some straightforward cmdlets that don’t create friction when the time comes to update scripts.

Another way of updating user account properties is with the Invoke-MgGraphRequest cmdlet, which runs a Graph API query. The advantage of this cmdlet is that if you can’t find a way to do something with an SDK cmdlet, you can refer to the Microsoft Graph documentation, find some example code, and run or repurpose it.

In this example, we create a hash table to hold the properties we want to update, convert the table to a JSON object, and pass it to a PATCH query run by Invoke-MgGraphRequest:

$Parameters = @{
   JobTitle = "Managing Editor, Periodicals"
   State = "Vermont"
   OfficeLocation = "Burlington" } | ConvertTo-Json
Invoke-MgGraphRequest -Method PATCH -Uri "https://graph.microsoft.com/v1.0/users/Sue.Ricketts@office365itpros.com" -Body $Parameters -ContentType "application/json; charset=utf-8"

Delete a User Account

The Remove-MgUser cmdlet soft-deletes a user account and moves it into Entra ID’s deleted items container, where it remains for 30 days until Entra ID permanently deletes the object. The cmdlet is very simple, and it doesn’t prompt for confirmation before proceeding to delete a user account.

Remove-MgUser -UserId $UserId

If you need to restore a soft-deleted account, run the Restore-MgUser cmdlet and pass the object identifier of the account you want to restore. See this article for information about how to list the set of soft-deleted user accounts.

Restore-MgUser -UserId $UserId

I’ve experienced some issues with the Restore-MgUser cmdlet in the 1.9.3 release of the SDK which I have reported to Microsoft. Basically, the cmdlet doesn’t work in this release. I’m sure the bug will be fixed soon.

Finding User Accounts

We’ve already seen how the Get-MgUser cmdlet fetches information for an individual user account. It also fetches sets of accounts. To fetch all the accounts in the tenant, run:

[array]$Users = Get-MgUser -All

I always specify that the variable used as the target for a set of objects is an array. This makes it easy to find how many objects are returned, as in:

Write-Host $Users.Count “User accounts found”

Note that unlike Graph API queries, the Get-MgUser cmdlet takes care of data pagination for the query and fetches all available objects.

If you don’t specify the All switch, the cmdlet fetches the first 100 accounts. You can fetch a specific number of accounts using the Top parameter, up to a maximum of 999.

[array]$Top500 = Get-MgUser -Top 500

The Filter parameter uses server-side filtering to restrict the amount of data returned. For instance, here’s how to find all the guest accounts in a tenant:

[array]$Guests = Get- MgUser -Filter "usertype eq 'Guest'" -All

While this filter returns the accounts who usage location (for Microsoft 365 services) is the U.S.

Get-MgUser -Filter "usagelocation eq 'US'"

You can combine properties in a filter. For example:

Get-MgUser -Filter "usagelocation eq 'US' and state eq 'NY'"

Another interesting filter is to find accounts created in a specific date range. This command finds all tenant non-guest accounts created between January 1, 2022 and Matrch 24. Note the trailing Z on the dates. The Graph won’t treat the date as valid if the Z is not present.

Get-MgUser -Filter "createdDateTime ge 2022-01-01T00:00:00Z and createdDateTime le 2022-03-24T00:00:00Z and usertype eq ‘Member’"

Support for SDK Problems via GitHub

Hopefully, the examples listed above are useful in terms of understanding the SDK cmdlets to perform basic management of Entra ID user accounts. If you run into a problem when converting scripts to use SDK cmdlets, you can report the problem (or browse the current known issues) on GitHub. Happy migration!

]]>
https://office365itpros.com/2022/03/24/entra-id-user-accounts-powershell/feed/ 9 54188
Delete and Restore Entra ID User Accounts with the Microsoft Graph PowerShell SDK https://office365itpros.com/2022/03/23/delete-entra-id-user-accounts/?utm_source=rss&utm_medium=rss&utm_campaign=delete-entra-id-user-accounts https://office365itpros.com/2022/03/23/delete-entra-id-user-accounts/#comments Wed, 23 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=54175

Understanding How to Delete Entra ID User Accounts and Restore Them Afterwards is a Critical Skill

According to message center notification MC344406 (18 March), in early April Microsoft plans to roll-out the capability of recovering deleted service principal objects. Service principals are critical parts of registered Entra ID apps, such as the apps used to execute Microsoft Graph API queries with PowerShell. They’re also used in Azure Automation accounts, the Microsoft Graph PowerShell SDK, and managed identities. In all cases, the service principals hold the permissions needed for an app or account to operate. The worldwide roll-out to all tenants should complete by late May.

When the capability is available, any time an administrator deletes a service principal (for instance, because a registered app is no longer needed) using the Entra admin center, PowerShell (using Remove-AzureADServicePrincipal), or the Microsoft Graph API, Entra ID will place the service principal into a soft-deleted state. This already happens today for user, group, device, and application objects.

Deleted Entra ID objects stay in the deleted items container for 30 days. When the retention period elapses (extending to maybe a few days afterwards), Entra ID proceeds to permanently delete the object.

During the retention period, administrators can restore an object, which makes it easy to recover if someone deletes an important item by accident. For now, the list deleted items API doesn’t support service principals, but it will after the roll-out. Figure 1 shows user objects in the deleted items container as viewed through the Graph Explorer.

Viewing deleted Entra ID user accounts via the Graph Explorer

Delete Entra ID user account
Figure 1: Viewing deleted Entra ID user accounts via the Graph Explorer

Using Old Azure AD Cmdlets

MC344406 features two cmdlets from the Azure AD Preview module:

In some respects, it’s odd that they use cmdlets based on the Azure AD Graph API because Microsoft has scheduled the Azure AD modules for retirement in March 2024.

Of course, apart from the licensing management cmdlets, the rest of the Azure AD cmdlets will continue to work after retirement, which makes it perfectly acceptable to specify the cmdlets now, especially if replacements in the Microsoft Graph PowerShell SDK are unavailable.

Using Microsoft Graph PowerShell SDK Cmdlets to Delete Entra ID User Accounts

The Microsoft Graph PowerShell SDK can be difficult to navigate. It’s composed of 38 separate sub-modules. Although cmdlets are gathered logically, it can still be hard to find the right cmdlet to do a job. As you’d expect, the current version (1.9.3) doesn’t appear to include cmdlets to handle soft-deleted service principal objects. For now, we can see how to perform common administrative actions with user accounts as a guide to what should be available for service principals.

With that in mind, here are the steps to soft-delete user accounts, list the accounts in the deleted items container, and hard-delete (permanently remove) an account.

Soft-Delete an Entra ID User Account

To soft-delete an Entra ID account, run the Remove-MgUser and pass the object identifier or user principal name of the account to delete. The cmdlet does not prompt for a confirmation and deletes the account immediately:

Remove-MgUser -UserId Sue.Ricketts@office365itpros.com

List Soft-Deleted Entra ID User Accounts

During the 30-day retention period in the deleted items container, you can recover the account from the Entra admin center or by running the Restore-MgUser cmdlet. Before we can run Restore-MgUser, we need to know the object identifiers of the objects in the deleted items container. This code:

  • Uses the Get-MgDirectoryDeletedItemAsUser cmdlet to fetch the list of deleted user accounts. The Property parameter can be ‘*’ to return all properties of the deleted objects, but in this case, I’ve chosen to limit the set of properties to those that I want to use.
  • Loops through the data returned by Entra ID to extract the properties we want to use. The different behaviour of the Azure AD cmdlets and the Microsoft Graph PowerShell SDK cmdlets is an example of why tenants need to plan the upgrade and testing of scripts which use old cmdlets.
  • Lists the soft-deleted user accounts.
[array]$DeletedItems = Get-MgDirectoryDeletedItemAsUser -All -Property Id, DisplayName, DeletedDateTime, UserPrincipalName, Usertype
If ($DeletedItems.count -eq 0) { 
   Write-Host "No deleted accounts found - exiting"; break 
}

$Report = [System.Collections.Generic.List[Object]]::new()

ForEach ($Item in $DeletedItems) {
    $DeletedDate = Get-Date($Item.deletedDateTime)
    $ReportLine = [PSCustomObject][Ordered]@{ 
           UserId   = $Item.Id
           Name     = $Item.displayName
           Deleted  = $DeletedDate
           "Days Since Deletion" = (New-TimeSpan $DeletedDate).Days
           Type     = $Item.userType
     }
    $Report.Add($ReportLine)
}

UserId                               Name                      Deleted             Days Since Deletion Type
------                               ----                      -------             ------------------- ----
92cef396-1bd3-4296-b06f-786e2ee09077 The Maestro of Office 365 19/02/2022 17:36:44                  31 Guest
c6133be4-71d4-47c4-b109-e37c0c93f8d3 Oisin Johnston            26/02/2022 18:13:26                  24 Member
2e9f1189-d2d9-4301-be57-2d66f3df6bb1 Jessica Chen (Marketing)  04/03/2022 11:52:48                  18 Member
8cd64635-bce6-4af0-8e64-3bebe354e9a4 Alex Redmond              05/03/2022 17:36:45                  17 Member
0f16501c-8302-468a-99a6-78c22b0903d2 Jennifer Caroline         18/03/2022 21:33:13                   3 Member
3a6116ab-0116-490e-bd60-7e0cd9f36c9d Sue Ricketts (Operations) 20/03/2022 19:53:29                   2 Member
4a25ccf0-17df-42cf-beeb-4fd449531b47 Stephen Rice              22/03/2022 19:30:06                   0 Guest

To restore a soft-deleted user account, run the Restore-MgDirectoryDeletedItem cmdlet and pass the account’s identifier. After restoring the account, remember to assign licenses to allow the account to access Microsoft 365 services.

Restore-MgDirectoryDeletedItem -DirectoryObjectId 3a6116ab-0116-490e-bd60-7e0cd9f36c9d

Remove Soft-Deleted Entra ID User Account

To remove a soft-deleted directory object, run the Remove-MgDirectoryDeletedItem cmdlet and pass the object identifier. Like Remove-MgUser, the cmdlet doesn’t ask for confirmation and permanent deletion happens immediately.

Remove-MgDirectoryDeletedItem -DirectoryObjectId f9d30b84-ad5f-4151-98f0-a55dafe30829

Time of Transition

We’re in a time of transition now as Microsoft does its best to retire the Azure AD modules and build the capabilities (and hopefully the documentation) of the Microsoft Graph PowerShell SDK. In the intervening period, any time you see an example using Azure AD cmdlets, try to convert it to use the SDK. It’s a great way to learn.


Keep up to date with developments like the Microsoft Graph PowerShell SDK 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.

]]>
https://office365itpros.com/2022/03/23/delete-entra-id-user-accounts/feed/ 10 54175
Microsoft Sets New Deprecation Schedule for Azure AD PowerShell https://office365itpros.com/2022/03/17/azure-ad-powershell-deprecation/?utm_source=rss&utm_medium=rss&utm_campaign=azure-ad-powershell-deprecation https://office365itpros.com/2022/03/17/azure-ad-powershell-deprecation/#comments Thu, 17 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=54064

What You Need to Do Before Azure AD and MSOL Modules Retire

Azure AD PowerShell retirement

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:

  • Reaffirmation that Microsoft will not retire the Azure AD Graph API on June 30, 2022. The Azure AD Graph is the component which underpins the Azure AD and MSOL modules. It’s a Graph API built especially for Azure AD before the Microsoft Graph established its position as the common API for Microsoft 365. The Azure AD team wants to deprecate their Graph API and embrace the Microsoft Graph, which is the basic reason for the planned deprecation of the Azure AD and MSOL modules.
  • Confirmation that the subset of the cmdlets in the Azure AD and MSOL modules which deal with user licensing will stop working earlier than the rest of the other cmdlets. Quite apart from the desire to move to the Microsoft Graph, these cmdlets are affected because Microsoft is moving to a new licensing management platform. Originally, the scheduled date for the transition was June 30, 2022. Microsoft pushed the date out eight weeks to August 26, 2022 and now it’s March 31, 2023. After this date, license management cmdlets like Get-AzureADSubscribedSKU won’t work.
  • Reconfirmation that the Microsoft Graph PowerShell SDK is the way forward.

Shifting Dates

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:

  • Scripts that manage licenses for Azure AD accounts (before August 26, 2022). This example of creating a license management report might help get you started.
  • Scripts that perform other Azure AD management operations (ideally before the end of 2022).

Microsoft Documents Its Migration Approach

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:

  • Find the Microsoft Graph equivalent of your Azure AD PowerShell cmdlets from the Cmdlet map.
  • Select the Microsoft Graph cmdlet to view the reference documentation and get the new syntax for the cmdlet.
  • Update your existing scripts using the new syntax.

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.

Migration Tips

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:

  • Don’t depend on the Microsoft Graph PowerShell SDK documentation for help with basic information like the format of input parameters. The documentation is machine-created and is shockingly bad in terms of its ability to guide people with real-life examples.
  • The SDK cmdlets are based on Graph API queries and often the documentation for those queries will help you understand how cmdlets work and the parameter values they expect.
  • The Graph Explorer is an excellent tool to help understand how Graph queries run and what they return.
  • Pay attention to parameters and switches. Some parameters of SDK cmdlets require a colon between the parameter and the value where an Azure AD or MSOL cmdlet does not. Some parameter and switch names change.
  • Don’t plan to run SDK scripts interactively. It will only lead to an accumulation of permissions for the service principal used by the SDK.
  • The SDK cmdlets handle pagination when necessary to retrieve all matching objects. Usually, there’s an -All parameter to help (like Get-MgGroup -All).
  • Sometimes you’ll need to use certificate-based authentication and a separate Azure AD registered to gain administrative access to data. The Teams tags report is a good example of when this technique is necessary.

We Feel Your Pain

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.

]]>
https://office365itpros.com/2022/03/17/azure-ad-powershell-deprecation/feed/ 6 54064
How to Create a Report About Teams Tags https://office365itpros.com/2022/03/16/teams-tags-report/?utm_source=rss&utm_medium=rss&utm_campaign=teams-tags-report https://office365itpros.com/2022/03/16/teams-tags-report/#comments Wed, 16 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=54045

Targeted Communications for Teams

Updated: 21-Feb-2024

In early 2020, Microsoft introduced Teams tags to facilitate “targeted communications.” In other words, to address subsets of the membership of a team with a @ mention when sending channel messages. The idea makes a lot of sense, especially in teams with very large memberships when @ mentioning the entire team or a channel only adds to the volume of messages that some recipients aren’t interested in or need to deal with.

The most recent change to tagging is in MC320163 (updated February 18, Microsoft 365 roadmap item 88318), which adds the option to allow team owners and members to create new tags. The update is currently rolling out.

Teams tags settings in the Team admin center
Figure 1: Teams tags settings in the Team admin center

Update: Microsoft is deprecating suggested tags.

How Much is Tagging Used?

The Office 365 for IT Pros eBook team uses Teams tags like Writers, Editors, and Guests to identify subsets of the membership. At least, we do when we remember to address messages with something like @Writers (usually the monthly reminder to get chapter updates done). Which brings me to the point that because tags are artifacts of teams, there’s no way for a tenant administrator to discover how widespread tags are across the teams in their organization.

That is, unless you use the Graph API where beta APIs are available to list the tags in a team and list the team members assigned to a tag. The latest version of the Microsoft Graph PowerShell SDK includes cmdlets to do the job (Get-MgTeamTag and Get-MgTeamTagMember), which is what I explore here.

Other cmdlets are available to create new tags or assign a tag to a team member (New-MgTeamTagMember). For example, this command adds a tag to a team member. The parameters are the group identifier, Azure AD user account identifier, and the tag identifier (as returned by Get-MgTeamTag).

New-MgTeamTagMember -TeamId $TeamId -TeamworkTagId $TagId -UserId $MemberId

Graph Permissions

As you might know, the Microsoft Graph supports two types of permission: delegated and application. Delegated permissions operate as if a user is performing the action, so the data available to that action is limited to whatever the user can access. Application permission allows access to data across the tenant. However, when you sign into an interactive Microsoft Graph PowerShell SDK session by running the Connect-MgGraph cmdlet, some applications restrict access to data as if you use delegated permission. In this instance, if you run Get-MgTeamTag against a team your account is a member of, the cmdlet returns details of the tags. However, if you run Get-MgTeamTag against a team you don’t have membership of, the cmdlet returns nothing.

The solution is to create an Entra ID registered application, assign it the necessary application permission (TeamWorkTag.Read.All), and upload a certificate to the app to use certificate-based authentication with the Microsoft Graph PowerShell SDK. The SDK recognizes that you’re not running a normal interactive session and is happy to access data in all teams.

The Code

All of which brings us to a point where we can write some PowerShell code to:

  • Connect to the Graph (the application must also have consent for the Team.ReadBasic.All permission to read details of teamsin the tenant).
  • Find all teams.
  • Loop through the teams to discover which teams use tags.
  • Loop through the tags to find which team members are assigned to each tag.
  • Report what we find.

Here’s some simple code to illustrate what’s possible.

$TenantId = 'Your tenant identifier'
# Identifier for the app holding the necessary permissions
$AppId = '1b58427d-1938-40de-9a5d-0b22c4f85c0c' 
# Thumbprint for an X.509 certificate uploaded to the app
$Thumbprint = "F79286DB88C21491110109A0222348FACF694CBD"

Connect-MgGraph -NoWelcome -TenantId $TenantId -AppId $AppId -CertificateThumbprint $Thumbprint
# Necessary scopes: Directory.Read.All, TeamworkTag.Read.All, Channel.ReadBasic.All, Team.ReadBasic.All
Write-Host "Finding Teams..."
[array]$Teams = Get-MgTeam -All | Sort-Object DisplayName
$Report = [System.Collections.Generic.List[Object]]::new()

ForEach ($Team in $Teams) {
  Write-Host "Processing team" $Team.DisplayName
  [array]$TeamTags = Get-MgTeamTag -TeamId $Team.Id -ErrorAction SilentlyContinue
  If ($TeamTags) { # The team has some tags
     ForEach ($Tag in $TeamTags) {
       $TagMembers = (Get-MgTeamTagMember -TeamId $Team.Id -TeamWorkTagId $Tag.Id) | Select-Object -ExpandProperty DisplayName 
       $ReportLine = [PSCustomObject][Ordered]@{
          Team        = $Team.DisplayName
          TeamId      = $Team.Id
          Tag         = $Tag.DisplayName
          Description = $Tag.Description
          Members     = $TagMembers -Join ", "
          TagId       = $Tag.Id }
       $Report.Add($ReportLine)
      } #End Foreach Tag
   } #End If TeamTags
} #End ForEach team
 
[array]$TeamsWithTags = $Report.TeamId | Sort -Unique
[array]$UniqueTags = $Report.TagId | Sort -Unique
Write-Host "Total teams:      " $Teams.Count
Write-Host "Teams with tags:  " $TeamsWithTags.Count
Write-Host "Total tags:       " $UniqueTags.Count

$Report | Select Team, Tag, Description, Members | Out-GridView

In my tenant, of 82 teams only 8 had tags and a total of 16 tags were in use. Figure 2 shows the results as viewed through Out-GridView.

Reporting Teams tags
Figure 2: Reporting Teams tags

Teams Tagging for All

Microsoft doesn’t have a public API to report how often people use Teams tags to address channel messages, so even though I know that there’s not all that many tags in the tenant, it could be true that they’re all heavily used. We just don’t know. In any case, approximately 10% of all teams use tags, so now I need to decide if I want to ignore the situation or help team owners understand the benefit of tagging.


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.

]]>
https://office365itpros.com/2022/03/16/teams-tags-report/feed/ 1 54045
Creating an Authentication Method Report for Entra IAccounts https://office365itpros.com/2022/03/03/azure-ad-accounts-authentication/?utm_source=rss&utm_medium=rss&utm_campaign=azure-ad-accounts-authentication https://office365itpros.com/2022/03/03/azure-ad-accounts-authentication/#comments Thu, 03 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=53754

Moving from Old Modules to the Microsoft Graph SDK for PowerShell

Update: This article describes a script to generate a report showing the MFA status for accounts and highlights administrative accounts that aren’t MFA-enabled.

Microsoft 365 tenants often create reports to understand the health of their Azure AD accounts. In June 2021, Microsoft announced that they are moving away from the Microsoft Online Services (MSOL) and Azure AD PowerShell modules and would deprecate the Azure AD Graph API at the end of June 2022. Customer feedback convinced Microsoft to push the deprecation date out to the end of 2022, and then to March 2024. However, the writing is on the wall for the Azure AD Graph API and it’s time to move to Graph-based interfaces.

Their future focus for PowerShell access to Entra ID data is the Microsoft Graph SDK for PowerShell. The only definite drop-dead date affecting the older modules is June 30, 2022, when Microsoft moves to a new license management platform. At this point, any cmdlet which retrieves license information for user accounts will cease working. Any scripts used for tenant license management that haven’t been updated now require urgent attention.

I have an MFA status script written in 2018. The script uses the MSOL cmdlets to check and report the “strong authentication methods” for each user account. The state of multi-factor authentication has moved on since 2018 to accommodate new methods like FIDO2 keys and passwordless authentication. These methods aren’t handled by the MSOL cmdlets. It’s therefore time to take a different approach.

Given Microsoft’s direction to use the Microsoft Graph SDK for PowerShell for Azure AD access, it seems like this is the right path to follow. I’ve done a reasonable amount with the SDK and have written several articles to report my progress. For example, here’s how to generate a licensing report for a tenant.

Not a Perfect SDK

The SDK is not perfect. Essentially, its cmdlets are wrappers around Graph API queries. If you’re used to dealing with Graph APIs, the SDK cmdlets will be second nature. If not, it will take time to become familiar.

Getting acquainted isn’t helped by the poor state of the documentation for the SDK cmdlets. Microsoft uses automatic text generation tools to create the documentation from source code. The result is often as useful as a snowball in a desert, especially in terms of practical examples and explanations about inputs. Again, if you know the Graph APIs, you probably won’t be surprised at what you find in the documentation, but the current state of the documentation is no credit to Microsoft and a barrier to adoption.

Steps in the Creation of the Report

The aim is to create a report showing the authentication methods used by Azure AD accounts and highlight accounts which might attention (hopefully, by enabling multi-factor authentication). According to a recent Microsoft report, only 22% of Microsoft 365 accounts use multi-factor authentication. Although this marks an improvement over the past, it’s still far too low a percentage. Perhaps people don’t know about recent improvements Microsoft has made in MFA processing to make it easier for people to use.

The script I wrote to create the report does the following:

  • Connects to the Graph endpoint with the following permissions: UserAuthenticationMethod.Read.All, Directory.Read.All, User.Read.All, Auditlog.Read.All. If the service principal for the SDK doesn’t have administrat9or consent of any permission, it must be granted at this point.
  • Set the Graph profile to beta to make sure that we have access to all the user account data.
  • Call the Get-MgUser cmdlet to fetch the set of Azure AD member accounts. The set returned excludes guest accounts but includes the accounts used for shared and resource mailboxes. To focus solely on licensed member accounts, you could apply a filter to look for accounts with at least one assigned license.
  • Loop through each account to check its authentication methods. To ensure that only active accounts are checked, the script calls the Get-MgAuditLogSignIn cmdlet to look for a sign-in record. This check can only go back 30 days.
  • For each active account, call the Get-MgUserAuthenticationMethod cmdlet to return the authentication methods used by the account. An account can use all the valid authentication methods from passwordless to the Microsoft authenticator app. The first time an account uses a method, Azure AD adds it to the list used by the account.
  • For each method, retrieve some information.
  • Report the results of checking each method.
  • After processing all user accounts, create a list of users for which an authentication method is available and check each account to see if one of the strong (MFA) methods is noted.
  • Create a final report and output it to a CSV file.

Figure 1 shows an example of the kind of output generated.

Azure AD Accounts and authentication methods
Figure 1: Authentication methods report for Azure AD accounts

You can download the script from GitHub. As always, the code is intended to illustrate a principal rather than being a fully-baked solution.

Automating the Check

A script like this is a good candidate for execution by an Azure Automation runbook. It can be run on a schedule and the results emailed to administrators for action. Getting a weekly or monthly reminder to improve the security posture of the organization by increasing the percentage of well-protected accounts can only be a good thing, can’t it?


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.

]]>
https://office365itpros.com/2022/03/03/azure-ad-accounts-authentication/feed/ 9 53754
How to Control the Display of People Insights in Microsoft 365 https://office365itpros.com/2022/03/02/people-insights/?utm_source=rss&utm_medium=rss&utm_campaign=people-insights https://office365itpros.com/2022/03/02/people-insights/#comments Wed, 02 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=53685

Item, Meeting, and People Insights Derived from User Signals

In August 2020, I wrote about how to control item insights using the Graph Explorer. Item insights are one of the three insights generated from Graph signals gathered from user activity and displayed in various places within Microsoft 365:

  • Item insights: Recommendations about documents available to a user which they might be interested in. This information shows up in the profile card in the “Files” section.
  • Meeting insights: When users open a meeting in their calendars, Outlook displays insights relevant to the meeting, such as documents and email covering the content of the meeting.
  • People insights: Information about people deemed to be relevant to an individual and who have a “public relationship” with that individual. For example, they have the same manager (as noted in Azure AD) or share a common membership of a public group or distribution list with fewer than 30 members. People insights show up on the profile card in the “works with” section, with the people who appear ranked in order of the public and private communications (direct email or meetings rather than group-based communications) between the listed individuals and the owner of the profile card.

Item and meeting insights are configurable through the Search & Intelligence section of Settings in the Microsoft 365 admin center (Figure 1). Both can be enabled or disabled, and item insights can be disabled for a set of users defined as members of a group.

Search and Intelligence options in the Microsoft 365 admin center
Figure 1: Search and Intelligence options in the Microsoft 365 admin center

Recently, Microsoft added the ability to customize people insights, but only by running a Graph query. No doubt the necessary GUI will soon appear in the Microsoft 365 admin center.

The Need for User Privacy

In most cases, insights are useful. Looking at the list presented in the people card (Figure 2), users probably don’t realize how the Graph determines who appears in the list and what a public relationship is versus a private relationship. All they see is a set of individuals who the owner of the profile card “works with.”

People insights in the user profile card
Figure 2: People insights in the user profile card

However, in places where personal privacy is taken more seriously than in more liberal regimes, exposing the list of people that someone works with could be considered a breach of that person’s privacy. Although the list is based on public relationships that anyone can see by checking the membership of the relevant groups, it’s still too much for some.

Suppressing People Insights

To suppress people insights, a tenant can:

  • Disable people insights completely. No one in the tenant sees this data.
  • Disable people insights for a group of people. Everyone else sees the people insights.

For instance, if an organization decreed that the employees from a specific country should not see people insights, you could create a group (not a dynamic group) and add the relevant employees as members. The next step is to retrieve the identifier for the group and the tenant identifier, which is easily done by running the Get-AzureADGroup and Get-AzureADTenantDetail cmdlets:

Get-AzureADGroup -SearchString Disabled

ObjectId                             DisplayName           Description
--------                             -----------           -----------
c9758609-d33b-4eea-976b-d8e43a2ad135 DisabledGraphInsights Members of this group do not have graph insights

Get-AzureADTenantDetail

ObjectId                             DisplayName             VerifiedDomain
--------                             -----------             --------------
d662313f-14fc-43a2-9a5a-d2e27f4f3476 Office 365 for IT Pros  Office365itpros.com

Given that the Azure AD module is due for deprecation in March 2024, you should consider using the Microsoft Graph PowerShell SDK cmdlets:

Get-MgGroup -Filter "startsWith(displayname, 'Disabled')"

Id                                   DisplayName           Description                                       GroupTypes
--                                   -----------           -----------                                       ----------
c9758609-d33b-4eea-976b-d8e43a2ad135 DisabledGraphInsights Members of this group do not have graph insights  {Unified}

Get-MgOrganization | Select Id, DisplayName

Id                                   DisplayName
--                                   -----------
d662313f-14fc-43a2-9a7a-d2e27f4f3478 Office 365 for IT Pros

With the two identifiers, we can go to the Graph Explorer and update the Insight settings control for people insights. This is done by running a Graph patch query to:

https://graph.microsoft.com/beta/organization/d662313f-14fc-43a2-9a5a-d2e27f4f3476/ settings/peopleInsights

The query includes a request body containing the identifier of the group holding the set of users to exclude from people insights:

{
  "disabledForGroup": " c9758609-d33b-4eea-976b-d8e43a2ad135"
}

Figure 3 shows the Graph Explorer after running the patch query. The response (200) means that the Graph accepted the command, and the response preview shows the current configuration for people insights in the tenant. Here we can see that people insights are enabled (isEnabledInOrganization is True) and that a group of excluded users exists.

Using the Graph Explorer to control People Insights
Figure 3: Using the Graph Explorer to control People Insights

No More People Insights

After updating the organization setting, it can take up to 24 hours before its effect is noticed in profile cards. Figure 4 shows the result. The organization data is present, but the people insights are absent for any user who’s a member of the excluded group.

The user profile card without People Insights
Figure 4: The user profile card without People Insights

Controlling people insights is an easy thing to do. If you have some users who object to this information being available to others, it will take less than five minutes to create the excluded group and update the organization settings. Add a few extra minutes to populate the group, and then it’s just a matter of waiting for the update to percolate to applications.


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.

]]>
https://office365itpros.com/2022/03/02/people-insights/feed/ 9 53685
Understanding How App Certification for Microsoft 365 Apps Works https://office365itpros.com/2022/02/23/app-certification-microsoft365/?utm_source=rss&utm_medium=rss&utm_campaign=app-certification-microsoft365 https://office365itpros.com/2022/02/23/app-certification-microsoft365/#comments Wed, 23 Feb 2022 01:00:00 +0000 https://office365itpros.com/?p=53581

The Many Ways to Stress the Need for Modern Authentication

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.

Microsoft’s App Compliance Program

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.

App certification information in AppSource
Figure 1: App certification information in AppSource

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.

App certification includes documenting API permissions
Figure 2: App certification includes documenting API permissions

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.

Downgraded Certification

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.

Azure AD Consent and Permissions settings
Figure 3: Azure AD Consent and Permissions settings

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 Rocky Road to App Certification

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.

]]>
https://office365itpros.com/2022/02/23/app-certification-microsoft365/feed/ 1 53581
How Microsoft Teams Displays Local Time in Profile Cards https://office365itpros.com/2022/02/21/teams-time-zone-profile-cards/?utm_source=rss&utm_medium=rss&utm_campaign=teams-time-zone-profile-cards https://office365itpros.com/2022/02/21/teams-time-zone-profile-cards/#comments Mon, 21 Feb 2022 01:00:00 +0000 https://office365itpros.com/?p=53593

Teams Local Time a Useful Feature Dependent on Exchange Calendar Settings

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.

Local time information appears on a Teams profile card
Figure 1: Local time information appears on a Teams profile card

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.

Puzzled by Time Zones

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.

Teams displays an interesting local time zone for a Bulgarian user
Figure 2: Teams displays an interesting local time zone for a Bulgarian user

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.

Two Time Zones for Outlook

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

OWA calendar settings detect a time zone mismatch
Figure 3: OWA calendar settings detect a time zone mismatch

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.

Checking Calendar Settings

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:

  • Fetches user mailboxes.
  • Checks the regional configuration to get the time zone set for the user location.
  • Checks the calendar configuration to get the time zone set for meeting hours.
  • Checks the user account properties to get their physical address.
  • Reports any mismatches found between the two time zones.

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

Time zone mismatches reported by PowerShell
Figure 4: Time zone mismatches reported by PowerShell

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.

Adjustments Best Left to Users

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.

]]>
https://office365itpros.com/2022/02/21/teams-time-zone-profile-cards/feed/ 27 53593
Understanding What’s in an Entra ID Access Token https://office365itpros.com/2022/02/17/understanding-entra-id-access-token/?utm_source=rss&utm_medium=rss&utm_campaign=understanding-entra-id-access-token https://office365itpros.com/2022/02/17/understanding-entra-id-access-token/#comments Thu, 17 Feb 2022 01:00:00 +0000 https://office365itpros.com/?p=53497

Critical Piece When Connecting to the Microsoft Graph

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

The Graph Explorer displays its Azure AD access token
Figure 1: The Graph Explorer displays its access token

In the remainder of this article, I explore what an Entra ID access token contains.

The Need for Access Tokens

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

Deciphering an Access Token

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

Claims and Scopes

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:

  • The tenant (tid).
  • The intended consumer of the token (aud): https://graph.microsoft.com.
  • The app name (app_displayname).
  • The app identifier (appid).
  • The security token service (STS) responsible for issuing the token (iss): https://sts.windows.net/a662313f-14fc-43a2-9a7a-d2e27f4f3478/.
  • The generation time for the token (iat).
  • The time when the token expires (exp). All dates are in Unix epoch time, so 1644837672 means 11:21:12 GMT on February 14, 2022. By default, access tokens issued by Entra ID last one hour, except those used by applications which support continual access evaluation (CAE), where Entra ID issues 28-hour access tokens because it can terminate access at any time and force the user to reauthenticate should a critical user event (like a password change) happen.
  • The identifier for the object in the Microsoft identity system used for authentication (oid). In this case, the script uses a registered Entra ID app, so the value is the service principal for the app. You can test this by running the Get-MgServicePrincipal cmdlet from the Microsoft Graph PowerShell SDK:

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.

Using the Access Token

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.

]]>
https://office365itpros.com/2022/02/17/understanding-entra-id-access-token/feed/ 6 53497
How to Report Groups Under the Control of the Microsoft 365 Groups Expiration Policy https://office365itpros.com/2022/02/09/microsoft-groups-expiration-policy/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-groups-expiration-policy https://office365itpros.com/2022/02/09/microsoft-groups-expiration-policy/#comments Wed, 09 Feb 2022 01:00:00 +0000 https://office365itpros.com/?p=53428

Yammer Communities Now Covered by Groups Expiration Policy

Updated 24 April 2023

A reader question about the Microsoft 365 Groups expiration policy caused me to review some PowerShell code I wrote to report the next renewal dates for the set of groups within the scope of the expiration policy. The question was related to Yammer (now Viva Engage), to know if the Microsoft 365 Group expiration policy covers the groups by Yammer and will remove inactive groups when necessary. The answer is yes; Microsoft updated policy processing last year to accommodate the Microsoft 365 groups used by Yammer communities when networks run in Microsoft 365 native mode. Microsoft confirmed coverage for Yammer communities by the groups expiration policy in MC324202 (published today). Microsoft 365 roadmap item 82186 also deals with the scenario and says that general availability occurred in January 2022.

In 2020, Microsoft changed the way the Microsoft 365 Groups expiration policy works to introduce automatic renewal. Instead of bothering group owners with email to remind them to renew the group, a background job looks for evidence that the group is active. If the evidence exists, Microsoft 365 renews the group automatically. Unfortunately, a limited set of signals govern renewal:

  • SharePoint Online: View, edit, download, move, share, or upload files.
  • Outlook: Join group, read/write message in the group mailbox, or like a message (in OWA).
  • Teams: Visit a Teams channel.
  • Yammer: View a post within a Yammer community or an interactive email in Outlook.

It’s debatable if a group is active if just one group member visits a Teams channel or views a post in a Yammer community. The Microsoft Graph gathers a wide array of signals about user activity and there’s surely a more precise method to determine group activity than the actions cited above. The Groups and Teams activity report is an example of how to code your own assessment of group activity.

In any case, the answer remains that you can add the Microsoft 365 groups used by Viva Engage communities to the Groups expiration policy through the Microsoft Entra admin center (Figure 1).

Figure 1: Defining groups to include in the expiration policy

You can also add groups to the policy with PowerShell.

The Groups expiration policy is appliable to selected groups or to all Microsoft 365 groups in the tenant. Users who are members of the groups covered by the policy must have Azure AD Premium P1 licenses.

PowerShell Code to Report Group Expiration

Writing code to report the expiration dates for groups isn’t difficult. The date when the group needs to be next renewed is in the ExpirationTime property. The only complication is to find when the group was last renewed. This data isn’t returned by the Get-UnifiedGroup cmdlet, so we need to use the Get-AzureADMSGroup cmdlet. Once we know where to get the dates, we can report what we find. This code runs after connecting to the Exchange Online and Azure AD PowerShell modules.

Write-Host "Finding Microsoft 365 Groups to check…"
[array]$ExpirationPolicyGroups  = (Get-UnifiedGroup -ResultSize Unlimited | ? {$_.ExpirationTime -ne $Null} | Select DisplayName, ExternalDirectoryObjectId, WhenCreated, ExpirationTime )
If (!($ExpirationPolicyGroups)) { Write-Host "No groups found subject to the expiration policy - exiting" ; break }
Write-Host $ExpirationPolicyGroups.Count “groups found. Now checking expiration status.”
$Report = [System.Collections.Generic.List[Object]]::new(); $Today = (Get-Date)
ForEach ($G in $ExpirationPolicyGroups) {
        $Days = (New-TimeSpan -Start $G.WhenCreated -End $Today).Days  # Age of group
        $LastRenewed = (Get-AzureADMSGroup -Id $G.ExternalDirectoryObjectId).RenewedDateTime
        $DaysLeft = (New-TimeSpan -Start $Today -End $G.ExpirationTime).Days
        $ReportLine = [PSCustomObject]@{
           Group       = $G.DisplayName
           Created     = Get-Date($G.WhenCreated) -format g
           AgeinDays   = $Days
           LastRenewed = Get-Date($LastRenewed) -format g
           NextRenewal = Get-Date($G.ExpirationTime) -format g
           DaysLeft    = $DaysLeft}
          $Report.Add($ReportLine)
} # End Foreach
CLS;Write-Host "Total Microsoft 365 Groups covered by expiration policy:" $ExpirationPolicyGroups.Count
Write-Host “”
$Report | Sort DaysLeft | Select Group, @{n="Last Renewed"; e= {$_.LastRenewed}}, @{n="Next Renewal Due"; e={$_.NextRenewal}}, @{n="Days before Expiration"; e={$_.DaysLeft}}

Total Microsoft 365 Groups covered by expiration policy: 74

Group                                Last Renewed     Next Renewal Due Days before Expiration
-----                                ------------     ---------------- ----------------------
Potholers (Team)                     02/02/2020 07:16 21/02/2022 07:16                     13
Office 365 Questions                 19/05/2017 11:12 14/03/2022 15:04                     34
Corona Virus News                    10/03/2020 21:56 30/03/2022 22:56                     51
Contract Workers                     12/03/2020 08:57 01/04/2022 09:57                     52
Plastic Production (Team)            25/03/2020 08:48 14/04/2022 09:48                     65

The code works, with two caveats:

  1. Get-UnifiedGroup is not a fast cmdlet. It’s OK to run it against a couple of hundred groups, but once that number grows, the time needed for the cmdlet to retrieve details of the groups to process gets longer and longer.
  2. The Get-AzureADMSGroup cmdlet is affected by Microsoft’s decision to retire the Azure AD module. Although the cmdlet will continue to run after June 30, 2023, you don’t know when it will cease functioning.

The solution for both speed and supportability is to use a Microsoft Graph API query to fetch group details.

Using the Graph to Fetch Microsoft 365 Groups to Report Expiration Details

Essentially, what we need to do is to replace the call to Get-UnifiedGroup with a Graph API query to return the set of groups in the tenant. The bonus is that the query returns the last renewed time, so there’s no need to use Get-AzureADMSGroup.

As with any script that calls Graph queries from PowerShell, you need a registered application in Azure AD to hold the permissions required to run the queries used by the script. In this case, we only need the Group.Read.All permission. After securing an access token, we can fetch the set of groups in the tenant using a lambda filter. The code shown below uses a function (Get-GraphData) to execute the Invoke-RestMethod cmdlet to fetch the data and page until all groups are retrieved. You can see the code for the Get-GraphData function in this script.

After fetching the set of groups, we create a report detailing the group name, its creation date, the date last renewed, and expiration date. The code used to process the data returned by Get-UnifiedGroup is modified to deal with the property names returned by the Graph query.

$uri = "https://graph.microsoft.com/beta/groups?`$filter=ExpirationDateTime ge 2014-01-01T00:00:00Z AND groupTypes/any(a:a eq 'unified')&`$count=true" 
[array]$Groups = Get-GraphData -AccessToken $Token -Uri $uri
If (!($Groups)) { Write-Host "No groups found subject to the expiration policy - exiting" ; break }
$Report = [System.Collections.Generic.List[Object]]::new(); $Today = (Get-Date)
ForEach ($G in $Groups) {
        $Days = (New-TimeSpan -Start $G.CreatedDateTime -End $Today).Days  # Age of group
        #$LastRenewed = $G.RenewedDateTime
        #$NextRenewalDue = $G.ExpirationDateTime
        $DaysLeft = (New-TimeSpan -Start $Today -End $G.ExpirationDateTime).Days
        $GroupsInPolicy++
        $ReportLine = [PSCustomObject]@{
           Group                   = $G.DisplayName
           Created                 = Get-Date($G.CreatedDateTime) -format g
          "Age in days"            = $Days
          "Last renewed"           = Get-Date($G.RenewedDateTime) -format g
          "Next renewal"           = Get-Date($G.ExpirationDateTime) -format g
          "Days before expiration" = $DaysLeft}
          $Report.Add($ReportLine)
} # End ForeachCLS;Write-Host "Total Microsoft 365 Groups covered by expiration policy:" $Groups.Count
Write-Host “”
$Report | Sort "Days before expiration"| Select Group, "Last renewed", "Next renewal", "Days before expiration" | Out-GridView

As you’d expect, things run much faster. Retrieving data through a Graph query is always quicker than using a PowerShell cmdlet and eliminating the call to Get-AzureADMSGroup for each group helps speed things up even further. Figure 2 shows the output.

Expiration dates for Microsoft 365 Groups

Microsoft 365 Groups Expiration policy
Figure 2: Expiration dates for Microsoft 365 Groups

An even easier solution is to replace the calls to Get-UnifiedGroup and Get-AzureADMSGroup with the Get-MgGroup cmdlet from the Microsoft Graph PowerShell SDK. Here’s the code, which is almost as fast as using the Graph API:

Write-Host "Finding Microsoft 365 Groups to check…"
[array]$ExpirationPolicyGroups = Get-MgGroup -Filter "groupTypes/any(c:c eq 'unified')" -All | ? {$_.ExpirationDateTime -ne $Null }
If (!($ExpirationPolicyGroups)) { Write-Host "No groups found subject to the expiration policy - exiting" ; break }
Write-Host $ExpirationPolicyGroups.Count “groups found. Now checking expiration status.”
$Report = [System.Collections.Generic.List[Object]]::new(); $Today = (Get-Date)
ForEach ($G in $ExpirationPolicyGroups) {
        $Days = (New-TimeSpan -Start $G.CreatedDateTime -End $Today).Days  # Age of group
        $DaysLeft = (New-TimeSpan -Start $Today -End $G.ExpirationDateTime).Days
        $ReportLine = [PSCustomObject]@{
           Group       = $G.DisplayName
           Created     = Get-Date($G.CreatedDateTime) -format g
           AgeinDays   = $Days
           LastRenewed = Get-Date($G.RenewedDateTime) -format g
           NextRenewal = Get-Date($G.ExpirationDateTime) -format g
           DaysLeft    = $DaysLeft}
          $Report.Add($ReportLine)
} # End Foreach
CLS;Write-Host "Total Microsoft 365 Groups covered by expiration policy:" $ExpirationPolicyGroups.Count
Write-Host “”
$Report | Sort DaysLeft | Select Group, @{n="Last Renewed"; e= {$_.LastRenewed}}, @{n="Next Renewal Due"; e={$_.NextRenewal}}, @{n="Days before Expiration"; e={$_.DaysLeft}}

Speeding Up Queries

The question about Yammer communities forced me to look at code and find an instance where I needed to replace a cmdlet before its deprecation next June. At the same time, I managed to speed up the code by introducing a Graph query. Things worked out for the best, but it does illustrate the need to check and update old scripts on an ongoing basis.


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.

]]>
https://office365itpros.com/2022/02/09/microsoft-groups-expiration-policy/feed/ 10 53428
How to Exploit Entra ID Sign-in Data to Detect Problem Service Principals https://office365itpros.com/2022/02/03/service-principal-sign-in-data-detect-problem-apps/?utm_source=rss&utm_medium=rss&utm_campaign=service-principal-sign-in-data-detect-problem-apps https://office365itpros.com/2022/02/03/service-principal-sign-in-data-detect-problem-apps/#respond Thu, 03 Feb 2022 01:00:00 +0000 https://office365itpros.com/?p=53160

Spring Clean Time for Apps Coming Soon

Last year, I wrote about the need to review and clean up Entra ID integrated applications. That article describes how to extract information from Entra ID o a CSV file and use the CSV to create a Microsoft List. To make it easy to access the list, we create a channel tab in Teams. Everything works to identify suspect apps that might need removal. I think that you should perform such a review periodically. It just makes sense.

Another way to monitor potentially suspicious app activity is to review sign in data for service principals. The intention is to identify unrecognized service principals signing into the tenant and figure out what apps are involved. Sign-ins can originate from well-known service principals used by Microsoft apps, third-party apps, or the service principals automatically created by Entra ID when tenants register apps to interact with the Graph (for instance, to authenticate calls made to Graph APIs in PowerShell scripts). Sign-in data for service principals is available through the Entra admin center (Figure 1) and now it’s accessible using the Microsoft Graph List SignIns API.

Sign-in logs for service principals in the Azure AD admin center
Figure 1: Sign-in logs for service principals in the Entra admin center

The reason why this update is important is that access to sign-in data via the Graph makes it possible to download the information for analysis or store it for long-term retention in an external repository. Although you can download sign-in data as a CSV file from the Entra admin center, it’s more flexible to access the information via Graph queries, especially when you want to probe the activity patterns of certain service principals.

Getting Sign-In Data from the Graph

Any application which wants to interact with the Graph requires consent for permissions to access data. In this instance, consent is needed the Directory.Read.All and AuditLog.Read.All application permissions. Delegate permissions can also be used, and in this case the account used must hold an administrative role capable of accessing the Entra ID sign-in logs.

A suitably-permissioned application can issue queries against the SignIns API. To fetch service principal sign-in data, the query executed by the application must use a Lambda qualifier to filter data. Apart from setting a date range to search for sign-in data, the important point is to filter against the signInEventTypes property to select sign-in events for service principals. Here’s an example of a query to fetch sign-in data for between 17:30 and 22:3 on 19 January.

https://graph.microsoft.com/beta/auditLogs/signIns?&$filter=createdDateTime ge 2022-01-19T17:30:00Z and createdDateTime le 2022-01-19T22:30:00Z and signInEventTypes/any(x:x eq 'servicePrincipal')

To test the query (or one which suits your purposes), use the Graph Explorer to see what the query returns.

I wrote a simple PowerShell script (downloadable from GitHub) to fetch service principal sign-in data for the last seven days. A quick summary of the data revealed that many sign-ins came from an app named Office 365 Reports. Curiously, an app used by a PowerShell script that I had posted on GitHub also showed up with 22 sign-ins. The Information Barrier Processor is the app used by Microsoft 365 to check user accounts against information barrier policies to ensure that no one is communicating with anyone when they shouldn’t.

$Report | Group SpName | Sort Count -Descending | Select Name, Count

Name                                         Count
----                                         -----
Office 365 Reports                             369
Graph Microsoft 365 Groups Membership Report    22
Information Barrier Processor                   21
Security and Audit                               5
PS-Graph                                         1

Resolving the large set of sign-ins was easy. The data stored in the list (Figure 2) revealed the service principal to belong to an Office 365 Reporting app originally published by Cogmotive (acquired by Quadrotech and then by Quest Software). I haven’t used the app in years, but the sign-ins kept on coming.

Service Principal information
Figure 2: Service Principal information

Over time, it’s easy to accumulate crud in the form of service principals installed for one reason or another. Testing an ISV product is a classic example, which is a good reason to always conduct tests in a test tenant instead of the production tenant. Or if you stop using an app, remember to clean up by removing service principals and other app debris that the app vendor might leave behind.

The sign-ins for the app used by the PowerShell script probably exist because I shared a copy of the script with my tenant identifier, the app identifier, and the app secret in place. I quickly replaced the script with a copy containing obfuscated credentials, but failed to change the app secret, meaning that anyone with an original copy could run the code. Now alerted, I removed the app secret. My suspicions were confirmed when a batch of failed sign-ins subsequently occurred for the app. This goes to prove how easy it is to create a potential compromise if you’re not careful.

Removing a Service Principal with PowerShell

You can clean up unwanted service principals with either the Entra admin center or PowerShell. I always have a PowerShell session open, so I chose that route. In this example, we find the object identifier for a service principal using its display name as a filter for the Get-MgServicePrincipal cmdlet. When sure that this is the right service principal to remove, we use the object identifier to remove the service principal with the Remove-MgServicePrincipal cmdlet.

$SP = Get-MgServicePrinicpal -filter "displayname eq 'Office 365 Reports'"

$SP

DisplayName        Id                                   AppId                                SignInAudience     
-----------        --                                   -----                                --------------     
Office 365 Reports 9ac957ae-160b-48d3-9a6f-f4c27acca040 507bc9da-c4e2-40cb-96a7-ac90df92685c AzureADMultipleOrgs 

Remove-MgServicePrincipal -ServicePrincipalId $Sp.id

Adding Context

A list of service principals known to the tenant is a valuable input to a review for unwanted or unnecessary apps holding some form of consent (permissions) to organization data. Adding context to the data by knowing which service principals are actively signing into the tenant makes it easier to prioritize action. The data is there, it’s available, and it’s up to you to decide what to do with it.


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.

]]>
https://office365itpros.com/2022/02/03/service-principal-sign-in-data-detect-problem-apps/feed/ 0 53160
How Microsoft 365 Notifications Show Active User Data for Workloads Affected by Service Updates https://office365itpros.com/2022/01/20/microsoft-365-notifications-users/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-notifications-users https://office365itpros.com/2022/01/20/microsoft-365-notifications-users/#comments Thu, 20 Jan 2022 01:00:00 +0000 https://office365itpros.com/?p=53127

Microsoft 365 Notifications User Counts Come from the Graph

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.

Usage data shows up in message center notifications (source: Microsoft)

Microsoft 365 Notifications
Figure 1: Microsoft 365 notifications with user data (source: Microsoft)

Editorial comment: The need for Kaizala is possibly now much reduced by the general availability of the Teams Walkie-Talkie feature.

The Problem with Microsoft Graph Usage Data

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.

Still a Good Change

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.

]]>
https://office365itpros.com/2022/01/20/microsoft-365-notifications-users/feed/ 2 53127
How to Determine the Age of a Microsoft 365 Tenant https://office365itpros.com/2022/01/14/find-age-microsoft-365-tenant/?utm_source=rss&utm_medium=rss&utm_campaign=find-age-microsoft-365-tenant https://office365itpros.com/2022/01/14/find-age-microsoft-365-tenant/#comments Fri, 14 Jan 2022 01:00:00 +0000 https://office365itpros.com/?p=53007

Use Teams, PowerShell, or the Graph

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.

Viewing the tenant creation date in the Teams admin center
Figure 1: Viewing the tenant creation date in the Teams admin center

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.

Using Exchange Online Data

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.

Administrator Accounts

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:

  • Find the set of accounts which currently hold the global administrator role. We omit the account returned with the object id 25cbf210-02e5-4a82-9f5c-f41befd2681a as this is a service principal used by Microsoft Rights Management services (you can confirm this by running Get-AzureADServicePrincipal -ObjectId 25cbf210-02e5-4a82-9f5c-f41befd2681a).
  • Check each account to find the creation date. This is slightly complicated when using the Azure AD PowerShell module because the creation date is part of the extension properties. We therefore use the Get-AzureADUserExtension cmdlet to extract the date and then store it in the array used to hold details about tenant administrators.
  • Sort the accounts by creation date and report the oldest.

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.

Use the Graph

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.

An Old Tenant

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.

]]>
https://office365itpros.com/2022/01/14/find-age-microsoft-365-tenant/feed/ 2 53007
New Way Available to Fetch a List of Microsoft Teams with the Microsoft Graph https://office365itpros.com/2022/01/13/list-teams-api-graph/?utm_source=rss&utm_medium=rss&utm_campaign=list-teams-api-graph https://office365itpros.com/2022/01/13/list-teams-api-graph/#respond Thu, 13 Jan 2022 01:00:00 +0000 https://office365itpros.com/?p=52958

Finding Teams in Groups

The fastest way to fetch the list of Teams in a Microsoft 365 tenant programmatically is to use the Graph API. PowerShell is fast enough in small tenants, but once there’s more than a couple of hundred teams (groups) to process, the Graph is usually a better choice. Unless you’ve got time to wait, of course.

I cover the topic in an article explaining how to fetch a list of Teams using the Groups endpoint. Because the Groups API returns all types of groups, you apply a filter to find the set of Microsoft 365 Groups which are team-enabled. For PowerShell, the commands needed to execute the Graph API call are:

$Uri = “https://graph.microsoft.com/V1.0/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')”
[array]$Teams = Invoke-WebRequest -Method GET -Uri $Uri -ContentType "application/json" -Headers $Headers | ConvertFrom-Json

The filter used with the query is a Lambda operator. In this case, it requests the Graph to return any Groups it finds where the provisioning option is set to “Team.” Using a filter to find Teams is a well-known technique exploited by developers: we use it in the Graph-based version of the Teams and Groups Activity Report script.

New API to Make it Easier to List Teams

In December, Microsoft published a new List Teams API in the beta version of the Graph. The API access the Teams endpoint `to fetch a list of teams without using a filter and works with both delegated and application permissions. Apps need consent for one of the Team.ReadBasic.All, TeamSettings.Read.All, TeamSettings.ReadWrite.All permissions to use the API. You can apply filters to the List Teams API to find specific teams if you don’t want the full set. The equivalent code to fetch all the teams in the tenant is:

$Uri = "https://graph.microsoft.com/beta/teams"
[array]$Teams = Invoke-WebRequest -Method GET -Uri $Uri -ContentType "application/json" -Headers $Headers | ConvertFrom-Json

Remember that the Graph uses paging to return data, so code needs to be prepared to process multiple pages of data to acquire the full set of teams.

In both cases, the set of Teams returned by the query is in an array called Value. In other words, to see the details of individual teams, you access the array using $Teams.Value. For example, the first team is available using $Teams.Value[0], the second with $Teams.Value[1], and so on. The count of teams returned in the array is available with $Teams.Value.Count. In the case of the Teams List API, it’s also available as $Teams.’@odata.count’.

Beta Queries

The List Teams API currently returns just three properties for a team. Here’s what you get for a team:

$Teams[0]

id                          : 109ae7e9-1f94-48d1-9972-64abab87b89a
createdDateTime             :
displayName                 : French Customers
description                 : Delivering great service to French customers
internalId                  :
classification              :
specialization              :
visibility                  :
webUrl                      :
isArchived                  :
isMembershipLimitedToOwners :
memberSettings              :
guestSettings               :
messagingSettings           :
funSettings                 :
discoverySettings           :

To get the other team properties (for instance, the archive status for a team), you must query the properties of an individual team. This isn’t difficult, and the same downside exists if you use the Groups endpoint to fetch a list of Teams. That endpoint returns many properties for a group, but not the teams properties.

The Graph Explorer is a good way to try out the new API. Make sure that you sign into your tenant and have consent to use at least the Team.ReadBasic.All permission. Then input a query and see what happens. Figure 1 shows the result of running the query: https://graph.microsoft.com/beta/teams?$filter=startswith(displayName, ‘Office 365’) to return the set of teams whose display name starts with Office 365. As noted above, along with the properties of the individual teams, the query also returns the count in @odata.count.

Using the Graph Explorer with the List Teams API
Figure 1: Using the Graph Explorer with the List Teams API

No Need to Update Now

If your programs or scripts often need to retrieve a list of teams for processing, you should keep an eye on the List Teams API to track its development. For now, there’s no need to update existing code. In time, it might be the case that the new API delivers better performance than going through the Groups endpoint or that the query returns all team properties without having to retrieve individual teams. Better performance and functionality are always welcome. Let’s see what happens as the new API makes its way from beta to production.


Learn how to exploit the Office 365 data available to tenant administrators through PowerShell and the Graph with the help of the many examples in the Office 365 for IT Pros eBook. We love figuring out how things work.

]]>
https://office365itpros.com/2022/01/13/list-teams-api-graph/feed/ 0 52958
API Deprecations Signal the Demise of Exchange Web Services https://office365itpros.com/2021/10/11/api-deprecations-signal-demise-exchange-web-services/?utm_source=rss&utm_medium=rss&utm_campaign=api-deprecations-signal-demise-exchange-web-services https://office365itpros.com/2021/10/11/api-deprecations-signal-demise-exchange-web-services/#comments Mon, 11 Oct 2021 01:00:00 +0000 https://office365itpros.com/?p=51882

EWS Sunset Will Impact Office 365 Tenants and ISVs

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.

Exchange Web Services APIs due for deprecation by March 31, 2022
Figure 1: Exchange Web Services APIs due for deprecation by March 31, 2022

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

Figuring Out the Impact

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:

  • Custom home-built programs and PowerShell scripts.
  • Third-party clients and client add-ons.
  • Third-party products.

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.

Searching for a Graph Replacement

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:

  • Moving data from legacy archive solutions to Exchange Online.
  • Importing data from third-party systems into Exchange Online (connectors).
  • Tenant-to-tenant migrations of Exchange mailboxes and archives.
  • Backup processing for Exchange Online mailboxes (user, shared, group, public folder, and other) to external third-party datacenters.

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.

]]>
https://office365itpros.com/2021/10/11/api-deprecations-signal-demise-exchange-web-services/feed/ 6 51882
How to Manage Anonymized User Data in Microsoft 365 Usage Reports https://office365itpros.com/2021/09/09/manage-anonymized-user-data-in-microsoft-365-usage-reports/?utm_source=rss&utm_medium=rss&utm_campaign=manage-anonymized-user-data-in-microsoft-365-usage-reports https://office365itpros.com/2021/09/09/manage-anonymized-user-data-in-microsoft-365-usage-reports/#respond Thu, 09 Sep 2021 01:00:00 +0000 https://office365itpros.com/?p=51433

From September 1, Pseudonymized by Default

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.

Anonymized usage data reported by the Microsoft 365 admin center
Figure 1: Anonymized usage data reported by the Microsoft 365 admin center

Reverting to Real User Data

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.

The tenant-wide setting controlling anonymization of user information in usage reports
Figure 2: The tenant-wide setting controlling anonymization of user information in usage reports

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",
               }

Access to User Information Limited to Certain Roles

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:

  • Global administrator.
  • Exchange administrator.
  • SharePoint administrator.
  • Teams administrator.
  • User administrator.
  • Helpdesk admin.
  • Service support admin, and:
  • Reports reader.

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

What a user with the Reports Reader role sees for usage data
Figure 3: What a user with the Reports Reader role sees for usage data

Governs Programmatic Access Too

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!

Good for Privacy

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.

]]>
https://office365itpros.com/2021/09/09/manage-anonymized-user-data-in-microsoft-365-usage-reports/feed/ 0 51433
How to Find Delve Accounts with Disabled Document Insights https://office365itpros.com/2021/09/03/find-delve-accounts-with-disabled-document-insights/?utm_source=rss&utm_medium=rss&utm_campaign=find-delve-accounts-with-disabled-document-insights https://office365itpros.com/2021/09/03/find-delve-accounts-with-disabled-document-insights/#respond Fri, 03 Sep 2021 01:00:00 +0000 https://office365itpros.com/?p=51311

Controlling Document Insights

The Microsoft Graph Insights API proves different views of users and documents:

  • Trending: Documents a user has access to which are popular with other users.
  • Used: Documents a user has accessed recently.
  • Shared: Documents shared with a user, including email and attachments in calendar appointments.

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

The Microsoft Graph Insights API (source: Microsoft)
Figure 1: The Microsoft Graph Insights API (source: Microsoft)

Delve and Sharing

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

Delve feature settings prevent the display of documents associated with a user
Figure 2: Delve feature settings prevent the display of documents associated with a user

Moving to the New Graph Sharing Controls

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:

  • An organization-wide setting to control insights privacy (isEnabledInOrganization). This is True if controls are active in the organization or False if not.
  • An Azure AD group to control the set of accounts who do not want to use insights (disabledForGroup). Individual users can disable insights through the Privacy options of their MyAccount page. However, this does not add their account to the group.

Access to these settings is available through the Search & Intelligence section of the Microsoft 365 admin center (Figure 3).

Settings to control item insights exposed in the Microsoft 365 admin center
Figure 3: Settings to control item insights exposed in the Microsoft 365 admin center

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:

  • Get a set of accounts to check. This can be done with Get-ExoMailbox or Get-AzureADUser. We need the object identifier to make the Graph call. Either of these cmdlets wll return the necessary identifiers. I like the mailbox cmdlet because it has better filtering capabilities.
  • Loop through the set of accounts and check the value of the Delve setting (contributionToContentDiscoveryDisabled). If True (insights are disabled), report the account.
  • Generate whatever output is required (I usually create a CSV file because of its flexibility).

You can download the script I used to report users with Delve insights disabled from GitHub.

Updating the Group

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.

CSV file for accounts with Delve item insights disabled
Figure 4: CSV file for accounts with Delve item insights disabled

We can then use the updated CSV file as the input for a script which:

  • Retrieves some tenant details.
  • Retrieves the current privacy control settings from the Graph.
  • Fetches the current membership of the group (we assume here that one is specified).
  • Import the set of accounts from the CSV file.
  • Loop through the accounts and add them to the Azure AD group if not already a member.

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.

]]>
https://office365itpros.com/2021/09/03/find-delve-accounts-with-disabled-document-insights/feed/ 0 51311
Microsoft Introduces Data Privacy Tag for Message Center Notifications https://office365itpros.com/2021/07/27/microsoft-365-privacy-message/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-privacy-message https://office365itpros.com/2021/07/27/microsoft-365-privacy-message/#comments Tue, 27 Jul 2021 00:36:00 +0000 https://office365itpros.com/?p=50860

Microsoft 365 Privacy Messages in Case of Data Compromise

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.

Tagging Service Messages

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

Microsoft assigns two tags to service update MC272885

Microsoft 365 privacy message
Figure 1: Microsoft assigns two tags to service update MC272885

The tags shown in the message center include:

  • Admin Impact: The change impacts the management of some aspect of the tenant. For example, a new API is available. MC272885 (described here) is deemed a change with administrator impact.
  • Feature Update: Microsoft has changed the way a feature works. For example, MC264095 describes how the default setting for guest access in Teams changes from off to on.
  • Major Update: The change described is considered major. For example, the retirement of Skype for Business Online on July 31 (MC266078) is obviously a big change in Office 365. Other updates tagged as major are debatable, but you can consider this tag to be a way for Microsoft to highlight important changes. Note: Unlike the other tags, this tag is marked by setting the IsMajorChange property of a message to $True.
  • New Feature: A new feature is on its way for an app. For example, MC230680 describes the introduction of reactions in Teams meetings. Microsoft often misses the date for feature introductions and republishes the update, which is what happened on MC230680 on June 30 when they published new dates for availability of the feature in the GCC and DOD clouds.
  • Retirement: Microsoft is removing a feature from the service. Skype for Business Online is an example, so is the final removal of Site mailboxes (MC266256). Not many shed tears when site mailboxes shuffled off into the great byte wastebasket.
  • User Impact: Many changes impact users in some way. For example, MC271629 advises administrators that Project Moca is moving its spaces to the OWA calendar.
  • Updated Message: This tag does not appear in the Microsoft 365 admin center. It’s used to flag service messages which have been updated since the original publication. This is usually due when Microsoft needs to clarify the meaning of the text.

Many updates have multiple tags. For instance, MC264095 has the major update, feature update, and user impact tags.

Analyzing Service Update 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:

  • The most popular tag is user impact (191) followed by admin impact (165).
  • There’s a surprising number of retirement messages (31).
  • Many updates are issued (96).

Your mileage might vary because Microsoft issues service updates to tenants based on the feature set licensed by the tenant.

What’s Changing

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.

]]>
https://office365itpros.com/2021/07/27/microsoft-365-privacy-message/feed/ 1 50860
How to Upgrade Office 365 PowerShell Scripts to Use the Graph API https://office365itpros.com/2021/07/26/microsoft-graph-powershell-upgrade/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-powershell-upgrade https://office365itpros.com/2021/07/26/microsoft-graph-powershell-upgrade/#comments Mon, 26 Jul 2021 01:34:00 +0000 https://office365itpros.com/?p=50813

Microsoft Graph PowerShell is Quicker

Without a doubt, using Graph API calls is much faster to retrieve Office 365 than PowerShell cmdlets are. It’s more obvious in complex scripts like the Groups and Teams activity script, which is much faster in its Graph variant (5.1) than its counterpart (4.8) due to Graph API calls replacing cmdlets from the Exchange Online and SharePoint Online modules. Speed matters, especially when a tenant supports thousands of groups, and the Graph API version of the script can process large quantities of groups where the pure PowerShell version struggles to cope.

Seeking an Example to Convert

Good as it is to have a speedier script, the complexity of the activity report is possibly not a good test case to illustrate the decision process that you should go through to decide if it’s a good idea to upgrade a script to use the Graph API, and then measure the improvement. Simplicity is better, so let’s explore what needs to be done to upgrade the Microsoft 365 Groups Membership report script. This script uses the following cmdlets:

  • Get-AzureADUser: Fetch the list of Azure AD accounts in the tenant. Microsoft announced their intention to retire the Azure AD module in June 2021, so this is a good example of the kind of script update needed to replace Azure AD calls with Graph API calls.
  • Get-UnifiedGroup: Fetch the list of Teams in the tenant.
  • Get-Recipient: Fetch the list of Groups a user account belongs to.

Two cmdlets are from the Exchange Online Management module, one is from the Azure AD module. A bunch of other processing is done to filter, sort, and process the data fetched by these cmdlets, but essentially the conversation involves replacing these cmdlets with Graph API calls. Sounds easy.

Using the Graph API

Before any script can use the Graph API, it needs to use an Azure AD registered app. The app serves as the holder for permissions to allow the script to access data. Every registered app has a unique identifier. When you create a registered app, you can generate an app secret. When running the app, we need to know the secret to prove we have permission to use the app.

Before making any calls, you need to know your tenant identifier. This is the GUID for a tenant as returned by the Get-AzureADTenantDetail cmdlet. The Connect-MicrosoftTeams cmdlet also returns this information. Bringing everything together, before we can use an app to access Graph APIs, we need to know the app identifier, app secret, and tenant identifier. You’ll often see code like this in scripts:

$AppId = "a09cf913-5ff9-48a2-8015-f28f2854df26"
$AppSecret = "u6X7_i8K-yhh-b4-z5FEmj_wH_M~nIOz4n"
$TenantId = "22e90715-3da6-4a78-9ec6-b3282389492b"

It’s an important part of working with Graph API apps to understand how permissions work and to ensure that apps receive only the permissions necessary to work with the data they process. In the case of our app, we need:

  • Group.Read.All: to read information about Microsoft 365 Groups in the tenant.
  • GroupMember.Read.All: to read information about the membership of Microsoft 365 Groups.
  • User.Read.All: to read information about Azure AD accounts in the tenant.

Like all programming tasks, it soon becomes second nature to assign the correct permissions, sometimes after browsing Microsoft’s documentation to find the correct permission.

An administrator gives consent to allow the app to use its assigned permissions. Hackers can use OAuth consents as a method to gain permissions to access data, so it’s wise to keep an eye on consents given within an organization, with or without Microsoft’s new App governance add-on for MCAS.

Access Token

Equipped with a suitably permissioned app, app secret, and tenant identifier, the app can request an access token by posting a request to the token endpoint. For Graph API calls to Office 365 data, that’s going to be something like:

https://login.microsoftonline.com/tenant-identifier/oauth2/v2.0/token

The bearer token issued in response confirms that the app has the necessary permissions to access data like Users, Groups, and Sites and whether access is read-only or read-write. The token is included in the authorization header of the requests made to the Graph APIs. Access tokens expire after an hour, so long-running programs need to renew their token to continue processing.

All of this sounds complicated, but once you do it for one script, it becomes second nature to acquire an access token and be ready to start using Graph API calls.

Replacing PowerShell Cmdlets

Like anything else, it takes a little while to become used to fetching data using Graph API calls. You must pay attention to the data that’s fetched. First, to limit the demand on resources, the Graph fetches limited data at one time and you must iterate until no more data is available (a process called pagination). Second, in some cases, the property names used by cmdlets vary to what’s used by the Graph API. For example, the Get-UnifiedGroup cmdlet returns the description of a group in the Notes property whereas the Groups API uses description.

The Graph Explorer is an online Microsoft tool to help developers become accustomed to Graph API syntax and data. You should use the Explorer to test calls before including them in a script. Debugging calls using the Graph Explorer saves a lot of time and heartache. Figure 1 shows the Graph Explorer being used to examine the transitive set of groups returned for a user.

Using the Graph Explorer to test Graph API Calls

Microsoft Graph PowerShell
Figure 1: Using the Graph Explorer to test Graph API Calls

The Graph Explorer is a Graph app. Like any other app, it needs permissions to access data. If a call fails, the Explorer tells you which permissions are missing and you can then consent to the assignment.

Fetching Azure AD Accounts

The first cmdlet we need to replace in the script is the one to fetch the list of Azure AD users in the tenant. In PowerShell, this is:

$Users = Get-AzureADUser -All:$true

The Graph Users API fetches the same data. Unlike the Get-AzureADUser cmdlet, a default set of properties is returned and we must be specific if we want other properties. Here’s the call used.

$Uri = https://graph.microsoft.com/v1.0/users?&`$select=displayName,usertype,assignedlicenses,id,mail,userprincipalname
$Users = Get-GraphData -AccessToken $Token -Uri $Uri

Get-GraphData is a wrapper function to take care of pagination and extraction of data returned by Graph API calls in a form like the PowerShell objects returned by cmdlets. You can make your life easier by including a function like this in any script which interacts with Graph API calls. To see an example of the function I use, download the Graph version of the group membership report script from GitHub.

Fetching Teams

The second call fetches the list of team-enabled groups. The script then creates a hash table to store the teams so that it can be used as a lookup to see if a group is team-enabled when reporting its properties. The PowerShell code uses a server-side filter with the Get-UnifiedGroup cmdlet to return the teams.

$Teams = Get-UnifiedGroup -Filter {ResourceProvisioningOptions -eq "Team"} -ResultSize Unlimited | Select ExternalDirectoryObjectId, DisplayName

The Graph Groups API equivalent uses the same kind of filter:

$Uri = "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')"
$Teams = Get-GraphData -AccessToken $Token -Uri $Uri

Fetching the Groups a User Belongs to

The last call we make is to find the set of groups a user account is a member of. The Get-Recipient cmdlet is very fast at returning the list of groups based on the distinguished name of an account. The user data returned by Get-AzureADUser doesn’t give us the distinguished name (it is an Exchange Online property), so we must run Get-Recipient twice: once to get the distinguished name, and then use the distinguished name to find the groups.

$DN = (Get-Recipient -Identity $User.UserPrincipalName).DistinguishedName
$Groups = (Get-Recipient -ResultSize Unlimited -RecipientTypeDetails GroupMailbox -Filter "Members -eq '$DN'" | Select DisplayName, Notes, ExternalDirectoryObjectId, ManagedBy, PrimarySmtpAddress)

The Graph Users API can resolve a transitive lookup against groups to find membership information for an account. We can therefor use a call like this:

$Uri = "https://graph.microsoft.com/v1.0/users/" + $user.id +"/transitiveMemberOf"
$Groups = Get-GraphData -AccessToken $Token -Uri $Uri

That’s it. All the other command in the script process data fetched from Azure AD or Exchange Online. Apart from the changes detailed above, the same code is used for both the PowerShell and Graph versions of the script.

Microsoft Graph PowerShell Delivers a Result

Your mileage may vary depending on the backend server you connect to, the state of load on the service, and other factors. My tests, which are surely as reliable as an EPA mileage figure, revealed that the PowerShell version processed accounts at a rate of about 0.6/second each. The Graph version reduced the time to about 0.4/second. In other words, a 50% improvement.

Not every script will benefit from such a speed boost and other scripts will need more work to install Graph turbocharging. But the point is that Graph-powered PowerShell is much faster at processing Office 365 data than pure PowerShell is. Keep that fact in mind the next time you consider how to approach building a PowerShell-based solution for Office 365.


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.

]]>
https://office365itpros.com/2021/07/26/microsoft-graph-powershell-upgrade/feed/ 17 50813
Microsoft Launches Preview of App Governance for Cloud App Security https://office365itpros.com/2021/07/21/microsoft-preview-app-governance/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-preview-app-governance https://office365itpros.com/2021/07/21/microsoft-preview-app-governance/#comments Wed, 21 Jul 2021 01:00:00 +0000 https://office365itpros.com/?p=50737

Applying Governance to Graph-Based Apps

Update (November 3): The App governance add-on is out of preview and generally available.

In mid-July, Microsoft introduced the preview of an app governance add-on for Microsoft Defender for Cloud Apps (MCAS), describing the new capability as a “security and policy management capability designed for OAuth-enabled apps that access Microsoft 365 data through Microsoft Graph APIs.”

There’s no doubt that tenants can accumulate a collection of Graph-enabled apps over time. The apps come from Microsoft, ISVs, and line of business apps. Indeed, any PowerShell script you write to interact with the Graph APIs gains its permissions through consent granted to an app registered with Azure AD. The net result is that you can end up with hundreds of registered apps, all with Graph permissions, some of which you might not know much about. App governance aims to deliver a structured management framework for those apps, leveraging information taken from Azure AD and MCAS.

Microsoft research clearly shows that attackers use illicit consent grants for Graph-based apps to extract and abuse data. Given the likelihood that organizations will have more Graph-based apps to manage over time, it’s important that administrators understand app usage in their tenant. Unfortunately, the need to review and analyze app usage often falls down task lists, which is the hole the app governance add-on attempts to close.

The Need for MCAS

MCAS isn’t included in any Office 365 plan. Office 365 E5 includes Office 365 Cloud App Security (OCAS), a cut-down version of the full-blown MCAS. Both operate on the same basis of data gathered from user and app activity, but MCAS delivers more functionality and covers more apps. For this pleasure, tenants need to pay more to license MCAS, unless they’ve already invested in Microsoft 365 E5, Enterprise Mobility and Security E5, or one of the other licenses which cover MCAS. You need MCAS to use the new add-on, which doesn’t work with OCAS.

Fortunately, Microsoft offers organizations the chance to run a 30-day trial for MCAS by signing up through the Purchase services section of the Microsoft 365 admin center. After starting the MCAS trial and assigning some licenses to accounts with suitable administrator roles, you can go ahead and start a trial of the add-on. Curiously, the add-on allows a 130-day trial, which might be due to its desire to capture and analyze usage data for apps over a reasonable period. Of course, if you sign up for both trials, MCAS expires after 30 days, and you won’t be able to use the add-on afterwards.

Using App Governance

App governance runs within the Microsoft 365 admin center. If your account isn’t licensed or doesn’t hold one of the necessary compliance roles, you’ll be told this unhappy news if you attempt to access the page. Licensed administrators see a preview of the app situation in the tenant (Figure 1).

App Governance overview
Figure 1: App Governance overview

I had recently been through an audit of apps based on grabbing app data from Azure AD and reviewing the data through Microsoft Lists, so there are fewer apps present in my tenant than existed previously. As I reviewed the data set, I found a couple of additional apps that I could disable or remove. You can disable an app by viewing its properties through App governance; to remove it, you need to use the Azure AD admin center. Disabling is a good first step to removing a potentially problematic app as you can easily enable the app if someone reports that a business case exists for its use.

In my tenant, app governance detected 33 high privileged apps and 13 overprivileged. Microsoft’s definitions for these categories are:

  • High privilege: Consent has been granted to the app for Graph permissions that allow the app to access data or change key settings in the organization.
  • Overprivileged: Consent has been granted to the app for Graph permissions the app doesn’t use.

To examine details of an app to understand why it falls into a certain category, click Apps, and peruse the list of apps to select and view the app properties (Figure 2).

Browsing the set of Graph-enabled apps
Figure 2: Browsing the set of Graph-enabled apps

As you can see, the portal includes several filters to limit the set of displayed apps. The set of available filters misses one to show disabled apps. This means that if you need to find a disabled app, perhaps to reenable it if the app had been disabled in error, you either need to know the name of the disabled app or do a lot of checking to find the right app.

Probing Permissions

Mmany apps fall into the high privilege category because they read user information. For example, the app used by Microsoft Ignite conference attendees to register has permission to see the user’s email and profile. Apps created by ISVs to read and report tenant data need access to the directory, and that is flagged as a high privilege permission because attacker apps also use the permission to find targets. Even Microsoft’s own Information Barriers app is flagged as high privilege because it has the Directory.Rewrite.All and Groups.Rewrite.All permissions. As always, understanding the context of what an app does is necessary to understand why it needs permissions.

App governance allows tenant administrators to automate checks by creating policies to monitor the creation of overprivileged or high privilege apps. This functionality works like the other alert policies available in the Microsoft 365 compliance center with the exception that the input data focuses on apps rather than actions. As you can see in Figure 1, policies quickly flag new apps which violate criteria. But as pointed out above, you then need to check the app to figure out if a problem really exists.

Permission Glitches

The add-on is a preview, so glitches are expected. For example, app governance flagged an app written to support adding organizational contacts to new user mailboxes as overprivileged. When I examined details of the app (Figure 3), the unused permission is Contacts.ReadWrite. This is odd because that’s the exact permission needed to write a new contact record into a mailbox.

Details of an app marked as overprivileged
Figure 3: Details of an app marked as overprivileged

Apart from app details and permissions, App Governance promises to show data about usage and users. Taking users first, I thought this information to be quite useless. The information shown on Figure 4 tells me that 237 consented users exist (Azure AD accounts with data the consents granted to the app covers). This figure includes both tenant and guest accounts and results because an administrator granted consent to the app for the entire organization (if consent is given for individual accounts, they are listed here). The five priority users are those marked as priority accounts. None of the priority accounts (including my own) had any trace of data uploaded or downloaded using the app. Given the app is Microsoft’s Graph Explorer, which I use to test Graph API queries almost daily, this was surprising.

User data reported for an app
Figure 4: User data reported for an app

After being disappointed with the data available for users, I didn’t hold out much hope for the Usage (beta) tab. And my expectations were met as precisely nothing showed up here. Instead, App governance informed me that no data was present. Oh well, it’s a preview.

DIY App Governance

As mentioned above, it is relatively simple to perform a DIY audit of Microsoft 365 Graph-enabled apps. Home-grown knowledge of apps used in a tenant is an advantage MCAS can’t deliver, but to exploit that knowledge, some work is necessary to acquire, refine, and understand the app inventory – and to keep on checking on a systematic basis.

App governance extends simple auditing by including policy-based management, categorizing apps based on the permissions they hold, and delivering some insights into app usage. Although still just a preview with all that implies, if your organization has MCAS, the add-on is a useful enhancement. If not, although the need to monitor the granting and usage of permissions in Graph-enabled apps is a real need, you might be able to construct your own method to achieve the goal.


Stay updated with developments across the Microsoft 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. We do the research to make sure that our readers understand the technology.

]]>
https://office365itpros.com/2021/07/21/microsoft-preview-app-governance/feed/ 2 50737
Managing Third-Party App Permissions in the Teams Admin Center https://office365itpros.com/2020/10/12/app-management-updates-teams-admin-center/?utm_source=rss&utm_medium=rss&utm_campaign=app-management-updates-teams-admin-center https://office365itpros.com/2020/10/12/app-management-updates-teams-admin-center/#comments Mon, 12 Oct 2020 04:00:21 +0000 https://office365itpros.com/?p=30532

Granting Consent for Data Access by Third-Party and LOB Apps

Described in Office 365 notification MC222892 (September 26), Microsoft has made several important changes to the way that third-party apps are managed in the Teams admin center. The changes are linked to Microsoft 365 roadmap item 67140 and are now available.

The Teams apps section of the admin center supports management of apps and the permission and setup policies used to deploy apps to users. The first change is that the listing of apps includes a permissions column to show when a third-party app needs permission, with the idea being that an admin can take care of consent centrally and so avoid the need for end users to have to seek consent when they want to use an app.

Apps published by Microsoft don’t need to be granted consent. Some third-party apps don’t need consent either because they do not interact with Microsoft 365 data like user accounts or sites. For instance, the Adobe Sign app allows users to sign documents with that service without accessing any Microsoft 365 data.

The Need for Permissions

Third-party apps or LOB apps created by a tenant can access Microsoft 365 data with the Microsoft Graph, but only if they receive permission to access the data. Microsoft Graph divides permissions into sets of actions that an app can perform. When you see View details in the Permissions column, you know that the app needs administrator consent (on behalf of the tenant) to access data via the Graph.

The listing for Teams apps now includes a permissions column
Figure 1: The listing for Teams apps now includes a permissions column

To give consent, select an app and look at the Permissions tab in its details and then Review permissions and consent. You must be able to sign in as a tenant administrator to give consent. Once signed in, you’ll see the permissions requested by the app. Figure 2 shows that the chosen app wants to read user profile information from Azure AD. Be aware that you’re granting consent for org-wide access to the requested information. If you’re happy that the app should have access to this data, click Accept.

Reviewing permissions requested by an app before granting permissions
Figure 2: Reviewing permissions requested by an app before granting permissions

When an app has received consent, you’ll see a notice to that effect under Org-wide permissions in the Permissions tab.

Azure AD App Registration

Apps that receive consent are registered with Azure AD. You can find details of all the apps registered in your tenant in the Enterprise applications blade of the Azure AD portal. Figure 3 shows details of an app which received consent through the Teams admin center. You can revoke permissions from an app at any time.

Viewing details of permissions granted to an app
Figure 3: Viewing details of permissions granted to an app

Resource Specific Consent

Office 365 notification MC218561 was announced in July (Microsoft 365 roadmap item 56605) to say that teams owners could give consent to apps to access data in the teams they managed. This feature is known as resource-specific consent (RSC) because the consent is limited to permissions for a specific resource (a group/team). Limiting the scope of the permissions assigned to an app to what it needs to function instead of giving it org-wide access makes a heap of sense.

Now fully deployed across Office 365, RSC is a Teams feature controlling access to team settings, channels, messages, apps, tabs, and membership. It depends on the tenant settings in the Consent and Permissions section of the Enterprise applications blade in the Azure AD portal (Figure 4). See this page for more information.

User assent settings in Azure AD
Figure 4: User assent settings in Azure AD

The ability to give resource-specific consent can be limited to a set of team owners rather than all team owners in the tenant.

Some apps don’t need access to data drawn from across the tenant and only need permissions to interact with specific Teams objects from the set supported by RSC (Figure 5).

Graph API permissions supported by Teams RSC
Figure 5: Graph API permissions supported by Teams RSC

You’ll recognize these apps because the RSC permissions they need are listed in the permissions tab of the app details. In Figure 6 we can see that the app needs to read a team’s settings, membership, and messages and create channels.

Viewing the RSC details for a Teams app
Figure 6: Viewing the RSC details for a Teams app

Add App to a Team

The last feature allows Teams admins to add apps to target teams to avoid the need for team owners to install the apps. This a preview feature that only works for apps designed to be installed within a team (normally accessed via a channel tab). By comparison, Teams app setup policies allow organizations to make apps available to users on a personal basis to use via the app navigation bar.

If you see that an app has “team” included in its capabilities listed under the About tab, you know it supports team scope. Template Chooser, Trello (Figure 7), and Zoho CRM are examples of apps with team scope.

Discovering if a Teams app can be scoped to a team
Figure 7: Discovering if a Teams app can be scoped to a team

To install an app into a team, select the app in the Manage Apps screen and then choose Add to team. You can then select the team to install the app into (Figure 8).

Installing an app into a team
Figure 8: Installing an app into a team

For more information, see the Teams documentation.

More Information About Apps

Given the growing number of apps in the Teams app store (760 as I write this), it’s obvious that a solid management framework is needed to control third-party apps, especially in how these apps use the Microsoft Graph to access data. The implementation of permission management is solid and is a very useful addition to the Teams admin center.

For more information about app permissions, consent, and RSC, view the Ignite session about Navigating the Microsoft Teams App Lifecycle (app permissions and consent is covered from about 34:20 in the video).


Managing Teams is what Chapter 12 of the Office 365 for IT Pros eBook is all about. You’ll find lots more interesting and useful information in Chapter 12 and all the other chapters of the book.

]]>
https://office365itpros.com/2020/10/12/app-management-updates-teams-admin-center/feed/ 1 30532
How to Customize the Azure AD Schema to Display the Drink Attribute in the Microsoft 365 Profile Card https://office365itpros.com/2020/09/23/microsoft-365-profile-card/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-profile-card https://office365itpros.com/2020/09/23/microsoft-365-profile-card/#comments Wed, 23 Sep 2020 01:00:50 +0000 https://office365itpros.com/?p=28235

Drink and the Active Directory Schema

Last week, Twitter was full of news about the drink attribute, which is part of the Active Directory schema and defined in Microsoft documentation as “The drink (Favorite Drink) attribute type specifies the favorite drink of an object (or person).” The Microsoft 365 profile card displays lots of information about people, but it doesn’t show their favorite beverage.

Quite why drink was ever added to Active Directory is a mystery, but it’s there for all versions of the Windows Server operating system from Windows Server 2003 and available for people to use as they wish. However, it’s not in Azure Active Directory, and again no one can explain why. But it’s not and, according to Microsoft, will not be.

Use Custom Attributes Instead for the Microsoft 365 Profile Card

The lack of drink in Azure AD poses a quandary to those who need to populate the attribute. You cannot extend the Azure AD schema to add attributes, so the only thing to do is to make the best of what’s available. In a nutshell, you can use one of the fifteen single-value custom predefined in Azure AD for organizations to use as they wish. You can’t rename the attributes, but you can use them to hold data.

A quick check of mailboxes revealed that CustomAttribute9 wasn’t in use. It’s important to check to make sure that the chosen attribute isn’t used to store information used for another purpose. With the decision made. To update CustomAttribute9 with a user’s drink preference, you can set the value for their mailbox with PowerShell using the Set-Mailbox cmdlet as follows:

Set-Mailbox -Identity James.Joyce -CustomAttribute9 "Beer"

Updating a custom attribute for an Exchange Online mailbox leads to synchronization of the information to the mailbox owner’s account in Azure AD. I spent some time looking at how to update the custom attribute using the Azure AD PowerShell module and could find no method to do this.

It would be nice to be allowed use one of the five multi-value custom attributes available for mailboxes and also in Azure AD (ExtensionCustomAttribute1 through ExtensionCustomAttribute5) as you could then store the preferred brand name along with the choice of beverage, but these attributes aren’t currently supported for customization of the profile card.

Update Azure AD Schema to Display Drink on the Microsoft 365 Profile Card

Once the chosen attribute is populated, we can use the Graph Explorer to update the Azure AD schema to make information about users’ preferred drinks appear in the profile card (also known as the people card). I used this payload to define that the contents of CustomAttribute9 is displayed as Drink in the Microsoft 365 profile card.

{
            "directoryPropertyName": "CustomAttribute9",
            "annotations": [
                {
                    "displayName": "Drink",
                    "localizations": [
                        {
                            "languageTag": "de",
                            "displayName": "Getränk"
                        }
                    ]
                }
            ]
        }

The customization to the profile card doesn’t happen quickly and it can take up to 24 hours before you see the effect. Eventually, all the necessary processes click into place and the profile card will display the information (Figure 1).

The user's beverage of choice is displayed in their Microsoft 365 profile card
Figure 1: The user’s beverage of choice is displayed in their Microsoft 365 profile card

It’s unlikely that many organizations will decide that including drink in a customized profile card is an essential contribution to the business. But in the interest of completeness, we felt it important to let people coming from the on-premises world to the cloud that although the drink attribute doesn’t exist in Azure AD, they can still make it show up.


Sometimes we come across strange but interesting technical topics as we research and write the Office 365 for IT Pros eBook. This post falls into that category. On a serious note, it’s yet another example of using the Graph Explorer to do real work. But apart from that… it’s just an excuse to have a drink.

]]>
https://office365itpros.com/2020/09/23/microsoft-365-profile-card/feed/ 4 28235
How to Report Microsoft 365 User Activity Using the Graph API and PowerShell https://office365itpros.com/2020/09/14/office-365-user-activity-report/?utm_source=rss&utm_medium=rss&utm_campaign=office-365-user-activity-report https://office365itpros.com/2020/09/14/office-365-user-activity-report/#comments Mon, 14 Sep 2020 00:06:57 +0000 https://office365itpros.com/?p=26831

Gathering Data for Multiple Workloads to Understand User Activity

For the last few months, I have been dabbling with a PowerShell script to extract and report usage data for multiple Office 365 workloads from the Microsoft Graph. The idea is that an Office 365 user activity report generated by fetching activity data from all the workloads reported in the Graph helps administrators to figure out if accounts are in use and if so, what they are used for. If an account isn’t in use, then you might remove it and save some licenses.

One of the joys of PowerShell is how quickly you can put a solution together. The corollary is sometimes that the solution isn’t as efficient as it could be, which often happens when you’re not a professional programmer. When I write a script, the most important thing is often to illustrate a principle and show how something works. When PowerShell scripts are deployed into production, they’re usually upgraded and improved by programmers to meet organizational standards and fit in with other scripts used to manage the infrastructure. For this reason, I don’t bother too much with tweaking for performance.

This script is different. It’s been picked up by several tenants who reported that the script works but it’s slow when asked to process data for thousands of accounts. This deserved some investigation which produced some improvements, such as using PowerShell’s Where method to filter data.

PowerShell Hash Tables

But PowerShell is not a database and storing data about account usage in PowerShell list objects only scales so far. There are many web articles covering PowerShell performance with large amounts of data, many of which point to using hash tables because they are very efficient for finding and retrieving data (see this article about how to use hash tables).

A hash table is a collection of key/value pairs. The keys are unique, and the values are often some information associated with the key. For instance, because Office 365 objects like groups and sites store sensitivity labels as GUIDs, I often create a hash table composed of the GUID (key) and label display name (value) which I can then use to interpret the GUIDs stored in objects. Here’s what the code looks like:

$Labels = Get-Label # Get set of current labels
$HashLabels = @{} # Create hash table
$Labels.ForEach( { # Populate the hash table with the GUID and display name of each label
       $HashLabels.Add([String]$_.ImmutableId, $_.DisplayName) } )

Anytime I need to find the display name of a label, I can do something like this:

$GUID = (Get-UnifiedGroup -Identity “Office 365 for IT Pros”).SensitivityLabel.GUID
Write-Host “Display name of label is” $HashLabels[$GUID]
Display name of label is Limited Access

Apart from their usefulness in situations like described above, hash tables are very fast when you use keyed access. Speed being of the essence when thousands of records are to be processed, I decided to investigate if hash tables could replace the list objects used by the script.

Keys and Values

Finding a key is no problem because the user principal name is unique for each account. Figuring out how to store all the data in the hash table value was another matter. That is, until I noticed that: ”the keys and values in a hash table can have any .NET object type…” In other words, you’re not limited to storing simple values in a hash table.

When the script extracts usage data for a workload (like Teams or Exchange) from the Graph, it processes each record to create a list of accounts and their usage data for that workload. After some experimentation, I was able to populate the hash table by:

  • Creating an array of the usage data for the workload for an account.
  • Appending the array to the existing usage data extracted from other workloads for the account (as stored in the hash table).
  • Writing the updated array back into the hash table.

This might be inelegant, but it works. After all workloads are processed, the result is a hash table keyed on the user principal name with a value composed of an array containing the usage data for all workloads for that user. Access to the data is via the user principal name. For example:

$datatable["Kim.Akers@Office365itpros.com"]

TeamsUPN             : Kim.Akers@office365itpros.com
TeamsLastActive      : 05-Sep-2020
TeamsDaysSinceActive : 5
TeamsReportDate      : 07-Sep-2020
TeamsLicense         : POWER BI (FREE)+ENTERPRISE MOBILITY + SECURITY E5+OFFICE 365 E5 WITHOUT
                       AUDIO CONFERENCING
TeamsChannelChats    : 7
TeamsPrivateChats    : 10
TeamsCalls           : 0
TeamsMeetings        : 5
TeamsRecordType      : Teams

ExoUPN             : Kim.Akers@office365itpros.com
ExoDisplayName     : Kim Akers
ExoLastActive      : 20-Aug-2020
ExoDaysSinceActive : 21
ExoReportDate      : 08-Sep-2020
ExoSendCount       : 8
ExoReadCount       : 19
ExoReceiveCount    : 392
ExoIsDeleted       : False
ExoRecordType      : Exchange Activity

The display is truncated here to show two of the six workload usage data extracted for an account.

Creating the report is then a matter of processing each account to extract the information and format the data. To do string comparisons and other calculations, I found that it was necessary to use the Out-String cmdlet to make the properties taken from the array into trimmed strings. It might be something to do with the way that the hash table values are stitched together from multiple arrays.

Faster Performance

After changing to hash tables, I observed a 70% performance gain in script execution time in my (small) tenant. I expect a much better gain in larger tenants where the advantages of hash table access become more pronounced. This feeling was realized in a test against 20K accounts which proved that the script is now capable of processing at circa 1,000 accounts per minute (Figure 1).

A thousand accounts a minute
Figure 1: A thousand accounts a minute

Update September 18: I received a note saying that the script processed 26,808 accounts at the rate of 3184.71 per minute!

The time required to fetch data from the Graph is the same as previous versions as is the time to prepare data for processing. All the improvement is in the report generation, which is where the hash tables excel. The tenant who processed the script against 20,000 accounts used the Office 365 user activity report (example shown in Figure 2) to identify 70 accounts assigned Office 365 E5 licenses that can now be reallocated or released (a potential saving of $29,400 annually).

Office 365 user activity report
Microsoft 365 user activity report
Figure 2: Reviewing account usage to locate underused Office 365 licenses

The Office 365 user activity report script is available from GitHub. If you have a suggestion for improving the performance further, please let comment on GitHub.


OK, we should be writing text for the Office 365 for IT Pros eBook instead of trying to work out how to speed up PowerShell scripts. But you learn a lot about an infrastructure when you program against it, so we’ll keep on scripting…

]]>
https://office365itpros.com/2020/09/14/office-365-user-activity-report/feed/ 20 26831
Preview of Teams Migration API Has Slack in Its Cross-Hairs https://office365itpros.com/2020/09/11/teams-migration-api-preview/?utm_source=rss&utm_medium=rss&utm_campaign=teams-migration-api-preview https://office365itpros.com/2020/09/11/teams-migration-api-preview/#comments Fri, 11 Sep 2020 07:39:32 +0000 https://office365itpros.com/?p=26820

Preview Migration API Documentation Available

ISVs have known about the preview version of the Graph-based Teams Migration API for some months. It’s a replacement for the beta migration API used by ISVs like Quadrotech, AvePoint, and BitTitan in their tenant-to-tenant migration products. These products have no issue moving email and documents, but the experience moving Teams data between tenants has been “challenging.”

Yesterday, MVP Tom Morgan published a review of the preview of the new API based on some Microsoft documentation. The API is very much a preview and not intended for production. Nevertheless, the documentation reveals that its intended purpose is to migrate data from a “third-party chat system” (like Slack) to Teams rather than between Office 365 tenants.

What the Migration API Does

The API does nothing to extract information from the source system or convert it into a format suitable for ingestion to Teams. What it does is:

  • Create new Teams to receive the ingested data. Interestingly, these Teams are marked as in “migration mode.” Microsoft says that this is “a special state that bars users from most activities within the team until the migration process is complete.”
  • Create channels in the target migration teams.
  • Import messages into the channels. The API is currently throttled at a low rate that’s suitable for testing but not for a full-blown migration. Migration is a one-shot effort. If ingestion fails in the middle of processing, the team must be deleted, recreated, and the migration restarted.
  • Complete the migration and release the teams for normal use. Once a team is placed into normal mode, no further data can be migrated to the team.
  • Add members to the teams. This can only happen after the migration is complete.

API Restrictions

All looks straightforward and what you’d expect from a migration API, but as it’s a preview many restrictions exist. The biggest is that messages can only be imported into public channels and there’s no way to import messages as personal chats or into private channels. Other things which are missing include support for emojis, reactions, at mentions, and some formatted text like quotes and code snippets. Other omissions like no support for cross-channel posts are probably not important in a migration context.

Microsoft’s Focus on Growing Teams

Microsoft is putting its effort into migrating competitor platforms to Teams. That’s understandable from their perspective as they want to grow the number of Teams users and weaken competitors. What’s not understandable is the lack of progress in an API for tenant to tenant migrations. The longer this lack exists, the more Teams data is accrued in tenants and the more data needs to be moved when companies owning Office 365 tenants merge, split, or are acquired.

No Joy for Teams Backups

The lack of a backup and restore API for Teams that covers team structure and content is also regrettable. Because Teams is so interconnected with the rest of the Microsoft 365 ecosystem, it is a difficult application to backup. But that’s no reason for Microsoft to ignore the need.


The Office 365 for IT Pros eBook is all about administration and management of Office 365 tenants and the Microsoft 365 ecosystem. We don’t cover migration of Teams in any depth, but we think it’s an important topic that’s worth mentioning here.

]]>
https://office365itpros.com/2020/09/11/teams-migration-api-preview/feed/ 4 26820
Customizing Privacy Controls for Microsoft Graph Insights with the Graph Explorer https://office365itpros.com/2020/08/06/customizing-item-insights-privacy-graph-explorer/?utm_source=rss&utm_medium=rss&utm_campaign=customizing-item-insights-privacy-graph-explorer https://office365itpros.com/2020/08/06/customizing-item-insights-privacy-graph-explorer/#comments Thu, 06 Aug 2020 09:32:45 +0000 https://office365itpros.com/?p=18251

The Graph is Grown Up and Needs New Controls

On August 4, Microsoft posted a blog called “Introducing new privacy controls with the Microsoft Graph” (later echoed in Office 365 Notification MC219941 dated August 6). The related Microsoft 365 roadmap item is 66462. Reading the blog post, you might be excused for not understanding the full context of the content. I needed to read it several times before comprehending what Microsoft announced in some fairly obtuse text. In a nutshell, they said:

  • Office Graph was introduced in 2014 to gather signals about user activity in Office 365 applications.
  • Delve was the first application to surface the results of those signals.
  • Users can disable the Office Graph in Delve settings (Figure 1). This has the effect that they don’t see insights about their document activity any longer.
  • The Office Graph has evolved to become the Microsoft Graph. More signals than ever before are gathered in the Graph and it’s appropriate to introduce new controls over the data used to derive insights such as the set of documents or sites available to a user which might be interesting to them.
  • The new controls are in the organization settings section of the Microsoft Graph schema. The controls can disable insights for the entire tenant (organization) or for the members of a Microsoft 365 or security group.
  • For the remainder of 2020, when evaluating if a user has access to insights, Microsoft will enforce the stricter of the Delve and organization controls. From 2021 on, the Delve controls will only be used in Delve and the rest of Office 365 will use the organization controls.
  • Microsoft will introduce per-user control over insights in the future. This control will replace the Delve setting.
Delve feature settings
Figure 1: Delve feature settings

Manipulating Graph Settings

You can’t argue against the logic that it’s best to move controls over Graph-derived insights away from a specific application into organization settings. My experience is that relatively few people use Delve today, possibly because the implementation of Microsoft Search across the Microsoft 365 suite has improved over the last few years. If this is true, then people probably don’t realize that control over insights is currently exerted through Delve.

While liking the idea of extra controls, the issue for tenant administrators might be how to implement the new settings. No GUI exists in the Microsoft 365 admin center and no PowerShell cmdlet is available.

Update: See below for where to apply updates using the Microsoft 365 admin center.

Instead, you must patch the Graph settings as described in this article. That’s just fine if you know how to patch the Graph and obviously not if you haven’t acquired that skill.

Graph Explorer Solves the Problem

The Graph Explorer is a utility built by Microsoft to demonstrate how to interact with the Graph. You can execute test commands or, after signing into your tenant, run the commands against real data. The Graph Explorer can therefore be used to update organization settings, just like you can use it to customize the Office 365 profile card.

First, you need to know your tenant identifier. This is easily found by running the Get-AzureADTenantDetail cmdlet (from the Azure AD module), where the value is returned as the ObjectId:

Get-AzureADTenantDetail

ObjectId                             DisplayName          VerifiedDomain
--------                             -----------          --------------
b662313f-14fc-43a2-9a7a-d2e27f4f3476 Office 365 IT Pros   Office365ITPros.com

With the tenant identifier, you can construct the URI needed to update the settings. For example, taking the value returned by Get-AzureADTenantDetail, the URI is:

https://graph.microsoft.com/beta/organization/b662313f-14fc-43a2-9a7a-d2e27f4f3476/settings/itemInsights

We need to populate the request body with the update we want to apply. To restrict insights for members of a selected group, the request body is something like shown below. The value passed is the object identifier of a Microsoft 365 or security group.

{
  "disabledForGroup": "c9758609-d33b-4eea-976b-d8e43a2ad135"
}

The easiest way to get the object identifier for a Microsoft 365 group is to run the Get-UnifiedGroup or Get-AzureADGroup cmdlets. For example:

Get-UnifiedGroup -Identity DisabledInsightsGroup | Select ExternalDirectoryObjectId

ExternalDirectoryObjectId
-------------------------
c9758609-d33b-4eea-976b-d8e43a2ad135

Get-AzureaADGroup -SearchString DisabledGraphInsights | Select ObjectId
                                           
ObjectId                             
--------                             
c9758609-d33b-4eea-976b-d8e43a2ad135 

If you pass an incorrect identifier, no users will be restricted.

Equipped with the URI and the request body, you can now update the settings using the Graph Explorer. As shown in Figure 2, you select PATCH as the command type, beta as the endpoint, input the URI and the request body, and then click Run query. You can see that a 200 (OK) response is returned and the response shows that the disabledForGroup setting holds the group object identifier.

Patching the organization settings with the Graph Explorer
Figure 2: Patching the organization settings with the Graph Explorer

The updated setting can take up to eight hours to be effective across all Microsoft 365 applications.

Exploring the Effect of No Insights

Normally, the Microsoft 365 profile card includes a section about recent documents a user has worked on that are accessible to the person viewing the card. Figure 3 shows the profile card as displayed by OWA and we can see three files listed on the card with the opportunity to see more.

The Office 365 profile card with document insights
Figure 3: The Microsoft 365 Profile Card with document insights

If we disable insights for the user or the complete organization, document insights don’t appear on the profile card (Figure 4). In this instance, the user hasn’t disabled document insights in Delve, but because Office 365 applies the stricter of the organization and Delve settings, the removal of the insights is dictated by the organization settings.

The Office 365 Profile Card without document insights
Figure 4: The Microsoft 365 Profile Card without document insights

Administrators Don’t Do Graph

Recently, Microsoft has started to push out Microsoft 365 tenant settings that can only be manipulated using Graph calls. I think the folks who create these updates miss the point that most administrators are not Graph API literate, simply because this is not a tool they use in their daily work. It’s good that the Graph Explorer exists and can act as a workaround.

It would be better for all if Microsoft created either the GUI in the admin center or a PowerShell cmdlet to control new tenant settings. It would then be easier for tenants to embrace new functionality and Microsoft would see a higher uptake for their work.

Update: Settings for Meeting Insights and Item Insights are now customizable through the Search & Intelligence section of the Microsoft 365 admin center.


This might seem like an arcane subject, but it’s important for Office 365 tenant administrators to understand the tools at their disposal and what those tools can do. We explain how in the Office 365 for IT Pros eBook. Subscribe today!

]]>
https://office365itpros.com/2020/08/06/customizing-item-insights-privacy-graph-explorer/feed/ 3 18251
Customizing the Microsoft 365 Profile Card with the Graph Explorer https://office365itpros.com/2020/08/05/microsoft-365-profile-card-custom/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-profile-card-custom https://office365itpros.com/2020/08/05/microsoft-365-profile-card-custom/#comments Wed, 05 Aug 2020 07:59:16 +0000 https://office365itpros.com/?p=17183

Making the Microsoft 365 Profile Card Your Own

Updated 12 November 2023

In Office 365 notification MC217813 (published 1 July and updated on 15 July), Microsoft announced that: “You will be able to customize profile cards according to your organization business needs. The profile card is sometimes referred to as a contact card or people card.” (Microsoft 365 roadmap item 61502).

The new functionality sounds promising. The profile or people card displays information about someone in different places in Microsoft 365 applications. For instance, you can select an email sender or recipient in Outlook or Outlook mobile to view the details of that person. If they’re someone in your organization, the profile card shows you information from their Azure Active Directory account. The information is more limited for external people.

Customizing the Profile Card

The announcement points to documentation about how to customize the profile card. Essentially, you update the ProfileCardProperty Graph resource to add new information to the Microsoft 365 profile card. Six standard Azure Active Directory properties can be added along with the fifteen custom attributes available for organizations to use as they wish (like CustomAttribute11, which Microsoft recently considered using to identify the room mailboxes needed for its Workspaces feature). Custom attributes are used for many purposes, including to hold organizational information (like cost centers and division names), mark mailboxes for special processing, and so on.

Nice as it is to be able to customize the profile card, the immediate barrier facing many tenant administrators is not knowing how to interact with the Graph to update the ProfileCardProperty resource. Not everyone has mastered Graph programming, and administrators who know PowerShell might not yet have ventured into interacting with the Graph (some examples of using PowerShell with the Graph are listed below).

Graph Explorer Delivers an Answer

As it turns out, the Graph Explorer makes it easy to apply the necessary changes. This is a browser interface to allow people to interact with the Graph and get to know how Graph transactions work against different endpoints, such as Groups, Outlook, Planner, and SharePoint. You can run commands against test data or, after signing into a tenant account, against live data. Programmers can grab code snippets generated for transactions by the Explorer in C#, Java, JavaScript, and Objective-C and include the code in their programs. All in all, the Graph Explorer is a very useful tool.

Using Graph Explorer to Update the Profile Card

The key points about using the Graph Explorer to update the ProfileCardProperty resource are:

  • Sign in with an administrative account for the tenant.
  • Use a POST command to update the https://graph.microsoft.com/v1.0/admin/people/profileCardProperties endpoint.
  • Put the update you want to execute in the request body. For example, to add the PostalCode property to the profile card, I put {“directoryPropertyName”:”Postalcode”} in the request body.
  • Click Run query when you’re ready. If everything’s been done right, you should see a 200 response. This means that the Graph has accepted the update.

Figure 1 shows the Graph Explorer after running a successful update. You can see the 200 response.

Using the Graph Explorer to update the ProfileCardProperty resource with a POST command

Microsoft 365 profile card
Figure 1: Using the Graph Explorer to update the ProfileCardProperty resource with a POST command

You can make multiple updates to add more properties or add all the properties at one time by including them in the request body. For example, this request body specifies three properties to add to the profile card:

{
  "directoryPropertyName": "StreetAddress",
  "directoryPropertyName": "PostalCode",
  "directoryPropertyName": "userPrincipalName"
}

Microsoft warns that it takes up to 24 hours for the changes to show on user profile cards. It also seems that you need to add updates one by one and wait until an update is active before applying another. To check the current state of the ProfileCardProperty resource, you can run a GET command to see what’s returned by https://graph.microsoft.com/v1.0/admin/people/profileCardProperties.

Figure 2 shows that I have added two of the standard properties (UserPrincipalName and StreetAddress) and CustomAttribute12. Note that the custom attribute is assigned a display name of “Cost Center” to make its use more obvious to end users.

Checking updates applied to the ProfileCardProperty resource with a GET command

Microsoft 365 profile card
Figure 2: Checking updates applied to the ProfileCardProperty resource with a GET command

The Microsoft Graph PowerShell SDK can retrieve details of custom properties. For example, this output shows that the tenant customized the profile to include three standard attributes for Azure AD accounts (userPrincipalName, StreetAddress, and PostalCode) and added two custom attributes to display the user’s cost center and preferred drink, which are stored in CustomAttribute12 and CustomAttribute9 respectively:

Connect-MgGraph -NoWelcome -Scopes "PeopleSettings.ReadWrite.All","PeopleSettings.Read.All"

Get-MgAdminPeopleProfileCardProperty | Format-List

Annotations           : {}
DirectoryPropertyName : userPrincipalName
Id                    :
AdditionalProperties  : {}

Annotations           : {Cost center}
DirectoryPropertyName : customAttribute12
Id                    :
AdditionalProperties  : {}

Annotations           : {}
DirectoryPropertyName : StreetAddress
Id                    :
AdditionalProperties  : {}

Annotations           : {}
DirectoryPropertyName : Postalcode
Id                    :
AdditionalProperties  : {}

Annotations           : {Drink}
DirectoryPropertyName : CustomAttribute9
Id                    :
AdditionalProperties  : {}

Viewing Custom Profile Cards in Applications

After the Graph processes everything and applications refresh their cache, you’ll see the customizations show up in the profile cards displayed by applications. Figure 3 shows how OWA displays customized contact information in the Microsoft 365 profile card while Figure 4 shows how the card appears in Outlook Mobile.

OWA displays a customized Microsoft 365 profile card
Figure 3: OWA displays a customized Microsoft 365 profile card
Outlook Mobile displays a customized Office 365 profile card
Figure 4: Outlook Mobile displays a customized Office 365 profile card

The customized attributes don’t appear for guest accounts.

If you make a mistake and add the wrong attribute or want to start over, you can remove an attribute from the organization settings by running a DELETE request. For example, to remove CustomAttribute15 from the set shown on the user profile card, run a DELETE request against https://graph.microsoft.com/v1.0/admin/people/profileCardProperties/customAttribute15

Gets Real Work Done

The notion of having to write code to interact with the Graph might well have turned people away from even considering customizing the profile card. The Graph Explorer allows you to do the job without writing a single line of code. It’s a great example of using an available tool to achieve a goal in a way that the authors of the feature probably never considered.

Some PowerShell with Graph Articles

How to report SharePoint Online site usage.

Fetching Azure AD Account Sign-in Information.

Fetching Planner Data for a Single User.

Generating a User Activity Report.

Handling pagination when fetching Graph data.

]]>
https://office365itpros.com/2020/08/05/microsoft-365-profile-card-custom/feed/ 40 17183
How to Report SharePoint Online Site Usage Data with PowerShell and the Graph https://office365itpros.com/2020/05/07/sharepoint-site-report-graph/?utm_source=rss&utm_medium=rss&utm_campaign=sharepoint-site-report-graph https://office365itpros.com/2020/05/07/sharepoint-site-report-graph/#comments Thu, 07 May 2020 00:02:45 +0000 https://office365itpros.com/?p=8355

Remove the Need for Administrator Access to Create a SharePoint Site Report

Following an article I wrote about using PowerShell to report SharePoint Online site storage usage, a reader asked if it is possible to create such a report without needing to sign in with a SharePoint Online administrator account. They’d like non-admins to be able to report data but don’t want them to run PowerShell scripts or access the admin center.

Global Reader Role and Usage Reports

The Global Reader role allows non-privileged access to reporting data exposed in some interfaces like the Microsoft 365 Admin Center. The reports section of the admin center (Figure 1) includes reports on SharePoint user and site activity.

The Reports section of the Microsoft 365 Admin Center
Figure 1: The Reports section of the Microsoft 365 Admin Center

The Admin Center includes an export option to download data as a CSV file. If you select the site usage report, this seems promising, until you realize that the report includes redirect sites and even the SharePoint Online Tenant Fundamental Site created when the tenant is initialized. Some of the fields output could be better formatted too.

Usage Reports Based on the Graph

The usage reports available in the Microsoft 365 Admin Center use the Graph reports API to generate their data. It’s very easy to write a quick and dirty PowerShell script to call the SharePoint Site Storage Usage API to return the same data as you see in the usage reports. Once the data is downloaded, you can manipulate it to meet your needs.

Granting Permissions for the Graph

To gain access to the Graph, PowerShell needs to use an app to authenticate with the Reports.Read.All and Sites.Read.All permissions. The basic idea is that you register an app for your tenant to use with PowerShell and generate an app secret to use for OAuth authentication. You then assign the necessary Graph permissions to the app. The request to add the permissions to the app must be approved by an administrator before the permissions can be used. You can then use the app identifier and secret to generate an access token for the Graph which contains the permission. Read this post for detailed steps of creating such an app for PowerShell to use.

Update: The current version of the script has been updated to use the Microsoft Graph PowerShell SDK. This removes the requirement to use an app. The information described below shows how to use an app to authenticate and make Graph API requests. The same results are obtainable using the Graph SDK.

Accessing the Graph

After the app is authorized with the necessary permissions, we can use it with PowerShell. This snippet:

  • Populates variables with the app identifier, secret, and tenant identifier. These values are unique to an app and to a tenant. You’ll have to change them for your tenant for this code to work.
  • Builds a request to get an access token.
  • Parses the returned token.
$AppId = "e716b32c-0edb-48be-9385-30a9cfd96155"
$TenantId = "c662313f-14fc-43a2-9a7a-d2e27f4f3478"
$AppSecret = 's_rkvIn1oZ1cNceUBvJ2or1lrrIsb*:='

# Build the request to get the OAuth 2.0 access token
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $AppId
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $AppSecret
    grant_type    = "client_credentials"}

# Request 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
$headers = @{Authorization = "Bearer $token"}
$ctype = "application/json"

With a token, we can issue the Graph request to fetch the SharePoint Online storage usage data:

# Get SharePoint files usage data
$SPOFilesReportsURI = "https://graph.microsoft.com/v1.0/reports/getSharePointSiteUsageDetail(period='D7')"
$Sites = (Invoke-RestMethod -Uri $SPOFilesReportsURI -Headers $Headers -Method Get -ContentType "application/json") -Replace "", "" | ConvertFrom-Csv

All that remains to be done is to parse the returned data and generate a report (a CSV file). You can download the script from GitHub. As always, the code is bare-bones and doesn’t include much in terms of error checking.

I also output the report data to Out-GridView (Figure 2) as it’s the easiest way to browse the information.

Reviewing the SharePoint site usage data retrieved from the Graph.

SharePoint Site Report.
Figure 2: Reviewing the SharePoint site usage data retrieved from the Graph

Advantages and Disadvantages

The big advantage of this approach is that no dependency exists on cmdlets in the PowerShell module for SharePoint Online or an administrator account. All the code is basic PowerShell that can be run by any user.

Because this approach uses data fetched from the Graph, the code is fast too – much faster than the version based on the SharePoint Online cmdlets, and the speed advantage becomes larger as the number of sites grows. This is because the Graph generates the report data and has it ready for fetching while the other approach requires you to generate the data for each site with the Get-SPOSite cmdlet. On the other hand, the Graph data is at least two days old, something that might not be too much of a concern when reviewing storage usage.

The downside is that the Graph usage data includes a limited set of properties. Some useful properties, like site files, active files, and views, aren’t returned by the Get-SPOSite cmdlet, but Get-SPOSite returns information like the site title, group identifier (to get a list of site owners), and sensitivity label among others.

Combine to Get Both

Combining data fetched from the Graph with that fetched by Get-SPOSite is the best of both worlds, even if you’ll need to use a SharePoint administrator account. The question is what data are needed. If you really need the extended information about a site, you’ll have to use the SharePoint Online module. But if all you need is simple storage data, the Graph can provide that information quickly, albeit if it’s slightly out-of-date.

]]>
https://office365itpros.com/2020/05/07/sharepoint-site-report-graph/feed/ 42 8355
Combining Microsoft Graph and Flow for Better Office 365 Adminstration https://office365itpros.com/2019/10/03/combining-microsoft-graph-flow-better-office-365-admin/?utm_source=rss&utm_medium=rss&utm_campaign=combining-microsoft-graph-flow-better-office-365-admin https://office365itpros.com/2019/10/03/combining-microsoft-graph-flow-better-office-365-admin/#respond Thu, 03 Oct 2019 00:25:30 +0000 https://office365itpros.com/?p=5035

By Gustavo Velez (gustavo@gavd.net)

Leveraging the Power of the Graph

The Microsoft Graph API offers a fast and easy way to find and modify Office 365 data. Combining the Graph with Microsoft Flow makes it possible to automate the steps in a task. Some impressive results can be created even by people who don’t have a programming background.

The Microsoft Graph API is a unified programmatic RESTful interface for all the components of Office 365. Via a single REST endpoint. Microsoft Graph API provides access to objects such as users, groups, mail, messages, notes, tasks, calendar, and Office Graph. Data can be retrieved from multiple Microsoft cloud services such as Exchange, OneDrive, SharePoint, OneNote, Planner and Azure Active Directory. One of the Microsoft Graph components is the Security API, an interface and schema used to integrate security solutions from Microsoft and other vendors.

Office 365 Flow

Microsoft Flow is the automation engine in Office 365. A flow is a workflow that combines tasks and data from multiple Office 365 applications and services to get work done. To create a flow, the user specifies what action should take place when a specific event occurs. No programming experience is required.

Once a flow is built, it can be managed on the desktop or through a mobile device. Flow integrates out-of-the-box with many Office 365 services and apps, including SharePoint, Power BI, PowerApps, and Dynamics 365, but can be extended to reach other systems.

Putting Graph and Flow Together

After a tenant is up and running, it’s good to know the current state of the Office 365 services consumed by users. But monitoring services and taking any necessary actions can be a boring and tiresome activity. This kind of repetitive work can be automated using tools as Flow and Graph. And, because the creation of flows can be done by non-programmers, a flow can be quickly built by administrators.

In this article, we will use the Office 365 Secure Score as an example. Secure Score is a self-assessment tool to test the security level of an Office 365 tenant and generates an assessment of how well the tenant is protected against the risk of a security breach or attack. Secure Score is available in the Security & Compliance Center or this link. Click the Improvement actions tab to see recommendations to improve tenant security and increase the tenant’s Secure Score (Figure 1).

Viewing improvement actions for Secure Score
Figure 1: Viewing improvement actions for Secure Score

Our aim is to get the recommendations using the Microsoft Graph Security API. The idea is to generate a weekly report containing security recommendations and email it to the administrator.

First Step: Register an application in Azure Active Directory

Because Flow communicates with Graph as an “external” component, it needs to be authenticated and authorized to get information from Office 365. This is done by registering the application in the Azure AD and assigning the app the needed permissions. The first step is to open the Azure Active Directory portal and select “App registrations” and then “New registration” (Figure 2). Give the app a name, select the account type and leave the Redirect URL empty.

Registering an Azure AD app
Figure 2: Registering an Azure AD app

After the app is registered, note the “Application (client) ID” and “Directory (tenant) ID” values (Figure 3) because these data are used later in the process.

Noting important app data
Figure 3: Noting important app data

Next, grant the registration permissions to access the Graph, selecting the option “API Permissions” on the left side menu. Click on “Add permissions”, then “Microsoft Graph” and, in the new window, “Application permissions”. Because we will use the “Secure Scores” of the Security API, scroll down in the window, open the “Security” section, and select the option “SecurityEvents.Read.All” (Figure 4). Always the lowest possible access level for any app you register. Save the changes.

Selecting permissions for the app
Figure 4: Selecting the Secure Score permissions for the app

Remember to grant the permissions assigned to the application using the button in the “Grant consent” section (Figure 5).

Granting consent for the permissions
Figure 5: Granting consent for the app to receive permissions

The next step is to create a secret that allows the application to run without any user credentials. Select “Certificates and secrets” on the menu at the left side, and then click the button “New client secret.” A popup window will start asking for how long the secret will be valid: one, two years, or never expires. Select one of the options and generate the secret. Make a note of the secret along with the Client and Tenant IDs (Figure 6).

Viewing the client secret for the app
Figure 6: Viewing the client secret for the app

Creating the Flow

It is now time to create the Flow and get the information required. Open https://flow.microsoft.com and go to “My flows”. Select “+ New” and “Scheduled – from blank.. Assign a name to the Flow and select how often, and when, it will start (Figure 7).

Creating a new scheduled Flow
Figure 7: Creating a new scheduled Flow

The Flow designer starts. The “Recurrence” step should be already populated using the information in Figure 7. Use the “+ New step” button to create a “Initialize variable” step to contain the first value needed (the Client ID) to use the Azure AD registration, giving it a name, selecting “String” as “Type”, and copying its value into the Flow. Repeat the operation to add two variables for the Tenant ID and the Secret (Figure 8).

Creating Flow variables for the Graph app
Figure 8: Creating Flow variables for the Graph app

Add a new variable to contain the filter for the query to Graph. This is necessary because URLs in Flow don’t allow to use whitespaces, and Graph doesn’t understand the convention to use “_x020_” to encrypt the spaces. The value for the variable will be “?$filter=userImpact eq ‘High'” (Figure 9).

Adding a filter for the Graph app
Figure 9: Adding a filter for the Graph app

Now add a new step to the Flow, this time of type “HTTP” to send the request to Graph. This activity requires a “Premium” account for Flow (Plan 1 or Plan 2). In the step, click on the “Show advanced options”. In the “Authentication” box select “Active Directory oAuth”. Configure the options using the following values:

    null
  • Method: GET
  • URI: We’ll explain this in a little while.
  • Authentication: Active Directory OAuth
  • Authority: https://login.microsoft.com
  • Tenant: use the variable with the directory id
  • Audience: https://graph.microsoft.com
  • Client ID: use the viable with the application id
  • Credential Type: Secret
  • Secret: use the secret variable

For the URI you can use any of the multiple queries possible to approach the Graph Security API. In this example we are using this:

https://graph.microsoft.com/v1.0/security/secureScoreControlProfiles?$filter=userImpact eq ‘High’

Because the query is filtered to get a similar response as the response given by the Office 365 user interface (User Impact High), we are using a combination of the string for the query and the value of the variable for the filter (Figure 10).

Inserting the Graph URI into the Flow
Figure 10: Inserting the Graph URI into the Flow

Save the Flow and run it because we need the output for the next step. From the output result window of the Flow, copy the “Body” response (Figure 11).

Checking the Graph response
Figure 11: Checking the Graph response

The Graph response is in JSON form, and it contains a plethora of data. We are interested in only the Title (that contains the description of the issue) and Remediation fields. Return to the Flow and open it for editing. Add a new action of the type “Parse JSON” at the end of the flow to extract the needed data. Use the “Body” of the HTTP query output as “Content” and click on the “Use sample payload to generate schema.” Flow opens a pop-up window where you can paste the result of the query run (Figure 11). Flow will generate the schema for the parser and create objects for each item in the response automatically (Figure 12).

Flow creates objects from the Graph JSON response
Figure 12: Flow creates objects from the Graph JSON response

The parser presents the data as text without any formatting. To create something that a user can read, add a new step in the Flow of the type “Create HTTP table”. For the data source (“From” field) select the “value” tag of the parser and select “Custom” in “Columns”. Create two columns, one called “Title” that uses the “title” object created for the parser, and other called “Remediation” for the “remediation” object (Figure 13).

Creating a HTML table in Flow
Figure 13: Creating a HTML table in Flow

Save and run the Flow again. Open the result and in the Table step you will see the data that we want to have, rendered in the correct format (Figure 14).

Examining the Secure Score Information Retrieved from the Graph
Figure 14: Examining the Secure Score Information Retrieved from the Graph

To send a message to the system administrator with the query results, add finally a “Send an email notification” step to the Flow. Use the “Output” of the table for the Body of the Email (Figure 15).

Creating an email notification in the Flow
Figure 15: Creating an email notification in the Flow

Save the Flow and run it manually. An Email will be received in a couple of seconds with the recommendations found by Graph (Figure 16).

The email with Secure Score information as received by the admin
Figure 16: The email with Secure Score information as received by the admin

From now on, the Flow will run automatically each month based on the configured schedule.

Conclusions

The Microsoft Graph offers a unified way to approach and interact with Office 365. At the other side of the equation, Flow allows us to use Graph, not only to get information from Office 365 but also to take decisions and make modifications if necessary. Both systems combined give the administrators of Office 365 a powerful tool to automate monitoring and maintenance activities.


Gustavo writes Chapter 23 of the Office 365 for IT Pros eBook, where he discusses the finer points of Microsoft Flow and PowerApps. If you haven’t tried to create anything with Flow, you might be surprised at what’s possible.

]]>
https://office365itpros.com/2019/10/03/combining-microsoft-graph-flow-better-office-365-admin/feed/ 0 5035