PowerShell – Office 365 for IT Pros https://office365itpros.com Mastering Office 365 and Microsoft 365 Fri, 06 Sep 2024 11:41:49 +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 PowerShell – Office 365 for IT Pros https://office365itpros.com 32 32 150103932 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
PnP PowerShell Changes Its Entra ID App https://office365itpros.com/2024/08/29/pnp-powershell-changes-app/?utm_source=rss&utm_medium=rss&utm_campaign=pnp-powershell-changes-app https://office365itpros.com/2024/08/29/pnp-powershell-changes-app/#comments Thu, 29 Aug 2024 05:00:00 +0000 https://office365itpros.com/?p=66182

Critical Need to Update Scripts Using PnP PowerShell Before September 9 2024

On August 21, 2024, the Pattern and Practices (PnP) team announced a major change for the PnP PowerShell module. To improve security by encouraging the use apps configured with only the permissions needed to process data within the tenant, the PnP PowerShell module is moving away from the multi-tenant Entra app (the PnP Management Shell, application identifier 31359c7f-bd7e-475c-86db-fdb8c937548e) used up to this point to require tenants to register a unique tenant-specific app for PnP.

Reading between the lines, the fear is that attackers will target the current PnP multi-tenant app and attempt to use it to compromise tenants. The multi-tenant app holds many Graph API permissions (Figure 1) together with a mixture of permissions for Entra ID, SharePoint Online, and the Office 365 service management API. Being able to gain control over such an app would be a rich prize for an attacker.

Some of the many permissions held by the multi-tenant PnP PowerShell app
Figure 1: Some of the many permissions held by the multi-tenant PnP PowerShell app

Swapping out one type of Entra app for another might sound innocuous, but it means that the sign-in command for PnP in every script must be updated. The PnP team will remove the current multi-tenant app on September 9, 2024, so any script that isn’t updated will promptly fail because it cannot authenticate. That’s quite a change.

The Usefulness of PnP PowerShell

I don’t use PnP PowerShell very often because I prefer to use Graph APIs or the Microsoft Graph PowerShell SDK whenever possible. However, sometimes PnP just works better or can perform a task that isn’t possible with the Graph. For instance, creating and populating Microsoft Lists is possible with the Graph, but it’s easier with PnP. SharePoint’s support for Graph APIs is weak and PnP is generally a better option for SharePoint Online automation, such as updating site property bags with custom properties (required to allow adaptive scopes to identify SharePoint Online sites). Finally, I use PnP to create files in SharePoint Online document libraries generated as the output from Azure Automation runbooks.

Creating a PnP Tenant Application

The first thing to do is to download the latest version of the PnP PowerShell module (which only runs on PowerShell 7) from the PowerShell Gallery. The maintainers update the module regularly. I used version 2.9.0 for this article.

The easiest way to create a tenant-specific application for PnP PowerShell is to run the Register-PnPEntraIDApp cmdlet:

Register-PnPEntraIDApp -ApplicationName "PnP PowerShell App" -Tenant office365itpros.onmicrosoft.com -Interactive

Make sure that you sign in with an account that has global administrator access. The cmdlet creates an Entra ID app and populates the app with some default properties, including a default set of Graph API permissions and a self-signed certificate for authentication. It doesn’t matter what name you give the app because authentication will use the unique application identifier (client id) Entra ID creates for the new app. The user who runs the cmdlet must be able to consent for the permissions requested for the app (Figure 2).

Consent sought for the default set of Graph permissions used by the PnP PowerShell app
Figure 2: Consent sought for the default set of Graph permissions used by the PnP PowerShell app

The Graph permissions allow read-write access to users, groups, and sites. Other permissions will be necessary to use PnP PowerShell with other workloads, such as Teams. Consent for these permissions is granted in the same way as for any other Entra ID app. Don’t rush to grant consent for other permissions until the need is evident and justified.

Using the Tenant App to Connect to PnP PowerShell

PnP PowerShell supports several ways to authenticate, including in Azure Automation runbooks. Most of the examples found on the internet show how to connect using the multi-tenant application. To make sure that scripts continue to work after September 9, every script that uses PnP PowerShell must be reviewed to ensure that its code works with the tenant-specific application. For instance, a simple interactive connection looks like this:

Connect-PnPOnline -Url https://office365itpros.sharepoint.com -ClientId cb5f363f-fbc0-46cb-bcfd-0933584a8c57 -Interactive

The value passed in the ClientId parameter is the application identifier for the PnP PowerShell application.

Azure Automation requires a little finesse. In many situations, it’s sufficient to use a managed identity. However, if a runbook needs to add content to a SharePoint site, like uploading a document, an account belonging to a site member must be used for authentication. This example uses credentials stored as a resource in the automation account executing the runbook.

$SiteURL = "https://office365itpros.sharepoint.com/sites/Office365Adoption"
# Insert the credential you want to use here... it should be the username and password for a site member
$SiteMemberCredential = Get-AutomationPSCredential -Name "ChannelMemberCredential"
$SiteMemberCredential
# Connect to the SharePoint Online site with PnP
$PnpConnection = Connect-PnPOnline $SiteURL -Credentials $SiteMemberCredential -ReturnConnection -ClientId cb5f363f-fbc0-46cb-bcfd-0933584a8c57

[array]$DocumentLibraries = Get-PnPList -Connection $PnpConnection | Where-Object {$_.BaseType -eq "DocumentLibrary"}
 
# Display the name, Default URL and Number of Items for each library
$DocumentLibraries | Select Title, DefaultViewURL, ItemCount

Ready, Steady, Go…

September 9 is not too far away, so the work to review, update, and test PnP PowerShell scripts needs to start very soon (if not yesterday). Announcing a change like this 19 days before it happens seems odd and isn’t in line with the general practice where Microsoft gives at least a month’s notice for a major change. I imagine that some folks coming back from their vacations have an unpleasant surprise lurking in their inboxes…

]]>
https://office365itpros.com/2024/08/29/pnp-powershell-changes-app/feed/ 10 66182
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
Adding Cost Center Reporting to the Microsoft 365 Licensing Report https://office365itpros.com/2024/07/23/microsoft-365-licensing-report-192/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-licensing-report-192 https://office365itpros.com/2024/07/23/microsoft-365-licensing-report-192/#comments Tue, 23 Jul 2024 07:00:00 +0000 https://office365itpros.com/?p=65683

Different Forms of Cost Centers

On June 20, I announced version 1.9 of the Microsoft 365 Licensing Report. A month later, version 1.92 is available for download from GitHub. This version adds support for reporting licensing costs by cost center. Here’s how it works.

Ever since Exchange Server added a set of 15 custom attributes to mailboxes, organizations have used the attributes to hold all kinds of information. Cost center numbers come in different formats. In Digital Equipment Corporation, the numbers (or rather, designation) were values like 8ZW and 9HPE. In Compaq and HP, the values were more like 1001910. In any case, organizations often store cost center values in custom attributes to allow a more precise assignment of costs than is possible using standard Entra ID account properties like city, department, and country.

For cost center reporting to work, it’s obvious that accurate cost center numbers must be present in Exchange mailbox properties. Sometimes cost centers are added when users join an organization and receive a mailbox and are never updated afterwards. In other instances, organizations have synchronization mechanisms in place to ensure that if a change is made to an employee’s cost center (usually in a HR database), that change also happens for mailbox properties.

It might also be possible to implement cost center reporting based on managers (if managers manage cost centers). To do this, the script would have to find all the managers and assume that any direct reports are in the same cost center as the manager. I discounted this method and chose the simpler approach of using cost centers stored in a custom attribute, but it wouldn’t be difficult to code because Entra ID links stores details of the manager for each user account. Storing a manager for an account is not mandatory, so the same problem of data accuracy and availability might be present.

Microsoft 365 Licensing Report Script Changes to Support Cost Centers

The script supports cost center reporting through a variable called $CostCenterAttribute, which holds the name of the custom attribute to use. The name stored in the variable is the Entra ID property name rather than the Exchange name, so it’s a value like extensionAttribute1. If $CostCenterAttribute is not defined, the report doesn’t attempt to generate any information about licensing cost per cost center.

Exchange Online synchronizes the values of the mailbox custom attributes to the Entra ID user accounts of the mailbox owners. The custom attributes are stored in a property called OnPremisesExtensionAttributes. The Get-MgUser command to fetch user account details is amended to include OnPremisesExtensionAttributes in the set of retrieved properties. A set of cost centers found in user accounts is derived from the information retrieved by Get-MgUser.

When scanning user accounts for license information, the script extracts the cost center for each account and stores it along with other licensing data in a PowerShell list. This allows the report to later loop through the set of cost centers found in user accounts and calculate the licensing spend for each cost center, much like the licensing spend analysis done for departments and countries.

Reporting Licensing Spend by Cost Center

The script then outputs the cost center licensing spend analysis along with the other spending data in the summary part of the report (Figure 1).

Cost center analysis in the Microsoft 365 licensing report
Figure 1: Cost center analysis in the Microsoft 365 licensing report

Custom Attributes Open Up Lots of Opportunity

In this instance, the Microsoft 365 licensing report uses a custom attribute to store a cost center value. It is easy to see how custom attributes could be used for other analysis. For example, if a custom attribute held details of major projects, you could report the licensing spend for each project. All of this is basic PowerShell, so feel free to experiment!


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/07/23/microsoft-365-licensing-report-192/feed/ 2 65683
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 Right Way to Replace the Remove-SPOExternalUser Cmdlet https://office365itpros.com/2024/07/11/remove-spoexternaluser-cmdlet/?utm_source=rss&utm_medium=rss&utm_campaign=remove-spoexternaluser-cmdlet https://office365itpros.com/2024/07/11/remove-spoexternaluser-cmdlet/#respond Thu, 11 Jul 2024 04:00:00 +0000 https://office365itpros.com/?p=65499

Microsoft Will Remove-SPOExternalUser Between July 29 and August 9

Message center notification MC806103 (27 June 2024) reports the deprecation of the Remove-SPOExternalUser cmdlet from the SharePoint Online management PowerShell module. Microsoft suggests that administrators replace the cmdlet with the Remove-AzureADUser cmdlet, which is a perfectly reasonable strategy if only the cmdlet isn’t part of the retired and soon-to-be deprecated AzureAD module.

Between July 29. 2024 and August 9, 2024, Microsoft will disable the Remove-SPOExternalUser cmdlet. When the block arrives in a tenant, attempts to run the cmdlet will be greeted with:

To streamline scope and permissions for external users, enhance access management, and strengthen our security posture, this cmdlet has been deprecated. Alternatively, please use the Remove-AzureADUser cmdlet in Microsoft Entra ID for user management.”

Microsoft 365 is so Large that No One Understands Everything

MC806103 is a classic example of Microsoft being such a large organization that no one knows what’s happening across the board, or even what’s happening within Microsoft 365. In this case, the SharePoint Online people want to deprecate the Remove-SPOExternalUser cmdlet. That’s a good idea because the cmdlet has low usage (I don’t think I have ever used it) and doesn’t really make sense inside the Microsoft 365 ecosystem where external access for applications like SharePoint Online is now governed using guest accounts. It makes perfect sense to remove overlapping or conflicting features and replace them with what you’d consider a component that’s closer to the core.

Entra ID is the directory of record for Microsoft 365. Individual workloads like SharePoint Online have their own directory, but everything flows back to Entra ID. Replacing the SharePoint Online cmdlet with an Entra ID cmdlet is the right thing to do. The problem is that the program manager in charge of making the transition obviously doesn’t know that the Entra ID team has been trying to deprecate the AzureAD and AzureADPreview modules since 2020. For the last few years, Microsoft has conducted an ongoing campaign to move tenants off these modules to use the Microsoft Graph PowerShell SDK.

What makes this laughable is that Microsoft launched the Entra PowerShell module in preview on June 27 in the hope that a dedicated Entra module (built on top of the Microsoft Graph PowerShell SDK) would help the remaining customers who have scripts that use the AzureAD and AzureADPreview modules to move to a modern platform. Obviously, whoever wrote MC806103 had no idea that this development was in train.

The Right Way to Replace Remove-ExternalSPOUser

The Get-SPOExternalUser cmdlet reports the external users registered for a SharePoint Online tenant. The last time I discussed its use, I observed that the Get-SPOExternalUser cmdlet is an odd cmdlet in some ways, but it does generate a list of external users from the SharePoint directory.

An external user record looks like:

RunspaceId    : 9630573b-c675-4697-a029-72d535e48613
Email         : charu.someone@microsoft.com
DisplayName   : Charu Someone
UniqueId      : 100320009C9C6789
AcceptedAs    : charsomeone@microsoft.com
WhenCreated   : 20/02/2020 19:45:02
InvitedBy     :
LoginName     :
IsCrossTenant : False

Remove-SPOExternalUser works like this:

Remove-SPOExternalUser -UniqueIDs ($User).UniqueId -Confirm:$false
Successfully removed the following external users
100320009C9C6789

The cmdlet removes the external user entry from SharePoint Online. It also removes the matching guest account, if one exists, from Entra ID. In my tenant there are quite a few lingering external accounts that don’t have matching Entra ID guest accounts. These accounts are just another form of digital debris that needs to be cleaned up.

The right way to remove an external account is to use the Remove-MgUser cmdlet from the Microsoft Graph PowerShell SDK:

$User = Get-MgUser -filter "mail eq 'andrew@proton.me"
Remove-MgUser -UserId $User.Id

Or, if you decide to use the preview Entra module:

$User = Get-EntraUser -SearchString 'AdamP@contoso.com'
Remove-EntraUser -ObjectId $User.Id

Either cmdlet has a much longer future ahead of it than the Remove-AzureADUser cmdlet has. In both cases, SharePoint Online synchronizes with Entra ID and removes the matching external user record.

It’s Just Hard to Keep Up

I don’t blame the individual program manager responsible for MC806103. It’s hard to keep up with everything that goes on within Microsoft 365 and all too easy to assume that a solution that works (for now) is the right long-term recommendation. Perhaps Microsoft needs a clearing house to cross-check dependencies outside the control of an individual development group before they publish information to customers?


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/07/11/remove-spoexternaluser-cmdlet/feed/ 0 65499
Adding Details of Authentication Methods to the Tenant Passwords and MFA Report https://office365itpros.com/2024/06/25/authentication-methods-v13/?utm_source=rss&utm_medium=rss&utm_campaign=authentication-methods-v13 https://office365itpros.com/2024/06/25/authentication-methods-v13/#comments Tue, 25 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65312

Revealing Full Details of Authentication Methods and Why This Might Be a Privacy Issue

Soon after releasing V1.2 of the Tenant Passwords and MFA Report (to add details about per-user MFA states), I was asked if it was possible to add more information for authentication methods, like the phone number used for SMS responses. My response was that I had covered the topic of reporting the details of authentication methods in a previous article and it was simply a matter of using the code from that article, updating it slightly to deal with the device-based passkeys recently introduced for Entra ID.

Not everyone likes cracking open a PowerShell script to insert code that they didn’t write. I don’t like messing with other peoples’ code either and will usually write my own version when necessary. In any case, I found some time and upgraded the script to include the expanded details, available in V1.3 of the script in GitHub.

Reporting Authentication Methods

Figure 1 shows the information about authentication methods registered for a user account in V1.2 of the report. The information given use the names from the MethodsRegistered property returned by the Get-MgBetaReportAuthenticationMethodUserRegistrationDetail cmdlet from the Microsoft Graph PowerShell SDK.

 Reporting the authentication methods registered for a user account.
Figure 1: Reporting the authentication methods registered for a user account

The problem is that the names aren’t very user-friendly. If you’re used to working with authentication methods, you probably recognize the values and understand what they mean. If not, this information might be useless.

More detail about the methods is available by running the Get-MgUserAuthenticationMethod cmdlet. Even so, some manipulation is necessary to generate human-friendly output. I’d done most of the work before, so it was easy to generate more information for each method. For instance, in Figure 2 you can see the mobile phone number used for SMS challenges and the version of the Authenticator app used for push notifications.

Expanded details of a user account's registered authentication methods.
Figure 2: Expanded details of a user account’s registered authentication methods

Because the script captures details in a PowerShell list, it’s also possible to query the list to find information like who uses a YubiKey FIDO2 key with a command like:

$Report | Where-Object {$_.'Authentication Methods' -like "*Yubikey*"}

The Privacy Issue

All was going well when I realized that the information generated about authentication methods might include some PII data, like the mobile phone number used for SMS responses. In most instances, I don’t think this will be a problem because details like mobile phone numbers are often included in the properties of Entra ID user accounts. The email addresses used to recover passwords via the Self-Service Password Reset (SSPR) feature are often personal accounts, so they might be more of an issue.

However, the regulations covering access to PII differs from country to country and it’s a good idea to cover all bases. The script now has a PrivacyFlag parameter. It’s a switch parameter, so the value is false by default. If set to true by including the parameter when running the script or by setting the flag explicitly, the script generates the names of the authentication methods without any details.

$PrivacyFlag = $true

On to The Next Version

I am sure that many other good ideas about how to add value to a report like this exist within the community. If you do, suggest the change through the Office 365 for IT Pros GitHub repository (for this script or any of our other scripts). Many people create a fork of our repository and work on updates that way. Whatever’s easier for you…


Learn more about how Microsoft 365 applications and Entra ID 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/06/25/authentication-methods-v13/feed/ 1 65312
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
Version 1.9 of the Microsoft 365 Licensing Report https://office365itpros.com/2024/06/20/microsoft-365-licensing-report-19/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-licensing-report-19 https://office365itpros.com/2024/06/20/microsoft-365-licensing-report-19/#comments Thu, 20 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65235

Highlighting License Costs for Disabled and Inactive Users with Color

The Microsoft 365 Licensing report is one of the more popular scripts I’ve written. The last set of updates added analysis of licensing costs by department and country. I maintain a list of things that people have asked me to add to the script. Last week, I wanted to take a break from the work to prepare the new edition of the Office 365 for IT Pros eBook, so I fired up Visual Studio Code and got to work.

On my to-list were the following:

  • Highlight disabled counts better and report the cost of licenses assigned to disabled accounts.
  • Highlight the cost of licenses assigned to user accounts that haven’t signed in for 90 days or more.
  • Add Excel worksheet output using the ImportExcel module.
  • Categorize the license spend for individual user accounts to be under, average, or high based on the average cost for the tenant.
  • Use color to highlight important points in the HTML report (Figure 1). I’m color blind, so the colors I selected to highlight different values might not be to your taste. If so, feel free to select different colors and modify the script by inserting the hex code values of those colors into the style sheet for the report.
  • Fix some small bugs. There’s always a couple to clean up.

Microsoft 365 Licensing Report (HTML file)
Figure 1: Microsoft 365 Licensing Report (HTML file)

Summarizing Licensing Costs

Figure 2 shows the updated summary of costs generated at the end of the HTML report. The cost analyses by country and department were in the last update, but I fixed a bug where the report didn’t deal as well as it should do when no licenses are assigned to accounts without a department or country.

Summary information for the Microsoft 365 Licensing Report.
Figure 2: Summary information for the Microsoft 365 Licensing Report

The new information is in the section for inactive user accounts and disabled user accounts. Each category lists the set of user accounts that match the criteria together with the total cost of licenses assigned. I used 90 days since the last sign-in to decide if an account is inactive. It’s easy to modify the script to use a higher or lower value, depending on how long it takes before your organization considers an account to be inactive.

Generating an Excel Worksheet for the Licensing Data

Many PowerShell scripts generate CSV files for their output. It’s natural that this should be the case. The Export-CSV cmdlet is part of base PowerShell, and the CSV file format is easy to work with and the data is easy to import back into a PowerShell array.

Some of the CSV files end up as Excel worksheets. It’s easy to do this by opening the CSV file with Excel and saving the file as a worksheet. The ImportExcel module supports the generation of worksheet in many different styles with data inserted into a table ready to be analyzed (Figure 3).

Microsoft 365 Licensing Report in an Excel worksheet.
Figure 3: Microsoft 365 Licensing Report in an Excel worksheet

The script checks if the ImportExcel module is available. If it is, the script generates an Excel worksheet. If not, the licensing data is exported to a CSV file.

Important Note and How to Get the Script

If you haven’t run the script before, make sure to read these Practical365.com articles to understand how the script works, how to generate the two (SKU and service plan) CSV files used by the script, and how to add cost data for Microsoft 365 subscriptions. Basically, some up-front work is necessary to prepare reference data for the script to use in its analysis. The code can extract details of user accounts and their assigned licenses from Entra ID, but turning GUIDs into human-friendly product names requires some help. The cost of Microsoft 365 subscriptions differs from country to country too.

You can download V1.9 of the script from GitHub.

Microsoft 365 tenants can have large quantities of licenses to manage. This script might help as written, or inspire you to create your own version tailored to the needs of your organization


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/20/microsoft-365-licensing-report-19/feed/ 2 65235
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
To Splat or Not to Splat, That’s the Question https://office365itpros.com/2024/06/12/splatting-powershell/?utm_source=rss&utm_medium=rss&utm_campaign=splatting-powershell https://office365itpros.com/2024/06/12/splatting-powershell/#comments Wed, 12 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65119

Splatting Helps the Readability of PowerShell Code

Splatting is a way to define and use values for parameters dent to PowerShell cmdlets and functions. Instead of specifying a value for each parameter when running a cmdlet, you create a hash table and add the parameters and their values to the hash table. Then you specify the hash table when running the cmdlet. The idea is to avoid long command lines that might or might not be broken up with backticks. Long command lines can sometimes be difficult to scan to understand exactly what the intent of a command is.

Those who endorse splatting say that it’s easy to forget a parameter or a backtick when composing long command lines. In addition, the parameters and values passed in long command lines can be harder to read than a nicely formatted hash table.

Using a development tool like Visual Studio Code will help to make sure that commands are properly formed. After that, using splatting to pass parameters is down to personal choice. And if you pay for a GitHub Copilot license, you’ll discover that Copilot does an excellent job of filling parameter values.

Example of Splatting

Here’s an example of how splatting works. The Set-User command updates many properties of a user account. Note the use of backticks to break the command over several lines:

Set-User -Identity Ben.James -Office 'Dublin Center' -City 'Dublin' `
-CountryOrRegion 'Ireland' -Department 'Sales and Marketing' `
 -DisplayName 'Ben James (Sales)' -Initials 'BJ' -Title 'Senior Lead Manager' ` 
-StateOrProvince 'Leinster' -StreetAddress '1, Liffey Walk' -PostalCode 'D01YYX1' `
-Confirm:$False

Splatting allows you to do this instead:

$Parameters = @{}
$Parameters.Add("Office", "Galway")
$Parameters.Add("Department", "Business Development")
$Parameters.Add("DisplayName", "Ben James (BusDev)")
$Parameters.Add("StateOrProvince", "Connacht")
$Parameters.Add("PostalCode", "GY1H1842")
$Parameters.Add("StreetAddress", "Kennedy Center")
$Parameters.Add("City", "Galway")
$Parameters.Add('Title', "Senior Development Manager")
$Parameters.Add("Identity", "Ben.James@office365itpros.com")
$Parameters.Add("Confirm", $false)

Set-User @Parameters

Adding or changing a parameter is a matter of updating the hash table.

An advantage of using splatting is that it is easy to update objects with common parameters. For instance, to update another user who shares the same office and location values, we can do this:

Set-User -Identity Jane.Sixsmith@office365itpros.com -DisplayName 'Jane Sixsmith' 
-Title 'Promotions Manager' @parameters

PowerShell applies the values for the parameters in the hash table except where a parameter value is explicitly passed.

Microsoft Graph PowerShell SDK Cmdlets And Splatting

In script examples used by articles on this site, we spell out parameters because of personal preference. In addition, the cmdlets in the Microsoft Graph PowerShell SDK can use a construct like splatting when updating or creating objects, meaning that splatting is less of an issue. Because the SDK cmdlets are based on Graph APIs, cmdlets that implement POST and PATCH requests require commands to pass a request body in the Body parameter.

Here’s an example of using the Update-MgUser cmdlet to update a set of properties for a user account with a request body created as a hash table:

$UserId = (Get-MgUser -UserId 'Michelle.duBois@office365itpros.com').id
$Body = @{}
$Body.Add("Office", "Galway")
$Body.Add("Department", "Business Development")
$Body.Add("DisplayName", "Ben James (BusDev)")
$Body.Add("State", "Connacht")
$Body.Add("PostalCode", "GY1H1842")
$Body.Add("StreetAddress", "Kennedy Center")
$Body.Add("City", "Galway")
$Body.Add('JobTitle', "Senior Development Manager")

Update-MgUser -UserId $UserId -BodyParameter $Body

The same hash table can be used with splatting:

Update-MgUser -UserId $UserId @Body

Even complex Graph SDK commands can be converted to splatting. Take the example of using the Get-MgUser cmdlet shown in Figure 1. This is an advanced Graph query because it’s checking the service plans held in (a multivalued property) assigned to user accounts to find accounts with a specific plan. As written, the command is spread over three lines using backticks.

A complex Get-MgUser command.

Splatting
Figure 1: A complex Get-MgUser command

Graph SDK cmdlets like Get-MgUser are based on GET Graph queries so there’s no need to pass a request body. However, the cmdlet parameters can be put into a hash table and passed to cmdlets. Here’s an example using an advanced query with Get-MgUser:

[guid]$SPOPlanId = "5dbe027f-2339-4123-9542-606e4d348a72"
$Body = @{}
$Body.Add("Filter", "assignedPlans/any(s:s/serviceplanid eq $SPOPlanId and capabilityStatus eq 'Enabled')")
$Body.Add("ConsistencyLevel", "eventual")
$Body.Add("Countvariable", "Test")
$Body.Add("Pagesize", "999")
$Body.Add("Property", "Id, displayName, userprincipalName, assignedLicenses, assignedPlans, department, country")
$Body.Add("Sort", "DisplayName")
$Body.Add("All", $true)

[array]$Users = Get-MgUser @Body

Note that switch parameters (like -All in this example) that don’t take a value when run in a command need $true as the value for their entry in the hash table.

Splatting is A Personal Choice

One of the nice things about PowerShell is the variety of styles supported for writing code. Some favor dense masses of commands including some pretty hard-to-understand pipelined code. Some eschew functions and others like to lay out commands with plenty of white space in between. Splatting does a job of defining and passing parameters. It’s up to you to decide how to use it.


Learn how to exploit the data and tools 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/06/12/splatting-powershell/feed/ 4 65119
Interpreting Audit Records for Teams Meeting Recordings (Again) https://office365itpros.com/2024/06/07/teams-meeting-recordings-june24/?utm_source=rss&utm_medium=rss&utm_campaign=teams-meeting-recordings-june24 https://office365itpros.com/2024/06/07/teams-meeting-recordings-june24/#comments Fri, 07 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65081

Change in Audit Records for Teams Meeting Recordings Since 2021

Three years ago, I wrote about how to use audit records to track the creation of Teams meeting recordings. The idea was to find the audit records created when a Teams meeting recording was uploaded to OneDrive for Business or SharePoint Online.

Time marches on and old blogs rot, as do old PowerShell scripts. Three years ago, Microsoft hadn’t completed the transition from Stream classic to Stream on SharePoint. The migration finished recently and Microsoft has moved to standardize how Teams meeting recordings and transcripts are stored in OneDrive for Business. Of course, OneDrive only holds recordings for personal meetings. Recordings for channel meetings, including Meet Now in the channel, end up in the SharePoint Online site belonging to the host team.

Closing a Compliance Gap

While some might think that I spend endless hours examining audit records, this is a fallacy. I check on an as required basis, which means that I didn’t notice that my script wasn’t working quite so well because the format of the audit records changed. One important change is that the user noted in all the audit records is app@sharepoint, the ubiquitous SharePoint utility account. No trace exists in the audit records about the user who recorded the meeting, as had happened before.

From a compliance perspective, this is a big deal. Audit records exist to track the actions taken by individuals and system processes, and in this case, it seems important to know who initiated a recording.

Unfortunately, there’s nothing in the audit record to indicate who initiated the recording of a channel message, so we’re left with the SharePoint app. Recordings for personal meetings used to end up in the OneDrive account of the user who started the recording (the organizer or a presenter). Some time ago, Microsoft changed this to a more logical arrangement where recordings always go into the meeting organizer’s OneDrive account. The URL of a OneDrive account contains the site URL, like:

https://office365itpros-my.sharepoint.com/personal/jane_ryan_office365itpros_com

Figuring Out the OneDrive Site Owner

It’s easy for a human to read the URL and know that the OneDrive account belongs to Jane.Ryan@office365itpros.com. With time, I could parse the URL to extract the email address, but I went for a simpler (faster) approach. I used the Get-SPOSite cmdlet from the SharePoint Online PowerShell module to fetch the set of OneDrive accounts in the tenant and created a hash table from the site URL and site owner. It’s fast to check the hash table with the site URL taken from an audit record to return the user principal name of the site owner:

$User = $OneDriveHashTable[$AuditData.SiteURL]
If ($null -eq $User) {
   $User = "SharePoint app"
}

Changes in Search-UnifiedAuditLog Too!

Another influence on the output was the change made by Microsoft in summer 2023 to how the Search-UnifiedAuditLog cmdlet works. Microsoft have denied to me that they did anything, but the evidence shows that:

  • The SessionCommand parameter must now be set to ReturnLargeSet to force the cmdlet to return more than 120 records.
  • Many more duplicate records are returned than before. This necessitates sorting by the unique audit event identifier to remove the duplicates.
  • Search-UnifiedAuditLog returns unsorted data. If a sorted set is important to you, make sure that you sort the audit records by creation date.
$Records = $Records | Sort-Object Identity -Unique | Sort-Object {$_.CreationDate -as [datetime]} -Descending

Of course, you can try to run high completeness searches with Search-UnifiedAuditLog, but I have not had good luck with this preview feature.

Figure 1 shows the output from the updated script, which is available from GitHub. Normal service is resumed.

Audit records for Teams Meeting Recordings.
Figure 1: Audit records for Teams Meeting Recordings

A Reminder to Check Audit Log Analysis Scripts

It would be nice if a script lasted a little longer, but the ongoing change within Microsoft 365 means that PowerShell developers need to keep a wary eye on updates that might affect production scripts. In this instance, the confluence of the Stream migration and the change to the Search-UnifiedAuditLog cmdlet made a mess of a perfectly good script. I guess life is like that sometimes. Maybe now is a good time to check your scripts that use the Search-UnifiedAuditLog cmdlet.


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/07/teams-meeting-recordings-june24/feed/ 2 65081
Report Delegated Permission Assignments for Users and Apps https://office365itpros.com/2024/06/06/delegated-permissions-report/?utm_source=rss&utm_medium=rss&utm_campaign=delegated-permissions-report https://office365itpros.com/2024/06/06/delegated-permissions-report/#comments Thu, 06 Jun 2024 07:00:00 +0000 https://office365itpros.com/?p=65048

Extract and Report Delegated Permission Assignments with the Microsoft Graph PowerShell SDK

When discussing permissions used to retrieve data with Graph API requests (including cmdlets from the Microsoft Graph PowerShell SDK), most of the time we refer to application permissions rather than delegate permissions. The reason is simple: when automating operations with PowerShell, tenant administrators usually process data drawn from multiple sources, like all user mailboxes or all groups. This level of processing requires application permissions.

Delegated permissions (also called scopes) allow apps to access information on behalf of the signed in user. Anything that user can access, the app can too. Usually, the user is the owner of a resource (like their mailbox), but they can gain access to information through an RBAC role, such as Teams administrator.

Delegated permissions are granted by a specific resource (like a Graph API) and represent the operations that an app can perform for the user. For instance, the Mail.Read scope allows an app to read messages in the user’s mailbox. The grant of consent for a delegated permission usually happens when a user signs into an app and the app discovers that consent for the required permission is not granted. At this point, Entra ID displays the consent prompt window to allow the user to give consent for the app to use the permission and proceed.

Reporting Permissions

Application permissions assigned to apps can be checked by examining the app role assignments for service principals. It’s a good idea to inventory app permissions periodically to ensure that apps don’t have high-profile permissions without good reason.

To report delegated permissions, we need to check delegated permission grants (otherwise called OAuth2 permission grants). These are delegated permissions granted for a client application to access an API on behalf of a signed-in user. The Microsoft Graph PowerShell SDK cmdlet used for this purpose is Get-MgOauth2PermissionGrant. The Directory.Read.All permission is required to read details of delegated permissions and user accounts.

Interpreting a Delegated Permission for Users

After connecting, run the Get-MgUser cmdlet to create an array of user accounts to query. Usually, I apply a filter to find licensed accounts. Once you have a set of accounts, it’s a matter of looping through the set to find the delegated permissions for each account:

[array]$Permissions = Get-MgUserOauth2PermissionGrant -UserId $User.Id -All

An individual permission assignment looks like this:

$Permission | Format-List

ClientId             : 5482d706-b547-4b9d-b159-b91a5776e0e9
ConsentType          : Principal
Id                   : BteCVEe1nUuxWbkaV3bg6YnEoxRs7QVAltG-nFdw96NYzfTvuBuZSJTeeV9la0oY
PrincipalId          : eff4cd58-1bb8-4899-94de-795f656b4a18
ResourceId           : 14a3c489-ed6c-4005-96d1-be9c5770f7a3
Scope                :  openid profile User.ReadWrite User.ReadBasic.All Sites.ReadWrite.All Contacts.ReadWrite People.Read Notes.ReadWrite.All Tasks.ReadWrite Mail.ReadWrite Files.ReadWrite.All Calendars.ReadWrite Group.Read.All Group.ReadWrite.All Directory.AccessAsUser.All Directory.ReadWrite.All User.ReadWrite.All IdentityRiskEvent.Read.All Reports.Read.All AuditLog.Read.All User.Read SecurityEvents.ReadWrite.All offline_access TeamSettings.Read.All TeamSettings.ReadWrite.All Mail.ReadBasic Chat.Read Chat.ReadBasic Analytics.Read
AdditionalProperties : {}
  • The client identifier points to the service principal for the client app. In this case, it is the Graph Explorer.
  • The principal identifier points to the identifier for the user account. Because we’re listing delegated permissions by user, the consent type for the permission is always Principal, meaning that the app is limited to impersonating the specific user. If the consent type is AllPrincipals, meaning that the app can use the consent to impersonate all users, the principal identifier would be empty.
  • The resource identifier points to the service principal for the resource. In this example, the resource identifier points to “Microsoft Graph” (the Graph API). The set of permissions (Scope) confirm this because they are Graph permissions. As you can see, the Graph Explorer has consent for many permissions. This is a normal situation if developers use the Graph Explorer to test different Graph APIs.

Processing Delegated Permissions for AllPrincipals

After processing the delegated permission assignments for user accounts, we process those for all principals (any user). The set of assignments is found with:

[array]$AppGrants = Get-MgOauth2PermissionGrant -filter "consentType eq 'AllPrincipals'" -All

Steps in the Script

The steps in the script are as follows:

  • Find the set of user accounts.
  • For each account, check if any delegated permissions exist.
  • For each permission, check the client app and resource.
  • Find the set of delegated permissions for all principals.
  • Do much the same as for individual assignments.
  • Report what’s been found.

Figure 1 shows the output generated.

Delegated permissions report.
Figure 1: Delegated permissions report

You can download the full script from GitHub.

Interpreting the Results

It’s inevitable that delegated permissions will accumulate over time. Looking at the results from my tenant, I see evidence of the iOS account migration to modern authentication from 2021, apps from conference organizers like Sessionize and Community Days, the app used to register for the Microsoft Technical Community, and so on. All these assignments are understandable. The question is whether the assignments are needed any longer and if not, should they be removed. That’s up to you…


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/06/delegated-permissions-report/feed/ 2 65048
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
Reporting Mailbox Audit Configurations https://office365itpros.com/2024/05/28/mailbox-audit-configuration-report/?utm_source=rss&utm_medium=rss&utm_campaign=mailbox-audit-configuration-report https://office365itpros.com/2024/05/28/mailbox-audit-configuration-report/#comments Tue, 28 May 2024 07:00:00 +0000 https://office365itpros.com/?p=64892

Make Sure that Mailbox Audit Configurations Capture Important Events

Following Microsoft’s announcement about the availability of the promised additional audit events for Purview Audit (standard) customers, some folks got in touch to ask if I had a script to report current mailbox audit configurations. As it happens, I didn’t, but cracking open Visual Studio Code and GitHub Copilot soon put that right.

How Not to Find Accounts with Purview Audit (Advanced) Licenses

My original plan was to find and report mailboxes owned by licensed user accounts. I wanted to know which accounts use Purview Audit standard and which use the advanced variant. This is more difficult than it seems because, as far as I can tell, there’s no Purview Audit standard service plan. At least, I can’t find one on the Microsoft page listing all the license and service plan identifiers.

There is a service plan called M365_ADVANCED_AUDITING (2f442157-a11c-46b9-ae5b-6e39ff4e5849), which seemed like a good candidate for Purview Audit (advanced). However, if you use the Get-MgUser cmdlet from the Microsoft Graph PowerShell SDK to find accounts with this service plan identifier in the assignedPlans property (see below), the service plan name returned for the identifier is “exchange.”

[guid]$PurviewAuditAdvancedPlanId = "f6de4823-28fa-440b-b886-4783fa86ddba"

[array]$Users = Get-MgUser -filter "assignedPlans/any(x:x/serviceplanid eq $PurviewAuditAdvancedPlanId)" -ConsistencyLevel eventual -CountVariable Test -Property Id, displayName, userprincipalName, assignedLicenses, assignedPlans

The service plan identifier appears in accounts that don’t have Office 365 E5 or Microsoft 365 E5 licenses, which are the products that include Purview Audit (advanced). This is because the service plan identifier has a disabled status in those accounts. To solve that problem, amend the filter to check for enabled service plans:

[array]$Users = Get-MgUser -filter "assignedPlans/any(x:x/serviceplanid eq $PurviewAuditAdvancedPlanId and capabilityStatus eq 'Enabled')" -ConsistencyLevel eventual -CountVariable Test -Property Id, displayName, userprincipalName, assignedLicenses, assignedPlans

But then I found that the resulting set of accounts only included those with Microsoft 365 E5 licenses. No trace existed of the Office 365 E5 accounts, even though Microsoft includes the Office 365 E5 license in the set with access to Purview Audit (advanced) in this useful comparison chart.

Microsoft documentation assures me that there is an app for Purview Audit (advanced). Usually, an app equates to a service plan. When I checked the Microsoft 365 admin center as directed, the app shows up under the moniker Microsoft 365 advanced auditing (Figure 1).

Microsoft 365 advanced auditing app listed for an account in the Microsoft 365 admin center.

Mailbox audit configuration
Figure 1: Microsoft 365 advanced auditing app listed for an account in the Microsoft 365 admin center

Disabling and enabling the app in the Microsoft 365 admin center disables and enables the 2f442157-a11c-46b9-ae5b-6e39ff4e5849 service plan behind the scenes. After all that, we know that a service plan called exchange controls an app called Microsoft 365 advanced auditing (aka the Microsoft Purview Audit (advanced) product) that only shows up in accounts with Microsoft 365 E5 licenses. It’s all very confusing, so I lost interest at this point.

Back to Scripting Mailbox Audit Configurations

After wasting too much time discovering the mess of service plans, product names, and SKUs, I went back to scripting and wrote some straightforward code to:

  • Connect to Exchange Online.
  • Run Get-ExoMailbox to find user and shared mailboxes.
  • Define some critical audit events to check for in the owner and delegate audit sets.
  • Check each mailbox to see if it uses the default audit configuration (maintained by Microsoft). Report the audit set defined in the configuration.
  • Check that the critical audit events are present in the owner and delegate audit sets and flag any critical audit events (like MailItemsAccessed) found missing.
  • Report what’s been found.
  • If the ImportExcel PowerShell module is available, generate an Excel worksheet containing the results (Figure 2). If not, generate a CSV file.

Reporting mailbox audit configurations with Excel
Figure 2: Reporting mailbox audit configurations with Excel

You can download the full script from GitHub.

A Note About Enabling Audit with Set-Mailbox

The script checks if auditing is enabled for a mailbox, and if it is, the script runs Set-Mailbox to set AuditEnabled to true. Microsoft documentation says that if mailbox auditing is turned on by default for an organization, mailbox auditing ignores the AuditEnabled mailbox property.

But their May 20 announcement about the new audit events says that “Every standard user mailbox should have AuditEnabled set to true to ensure all audit records are uploaded to Purview Audit” and “Please note that this Set-Mailbox command must be run for every Standard license user regardless of its current value to correctly enable their mailbox to upload the new standard logs to Purview Audit.” Microsoft documentation is confusing on this point. I think the situation is that Microsoft manages mailbox auditing for accounts with Purview Audit advanced licenses while manual intervention is needed for mailboxes with Purview Audit standard, Whatever the reason, it’s always better to be safe than sorry when dealing with audit events, the script runs Set-Mailbox. You can certainly eliminate this section of the script to speed things up if you want to.

Feel free to improve and embellish the script to meet your needs. In the meantime, I need a headache tablet to recover from the trials of audit licensing.


Stay updated with developments like new events for mailbox audit configurations 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/05/28/mailbox-audit-configuration-report/feed/ 1 64892
More Microsoft Graph PowerShell SDK Problems https://office365itpros.com/2024/05/06/microsoft-graph-powershell-sdk-217/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-powershell-sdk-217 https://office365itpros.com/2024/05/06/microsoft-graph-powershell-sdk-217/#comments Mon, 06 May 2024 04:00:00 +0000 https://office365itpros.com/?p=64674

Odd Replacement Cmdlets appear in Microsoft Graph PowerShell SDK V2.17 and Azure Automation Issues in V2.18

Updated 15 May 2024

Microsoft Graph PowerShell SDK V2.18

Last week was odd for me. I headed to Orlando for the M365 Community Conference on Sunday and arrived at the Dolphin hotel feeling somewhat odd. A few hours later, I was flat on my back unable to move with a combination of a horrible cough and the effects of a norovirus. Enough to say that it wasn’t pretty.

The conference swung into full action on Tuesday, and I spoke (between coughs) about the Microsoft Graph PowerShell SDK. I think it’s fair to say that I have a love-hate relationship with the software. I like the access to all sorts of Microsoft 365 data enabled through the SDK cmdlets, but I dislike some of its foibles.

I also hate when Microsoft makes changes that seem to be firmly in the category of shooting itself in the foot, like the spurious output generated for cmdlets introduced in V2.13.1 and worsened in V2.14. Stuff like this shouldn’t get through basic testing.

Update: Microsoft has released V2.19 of the SDK to fix the reported problems. They describe the affected cmdlets in a May 15 blog.

The Case of the DirectoryObjectByRefs Cmdlets

Which brings me to a problem that seems to have surfaced in V2.17. Until this time, the Remove-MgGroupMemberByRef cmdlet worked to remove a member from a group by passing the user account identifier for the member and the group identifier. With V2.17, the following happens:

Remove-MgGroupMemberByRef -DirectoryObjectId $UserId -GroupId $GroupId
Remove-MgGroupMemberByRef: A parameter cannot be found that matches parameter name 'DirectoryObjectId'

The same happens with the Remove-MgGroupOwnerByRef cmdlet to remove a group member (but not if the action would leave the group ownerless).

Microsoft’s response is documented here and it is a calamity. Not only does it appear that other cmdlets are involved (like Remove-MgApplicationOwnerByRef – I have asked Microsoft for a definitive list), but the fix is terrible. No experienced PowerShell person would think that it is a good idea to fix a problem in a cmdlet by introducing a brand-new cmdlet, but that’s what Microsoft did by including cmdlets like Remove-MgGroupMemberDirectoryObjectByRef and Remove-MgGroupOwnerDirectoryObjectByRef in V2.17.

The SDK developers might be pleased that V2.17 contains functional cmdlets to remove group members and owners, but anyone who wrote scripts prior to V2.17 based on the old cmdlets is left high and dry.

I hadn’t noticed the problem because I haven’t run the affected cmdlets for a while. But Ryan Mitchell of Northwestern University had, and he brought the matter to my attention after the session at the Microsoft 365 Community Conference. Suffice to say that the necessary protests have been made in the right quarters. I had the opportunity in Orlando to chat with some senior members of the Graph development team who acknowledged that this is not the way that cmdlet problems should be addressed and that overall Graph SDK quality must be improved. Specifically for the group cmdlets, Microsoft is investigating how the situation developed. It could be that this is a side effect of the famous AutoRest process that generates SDK cmdlets from Graph APIs. We’ll see in time.

Update May 9: Microsoft has published V2.19 of the SDK to address the problem with cmdlet renaming. They’ve introduced aliases to make sure that scripts continue to work with the old cmdlets.

Microsoft Graph PowerShell SDK V2.18 and Azure Automation

Microsoft released V2.18 of the Microsoft Graph PowerShell SDK last week. After installing the new module and running some tests, everything checked out and I duly tweeted that the new module was available.

But problems lurked for Azure Automation runbooks configured for PowerShell 5.1 because people noted that they couldn’t use the Groups module after connecting with a user-provided access token obtained using the Get-AzAccessToken cmdlet. Everything works with PowerShell 7, but not with the earlier release. It seems like a clash occurs between the version of the Azure identity assembly loaded by the AzAccounts module. In any case, Microsoft is investigating (here are the full details) and the advice is to stay with V2.17 if you use Azure Automation until Azure updates their assembly.

Time for a Checkup

It’s disappointing to see issues like these continue to appear in new versions of the Microsoft Graph PowerShell SDK. Basic testing and some knowledge about how people use PowerShell in practice should have caught these issues. Their existence lessens faith in the SDK. After all, who wants to chase new bugs in a module that’s refreshed monthly?

Chapter 23 of the Office 365 for IT Pros eBook referenced examples of the cmdlets affected by the V2.17 issue. We’ve issued update 107.1 with amended text. The nature of an eBook means that it’s much easier to address problems in text than with printed books and we do try and fix known issues as quickly as we can. For everyone else who uses the Microsoft Graph PowerShell SDK for group management or Azure Automation, it’s time to check that everything’s working as expected.


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/05/06/microsoft-graph-powershell-sdk-217/feed/ 4 64674
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
How to Remove a Single Service Plan from User Accounts with PowerShell https://office365itpros.com/2024/04/23/remove-service-plan-powershell-2/?utm_source=rss&utm_medium=rss&utm_campaign=remove-service-plan-powershell-2 https://office365itpros.com/2024/04/23/remove-service-plan-powershell-2/#respond Tue, 23 Apr 2024 07:00:00 +0000 https://office365itpros.com/?p=64426

Remove Service Plans with the Microsoft Graph PowerShell SDK

In 2021, I wrote about how to remove a single service plan from multiple Entra ID user accounts with PowerShell. The original script used cmdlets from the Microsoft Online Services (MSOL) module. To cover all bases, I updated the post with versions of the script using cmdlets from the AzureAD and the Microsoft Graph PowerShell SDK. Microsoft has deprecated the MSOL and AzureAD modules and the final retirement of these modules is due on March 30, 2025.

The problem with updating a script to replace cmdlets is the tendency to keep the same flow and logic. In other words, the script that started off using MSOL cmdlets behaves in much the same way when updated to use Graph SDK cmdlets. It’s natural that things happen in this way because those updating the code want to get the work done as quickly as possible. Who has the time to sit back and ask if code can be improved during script updates, even if new tools like GitHub Copilot are available.

I’ve been using GitHub Copilot integrated into Visual Studio Code for the last month or so. I’m not sure that Copilot has created any great new code in my scripts, but it certainly has an uncanny ability to auto-complete lines of code and comments, just like Word does when I write. I like GitHub Copilot and recommend the combination of it and Visual Studio Code to anyone who writes PowerShell for Microsoft 365.

What the Script Does to Remove Service Plans from Accounts

Which brings us neatly to some upgrades for the version of the script based on the Microsoft Graph PowerShell SDK. The original script:

  • Lists the set of subscriptions (bought products) found in the tenant and asks the administrator to select a product to modify.
  • Lists the set of service plans for the selected product and asks the administrator to select the service plan to disable. For example, a tenant might decide that they don’t wish to use Viva Engage, so they will remove the Viva Engage Core and Viva Engage Seeded service plans from all accounts with the selected product. This is exactly what happens when an administrator edits a user account with the Microsoft 365 admin center and removes access to some of the apps listed for the user (Figure 1). Obviously, it’s much faster to use PowerShell to remove service plans from multiple accounts.
  • Runs a cmdlet to disable the selected service plan for all user accounts that have the selected license.

Removing service plans from an Entra ID account.

How to remove service plans with PowerShell
Figure 1: Removing service plans from an Entra ID account

There’s not much in terms of cmdlets in the script. Get-MgSubscribedSku returns the set of products and service plans. Get-MgUser finds user accounts and Set-MgUserLicense disables the selected service plan for each account. It’s all very straightforward.

Upgrading the Script to Remove Service Plans Faster

Then someone complained that they couldn’t get the script to work in their tenant. Perhaps consent had not been granted for the Directory.ReadWrite.All permission (scope), which is necessary to read the set of subscribed products, read user information, and update user licenses. Or perhaps the person used an interactive session, and the signed-in account didn’t hold one of the necessary administrative roles (remember, delegated permissions are used for Graph SDK interactive sessions). For whatever reason, it was good enough to check the code to see if any improvements were possible.

I found four areas to update:

  1. Some products (like Office 365 E3 or Microsoft 365 E5) are composite licenses that span many service plans. Each service plan has a target. User service plans can be disabled or enabled on a per-user basis. Company service plans are managed at the tenant level. The new code makes sure that the script only lists user service plans for the user to select.
  2. The cmdlets in the MSOL and AzureAD modules didn’t boast good server-side filtering capabilities to find accounts assigned specific licenses, so filtering happens client-side. The complex filters supported by the Graph for user accounts allows the Get-MgUser cmdlet to find the precise set of accounts with the selected license. This change makes the script much more efficient in large tenants.
  3. The previous iteration of the script didn’t check if a service plan was already disabled before attempting to disable a plan. It does now.
  4. The previous iteration didn’t handle errors well and the report generated by the script could include items where the removal of a service plan didn’t work. Better error handling sorted this problem.

You can download the updated script from GitHub.

The Principle is Proved, Now Let Your Imagination Run Wild

The script to remove service plans is intended to demonstrate a principle of license management for Microsoft 365 user accounts. It would be easy to amend the script in different ways. For instance, you could allow the administrator to select multiple service plans to remove or eliminate the need to select a product and find a target service plan in any of the licenses assigned to a user. It’s PowerShell, so let your imagination run wild and improve the script to meet the needs of your tenant.


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/04/23/remove-service-plan-powershell-2/feed/ 0 64426
Modifying the Teams Tenant Federation Configuration with PowerShell https://office365itpros.com/2024/04/09/tenant-federation-configuration/?utm_source=rss&utm_medium=rss&utm_campaign=tenant-federation-configuration https://office365itpros.com/2024/04/09/tenant-federation-configuration/#comments Tue, 09 Apr 2024 07:00:00 +0000 https://office365itpros.com/?p=64400

Blocking Sub-Domains in the Tenant Federation Configuration

The publication of message center notification MC770792 (5 April 2024) describing a new Teams tenant federation setting to block all sub-domains of a blocked domain seems like a very good idea. After all, if you decide to block inbound connections from “malware.com.” it’s likely that you also want to block sub-domains like “marketing.malware.com.”

Microsoft says that the update should be in all tenants by mid-April. From an administrator perspective, the change becomes active with version 6.1 of the Microsoft Teams PowerShell module, which adds support for the BlockAllSubdomains switch for the Set-CsTenantFederationConfiguration cmdlet. For example:

Set-CsTenantFederationConfiguration -BlockAllSubdomains $True -BlockedDomains "malware.com"

The new setting isn’t used by default and won’t affect existing block lists. If you do use it, Microsoft notes that the setting blocks “all new communication to and from subdomains in the Block list… Existing 1:1 chats with users from blocked subdomains will be disabled. In existing group chats with users from blocked subdomains, the users from the blocked subdomains will be removed from the group chat.”

Updating the Allow List

In September 2022, I wrote an article explaining how to update the Teams external federation configuration with PowerShell. The idea was to create an allow list for federated chat based on the home domains for guest accounts known in the tenant directory. The article was a response to the theoretical “GIFShell” attack against Teams by a security researcher. Having an allow list of known domains means that users can only communicate with users belonging to domains in the allow list using one-to-one federated chat. It’s still the most effect way of blocking potential malware arriving in a tenant via Teams chat with an attacker.

I looked over the code to remind myself about how to manipulate the tenant federation configuration and realized that a nice update would be to check the domains for guest accounts to make sure that they are Microsoft 365 tenants before adding them to the tenant federation configuration. For instance, guest accounts might belong to domains like gmail.com, yahoo.com, and outlook.com, but there’s no need to have these large consumer domains in the configuration.

The technique explained in the article about tenant identifiers provided the foundation for the solution. I created a function to check if a domain is a Microsoft 365 tenant and call the function to check a domain before including it in the list to update the tenant federation configuration with. Here’s the function:

function Get-DomainByCheck {
# Check a domain name to make sure that it's active
  param (
      [parameter(Mandatory = $true)]
      $Domain
  )

  $Uri = ("https://graph.microsoft.com/v1.0/tenantRelationships/findTenantInformationByDomainName(domainName='{0}')" -f $Domain) 
  Try {	
    [array]$Global:DomainData = Invoke-MgGraphRequest -Uri $Uri -Method Get -ErrorAction Stop
    If ($DomainData.displayname -in $UnwantedRealms) {
      Return $false
    } Else {
      Return $true
    }
  } Catch {
    Return $false
  }
}

Domains that pass the test are added to the tenant federation configuration, which is also available through the Settings & Policies section of the Teams admin center (Figure 1).

Tenant federation configuration in the Teams admin center.
Figure 1: Tenant federation configuration in the Teams admin center

Dealing with Unwanted Domains

You’ll notice that the function checks against an array called $UnwantedRealms. If a domain is found in the array, the function returns false to indicate that the domain shouldn’t be added to the tenant federation configuration. The script defines the array as follows:

$Global:UnwantedRealms = "MSA Realms", "Test_Test_Microsoft"

If the Graph findTenantInformationByDomainName API matches a Microsoft 365 tenant, its display name is returned in the domain information fetched by the request. For instance, if the function checks Microsoft.com, the display name is Microsoft. But if it checks a domain which is federated for identity purposes with Entra ID, like gmail.com, the display name is “MSA Realms.” And the display name returned for the domains used by Teams to deliver email to channels (like amer.teams.ms) is “Test_Test_Microsoft.” Perhaps the engineers never thought that the display name they selected for these domains would ever see the light of day…

Why would guest accounts have email addresses belong to Teams channels? The SMTP addresses generated by Teams for channels can be given to guest accounts to allow the account to be a member of a Microsoft 365 group. Any email sent to the group will automatically end up as a channel conversation and serve as a record of that email interaction. Another method to bring email into Teams is to create mail contacts with Teams channel addresses and include them in distribution lists. In any case, we don’t need to include the Teams email domains in the tenant federation configuration, which is why the script excludes them.

Scripting Makes Processing Multiple Domains Easier

The Teams tenant federation configuration is easy to maintain through the Teams admin center. PowerShell makes it easier when large numbers of domains are involved. If you want to see the code I used, download the script 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/2024/04/09/tenant-federation-configuration/feed/ 1 64400
How to Retrieve Loop Workspaces Data with PowerShell https://office365itpros.com/2024/04/08/loop-workspaces-report-ps/?utm_source=rss&utm_medium=rss&utm_campaign=loop-workspaces-report-ps https://office365itpros.com/2024/04/08/loop-workspaces-report-ps/#comments Mon, 08 Apr 2024 08:00:00 +0000 https://office365itpros.com/?p=64322

Report More than 200 Loop Workspaces Requires Fetching Pages of Data

In November 2023, I wrote about a PowerShell script I developed to report the storage consumed by Loop workspaces. The script worked. That is, it worked until a tenant had more than 200 workspaces at which point the script ceased to report details for any more workspaces. This point was recently made to me by a reader after they discovered that the script didn’t produce the desired results in their tenant. Obviously, I didn’t do enough testing to encounter the limit.

Investigation (reading the documentation for the Get-SPOContainer cmdlet) revealed that the cmdlet implements a primitive form of pagination. The default mode is to fetch the first 200 workspaces, but if you know that more workspaces exist, you can add the Paged parameter to the cmdlet.

Odd Pagination to Fetch More Loop Workspaces

APIs implement pagination when they want to limit the amount of data that an app can fetch in one operation. The Graph APIs use pagination for this reason (some of the cmdlets in the Microsoft Graph PowerShell SDK can perform automatic pagination). The idea is that an app fetches the first page, checks to see if a token (pointer) to the next page is present, and if so, the app uses the token to fetch that page. The process continues until the app has fetched all available pages.

In the case of the Get-SPOContainer cmdlet, if more workspace data are available, the 201st record in the set fetched from SharePoint is a pointer to the next page of (up to) 200 workspaces. Oddly, the information is in the form of a string followed by the actual token. Here’s an example:

Retrieve remaining containers with token: UGFnZWQ9VFJVRSZwX0NyZWF0aW9uRGF0ZVRpbWU9MjAyNDAzMzAlMjAwMCUzYTU5JTNhMjUmcF9JRD0yMDA=

To fetch the next page, run the Get-SPOContainer cmdlet and specify both the Paged and PagingToken parameters. The value passed in the PagingToken parameter is the token extracted from the record referred to above. The code must also remove the record from the set that will eventually be used for reporting purposes because it doesn’t contain any information about a workspace. For example:

$Token = $null
If ($LoopWorkspaces[200]) {
    # Extract the token for the next page of workspace information
    $Token = $LoopWorkSpaces[200].split(":")[1].Trim()
    # Remove the last item in the array because it's the one that contains the token
    $LoopWorkspaces = $LoopWorkspaces[0..199]
}

Looping to Fetch All Pages

A While loop can then fetch successive pages until all workspaces are retrieved. The curious thing is that at the end of the data, Loop outputs a record with the text. “End of containers view.” It’s just odd:

While ($Token) {
    # Loop while we can get a token for the next page of workspaces
    [array]$NextSetofWorkSpaces = Get-SPOContainer -OwningApplicationID a187e399-0c36-4b98-8f04-1edc167a0996 `
      -PagingToken $Token -Paged
    If ($NextSetofWorkSpaces[200]) {
        $Token = $NextSetofWorkSpaces[200].split(":")[1].Trim()
        $NextSetofWorkspaces = $NextSetofWorkspaces[0..199]
    } Else {
        $Token = $Null
        If (($NextSetofWorkSpaces[$NextSetofWorkspaces.count -1]) -eq "End of containers view.") {  
            # Remove the last item in the array because it contains the message "End of containers view."
            $NextSetofWorkspaces = $NextSetofWorkspaces[0..($NextSetofWorkspaces.count -2)]
        }             
    }
    $LoopWorkspaces += $NextSetofWorkspaces
}

Eventually, you have an array of all the Loop workspaces and can report it as in the previous script (Figure 1).

Figure 1: Reporting Loop workspaces

The script with the updated code can be downloaded from GitHub.

Another Example of SharePoint PowerShell Strangeness

I have no idea why the Loop developers thought it was a good idea to implement their unique style of PowerShell pagination in the Get-SPOContainer cmdlet. What they should have done is implement the All cmdlet as done elsewhere, like the Get-SPOSite cmdlet. Supporting easy retrieval of all workspaces together with server-side filtering capability would be more than sufficient for most scenarios and would result in simpler code to develop and maintain.

Last month, I wrote wondering if Microsoft cared about SharePoint PowerShell. This is yet another example of strangeness in SharePoint PowerShell that reinforces my feeling that no one in Microsoft does care.


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/08/loop-workspaces-report-ps/feed/ 5 64322
Graph and PowerShell Hiccups for the Groups and Teams Report Script https://office365itpros.com/2024/03/22/groups-and-teams-activity-report/?utm_source=rss&utm_medium=rss&utm_campaign=groups-and-teams-activity-report https://office365itpros.com/2024/03/22/groups-and-teams-activity-report/#respond Fri, 22 Mar 2024 08:00:00 +0000 https://office365itpros.com/?p=64226

Fixing Bugs and Applying Workarounds for the Groups and Teams Activity Report

Microsoft 365 Groups and Teams Activity Report.

The Microsoft 365 Groups and Teams activity report is generated by a PowerShell script that I’ve been working on years. The first version is listed as created in July 2016, which is soon after Office 365 Groups made their debut.

Some recent changes caused me to refresh the Groups and Teams Activity Report script (the current version is 5.13 and it is available from GitHub). The major issues are listed below.

Continuing Woes with SharePoint Usage Data

The Graph Usage Reports API continues to have a problem with SharePoint Online usage data. The URL for a SharePoint site is not included in the data, which makes it very difficult to match the usage statistics with a site. As a workaround, the script fetches details of all sites in the tenant using the List Sites API to build a list of site URLs, which is then matched up with the SharePoint usage data. Matching is imperfect but works 99% of the time, which is close enough for a workaround that I hope will become unnecessary soon when Microsoft fixes the Usage Reports API.

Disappearing Group Owner Names

Some people noted that groups were being reported with no owners when they absolutely had some owners. The script calls the Groups API to retrieve owner information using GET requests like this:

https://graph.microsoft.com/v1.0/groups/33b07753-efc6-47f5-90b5-13bef01e25a6/owners?

Weirdly, the information retrieved only included the identifier for group owner accounts. The display name needed by the report was blank. I know that Microsoft encourages developers to include a Select statement in Graph queries to limit the number of properties retrieved for objects. This increases performance and reduces the amount of data that must be transferred from the service to an app. I therefore changed the request to:

https://graph.microsoft.com/v1.0/groups/33b07753-efc6-47f5-90b5-13bef01e25a6/owners?$select=id,displayName,mail

Everything worked, which is good, but when I retested with the original call a few days later, all the expected properties were there.

@odata.type       : #microsoft.graph.user
id                : eff3cd58-1bb8-4899-94de-795f656b4a18
businessPhones    : {+353 1 2080705}
displayName       : Tony Redmond
givenName         : Tony
jobTitle          : Chief Executive Officer
mail              : Tony.Redmond@office365itpros.com
mobilePhone       : +353 86 01629851
officeLocation    : Derrigimlagh
preferredLanguage : en-IE
surname           : Redmond
userPrincipalName : Tony.Redmond@office365itpros.com

My conclusion is that a bug (or perhaps an attempt to introduce a performance enhancement) suppressed the output of the properties for some period in the recent past. If this is the case and Microsoft reverted to previous behavior, it would explain what happened. But it could be something else, and the learning from the experience is that it is better to be explicit when requesting data from the Graph. Use Select to tell the Graph the set of properties needed by an app and all should be well.

Converting Strings to Dates

The data generated by the usage reports API includes the date when the monitored object was last active. This information is important in terms of knowing if a group or team is active. The date is in string format and must be converted to a datetime object to calculate the number of days since the last activity. Normally, casting the date as a datetime object is enough, but then you run into the problem that date format differs across cultures and the script throws the “string was not recognized as a valid datetime” error.

The script hadn’t had the problem before, but then I had a report that the code to convert the last activity date for a team failed. The date seemed OK (21-Sept-2023) and the code worked perfectly on my workstation, but failed elsewhere when the date format defined for PowerShell in the user’s chosen culture didn’t recognize 21-Sep-2023 as a valid date. The solution is to define the expected input string format for the cast. Here’s the current code:

[datetime]$LastItemAddedToTeams = [datetime]::ParseExact($ThisTeamData.LastActivity, "dd-MMM-yyyy", $null)

Hopefully, this fix will resolve the issue no matter what local culture is chosen for PowerShell. The learning here is that Microsoft 365 and Graph APIs output dates in different formats so some care is needed to handle dates properly, especially if you expect code to run in different countries.

Learnings for the Groups and Teams Activity Report

If I was a professional PowerShell developer, I probably would have taken more care with date objects. However, no one can be blamed when their scripts misbehave due to problems introduced by Microsoft. It’s a warning to keep an eye out for changes – or to build better error handling into scripts.

Speaking of which, I might convert the Groups and Teams Activity Report script to use the Microsoft Graph PowerShell SDK. This would simplify matters because the code wouldn’t have to deal with pagination and renewing access tokens (because the script is used to process reports for tens of thousands of groups, it can take hours to run, and the access token must be renewed hourly). Simpler code is easier to maintain… or so the theory goes.


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/03/22/groups-and-teams-activity-report/feed/ 0 64226
How to Convert an Entra ID External Account to Internal https://office365itpros.com/2024/03/21/convert-to-internal-user/?utm_source=rss&utm_medium=rss&utm_campaign=convert-to-internal-user https://office365itpros.com/2024/03/21/convert-to-internal-user/#comments Thu, 21 Mar 2024 08:00:00 +0000 https://office365itpros.com/?p=64207

Use the Entra Admin Center or PowerShell to Convert to Internal User Accounts

Many Microsoft 365 tenants support a mixture of internal and external accounts. Internal accounts are member accounts that authenticate with the tenant. External accounts authenticate somewhere else, such as another Microsoft 365 tenant. The most common form of external accounts found in Microsoft 365 tenants are guest accounts created to participate in team or group memberships or for sharing. Other examples are the accounts synchronized into a tenant directory through membership of a Microsoft 365 multi-tenant organization (MTO).

The Convert to Internal User Feature (Preview)

A recent preview feature introduced by the Entra ID team allows organizations to convert accounts from external to internal. In effect, the code takes an external account identity, breaks the link to the original account, and makes the account local. The original account remains intact and is not removed, so some cleanup might be necessary to remove duplicates.

The Entra admin center includes an option in the user account overview to convert the account (Figure 1). The option is only available for external accounts.

The convert to internal user option in the Entra admin center.

Convert to internal user
Figure 1: The convert to internal user option in the Entra admin center

Selecting the option displays a dialog to allow the administrator to specify the user principal name, password, and (optionally) email address for the converted account (Figure 2).

Adding properties to convert an external user to be internal.
Figure 2: Adding properties to convert an external user to be internal

The conversion process preserves the account’s membership in Microsoft 365 groups and teams. However, some background synchronization must happen to make sure that all workloads recognize that the account is now internal. In most cases, signing out of all services should be enough (you can force this by revoking the account’s access token), but you might need to remove the Teams cache to force a rebuild of team rosters.

Convert to Internal User Accounts with PowerShell

Being able to convert an external account to internal through a portal is great for a one-off operation, such as when a contractor joins the organization as a permanent employee. It’s not so good when dealing with large-scale account changes like those that happen during corporate mergers and acquisitions. This is where the automation capabilities of PowerShell are invaluable.

The steps needed to convert an external account to internal with PowerShell are straightforward:

  • Connect to the Microsoft Graph. My example uses an interactive Microsoft Graph PowerShell SDK session.
  • Find the source account and check that it is an external identity. My test is that an account is external if the email address for the account doesn’t belong to any of the tenant’s registered domains.
  • Figure out the new user principal name, email address, and a temporary password. Create a password profile to force the user to create a new password the next time they sign in.
  • Call the convertExternalToInternalMemberUser API to make the change. The API is currently accessed through the beta endpoint. The new User-ConvertToInternal.ReadWrite.All Graph permission allows access to the API.
  • If everything works, update the account’s Mail property and revoke the account’s access token.

Here’s the code that does most of the work:

$PasswordProfile = @{}
$PasswordProfile.Add('password',$NewPassword)
$PasswordProfile.Add('forceChangePasswordNextSignIn', $true)

# Create the parameters to convert the account
$NewAccountParameters = @{}
$NewAccountParameters.Add('userPrincipalName', $NewUserPrincipalName)
$NewAccountParameters.Add('passwordProfile', $PasswordProfile)

Write-Host "Switching the account to be internal..."
# Switch the account to make it internal
$Uri = ("https://graph.microsoft.com/Beta/users/{0}/convertExternalToInternalMemberUser" -f $SourceUser.Id)
$NewAccount = Invoke-MgGraphRequest -Uri $Uri -Body $NewAccountParameters -Method POST -ContentType "application/json"

# If we get back some account details, check to make sure that they're what we expect
If ($NewAccount) {
    $CheckNewAccount = Get-MgUser -UserId $SourceUser.Id -Property id, displayName, userPrincipalName, UserType
    If ($CheckNewAccount.usertype -eq 'Member' -and $CheckNewAccount.UserPrincipalName -eq $NewUserPrincipalName) {
        Update-MgUser -UserId $CheckNewAccount.Id -Mail $NewUserPrincipalName
        $RevokeStatus = Revoke-MgUserSignInSession -UserId $CheckNewAccount.Id
        Write-Host ("{0} is now a {1} account" -f $CheckNewAccount.UserPrincipalName, $CheckNewAccount.userType)
        Write-Host ("The temporary password for the account is {0}" -f $NewPassword)
        Write-Host ("Remember to assign some licenses to the converted account and to remove it from the previous source.")
    }
}

You can download the full script from GitHub.

Some Cleanup Necessary

Being able to switch a user account from external to internal is a useful feature. Remember that some cleanup is necessary to make the newly switched account a full member of the organization. It’s important to assign licenses to the account after its conversion as otherwise the account won’t be able to access Microsoft 365 services. In addition, some adjustments might be necessary to ensure that the account properties are fully populated so that the Microsoft 365 profile card displays correct information and functionality like dynamic groups and dynamic administrative units pick up the new account as appropriate.


Learn more about how the Entra ID and the rest of the Microsoft 365 ecosystem 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/2024/03/21/convert-to-internal-user/feed/ 5 64207
Does Microsoft Care about SharePoint Online PowerShell? https://office365itpros.com/2024/03/19/sharepoint-online-powershell/?utm_source=rss&utm_medium=rss&utm_campaign=sharepoint-online-powershell https://office365itpros.com/2024/03/19/sharepoint-online-powershell/#comments Tue, 19 Mar 2024 08:00:00 +0000 https://office365itpros.com/?p=64133

No Evidence that Microsoft Cares as Pnp.PowerShell Fills the Gap

SharePoint Online PowerShell

I last wrote about the state of SharePoint Online PowerShelll in 2020. At the time, I focused on Microsoft’s PowerShell module (Microsoft.Online.SharePoint.PowerShell), which is downloadable from the PowerShell Gallery. Based on the gallery statistics, the module is popular as each version attracts hundreds of thousands of downloads. Microsoft also updates the module monthly. On the surface, everything seems wonderful, and the module is in rude health.

If only this was true, but it’s not. It’s true that Microsoft updates the module to add tenant settings to control new features as they appear (like request files), but there doesn’t seem to be a coordinated plan about how Microsoft plans to support management of SharePoint Online through PowerShell.

Lack of Progress with Graph API

In 2022, Microsoft released the initial (beta) version of a Graph API to access and update SharePoint tenant settings. Apart from supporting the SharePoint settings API through the production (V1.0) endpoint, Microsoft doesn’t seem to have made much progress with the API since 2020. At least, the same set of tenant settings are visible two years on.

On the upside, SharePoint Online tenant settings are accessible using the Microsoft Graph PowerShell SDK. For instance, the Get-MgAdminSharepointSetting cmdlet reports the supported settings:

Connect-MgGraph -NoWelcome -Scopes SharePointTenantSettings.Read.All

Get-MgAdminSharepointSetting | Format-List

AllowedDomainGuidsForSyncApp                    : {}
AvailableManagedPathsForSiteCreation            : {/sites/, /teams/}
DeletedUserPersonalSiteRetentionPeriodInDays    : 60
ExcludedFileExtensionsForSyncApp                : {*.rar, *.zip}
Id                                              :
IdleSessionSignOut                              : Microsoft.Graph.PowerShell.Models.MicrosoftGraphIdleSessionSignOut
ImageTaggingOption                              : enhanced
IsCommentingOnSitePagesEnabled                  : True
IsFileActivityNotificationEnabled               : True
IsLegacyAuthProtocolsEnabled                    : True
IsLoopEnabled                                   : True
IsMacSyncAppEnabled                             : True
IsRequireAcceptingUserToMatchInvitedUserEnabled : True
IsResharingByExternalUsersEnabled               : False
IsSharePointMobileNotificationEnabled           : True
IsSharePointNewsfeedEnabled                     : False
IsSiteCreationEnabled                           : True
IsSiteCreationUiEnabled                         : True
IsSitePagesCreationEnabled                      : True
IsSitesStorageLimitAutomatic                    : True
IsSyncButtonHiddenOnPersonalSite                : False
IsUnmanagedSyncAppForTenantRestricted           : False
PersonalSiteDefaultStorageLimitInMb             : 5242880
SharingAllowedDomainList                        : {Microsoft.com…}
SharingBlockedDomainList                        : {Gmail.com}
SharingCapability                               : externalUserAndGuestSharing
SharingDomainRestrictionMode                    : none
SiteCreationDefaultManagedPath                  : /sites/
SiteCreationDefaultStorageLimitInMb             : 26214400
TenantDefaultTimezone                           : (UTC) Dublin, Edinburgh, Lisbon, London
AdditionalProperties                            : {[@odata.context, https://graph.microsoft.com/v1.0/$metadata#admin/sharepoint/settings/$entity]}

And the Update-MgAdminSharepointSetting cmdlet updates a setting:

$Body = @{}
$Body.Add("IsResharingByExternalUsersEnabled",$true)
Update-MgAdminSharepointSetting -BodyParameter $Body

SharePoint Online PowerShell is Windows PowerShell

Getting back to the PowerShell module, Microsoft has not updated it to support PowerShell 7. This might not be a problem if you always use Windows, but it does limit platform coverage. Attempting to load and use the module with PowerShell 7 usually fails, especially when multifactor authentication is involved.

The Community Approach to SharePoint Online PowerShell

This brings me to the Pnp.PowerShell module, also available from the PowerShell gallery. Based on the download numbers, Pnp.PowerShell seems to be four to five times more popular than the official Microsoft SharePoint Online module. This state is probably due to:

  • Development driven by a committed set of community advocates.
  • Wider coverage of SharePoint commands. The module spans over 650 cmdlets while the Microsoft.Online.SharePoint.PowerShell module has 250. Part of the reason for the dramatic difference in cmdlet count is that Pnp.PowerShell dips into other Microsoft 365 workloads associated with SharePoint Online like Teams, Planner, Flow, and Entra ID. Another is that Pnp.PowerShell includes cmdlets to create objects like files in SharePoint Online document libraries (here’s an example) that aren’t within the scope of the administrator-centric SharePoint module
  • Frequent updates to introduce new features and support for changes within SharePoint Online.
  • Solid documentation.

Because Pnp.PowerShell is a community effort rather than something produced by Microsoft, some organizations are reluctant to use it. They fear that support for bug fixes will be limited or that some catastrophic bug will creep in due to a lack of testing. My experience is that the community developers are very responsive and do better testing than many Microsoft development groups (an example being the recent bugs afflicting the Microsoft Graph PowerShell SDK). There’s no reason to avoid using Pnp.PowerShell, subject to the normal requirements to test new versions and ensure that every cmdlet does what you expect.

Moving Forward with SharePoint Online PowerShell

Pnp.PowerShell wins the contest for popularity and coverage when it comes to PowerShell access to SharePoint Online. The official module appears stuck in time, and I know of no advocate within Microsoft who wants to bring it forward. The Graph tenant settings API started but hasn’t done much since 2022. Perhaps Microsoft should simply take Pnp.PowerShell over? Or maybe not, because then we might have three modules in a static state instead of two.


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/03/19/sharepoint-online-powershell/feed/ 3 64133
Despite the Doubters, Microsoft 365 Administrators Should Continue Using PowerShell https://office365itpros.com/2024/03/08/microsoft-365-powershell/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-powershell https://office365itpros.com/2024/03/08/microsoft-365-powershell/#comments Fri, 08 Mar 2024 01:00:00 +0000 https://office365itpros.com/?p=63995

Microsoft 365 PowerShell Automates Management Operations Quickly, Easily, and Cheaply, No Matter What an MVP Says

Why Microsoft MVPs shouldn't endorse ISV software products.

Microsoft 365 PowerShell

My strong view that it’s often a bad idea for Microsoft MVPs to endorse ISV products (with or without payment) was reinforced by a recent article titled “6 Reasons Why Identity Admins Should Retire Scripting” written by Sander Berkouwer (described as an Extraordinary Identity Architect in his LinkedIn profile).

Update: The original article is no longer available on the ENow Software site. It seems like they pulled it soon after this article appeared.

Update 2 (March 12): ENow Software republished an amended article. It still contains inaccuracies and demonstrates a lack of knowledge and awareness about the role and function of the Microsoft Graph PowerShell SDK.

The article is a thinly disguised pitch for ENow Software’s App Governance Accelerator product. Basically, Berkouwer says that Entra ID administrators (who are often the same people as Microsoft 365 tenant administrators) should eschew PowerShell and leave management automation to ISVs. It’s a ridiculous position that is insulting to the many IT professionals who work with PowerShell daily.

I’m all for strong ISV participation in the market and have worked with ENow Software and other ISVs during my career. Because the cloud is a more closed environment, it’s more difficult for ISVs to find niches to exploit in the Microsoft 365 ecosystem than in on-premises environments. It’s natural for ISVs to respond by seizing every opportunity to publicize their products. In doing so, many ISVs seek the endorsement of “an expert,” like a Microsoft MVP. In my eyes, these endorsements are close to worthless.

How Microsoft 365 PowerShell Helps Administrators

The major theme developed by Berkouwer is to question whether writing PowerShell scripts is a good use of administrator time and lays out six “reasons to retire this practice.” My perspective is that understanding how to use PowerShell is a fundamental skill for Microsoft 365 administrators to acquire. You don’t have to be proficient, but PowerShell helps administrators to understand how Microsoft 365 works. This is especially true of using Graph APIs, including through the Microsoft Graph PowerShell SDK.

Here are the six reasons advanced for why administrators shouldn’t spend time writing scripts.

Microsoft renamed Azure AD: Including this as a reason to stop writing PowerShell scripts is simply silly and undermines the author’s credibility. Product rebranding happens. The important point is what a product does. Should we stop using the Microsoft Purview solutions simply because Microsoft decided to bring them all under the Purview brand? Or perhaps Yammer customers should have fled when Microsoft renamed it as Viva Engage?

Don’t trust random scripts you find on the internet… “written by everyone’s favorite Microsoft Most Valuable Professional.” This has been the advice given about PowerShell scripts since 2006. It is not a blinding insight into new knowledge. Great care is required with any code downloaded from the internet, including any of the 250-odd scripts available from the Office 365 for IT Pros GitHub repository.

Downloaded code, even written by a favorite MVP, should never be run before it is thoroughly checked and verified. But it’s also true that many scripts are written to demonstrate principles of how to do something instead of being fully worked-out solutions. Before people put PowerShell code into production, it must meet the needs and standards of the organization. For instance, developers might tweak a script to add functionality, improve error handling, or log transactions. Michel de Rooij addresses some of these challenges in his Practical365.com column.

Berkouwer’s assertion ignores the enormous value derived from how the community shares knowledge, especially at a time when tenants are upgrading scripts to use the Graph SDK. Without fully worked out examples, how could people learn? I learned from examples when PowerShell first appeared with Exchange Server 2007 in 2006. I still learn from examining PowerShell scripts written by others today. And many maintain the scripts shared through GitHub repositories.

The greater use of GitHub repositories and their inbuilt facilities to report and resolve issues helps people to share and maintain code. In addition, GitHub Copilot helps developers reuse PowerShell code that’s stored in GitHub to develop new solutions. The net is that it is easier than ever before to develop good PowerShell code to automate tenant operation.

Least Priviliged Principle. It’s true that the changeover from older modules like MSOL and AzureAD to the Graph SDK brings a mindset change. Instead of assuming that you can do anything once you connect to a module with an administrator account, some extra care and thought is needed to ensure that you use the right Graph permissions (delegated or application). Right permission means the lowest privileged permission capable of accessing the data a script works with. Yes, this is a change, but finding out what Graph permissions to use is not a difficult skill to master and I utterly fail to see why Berkouwer considers it to be such a big problem. If anything, adopting the least privileged principle drives better security practice, and that’s goodness.

The only constant in life is change. Yes, change is ongoing all the time across the Microsoft 365 ecosystem, but it is untrue that people can’t keep pace with that change. Microsoft publishes change notifications and although they’re not perfect and don’t include everything that changes (like Entra ID updates), a combination of the message center notifications (perhaps leveraging the synchronization of message center information to Planner) and RSS feeds to track important Microsoft blogs is all that’s needed.

There’s no evidence to suggest that ISVs are any better at tracking change within Microsoft 365. If anything, ISV development cycles, the need for testing, and customer desire for supportable products can hinder their ability to react quickly to changes made by Microsoft.

Maintaining and updating scripts. I’m unsure why the European Cyber Resilience Act is introduced into the discussion. It seems like some FUD thrown into the debate. PowerShell scripts are like any other code used in production. They must have a designated owner/maintainer and they should be checked as new knowledge becomes available, just like programs written using C# or .NET must be checked when Microsoft releases updates. ISVs have the same problems of code maintenance, so handing a task over to an ISV might resolve a tenant of some responsibility without being a magic bullet.

Zero trust. “When you run scripts for monitoring and security reporting purposes, they must provide instantaneous, useful information.“ Well, it would be nice if tenants always had instantaneous data to process but the singular fact is that tenants and ISVs share the same access via public APIs to information like usage reports, audit logs, license data, sign-in logs, workload settings, and so on. For instance, the data used to create a licensing report comes from Entra ID user accounts and a Microsoft web page. The data that the ENow App Governance Accelerator product comes from Entra ID and is easily accessed and reported using PowerShell (here’s an example).

ISVs and PowerShell Access the Same Microsoft 365 Data

ISVs don’t have magic back doors to different information that suddenly throws new light onto the inner functioning of Microsoft 365. ISVs might develop innovative ways of using information and use those methods to create new features, but that’s not the instantaneous, useful information that Berkouwer wants.

If Microsoft 365 tenants want to run PowerShell scripts to check what turns up in audit and other logs, a simple solution exists in the shape of Azure Automation runbooks executed on a schedule. It’s not hard to translate a regular PowerShell script to execute in Azure Automation and the support for managed identities in the major Microsoft 365 modules makes authentication for runbooks easy and highly secure. Here’s an example of using Azure Automation to create a daily risk report for Microsoft 365 tenants.

No Reason to Dump Microsoft 365 PowerShell

The solution is emphatically not to dump PowerShell scripts for an ISV product. Well-written PowerShell is as robust and secure as any ISV product. It’s worth noting here that Microsoft uses tons of PowerShell in its operations.

No single off-the-shelf product can cater for the different aspects of Microsoft 365 tenant management. ISV products have bugs, need to be supported, sometimes do a worse job than tenant-developed scripts, and no guarantee exists that the products will keep up with changes within Microsoft 365. Deploying ISV products also involves additional costs to pay for licenses and support.

On the other hand, ISV products are usually developed and maintained by very experienced professionals who are dedicated to that task (and don’t have to worry about day-to-day tenant management), so they have the time and space to think more deeply about what their product does.

ISVs Should Compete on their Merits, Not with False Arguments

I have the height of respect for Microsoft 365 ISVs and the products they create and support. Those of us who have worked in this space understand the challenges of running ISV operations and how difficult it is to succeed in a very competitive market. Product reviews do help, but only when the review focuses on explaining the strengths and weaknesses of a product after the reviewer spends a reasonable amount of time getting to understand the technology and how it fits into the ecosystem it works in.

Many ISV offerings work extremely well and do a good job of filling gaps left by Microsoft. I applaud the innovation I see in many ISV products and how they add real value to the Microsoft 365 ecosystem. ISVs do not need to be supported by artificial arguments, especially laughable advice to avoid using one of the most valuable tools available in tenant management toolboxes. If Sander would like some help understanding the usefulness of the Microsoft Graph PowerShell SDK, I’ll be delighted to help if he attends my session at the Microsoft 365 Conference in Orlando.


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/03/08/microsoft-365-powershell/feed/ 3 63995
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
Why You Should Not Upgrade to Microsoft Graph PowerShell SDK V2.14 https://office365itpros.com/2024/02/20/microsoft-graph-powershell-sdk-bug/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-powershell-sdk-bug https://office365itpros.com/2024/02/20/microsoft-graph-powershell-sdk-bug/#comments Tue, 20 Feb 2024 01:00:00 +0000 https://office365itpros.com/?p=63786

Problem Found in Microsoft Graph PowerShell SDK V2.13.1 Worsens in V2.14 – But Now We Have V2.15 So All is Well

Usually, I recommend that people upgrade their workstations when new versions of the Microsoft Graph PowerShell SDK appear. I cannot do this for Version 2.14, which Microsoft released on February 17, 2024. The reason is that V2.14 makes a problem that appeared in V2.13.1 (Figure 1) even worse. The problem is described here. It only affects SDK cmdlets and does not impact regular Graph queries.

The spurious output generated by Microsoft Graph PowerShell SDK V2.13.1.
Figure 1: The spurious output generated by Microsoft Graph PowerShell SDK V2.13.1

According to Microsoft, they are working on a fix to make response headers optional. From the wording of the GitHub report, it seems like Microsoft introduced the issue by making a change for cmdlets to output response headers without thinking about how this might affect customers. No date is given when the fix might be available.

Update: Microsoft has released V2.14.1 of the Microsoft Graph PowerShell SDK to fix the reported problems. After downloading and installing V2.14.1 for both interactive and background (Azure Automation) use, it appears that the issue with cmdlets returning spurious output is fixed.

According to the PowerShell Gallery statistics, 22,936 people downloaded the flawed V2.14 and 105,907 downloaded the problematic 2.13.1. Let’s hope that their scripts were not affected by the bug.

Microsoft obviously has a problem testing the SDK before release. In addition to 2.14.1, they have issued four other point releases to fix problems discovered soon after releasing a new version (2,13.1, 2.11.1, 2.9.1, and 2.6.1). This is not evidence of high-quality software engineering. The developers need to improve testing and not rush new versions into production without ensuring that new software will impact customers.

Update (22 February 2024): Microsoft has released V2.15 of the SDK. They say that this release includes fixes two specific bugs that they wanted to close. Details are posted here. Although it’s laudable to close bugs, issuing a succession of new releases and point updates doesn’t create the impression of stability and robustness that the SDK should project.

The Case of the Unwanted ResponseHeaders Object

In a nutshell, instead of returning a single object when cmdlets like Get-MgUser and Get-MgGroup use a valid identifier to find an object, the cmdlets return an array containing an unwanted ‘ResponseHeaders’ object. Here’s an example:

Get-MgUser -UserId Terry.Hegarty@office365itpros.com

DisplayName   Id                                   Mail                              UserPrincipalName
-----------   --                                   ----                              -----------------
Terry Hegarty 75ba0efb-aed5-4c0b-a5de-be5b65187c08 Terry.Hegarty@office365itpros.com Terry.Hegarty@office365itpros.c…

ResponseHeaders : {0bde6f40-9291-4457-9da8-59484710f11a}

You might not notice this problem if you format the output, but there is an extra line in the output:

Get-MgUser -UserId Terry.Hegarty@office365itpros.com | Format-Table DisplayName, userPrincipalName

DisplayName   UserPrincipalName
-----------   -----------------
Terry Hegarty Terry.Hegarty@office365itpros.com

Examining what is returned, we see that it is an array with two items:

$User = Get-MgUser -UserId Terry.Hegarty@office365itpros.com
$User.GetType()

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

$User.count
2

The problem doesn’t appear if you use a filter to find objects. Only a lookup against the object identifier is affected:

Get-MgUser -Filter "displayname eq 'Terry Hegarty'"

DisplayName   Id                                   Mail                              UserPrincipalName
-----------   --                                   ----                              -----------------
Terry Hegarty 75ba0efb-aed5-4c0b-a5de-be5b65187c08 Terry.Hegarty@office365itpros.com Terry.Hegarty@office365itpros.c…

The same behavior is observed with other cmdlets. Here’s an example with the Get-MgGroup cmdlet:

Get-MgGroup -GroupId '78b47932-b35f-4b26-94c2-3228cb234b07'

DisplayName   Id                                   MailNickname Description            GroupTypes
-----------   --                                   ------------ -----------            ----------
PL Test Group 78b47932-b35f-4b26-94c2-3228cb234b07 pltestgroup  Preservation Lock Test {Unified}

ResponseHeaders : {5eec6516-3352-4c98-a7de-f4231a2b0c4d}

Get-MgGroup -filter "Displayname eq 'PL Test Group'"

DisplayName   Id                                   MailNickname Description            GroupTypes
-----------   --                                   ------------ -----------            ----------
PL Test Group 78b47932-b35f-4b26-94c2-3228cb234b07 pltestgroup  Preservation Lock Test {Unified}

The problem is significantly worse in V2.14 because the cmdlets now return a hash table containing what appears to be debug information (you see the same information if you run Get-MgUser or Get-MgGroup with the -Debug switch). Here’s an extract of some of the information returned.

Date                           {Mon, 19 Feb 2024 11:37:26 GMT}
Strict-Transport-Security      {max-age=31536000}
OData-Version                  {4.0}
x-ms-ags-diagnostic            {{"ServerInfo":{"DataCenter":"North Europe ","Slice":"E","Ring":"5","ScaleUnit":"002…
Cache-Control                  {no-cache}
Transfer-Encoding              {chunked}

The issue occurs in many other SDK cmdlets. I focus on the user and group cmdlets here because they are possibly the highest-profile cmdlets in the SDK.

Don’t Upgrade for Now

The bottom line is that you should not update to a new version of the Microsoft Graph PowerShell SDK until Microsoft fixes the problem and removes the spurious output from cmdlets. The last good version of the SDK is V2.12. If you do upgrade to a newer version, be prepared to check scripts to make sure that code runs as normal.

In saying this, I note that the problem has not affected any script that I have worked on since installing V2.13.1 about ten days ago. The reason is probably that most of my scripts use filters to fetch user or group objects for processing. However, some scripts do run Get-MgUser or Get-MgGroup to process individual objects and that’s where the problem will arise.

Final Countdown for MSOL and AzureAD Modules

This problem happened at a sensitive time for the Microsoft 365 PowerShell community. On March 30, 2024, Microsoft will finally retire the old AzureAD and MSOL modules. This process has been ongoing for quite some time and Microsoft has already disabled the cmdlets that deal with license management assignments.

With 38 days to go (at the time of publication) before the old modules retire, the Microsoft Graph PowerShell SDK should be in good health and ready to accommodate everyone who needs to upgrade scripts. Introducing a flaw affecting the cmdlets that access user and group objects which are likely targets for conversion from old scripts is not good news. Let’s hope the fix arrives soon and Microsoft is more careful about making changes to the SDK that could break customer scripts in the future.


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/02/20/microsoft-graph-powershell-sdk-bug/feed/ 5 63786
Tracking Licensing Costs for Microsoft 365 Tenants https://office365itpros.com/2024/02/14/microsoft-365-licensing-report/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-licensing-report https://office365itpros.com/2024/02/14/microsoft-365-licensing-report/#comments Wed, 14 Feb 2024 01:00:00 +0000 https://office365itpros.com/?p=63686

Microsoft 365 Licensing Report Details Costs Per User to Find Optimizations

Recently, I released an update to my Microsoft 365 Licensing Report PowerShell script to include the ability to assign costs to user accounts. The idea is to give administrators information about how much the cumulative annual license charges are for each account. Combining cost data with insight about account activity in a tenant (generated with the user activity report script or by reference to the individual workload usage reports in the Microsoft 365 admin center), administrators can figure out if users have the right licenses they need to work and no licenses are assigned to inactive accounts.

Managing the cost of Office 365 and Microsoft 365 licenses has always been important. As Microsoft puts more focus on driving revenue through high-priced add-ons such as Teams Premium ($120/year) and Copilot for Microsoft 365 ($360/year), it’s even more essential to keep close tabs on license assignments. There’s no point in assigning a Copilot license to someone who’s inactive or whose usage pattern indicates that they might not take advantage of the license. No one is rewarded for overspending on licenses.

Adding Cost by Department and Cost by Country to the Microsoft 365 Licensing Report

Almost immediately after releasing the updated script, calls came in to ask if it was possible to generate an analysis of licensing cost by country and by department. My initial response was “sure” and I set to figuring out the best way to implement the change.

Because the report script tracks license costs per user, the simple method is to:

  • Find the sets of departments and countries in user accounts.
  • For each department (or country), calculate the sum of license costs.
  • Include the information in the report.

The same approach works to analyze license costs for any user account property fetched by the initial Get-MgUser command at the start of the script. If the set of regular account properties don’t work for your organization, you could use an Exchange custom attribute to store the required values. For instance, you could include a cost center number in a custom attribute. Here’s how to access Exchange custom attributes with Get-MgUser. You’ll need to extract the information from the custom attribute before you can use it in the script.

The Problems Caused by Inaccurate Directory Data

The obvious problem is that sometimes the properties of user accounts don’t include a department or country. Account properties should hold accurate properties, but unfortunately this sometimes doesn’t happen because administrators fail to add properties to accounts, or a synchronization process linking a HR system to Entra ID encounters problems, or something else conspires to erode directory accuracy. The point is that inaccurate or missing user account properties result in bad license accounting.

The first order of business is therefore to validate that the account properties that you want to use for license cost reporting exist and are correct. This article explains how to detect user accounts with missing properties. Making sure that properties are accurate requires an extra level of review. The value of the country property assigned to user accounts shouldn’t change frequently, but properties like department and office might.

Reporting Licensing Costs for Country and Department

After making sure that all the necessary user account properties are in place (and accurate), the code to generate cost analyses based on department and country worked like a dream. The script also required an update to insert the new data into the output report, including warnings for administrators when costs cannot be attribute to countries or departments because of missing account properties. Figure 1 shows the result.

Costs for departments and countries shown in Microsoft 365 Licensing Report.
Figure 1: Costs for departments and countries shown in Microsoft 365 Licensing Report

The code changes are in version 1.6 of the report script, which you can download from GitHub. If you haven’t run the script before, make sure that you read the previous Practical365.com articles to understand how the script works and how to generate the two (SKU and service plan) CSV files used by the script.

Remember that this script is intended to demonstrate the principles of interacting with and interpreting Entra ID user account and license information with the Microsoft Graph PowerShell SDK. It’s not intended to be a bulletproof license cost management solution. Have fun with PowerShell!


Learn how to exploit the data available to Microsoft 365 tenant administrators (like licensing information) through the Office 365 for IT Pros eBook. We love figuring out how things work.

]]>
https://office365itpros.com/2024/02/14/microsoft-365-licensing-report/feed/ 19 63686
How Many Message Center Announcements End Up Being Delayed? https://office365itpros.com/2024/02/09/message-center-posts-sdk/?utm_source=rss&utm_medium=rss&utm_campaign=message-center-posts-sdk https://office365itpros.com/2024/02/09/message-center-posts-sdk/#comments Fri, 09 Feb 2024 01:00:00 +0000 https://office365itpros.com/?p=63615

Use the Microsoft Graph PowerShell SDK to Analyze Service Update Messages

In November 2020, I wrote an article about the number of Microsoft 365 message center posts about new features that ended up being delayed. At the time, 29.27% of message center posts needed to adjust their published date for feature availability. Being of a curious nature, I wondered if Microsoft is better at predicting when they can deliver software across the spectrum of Microsoft 365 applications.

The code I used in 2020 is now obsolete. Microsoft moved the service communication API from the old manage.office.com endpoint to the service communications Graph API and access to message center posts is through the service update message resource. Because the service communications API is a full-fledged Graph API, cmdlets in the Microsoft Graph PowerShell SDK are available to work with message center posts. For instance, the Get-MgServiceAnnouncementMessage cmdlet retrieves message center posts. This command shows how to retrieve posts for the last seven days:

$SevenDaysAgo = (Get-Date).AddDays(-7)
$CheckDate = (Get-Date($SevenDaysAgo) -format s) + "Z"  
[array]$MCPosts = Get-MgServiceAnnouncementMessage -filter "StartDateTime ge $CheckDate"

Adding the “Z” to the sortable date generated by the Get-Date cmdlet is important for the filter to work.

Updating the Code

The code written in 2020 uses a registered Entra ID app to obtain an access token and fetch the message center posts. Updating the script involved:

  • Removing the code to obtain an access token and replacing it with a call to the Connect-MgGraph cmdlet specifying the ServiceMessage.Read.All scope (permission).
  • Run the Get-MgServiceAnnouncement cmdlet with the All parameter to fetch all available message center posts.
  • The data returned for message center posts using the service communications Graph API differs from that returned by the old API. Some adjustment was necessary in the script to update property names and the content returned for some properties.
  • Addition of some code to calculate the percentage of delayed feature announcements. In 2020, this was done using Excel. The basic test for a delay is the presence of the string “(Updated)” in the title for a message center post. No attempt is made to compute the length of the delay because message center posts don’t contain a structured property with this information. Instead, information about delays is conveyed in the text. For example, “We will begin rolling out in mid-September 2023 (previously late August) and expect completion by mid-February 2024 (previously late January).

Comparing Results

In 2020, the results looked like this:

 		Notifications	Updates		Percent updated
Teams		58		22		37.93%
SharePoint	37		14		37.84%
Exchange	30		9		30%
Yammer		10		4		44.44%
Intune		8		0		—-
Power Apps	5		0		—-

On February 5, 2024, the Get-MgServiceAnnouncement cmdlet fetched 552 message center posts for my tenant. This is a higher amount than in 2020 because the tenant subscriptions now include some Microsoft 365 E5 licenses covering more apps. The number of message center posts available in a tenant vary depending on the active subscriptions that exist within the tenant.

Figure 1 shows the results. Nearly a third of all message center posts are delayed. Teams remains the workload that issues most message center posts (83), but its performance in terms of avoiding delays has worsened from 38.93% to 57.24% This might be due to the transition from the classic Teams client to the new Teams client (due to be complete by the end of March), or it might be that the Teams product managers have real difficulty in predicting when software might be ready for deployment.

Percentage of delayed message center posts by workload.
Figure 1: Percentage of delayed message center posts by workload

Some message center posts cover multiple workloads and it’s hard to know where the responsibility lies for a delay. The data is therefore indicative rather than definitive. To be sure about where delays lie, you’d need to examine the text of each message center post and extract and collate the details.

You can download the updated script from GitHub.

Easier to Work with Message Center Posts

Being able to work with service communication data through Microsoft Graph PowerShell SDK cmdlets makes the information more accessible than before. Some of the improvements introduced by Microsoft for message center posts since 2020 aren’t available. The relevance property appears to have disappeared from the Microsoft 365 admin center and the number of active users for a workload, which does show up in the message center, is missing from the properties returned by the SDK cmdlet. But the rest of the information you might want is available and ready to be sliced and diced as you want.


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/09/message-center-posts-sdk/feed/ 2 63615
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
New MSIdentityTools Cmdlet to Report OAuth Permissions https://office365itpros.com/2024/02/05/export-msidappconsentgrantreport/?utm_source=rss&utm_medium=rss&utm_campaign=export-msidappconsentgrantreport https://office365itpros.com/2024/02/05/export-msidappconsentgrantreport/#respond Mon, 05 Feb 2024 00:06:00 +0000 https://office365itpros.com/?p=63591

The Export-MsIdAppConsentGrantReport Cmdlet Makes it Easier for Tenant Administrators to Track OAuth Permissions for Apps

As readers of my articles know, I have often discussed the topic of monitoring and checking OAuth permissions assigned to apps, usually using the Microsoft Graph PowerShell SDK to fetch and interpret permissions in a way that makes sense to tenant administrators. A recent example is an article about how to generate a report about OAuth permissions.

The need to understand the permissions assigned to apps was underscored by the recent Midnight Blizzard attack on Microsoft corporate mailboxes. The fact that an OAuth app can exist with permissions necessary to exfiltrate email and attachments from mailboxes without Microsoft’s administrators and security professionals detecting its presence for several months, highlights the challenge facing every tenant administrator.

A New MsIdentityTools Cmdlet

And that’s why the creation of the Export-MsIdAppConsentGrantReport cmdlet is such welcome news. Not every tenant administrator can master the PowerShell cmdlets used to interrogate apps or understand the data that comes back. It’s a lot easier when a single cmdlet does the job. Export-MsIdAppConsentGrantReport is part of the MSIdentity Tools module, developed and maintained by members of the Entra ID product group to help with different aspects of directory management.

You can get version 2.0.52 of the MsIdentityTools module by installing it from the PowerShell gallery.

Install-Module -Name MSIdentityTools -Force -Scope AllUsers -RequiredVersion 2.0.52

Because of a dependency, the MSIdentityTools module also installs the Microsoft.Graph.Authentication module (part of the Microsoft Graph PowerShell SDK). Oddly, it installs version 2.9.1 of the Authentication module instead of the current version (2.12). Apart from occupying some extra disk space, no great harm is done and MSIdentityTools is happy to use 2.12.

Running Export-MsIdAppConsentGrantReport

Generating a report with the Export-MsIdAppConsentGrantReport cmdlet is easy. This code connects to the Microsoft Graph PowerShell SDK, imports the ImportExcel module (needed to generate an Excel worksheet), and creates the report in the form of a worksheet:

Connect-MgGraph -Scopes Directory.Read.All -NoWelcome
Import-Module ImportExcel
Export-MsIdAppConsentGrantReport -ReportOutputType ExcelWorkbook -ExcelWorkbookPath c:\temp\OAuthAppPermissionsReport.xlsx

The cmdlet uses Microsoft Graph API calls to read and analyze information about service principals. It then calls cmdlets from the ImportExcel module to generate a multi-sheet workbook. Figure 1 shows one of the sheets listing Graph and other permissions (like the right for an app to run cmdlets from the Teams PowerShell module as an administrator).

Excel worksheet generated by the Export-MsIdAppConsentGrantReport cmdlet
Figure 1: Excel worksheet generated by the Export-MsIdAppConsentGrantReport cmdlet

Even better, the Export-MsIdAppConsentGrantReport cmdlet can generate its data as a PowerShell object:

[array]$AppData = Export-MsIdAppConsentGrantReport -ReportOutputType PowerShellObjects

The reason why this facility is so good is that the cmdlet does a lot of heavy lifting to fetch information about service principals and permissions and delivers them in an array that’s easy for PowerShell scripts to consume. In effect, this eliminates a lot of code in scripts like those that I’ve written to report permission assignments. Instead of running Get-MgServicePrincipal and parsing the results to find and interpret data, developers can run Export-MsIdAppConsentGrantReport and use its output instead.

For example, this command finds the service principals that hold the Mail.Send permission. This is a high-priority permission because Mail.Send allows the app to send email from any mailbox unless limited by RBAC for Applications.

$Appdata | Where-Object Permission -match 'Mail.Send' | Format-Table ClientDisplayName, Appid, Permissiontype

ClientDisplayName                                                 AppId                                PermissionType
-----------------                                                 -----                                --------------
MalwareExample                                                    d868053d-58bc-4010-a659-23de72d14669 Application
PowerShellGraph                                                   8f005189-8c58-4fb5-a226-8851e13490cb Application
MailSendApp                                                       970e01d1-ce75-46ba-a054-4b61c787f682 Application
ExoAutomationAccount_Y6LgjDYIfPnxmFzrqdbaClsnTD/gN4BNnVMywiju5hk= 45923847-be5b-4e29-98c5-bc9ab0b5dc95 Application
ManagedIdentitiesAutomation                                       b977a222-3534-4625-980d-e2f864d3a2d5 Application
Microsoft Graph PowerShell SDK Cert                               d86b1929-b818-411b-834a-206385bf5347 Application
PnP Management Shell                                              31359c7f-bd7e-475c-86db-fdb8c937548e Delegated-AllPr…
MailSendAppDelegate                                               0fb521aa-8d32-4c0b-b124-565a1d8c4abe Delegated-AllPr…
MailSendAppDelegate                                               0fb521aa-8d32-4c0b-b124-565a1d8c4abe Delegated-AllPr…
PowerShellGraph                                                   8f005189-8c58-4fb5-a226-8851e13490cb Delegated-AllPr…
IMAP access to Shared Mailbox                                     6a90af02-6ac1-405a-85e6-fb6ede844d92 Delegated-AllPr…
Microsoft Graph Command Line Tools                                14d82eec-204b-4c2f-b7e8-296a70dab67e Delegated-AllPr…
Microsoft Graph Command Line Tools                                14d82eec-204b-4c2f-b7e8-296a70dab67e Delegated-AllPr…

Notice that some duplicates are present. These are probably due to a glitch in the cmdlet that will be squashed soon.

Because the array is a PowerShell object, you can export it in whatever format you want, including CSV, Excel, and HTML.

Not a Panacea, Just a Tool

The Export-MsIdAppConsentGrantReport cmdlet is a valuable contribution to the tenant administrator toolbox, but it’s not a silver bullet that will stop over permissioned OAuth apps. It’s also not a replacement for administrators acquiring knowledge about how Entra ID apps acquire and use permissions (application and delegated) and how to extract that information from Entra ID using Graph API requests or Microsoft Graph PowerShell SDK cmdlets. Think of Export-MsIdAppConsentGrantReport as a useful tool, no more, no less. It’s great to have.


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/2024/02/05/export-msidappconsentgrantreport/feed/ 0 63591
Microsoft Deprecates Old Exchange Audit Search Cmdlets https://office365itpros.com/2024/01/29/search-unifiedauditlog-changes/?utm_source=rss&utm_medium=rss&utm_campaign=search-unifiedauditlog-changes https://office365itpros.com/2024/01/29/search-unifiedauditlog-changes/#comments Mon, 29 Jan 2024 01:00:00 +0000 https://office365itpros.com/?p=63505

Future Focused on Unified Search Log

A January 26 post in the Microsoft Technical Community announced that Microsoft intends to retire the old cmdlets that report Exchange mailbox and administrative audit events on April 30, 2024. The cmdlets involved are Search-AdminAuditLog, Search-MailboxAuditLog, New-AdminAuditLogSearch, and New-MailboxAuditLogSearch. Microsoft says that the replacement is the Search-UnifiedAuditLog cmdlet.

Microsoft’s assertion is correct. Unlike their plan to retire the Search-Mailbox cmdlet at the end of March 2024, I think it is a good idea to deprecate the four search cmdlets because they only confuse the Microsoft 365 audit search landscape. The cmdlets appeared in Exchange 2010 as part of the introduction of audit functionality for Exchange Server. Today, the audit events gathered by Exchange Online flow into the unified audit log and there’s no need to interrogate the copies of the audit events retained in user mailboxes. The unified audit log is what is searched using the Audit Log feature in the Purview compliance portal (Figure 1).

 Running a search against the unified audit log.

Search-UnifiedAuditLog
Figure 1: Running a search against the unified audit log.

It might be the case that some old scripts exist that depend on finding mailbox or admin audit events in Exchange, but it’s relatively easy to convert those scripts to use Search-UnifiedAuditLog.

Until the Search-UnifiedAuditLog Cmdlet Changes Without Warning

At least, it would be if Microsoft didn’t change how the Search-UnifiedAuditLog cmdlet works without warning, which is what they did in late summer 2023. Unannounced and unexplained change allied to slow delivery of commitments to make some important audit events available to Office 365 E3 tenants have shaken my confidence in Search-UnifiedAuditLog recently,

Anything to do with auditing needs to be consistent and precise. As seen with unannounced change, consistency is not something that I associate with the Search-UnifiedAuditLog cmdlet. Precision is often poor too. The group that manages the flow of audit events into the unified audit log insists on consistency for the base properties, such as the timestamp, name of the operation, the user responsible for an action, and so on. Things become far murkier when it comes to the AuditData property, which holds information deemed necessary by a workload to communicate details of an action.

The Mysteries of AuditData

AuditData is a JSON-formatted structure. There’s nothing wrong with that. My objections focus on the arbitrary inclusion of information in the structure. As an example, reporting details of license assignments to Entra ID user accounts is challenging. Entra ID generates audit events, but the content of AuditData is often obscure and defies interpretation. With over 1,600 different audit events flowing into the unified audit log, insisting on coherence and clarity in all events must be like cleaning the mythical Augean stables. But without full and precise information in audit events, the unified audit log loses credibility and becomes less valuable than it could be.

I should say that I regard the unified audit log as an extraordinarily valuable source of information about what actually happens within a Microsoft 365 tenant. All tenant administrators should know how to interrogate the audit log and understand (at least roughly) what the audit events returned by a search mean. Skilled tenant administrators go deeper and use the audit log as a source of understanding for how Microsoft 365 workloads work. Not everyone has the time to master the audit log at this depth, but it’s certainly a good goal to work toward.

Remove Decrepit Cmdlets But Fix Search-UnifiedAuditLog

I have zero problem with Microsoft removing old and decrepit cmdlets from the Exchange Online management module. It’s the right thing to do. I just wish that Microsoft would fix the problems in the Search-UnifiedAuditLog cmdlet before they did anything else. Everyone who works with Microsoft 365 audit data would benefit and it would establish a solid foundation for the future. Which would be nice.


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/01/29/search-unifiedauditlog-changes/feed/ 2 63505
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
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
Managing Passwords for Entra ID Accounts with PowerShell https://office365itpros.com/2024/01/08/password-profiles/?utm_source=rss&utm_medium=rss&utm_campaign=password-profiles https://office365itpros.com/2024/01/08/password-profiles/#respond Mon, 08 Jan 2024 01:00:00 +0000 https://office365itpros.com/?p=63070

Using Password Profiles for Entra ID Accounts

Although passwordless authentication is in the future for many Entra ID accounts, the indications are that it will take time for Microsoft 365 tenants to get to the point where going passwordless is possible. The ongoing struggle to encourage tenants to adopt multifactor authentication (MFA) as the norm is one such indication. All of which means that tenant administrators will need to manage Entra ID account passwords for some time to come.

The Microsoft 365 admin center and Entra ID admin center both include facilities to reset user account passwords. The Entra ID option is effective but basic. As shown in Figure 1, Entra ID generates a temporary password and shows it to the administrator. The user must reset their password when they next sign in.

Resetting a user account password in the Entra ID admin center.

Password profiles.
Figure 1: Resetting a user account password in the Entra ID admin center

The Microsoft 365 admin center option is more flexible because the administrator can choose what password to set, whether the user must reset their password at first sign-in, and can have Microsoft 365 email the password to the administrator’s mailbox.

Nice as it is to have administrative GUIs for password management, automation through PowerShell is often more important for tenant operations. The Microsoft Graph PowerShell SDK contains capabilities to add passwords to new accounts or update passwords for existing accounts.

Generating User Account Passwords

To start, we need a password. Subject to the Entra ID password limitations, you can make up and assign any kind of password to an account. However, it’s better if the password is complex enough to provide protection until the account owner resets the password. There are many examples of password generators for PowerShell available. One thing to be aware of is that some code works for PowerShell 5 but not for PowerShell 7. For instance, the first of the three examples in this article doesn’t work when run on PowerShell 7. The other two examples do work and the last is a good basis to start with.

Adding a Password to a New User Account

To create a password for a new user account, we need a hash table to hold a “password profile.” A password profile is a Graph resource type representing password settings for an account. To create a random password, I generated it using the function described in the article mentioned above. In this case, the profile tells Entra ID the value to use to set the account password and to require the account to change the password the next time they sign in.

$NewPassword = Get-RandomPassword 8

$NewPasswordProfile = @{}
$NewPasswordProfile.Add("Password", $NewPassword)
$NewPasswordProfile.Add("ForceChangePasswordNextSignIn",$True)

The New-MgUser cmdlet takes the password profile as the value for the PasswordProfile parameter along with all the other parameters passed to create an account:

$NewUser = New-MgUser -UserPrincipalName "Ann.Conroy@office365itpros.com" `
  -DisplayName "Ann Conroy (GM Datacenters)" `
  -PasswordProfile $NewPasswordProfile -AccountEnabled `
  -MailNickName Ann.Conroy -City NYC `
  -CompanyName "Office 365 for IT Pros" -Country "United States" `
  -Department "IT Operations" -JobTitle "GM Datacenter Operations" `
  -BusinessPhones "+1 676 830 1201" -MobilePhone "+1 617 4466515" `
  -State "New York" -StreetAddress "1, Avenue of the Americas" `
  -Surname "Conroy" -GivenName "Ann" `
  -UsageLocation "US" -OfficeLocation "NYC" -PreferredLanguage 'en-US'

Because the ForceChangePasswordNextSignIn setting is true, the user can use the assigned password to sign in, whereupon Entra ID forces them to set a new password (Figure 2).

Password profile settings prompt a user to change their password.
Figure 2: A user is prompted to change their password

See this article for more information about creating new Entra ID accounts.

Updating a Password for a User Account

Updating a user account with a new password follows the same path. Create a password profile containing the parameters and run the Update-MgUser cmdlet to change the password. If you don’t want to force the user to create a new password after they sign in, make sure that the ForceChangePasswordNextSignIn setting in the password profile is false.

$PasswordProfile = @{}
$PasswordProfile.Add($NewPasswordProfile.Add("Password", $UpdatedPassword)
Update-MgUser -UserId $NewUser.Id -PasswordProfile $PasswordProfile

If you subsequently want a user to set up multifactor authentication (MFA) for their account, use a different password profile where the forceChangePasswordNextSignInWithMfa setting is $True. Don’t include a password value in the profile.

After updating the account, the next time the user attempts to sign in, Entra ID prompts them to configure an authentication method and then forces a password change. Here’s an example of a password profile to force an account to configure MFA:

$MFAResetProfile = @{}
$MFAResetProfile.Add("ForceChangePasswordNextSignIn",$true)
$MFAResetProfile.Add("ForceChangePasswordNextSignInWithMFA",$true)
Update-MgUser -UserId $UserId -PasswordProfile $MFAResetProfile

Disabling Password Expiration

Microsoft recommends that organizations do not force users to change passwords and that they disable the requirement to change passwords in the password expiration policy (accessed through the Security and Privacy tab of Org settings in the Microsoft 365 admin center). This setting applies to all user accounts. You can disable password expiration for an account as follows:

Update-MgUser -UserId Ann.Conroy@Office365itpros.com -PasswordPolicies DisablePasswordExpiration

Disabling password expiration isn’t something I would do without the additional protection afforded by MFA, especially for accounts holding administrative roles. Microsoft’s initiative to roll out managed conditional access policies to eligible tenants (those with Entra ID premium licenses) is yet another attempt to increase the percentage of accounts protected by MFA. Expect to see more efforts in this space as 2024 develops.


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/01/08/password-profiles/feed/ 0 63070
Reporting Entra ID Admin Consent Requests https://office365itpros.com/2023/12/22/admin-consent-requests/?utm_source=rss&utm_medium=rss&utm_campaign=admin-consent-requests https://office365itpros.com/2023/12/22/admin-consent-requests/#comments Fri, 22 Dec 2023 01:00:00 +0000 https://office365itpros.com/?p=62930

Use PowerShell to Find and Report Details of Admin Consent Requests

Dinesh asked “How can I generate a report of Admin Consent Requests received by Entra ID? I’m specifically looking for information such as who sent the consent request, which application was involved, what API permissions the application requested, and how many users have already requested the consent.”

I was busy and didn’t pay too much attention to the question apart from offering some suggestions about using Fiddler (or even Graph X-Ray) to see what requests the Entra ID admin center generated. Like in many situations with Microsoft 365, the key to starting a PowerShell script is to find out what cmdlet to fetch information with.

In any case, I was delighted when Dinesh reported that he had found the necessary cmdlet (Get-MgIdentityGovernanceAppConsentRequest from the Microsoft Graph PowerShell SDK) to answer his question. It’s always great (and far too rare) when someone who asks a question goes ahead to do the necessary research to answer their own question.

Workflow for Admin Consent Requests

Administrator consent requests are an Entra ID workflow to allow users to request administrators to grant consent for enterprise applications that they want to use. You do not want end users to grant consent for applications to access data, but you also don’t want to get in the way of people doing real work. The answer is to enable the workflow to permit users to submit requests for administrator approval.

When the workflow is active, when users attempt to use an enterprise application with permissions that are not yet approved, Entra ID prompts the user to request approval. Figure 1 shows what happens when a user attempts to sign into the Microsoft Technical Community.

A user requests consent for permissions for an application.

Admin consent request.
Figure 1: A user requests consent for permissions for an application

The first time this happens in a tenant, the application attempts to create a service principal as its representation in the tenant. This cannot happen until consent is gained for the permissions it needs. In this case, the user cannot grant consent, so Entra ID routes the request to the users identified as approvers. Requests arrive via email (Figure 2). The user who generates the request also receives email notification that their request is under review.

Email notification to administrator seeking consent for application permissions.
Figure 2: Email notification to administrator seeking consent for application permissions

Oddly, the request email shows the alternative email address for the requestor instead of their primary SMTP address. This might be a glitch. In any case, when the reviewer opens the request in the Entra ID admin center, they see details of the application (Figure 3). To approve the request, they must sign in to see the requested permissions and proceed to give or refuse consent.

Reviewing a user request for consent for application permissions.
Figure 3: Reviewing a user request for consent for application permissions

The user who generates a request receives an email notification to tell them about the reviewer’s decision. Overall, it’s a simple but effective workflow.

The Code

Dinesh’s code works and is a good example of extracting and processing Entra ID information. I reworked it a little to add a check for high-profile permissions that should draw additional attention from administrators. These permissions include the ability to read everything from the directory, access all users, groups, sites, and so on. The data returned for consent requests includes some user details (user principal name and identifier). I added a call to Get-MgUser to retrieve other details that might be useful such as their department, job title, and country.

You can download the script from GitHub. Normal caveats apply – better error checking and formatting of the output would be beneficial. Nevertheless, the code proves the principles involved in using PowerShell to retrieve and process admin consent requests.

The Power of Community

I receive many requests for assistance, some of which are along the lines of “please write a script for me.” I ignore these requests because I am not in the business of doing work that other people should do for themselves. It’s always better when someone works out how to accomplish a task using their own brainpower, just like Dinesh did.


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/2023/12/22/admin-consent-requests/feed/ 10 62930
Entra ID Improves Registered App Security https://office365itpros.com/2023/12/11/app-instance-property-lock/?utm_source=rss&utm_medium=rss&utm_campaign=app-instance-property-lock https://office365itpros.com/2023/12/11/app-instance-property-lock/#comments Mon, 11 Dec 2023 01:00:00 +0000 https://office365itpros.com/?p=62783

Changes to App Instance Property Lock and Sign-In Audience

In March 2023, I wrote about a preview feature that allows application developers to lock the properties of service principal objects using the app instance property lock. That feature is now embedded in Entra ID and according to a recent “what’s new in Entra ID” post in the Microsoft Technical Community, “starting March 2024, new applications created using (the) Microsoft Graph application API will have “App instance lock” enabled by default.”

The same post also says that the default sign-in audience for new Entra ID apps will be “AzureADMyOrg” (just the owning tenant) rather than “AzureADandPersonalMicrosoftAccount.” That’s a good idea because most Entra ID apps are created for exclusive use within a tenant.

Both changes are intended to reduce the potential attack surface exposed through Entra ID apps. The first limits what administrators can do to service principals created for enterprise apps in their tenant and closes a hole exploited by attackers in the past. The second makes it more likely that app creators will opt to restrict access to their apps to the owning tenant. Given the number of apps that exist in Microsoft 365 tenants, both are welcome changes.

Locking App Properties

Only the app developer can choose to use the app instance property lock. This decision typically made by developers of multi-tenant enterprise applications of the type distributed by Microsoft, Adobe, and other software vendors. Entra ID creates a service principal within the tenant where the app runs to hold permissions assigned by the host tenant. The service principal inherits properties from the enterprise app, but if the app instance lock is not in force, the credentials used by the app can be changed using Graph API requests or Microsoft Graph PowerShell SDK cmdlets. If an attacker gains access to a tenant, they could therefore create credentials to allow them to use the app and the permissions assigned to the app. These permissions could allow extensive access to user data, such as all sites, all accounts, all mailboxes, and so on.

Tenants can set the app instance property lock for their own apps. New apps created using the Entra ID admin center set the app instance property lock by default for all supported properties, but older apps probably don’t have the lock enabled. I’m not sure when Entra ID changed the default behavior, but the apps created in my tenant prior to September 2023 do not have the lock enabled. You can update an app by selecting its Authentication properties and then App Instance Property Lock (Figure 1).

Updating the app instance property lock for a registered Entra ID app.
Figure 1: Updating the app instance property lock for a registered Entra ID app

Some apps that show up in a tenant’s app registration list are not created by the tenant. For instance, two apps called SharePoint Online Client Extensibility Web Application Principal and SharePoint Online Client Extensibility Web Application Principal Helper are created automatically for use with the SharePoint Framework to access Microsoft Graph and third-party APIs. It’s unclear why Microsoft doesn’t use a multi-tenant enterprise app instead.

Updating the App Instance Property Lock

Given that new apps have the app instance property lock set, it’s probably a good idea (and can do no harm) to update existing apps to set the lock. This is easily done with the Microsoft Graph PowerShell SDK by:

  • Run Get-MgApplication to find the set of apps.
  • Check each app to see if the lock is set.
  • If not, call Update-MgApplication to set the lock.

Here’s some example code to illustrate the principal:

ForEach ($App in $Apps) {
  $ServiceLock = $App | Select-Object -ExpandProperty ServicePrincipalLockConfiguration
  Write-Host ("Now processing {0}" -f $App.displayName)
  If ($ServiceLock.IsEnabled -eq $True) {
    Write-Host ("The {0} app is already enabled" -f $App.displayName) -ForegroundColor Red
  } Else {
    Write-Host ("App Instance Property Lock Not enabled for {0}; updating app" -f $App.displayName)
    Update-MgApplication -ApplicationId $App.Id -ServicePrincipalLockConfiguration $AppInstanceLockConfiguration
}

You can download the full script from GitHub. The script includes some setup that’s necessary such as signing into the Graph SDK with the necessary permission and creating a hash table containing the parameters for use by Update-MgApplication. The script also generates a report about the apps it updates.

Maintain Your Apps

The changes Microsoft is making is a good reminder that it’s important to keep an eye on the apps registered in a tenant to ensure their security and that they have appropriate credentials and permissions, and to remove unrequired apps. I know I could do a better job of app maintenance, but at least the app instance property lock is set for all apps now.


Keep up to date with developments in Entra ID 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/12/11/app-instance-property-lock/feed/ 1 62783
Entra ID Captures Timestamp for Last Successful Sign In for User Accounts https://office365itpros.com/2023/12/08/lastsuccessfulsignindatetime/?utm_source=rss&utm_medium=rss&utm_campaign=lastsuccessfulsignindatetime https://office365itpros.com/2023/12/08/lastsuccessfulsignindatetime/#comments Fri, 08 Dec 2023 01:00:00 +0000 https://office365itpros.com/?p=62759

Big Difference Between Last Sign in and Last Successful Sign In

Yesterday, I saw a tweet from Entra ID program manager Merill Ferando announcing that the Graph signInActivity resource type (beta) now supports the lastSuccessfulSignInDateTime property. This is good news because it makes it much easier to find out when a user last successfully accessed a tenant. Being forced to filter the Entra ID sign-in logs to separate out bad attempts to sign-in from successful attempts has long been a frustration for administrators (here’s an example).

Using the LastSignInDateTime Property

Until now, the signInActivity resource supported the LastSignInDateTime property. The property is useful when reviewing account activity. For instance, this PowerShell snippet finds user accounts with a sign-in in the last 14 days.

[array]$Users = Get-MgUser -Filter "signInActivity/lastSignInDateTime ge $([datetime]::UtcNow.AddDays(-14).ToString("s"))Z" -All `
-Property displayname, Id, userPrincipalName, SignInActivity, userType | `
Sort-Object DisplayName | Select-Object @{n="Last Sign in";e={$_.SignInActivity.lastSignInDateTime}}, DisplayName, Id, UserPrincipalName, UserType
[array]$TenantUsers = $Users | Where-Object {$_.UserType -eq "Member"} | Sort-Object {$_.'Last Signin' -as [datetime] } -Descending
$TenantUsers | Format-Table 'Last Sign in', DisplayName, UserPrincipalName

Last Sign in         DisplayName                      UserPrincipalName
-----------          -----------                      -----------------
06/12/2023 13:03:57  Lotte Vetler                     Lotte.Vetler@office365itpros.com
06/12/2023 13:01:22  Chris Bishop                     Chris.Bishop@office365itpros.com
04/12/2023 22:04:43  Rene Artois                      Rene.Artois@office365itpros.com

More developed examples include using the lastSignInDateTime property to find underused accounts, or reporting the timestamp when assessing if guest accounts are in active use.

The difference between lastSignInDateTime and lastSuccessfulSignInDateTime property is:

  • lastSignInDateTime is the timestamp for the last interactive sign-in for a user account. An attempted sign-in might be unsuccessful (for example, a multi-factor authentication challenge fails), but Entra ID still updates the timestamp.
  • lastSuccessfulSignInDateTime is the timestamp for the last successful sign-in (interactive or non-interactive) for a user account.

Taking the example above, some of the timestamps reported might not represent successful sign ins, and that’s the issue the new property aims to address.

Caveats for LastSuccessfulSignInDateTime

Before we all get excited, some caveats exist:

  • Tenants need Entra ID P1 licenses to access sign-in reports via the Graph. If you attempt to run the example code described here and the tenant doesn’t have an Entra ID P1 license, you’ll see a “Neither tenant is B2C or tenant doesn’t have premium license” error. Microsoft’s documentation is unclear about whether the account used needs a license or the existence of Entra ID P1 in the tenant is sufficient. To be sure, use a licensed account.
  • The last successful sign in timestamp is currently available only through the beta endpoint. There’s no indication when it might be available through the V1.0 API endpoint. Some tenants have restrictions governing code written against the beta endpoint.
  • The Get-MgBetaUser cmdlet in the Microsoft Graph PowerShell SDK supports the last successful timestamp using SDK V2.11.1 or later.

$User = Get-MgBetaUser -Userid aff4cd58-1bb8-4899-94de-795f656b4a18 -Property SigninActivity

$User.signinactivity | Select-Object Last*

LastNonInteractiveSignInDateTime  : 15/12/2023 19:08:20
LastNonInteractiveSignInRequestId : c8c27d68-1a8f-4b33-a04d-4439404f1500
LastSignInDateTime                : 15/12/2023 14:46:43
LastSignInRequestId               : 1ebe266d-c3cd-479b-b7e6-abc0be5ace00
LastSuccessfulSignInDateTime      : 15/12/2023 19:08:20
LastSuccessfulSignInRequestId     : c8c27d68-1a8f-4b33-a04d-4439404f1500

Microsoft’s documentation says that from December 1, 2023, Entra ID captures the lastSuccessfulSignInDateTime property for user accounts. However, I see the property populated for accounts from mid-November. The difference can be accounted for by the time required to deploy changes across all Microsoft 365 tenants.

Population of the lastSuccessfulSignInDateTime property is not retrospective, so the only values available are from December 1, 2023. Currently, the property is available only through the beta API. Access to sign-in activity logs requires Entra ID P1 licenses.

Testing the LastSuccessfulSignInDateTime Property

There’s nothing like writing a PowerShell script to exercise a new property. I wrote a script (downloadable from GitHub) to find user accounts with licenses and report the lastSuccessfulSignInDateTime and lastSignInDateTime properties for each account. The script also computes the number of days since a last successful sign in and last sign in. As you can see from Figure 1, a difference does exist between the two properties.

Differences between the lastSuccessfulSignInDateTime and lastSignInDateTime properties.
Figure 1: Differences between the lastSuccessfulSignInDateTime and lastSignInDateTime properties

As noted above, the new property is only available through the beta endpoint. If this causes you a problem, you’ll have to wait for Microsoft to apply the necessary magic to upgrade the signInActivity resource type in the V1.0 endpoint. If not, consider reviewing scripts that perform activity date checks for user and guest accounts to figure out if reporting successful sign-in actions makes a difference to the accuracy of the script output.


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/12/08/lastsuccessfulsignindatetime/feed/ 11 62759
Reporting User and Group Assignments for Enterprise Applications https://office365itpros.com/2023/11/22/enterprise-app-assignments/?utm_source=rss&utm_medium=rss&utm_campaign=enterprise-app-assignments https://office365itpros.com/2023/11/22/enterprise-app-assignments/#respond Wed, 22 Nov 2023 01:00:00 +0000 https://office365itpros.com/?p=62501

How to Find and Document Assignments for Entra ID Enterprise Applications

A reader asked:

I am trying to execute Microsoft Graph that it can grab all my Enterprise Applications in my tenancy and export to CSV the application name and user and groups assigned to the groups.”

There’s a couple of things to unpack here before discussing potential answers. First, enterprise applications are Entra ID registered applications. Companies like Microsoft or Apple create enterprise applications for use in multiple tenants. For example, if you signed up to attend a Microsoft conference using your Entra ID credentials, the process is handled by an enterprise app called Microsoft Events. The home tenant identifier registered for the app is 72f988bf-86f1-41af-91ab-2d7cd011db47, which this site tells us is the identifier for Microsoft’s tenant.

Often enterprise applications act as an entry point to a service. For example, the properties of the IdPowerToys app (Figure 1) contain a link to the site where the service runs to document conditional access policies in PowerPoint.

Enterprise app registration for the idPowerToys app
Figure 1: Enterprise app registration for the idPowerToys app

Service Principals

When an enterprise application is used within a tenant, Entra ID creates a service principal to hold the permissions and assignments for the application within that tenant. If you want, the service principal is the instantiation of the application within the tenant that holds permissions and other information for the application. Other objects, like Azure Automation accounts also have service principals used to hold permissions and roles, such as those needed to access user data via Graph APIs.

By default, enterprise applications are accessible by all users. To control access, administrators can update application properties to require assignment. This means that Entra ID will only issue an access token for the application to users and groups granted access through assignment. It is the way to lock down access to enterprise applications.

Finding Enterprise Applications

To answer the question, we must find the set of enterprise applications in the tenant that are homed in other tenants. The way to do this is to run the Get-MgServicePrincipal cmdlet from the Microsoft Graph PowerShell SDK. Two steps are necessary. First, find the service principals known in the tenant. Second, filter the set to extract those with a tenant identifier that is not the same as your tenant:

[array]$ServicePrincipals = Get-MgServicePrincipal -All
[array]$EnterpriseApps = $ServicePrincipals | Where-Object {$_.AppOwnerOrganizationId -ne $TenantId} | Sort-Object DisplayName

The filter shown above creates a set of enterprise apps. If you want to further refine the filter to only find apps where role assignment is required, change it to:

 [array]$EnterpriseApps = $ServicePrincipals | Where-Object {$_.AppOwnerOrganizationId -ne $TenantId -and $_.AppRoleAssignmentRequired -eq $True} | `
        Sort-Object DisplayName

The next step is to loop through the set of apps and run the Get-MgServicePrincipalAppRoleAssignedTo cmdlet to check if any assignments exist. If any do, it’s easy to grab the details for a report.

ForEach ($App in $EnterpriseApps) {
    [array]$Assignments = Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $App.Id | Where-Object {$_.PrincipalType -ne 'ServicePrincipal'}
    If ($Assignments) {
        $i++
        Write-Host ("Found assignments for {0}" -f $App.DisplayName)
        ForEach ($Assignment in $Assignments) {
            $ReportLine = [PSCustomObject]@{
                TimeStamp   = $Assignment.CreatedDateTime  
                Id          = $Assignment.Id
                DisplayName = $Assignment.PrincipalDisplayName 
                UserId      = $Assignment.PrincipalId
                Type        = $Assignment.PrincipalType
                Resource    = $Assignment.ResourceDisplayName
                ResourceId  = $Assignment.ResourceId
            }
            $Report.Add($ReportLine)
        }
    }

Note the filter used with the Get-MgServicePrincipalAppRoleAssignedTo cmdlet. This removes assignments to service principals such as those used to hold permissions for Azure Automation accounts. Here’s an example of an assignment to an Azure Automation account to allow it to act like an account holding the Exchange Administrator role.

TimeStamp   : 28/01/2022 15:47:35
Id          : ag5Go0LJzUWdGNo2BTCsaYJIbAAI79JLkTVN2fzhjh0
DisplayName : ExoAutomationAccount_Y6LgjDYIfPnxmFzrqdbaClsnTD/gN4BNnVMywiju5hk=
UserId      : a3460e6a-c942-45cd-9d18-da360530ac69
Type        : ServicePrincipal
Resource    : Office 365 Exchange Online
ResourceId  : dacf6086-a190-467a-aadd-d519472b8d1d

You can download the script I used from GitHub.

The Output

After filtering, what remains are the app assignments to users and groups, the details of which the script captures and reports. Figure 2 shows an example of the output.

Enterprise apps and user/group assignments
Figure 1: Enterprise apps and user/group assignments

My name features heavily in the list because I installed many of the apps in my tenant. Some of the apps and associated assignments are quite old, a fact that underlines the need to review and remove unused or obsolete apps periodically. The duplicate entries for the Graph Explorer is due to an assignment captured when the app was first installed followed by an explicit assignment to prevent access to the app to anyone but my account.

None of this is particularly difficult to do. The trick, as is often the case with Microsoft 365, is to know where to start looking. And perhaps some luck when navigating through the documentation!


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/22/enterprise-app-assignments/feed/ 0 62501
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
Report Email Proxy Addresses for Exchange Online Mail-Enabled Objects https://office365itpros.com/2023/11/16/email-proxy-address-report/?utm_source=rss&utm_medium=rss&utm_campaign=email-proxy-address-report https://office365itpros.com/2023/11/16/email-proxy-address-report/#respond Thu, 16 Nov 2023 01:00:00 +0000 https://office365itpros.com/?p=62456

List All Email Proxy Addresses for Exchange Online Objects

A reader of the Office 365 for IT Pros eBook asked if there’s an easy way to see a list of all the email addresses used in a tenant. The simple answer is that there’s no out-of-the-box method to see this information. Some work is needed to extract and report a list of all email addresses. It’s not just the primary SMTP address for mailboxes either – such a report must include all the proxy SMTP addresses for all mail-enabled objects.

Proxy Addresses

An Exchange Online mail-enabled object such as a user mailbox can have up to 300 different proxy addresses. Although most inbound email arrives addressed to a mail-enabled object’s primary SMTP address, Exchange Online can deliver messages to any of the proxy addresses (aliases) assigned to a mail-enabled object. Outbound, Exchange Online mailboxes can send email using any proxy address.

Proxy addresses are permanently assigned to a mail-enabled object and stored in the Exchange Online directory. They’re not the same as plus addressing, which individual people can use to create a specific form of a proxy address to receive email from certain senders. Apart from SMTP addresses, the proxy addresses assigned to user mailboxes include SIP and SPO addresses. The first is used for federated chat; the second is used to store SharePoint Online information into user mailboxes. The Microsoft 365 substrate ingests SharePoint Online content into mailboxes to create ‘digital twins’ that are used by cloud processes and services.

Creating a PowerShell Script to Report SMTP Proxy Addresses

Now that we’ve got the definitions out of the way, let’s use PowerShell to answer the question. The steps involved are very straightforward and can be summarized as:

For each type of mail-enabled objects supported by Exchange Online, find the SMTP proxy addresses and report them.

I wrote a script to do the job (you can download the code from GitHub). It breaks processing up into:

  • Mailboxes (user, shared, room, and resource).
  • Group mailboxes (for Microsoft 365 groups).
  • Mail-enabled public folders.
  • Distribution lists.
  • Dynamic distribution lists.

For instance, here’s the code to process mailboxes:

Write-Host "Fetching details of user, shared, equipment, and room mailboxes..."
[array]$Mbx = Get-ExoMailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox, SharedMailbox, RoomMailbox, EquipmentMailbox
Write-Host ("Processing details for {0} mailboxes..." -f $Mbx.count)
ForEach ($M in $Mbx) {
    ForEach ($Address in $M.EMailAddresses) {
        $AddressType = $Address.Split(":")[0]
        $AddressProxy = $Address.Split(":")[1]
        If ($AddressType -eq 'smtp') {
            $ReportLine = [PSCustomObject]@{ 
                ProxyAddress = $AddressProxy
                Name         = $M.DisplayName
                UPN          = $M.userPrincipalName
                ObjectId     = $M.ExternalDirectoryObjectId
                Type         = $M.RecipientTypeDetails
            }
            $Report.Add($ReportLine)
        }
    }
}

The code examines each proxy address. If its address type is SMTP, the script records the address and some other information. It’s really that simple. Figure 1 shows some of the list of SMTP proxy addresses extracted from my tenant.

Listing of Exchange Online proxy addresses for different objects

Email proxy address
Figure 1: Listing of Exchange Online proxy addresses for different objects

Using the List of Email Proxy Addresses

The next question is how to use such a list? One idea is to load some of the list of proxy addresses into a hash table and use the table to add extra detail to the information provided in message trace logs by resolving email addresses to find the display name for message recipients.

To test the idea, I enhanced some code from the article about using message trace logs to analyze email traffic to add the creation and population of a hash table using data imported from the CSV file output for the list of proxy addresses. For each message in the trace data, I then attempt to find a match in the hash table and include the name of the recipient if found. The added code is in italics.

[array]$EmailProxies = Import-CSV "C:\Temp\EmailProxyAddresses.csv"
$EmailProxyHash = @{}
ForEach ($E in $EmailProxies) {
   $EmailProxyHash.Add($E.ProxyAddress, $E.Name) }

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

ForEach ($M in $Messages) {
   $Direction = "Inbound"
   $DisplayName = $Null
   $SenderDomain = $M.SenderAddress.Split("@")[1]
   $RecipientDomain = $M.RecipientAddress.Split("@")[1]
   If ($SenderDomain -in $Domains) {
      $Direction = "Outbound" 
   }
   $DisplayName = $EmailProxyHash[$M.RecipientAddress] 

   $ReportLine = [PSCustomObject]@{
     TimeStamp       = $M.Received
     Sender          = $M.SenderAddress
     Recipient       = $M.RecipientAddress
     Name            = $DisplayName
     Subject         = $M.Subject
     Status          = $M.Status
     Direction       = $Direction
     SenderDomain    = $SenderDomain
     RecipientDomain = $RecipientDomain
    }
    $MessageReport.Add($ReportLine)
}

No doubt others will find more creative ways to use the listing of email proxy addresses. If you do, let us know in the comments.


Learn more about how the Office 365 applications really work on an ongoing basis by subscribing to the Office 365 for IT Pros eBook. The monthly updates for the book keep subscribers informed about what’s important across the Office 365 ecosystem.

]]>
https://office365itpros.com/2023/11/16/email-proxy-address-report/feed/ 0 62456
Customizing the Microsoft 365 User Profile Card with the Microsoft Graph PowerShell SDK https://office365itpros.com/2023/11/15/user-profile-card-sdk/?utm_source=rss&utm_medium=rss&utm_campaign=user-profile-card-sdk https://office365itpros.com/2023/11/15/user-profile-card-sdk/#comments Wed, 15 Nov 2023 01:00:00 +0000 https://office365itpros.com/?p=62444

Use SDK Cmdlets to Add Properties to the Microsoft 365 User Profile Card

The Microsoft 365 profile card displays information about users. The information show in a profile card comes from the user’s Entra ID account. By default, Microsoft 365 limits the properties shown by the profile card to the set that they consider to be most important. Organizations can customize the Contact tab of the  profile card to reveal some of the properties that are not shown by default. However, not every property of an Entra ID user account is available for the profile card. Some, like the employee hire date, are not supported.

In 2020, I wrote about how to customize the profile card. At that time, the profile card API was in beta. The production Graph endpoint now supports the profile card API, and it’s also supported by cmdlets in the Microsoft Graph PowerShell SDK. I used version 2.9 of the SDK for this article.

The Microsoft documentation for customizing the profile card is a little outdated. At the time of writing, it uses V1.0 of the SDK and is based on the beta API. Because the API is now in production, it follows that the latest SDK cmdlets use that API. In any case, the instructions contained in the documentation are a reasonable guide.

Customized User Profile Card Available to All

The most important point about customizing the profile card is that any change is exposed to all users. You cannot customize the profile card for a subset of the user population such as a targeted administrative unit. This fact creates difficulties in multinational organizations where local privacy regulations might prevent the display of certain information held in user account properties.

As already mentioned, only certain user account properties are available for customization. Basically, you can add six standard properties:

  • UserPrincipalName.
  • Fax.
  • StreetAddress.
  • PostalCode.
  • StateOrProvince.
  • Alias.

Of course, including these properties in the profile card is useless unless information is populated in the directory for all user accounts. That often doesn’t happen.

The the fifteen custom attributes inherited from Exchange Server (different to the custom security attributes that can be defined for Entra ID) are also supported. Many organizations use these attributes to store information about users such as personnel numbers, cost centers, employee type, employee job codes, seniority level, and even the date of last promotion. Dynamic distribution lists and dynamic Microsoft 365 groups.

Add an Attribute to the Profile Card

To add one of the six standard or fifteen custom attributes to the profile card, construct a payload in a PowerShell hash table. The table contains the property name and its display name (annotation). Then run the New-MgAdminPeopleProfileCardProperty cmdlet passing the hash table as the body parameter (the same as passing a request body to a Graph request). This command adds CustomAttribute15 and tells Microsoft 365 that the user profile card should refer to the property as the “Employee Type.”

Connect-MgGraph -Scopes "PeopleSettings.ReadWrite.All","PeopleSettings.Read.All" -NoWelcome

$AddPropertyDetails = @{
  directoryPropertyName = "CustomAttribute15"
  annotations = @(
    @{ displayName = "Employee Type" }
  )
}

$AddPropertyDetails

Name                           Value
----                           -----
directoryPropertyName          CustomAttribute15
annotations                    {Employee Type}

New-MgAdminPeopleProfileCardProperty -BodyParameter $AddPropertyDetails

Id DirectoryPropertyName
-- ---------------------
   CustomAttribute15

It takes at least 24 hours before the profile card picks up customized properties. When they do, they appear on the Contact tab of the profile card. Figure 1 shows three custom properties for cost center, preferred drink (a historic part of the Active Directory schema), and employee type. If a custom properties doesn’t contain any information for a user, it won’t appear on the profile card.

Custom properties shown on the Microsoft 365 user profile card
Figure 1: Custom properties shown on the Microsoft 365 user profile card

To check the set of attributes added to the profile card with PowerShell, run the Get-MgAdminPeopleProfileCardProperty cmdlet. This output tells us that the profile card is configured to display three optional properties and three custom properties:

Get-MgAdminPeopleProfileCardProperty

Id DirectoryPropertyName
-- ---------------------
   userPrincipalName
   customAttribute12
   StreetAddress
   Postalcode
   CustomAttribute9
   CustomAttribute15

If you make a mistake, you can remove a property from the profile card by running the Remove-MgAdminPeopleProfileCardProperty cmdlet. For example, this command removes the Drink attribute (stored in CustomAttribute9) from the profile card:

Remove-MgAdminPeopleProfileCardProperty -ProfileCardPropertyId CustomAttribute9

Note that the profile card property id parameter is case sensitive. You must pass the property name as returned by the GetMgAdminPeopleProfileCardProperty cmdlet.

Adding a Translated Label for a Custom Profile Card Property

The documentation makes a big thing about defining a language-specific value for a custom property. This is done using the Update-MgAdminPeopleProfileCardProperty cmdlet. This example shows how to add a French language label for CustomAtrribute15:

$LocalizationHash = @{}
$LocalizationHash.Add("languagetag","fr-FR")
$LocalizationHash.Add("displayName","Type d’employé")

$UpdatePropertyDetails = @{
   annotations = @(
    @{
    displayName = "Cost Center"
   localizations = @( $LocalizationHash )
     }
    )
}
Update-MgAdminPeopleProfileCardProperty -ProfileCardPropertyId 'customAttribute15' -BodyParameter $UpdatePropertyDetails

This command works, but it only works for a single language. If you want to have labels for multiple languages, you’ll be sadly disappointed because Update-MgAdminPeopleProfileCardProperty (and its Graph API counterpart) overwrite the language configuration each time.

You could argue that this is disappointing for multinational organizations that want to have fully-translated interfaces for all languages in use. Being restricted to a single language alternative is a strange approach to localization, especially for a company that does so much to deliver local language translations for user interfaces. The counterargument is that the properties chosen for display in the profile cards are likely to be well understood by anyone in an organization.

Work Still to Do

Not much has changed in customizing the profile card since 2020. The API is now production rather than beta and the Graph SDK supports the action, but that’s it. Coverage for multiple local language labels would be nice but that’s still elusive.


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/11/15/user-profile-card-sdk/feed/ 11 62444
Reporting the Storage Used by Loop Workspaces https://office365itpros.com/2023/11/08/loop-workspace-storage/?utm_source=rss&utm_medium=rss&utm_campaign=loop-workspace-storage https://office365itpros.com/2023/11/08/loop-workspace-storage/#comments Wed, 08 Nov 2023 01:00:00 +0000 https://office365itpros.com/?p=62331

Understand the Impact Loop Workspaces Have on SharePoint Online Quota

Message center notification MC678308 (updated 2 November 2023) explains that the storage consumed by Loop workspaces (created with the Loop app rather than Loop components in Teams and Outlook) will count against tenant storage quotas. During the preview, Microsoft allowed people to use the Loop app without a license and create as many workspaces as they liked. The only limitation was on the size of an individual workspace, which was capped at 5 GB. Workspace data is held in a special form of SharePoint storage called Syntex repository services and Microsoft didn’t limit the storage occupied by the workspaces.

All good things come to an end. As the Loop app approaches the end of its public preview stage and moves toward general availability (I expect an announcement at the Ignite conference), Microsoft has revealed its hand with respect to licensing and storage. Only people with certain Microsoft 365 product licenses will be able to create new workspaces

Loop Counts Against Storage Now

According to MC678308, Microsoft will start counting Loop workspaces against tenant storage quotas between late October and late November 2023. When the change goes into effect for a tenant, the maximum size of a workspace increases from 5 GB to 1 TB.

The exact impact on a tenant is hard to know unless you use the Get-SPOContainer cmdlet in the SharePoint Online management module to fetch details of each tenant. For example, this command fetches details of existing workspaces:

[array]$LoopWorkspaces = Get-SPOContainer -OwningApplicationID a187e399-0c36-4b98-8f04-1edc167a0996
If (!($LoopWorkspaces)) {
    Write-Host "Can't get Loop workspaces - exiting"; break
}

The details reported by Get-SPOContainer miss some important information. For instance, while the creation date for a workspace is available, the last updated date is not, nor is detail about the person who last updated the workspace. Understanding the date when a workspace was last changed is critical to knowing if a workspace is in active use.

Reporting Loop Workspaces

This code generates a report with details of the storage used by each workspace and whether the workspace owners have one of the four licenses required to create new Loop workspaces:

$Report = [System.Collections.Generic.List[Object]]::new()
$TotalBytes = 0; $LicenseOK = 0; $i = 0
ForEach ($LoopSpace in $LoopWorkspaces) {
    $i++
    Write-Output ("Analyzing workspace {0} {1}/{2}" -f $LoopSpace.ContainerId, $i, $LoopWorkspaces.count)
    # Get detail of the workspace
    $LoopSpaceDetails =  Get-SPOContainer -OwningApplicationID a187e399-0c36-4b98-8f04-1edc167a0996 -Identity $LoopSpace.ContainerId
    # Get detail about the owner
    [array]$Owners = $LoopSpaceDetails.Owners
    ForEach ($Owner in $Owners) {
        $LicenseFound = $Null; $LoopLicenseStatus = "Unlicensed";  $LicenseName = $Null
        # Find if the Loop service plan is successfully provisioned for the account
        [array]$UserLicenseData = Get-MgUserLicenseDetail -UserId $Owner
        $LoopLicense = $UserLicenseData | Select-Object -ExpandProperty ServicePlans | `
             Where-Object {$_.ServicePlanId -eq $LoopServicePlan} | Select-Object -ExpandProperty ProvisioningStatus
        If ($LoopLicense -eq 'Success') {
            $LicenseOK++
            $LoopLicenseStatus = "OK"
        }
        # Find what SKU the Loop service plan belongs to
        $User = Get-MgUser -UserId $Owner -Property Id, displayName, department, UserPrincipalName
        [array]$SKUs = $UserLicenseData.SkuId
        ForEach ($Sku in $Skus) {
            $LicenseFound = $LoopValidLicenses[$Sku]
            If ($LicenseFound) {
                $LicenseName = $LicenseFound
            }
        }
    }
    [array]$Members = $Null
    [array]$Managers = $LoopSpaceDetails.Managers
    ForEach ($Manager in $Managers) {
        $Member = Get-MgUser -UserId $Manager
        $Members += $Member.DisplayName
    }

    $StorageUsed = "{0:N2}" -f ($LoopSpaceDetails.StorageUsedInBytes/1MB)
    $TotalBytes = $TotalBytes + $LoopSpaceDetails.StorageUsedInBytes

    $ReportLine = [PSCustomObject]@{
        ContainerId    = $LoopSpace.ContainerId
        App            = $LoopSpaceDetails.OwningApplicationName
        Name           = $LoopSpace.ContainerName
        Description    = $LoopSpace.Description
        Owner          = $User.DisplayName
        UPN            = $User.UserPrincipalName
        License        = $LoopLicenseStatus
        Product        = $LicenseName
        Members        = ($Members -Join ", ")
        Created        = $LoopSpaceDetails.CreatedOn
        SiteURL        = $LoopSpaceDetails.ContainerSiteUrl
        "Storage (MB)" = $StorageUsed
    }
    $Report.Add($ReportLine)
}

Figure 1 shows an extract of the information captured by the script. You can see that the James Ryan account is deemed to be unlicensed. This is because the account doesn’t hold a product licenses containing the Microsoft Loop service plan. Also note that new users all receive the Ideas workspace to help get them started with the Loop app. The workspace isn’t large (0.11 MB), but it’s a bit cheeky for Microsoft to charge for it.

 Reporting Loop workspace storage
Figure 1: Reporting Loop workspace storage

Checking individual workspace containers is not a fast operation. The script can be sped up by removing the Get-MgUser commands used to fetch details about the licenses possessed by workspace owners.

You can download the complete script from GitHub. Remember that the intention of the script is to illustrate a principal rather than being a complete solution. Feel free to make whatever changes you deem to meet the circumstances of your tenant.

Update: The original script was limited to reporting the first 200 workspaces in a tenant. An updated script handles pagination to find and report all workspaces.

No Immediate Impact

It’s unlikely that Loop workspaces will have much of an impact on SharePoint Online tenant storage quotas in the immediate future. Documents will continue to be the major consumer of quota, even when tenants have the Microsoft 365 licenses necessary for users to create new Loop workspaces. Even so, it’s a good idea to keep an eye on how Loop is being used and how much space its files occupy.


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/08/loop-workspace-storage/feed/ 9 62331
Reducing the Memory Footprint of Exchange Online PowerShell https://office365itpros.com/2023/11/06/exchange-online-powershell-memory/?utm_source=rss&utm_medium=rss&utm_campaign=exchange-online-powershell-memory https://office365itpros.com/2023/11/06/exchange-online-powershell-memory/#respond Mon, 06 Nov 2023 01:00:00 +0000 https://office365itpros.com/?p=62298

Three Steps to Shrinking Memory Demands for Exchange Online PowerShell

Exchange Online PowerShell Performance
Exchange Online PowerShell memory

I read the Exchange team’s blog post about Exchange Online PowerShell performance. The team behind the Exchange Online management module made three recommendations:

  • Don’t load the module help when running scripts in non-interactive sessions. In other words, humans might need some help to understand what cmdlets do but computers don’t. Computers never ask for more detail about the commands they’re asked to execute. They just get on with the job.
  • Restrict the number of cmdlets loaded into a PowerShell session. The Exchange Online management module spans some 800 cmdlets. Any session is likely to use less than ten cmdlets. In fact, many scripts might only use the REST-based cmdlets introduced in 2019. These cmdlets (like Get-ExoMailbox and Get-ExoMailboxStatistics) are always loaded by the module into memory along with some of the more recently introduced cmdlets, like Get-UserBriefingConfig. Along with the REST cmdlets, a script might use one or two modernized cmdlets (like Get-User). Modernized means that the cmdlets no longer support basic authentication and have discarded dependencies like WinRM.
  • Create a new PowerShell process for each Exchange Online session. The idea here is that the new session starts off with an empty cache and that reduces the memory footprint. Sounds good, but I bet not many will follow this guidance. I say this for two reasons. First, people are generally lazy and don’t want to go through the hassle of starting and shutting down perfectly good PowerShell sessions. Second, most people working with Microsoft 365 load several modules such as the Microsoft Graph PowerShell SDK and Teams.

In any case, the advice is appreciated and should be considered in the light of whatever work you do with the Exchange Online management module.

New Parameter for Connect-ExchangeOnline

To support avoiding cmdlet help, the latest version of the Exchange Online management module (3.4) boasts the SkipLoadingCmdletHelp parameter. The implementation works very nicely and speeds up module loading. I recommend that you use this parameter in every script that runs without human intervention. For instance, I have some work to do to upgrade scripts (runbooks) written to run using Azure Automation. The next time I touch the code for any of the runbooks, I’ll be sure to add the parameter to Connect-ExchangeOnline.

Avoiding memory overhead like this should be very helpful in any script that combines the Exchange Online management module with the Microsoft Graph PowerShell SDK. When Microsoft created V2 of the Microsoft Graph PowerShell SDK, they split the cmdlets into V1.0 and beta sets to reduce the overhead of loading the SDK for Azure Automation runbooks.

Remember to update your Azure Automation accounts with the latest module so that runbooks can take advantage of the new feature. That might be even more important than keeping the module updated on your workstation (here’s a handy script that I use for that purpose).

Figuring Out Exchange Online PowerShell Cmdlets

Building a list of Exchange Online cmdlets used in a script is a matter of checking the cmdlets called and understanding if they come from the Exchange Online management module and then constructing a list to use with Connect-ExchangeOnline. The Get-Command module returns a small subset of the available cmdlets.

Get-Command -Module ExchangeOnlineManagement

Do not include any of these cmdlets in the set passed to Connect-ExchangeOnline as you’ll get an error like “OperationStopped: No cmdlet assigned to the user have this feature enabled.”

When you’ve determined the set of cmdlets needed by a session, put them in an array and pass the array in the CommandName parameter. This example combines the parameter to skip loading cmdlet help with a defined set of cmdlets to load into a session.

$Cmdlets = "Set-User", "Get-User", "Get-OWAMailboxPolicy", "Set-OWAMailboxPolicy"
connect-ExchangeOnline -SkipLoadingCmdletHelp -CommandName  $Cmdlets

Detail is Important When Running Exchange Online PowerShell

Of course, you could ignore these recommendations and continue running your scripts as before. It’s a good tactic if the scripts work and you’re happy. On the other hand, if you’re looking for some extra performance and reduced memory consumption, these tips are worth considering. I suspect that the folks who will benefit most are those who run PowerShell against tens of thousands of objects (mailboxes, user accounts, etc.).


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/2023/11/06/exchange-online-powershell-memory/feed/ 0 62298
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
Creating a Teams Directory in a SharePoint Online List https://office365itpros.com/2023/10/24/create-sharepoint-list-pnp/?utm_source=rss&utm_medium=rss&utm_campaign=create-sharepoint-list-pnp https://office365itpros.com/2023/10/24/create-sharepoint-list-pnp/#comments Tue, 24 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=62079

Create SharePoint List from Data Extracted from Teams

The article discussing a PowerShell script to generate a Teams directory explains how to create output files in different formats that can be used to make the directory available to users. For instance, you could post a HTML format version of the directory in a SharePoint Online site. Discussion about the post generated some nice ideas, amongst which was the suggestion to output the directory as a SharePoint list (aka Microsoft Lists).

I haven’t done much to manage SharePoint lists with PowerShell, so this seemed like a nice opportunity to explore the idea and increase my knowledge.

Choosing the Right Module to Create SharePoint List

The first order of business is to choose a PowerShell module for the task. I started off with the Microsoft Graph PowerShell SDK, which includes cmdlets like New-MgSiteList and Get-MgSiteList. Unhappily, I ran into several problems with SDK cmdlets (V2.8) that I’ve reported to Microsoft. The documentation and examples for these SDK site cmdlets are not as good as other areas covered by the SDK, so the problems could be due to misunderstanding on my part.

This brought me to the Pnp.PowerShell module (aka “Microsoft 365 Patterns and Practices PowerShell Cmdlets”). PnP is a community effort to create resources that help people to build app on the Microsoft 365 platform. The big advantage of PnP is that its cmdlets can interact with SharePoint Online content like list items where the Microsoft SharePoint management module is limited to tenant and site settings.

Basic Steps in the Script to Add Teams Directory Records and Create SharePoint List

The basic steps in the script are:

  • Connect to the site that stores the list. I created a communications site for this purpose.
  • Look for the list and if found, remove it because it’s easier to create and populate a new list instead of attempting to synchronize changes since the last update for the team directory.
  • Create the list and the columns used to store team directory information. Many templates are available for Lists. I used the Links template and removed one of the two default columns.
  • Populate the list with new items. To do this, the script reads the information in from the CSV file created by the original script and writes them as new list items.

PnP.PowerShell Cmdlets Used to Create SharePoint List

Translating the above into PnP PowerShell, the script uses the following cmdlets:

  • Connect-PnpOnline to connect to the target site. PnP supports different forms of authentication. For the purpose of this demonstration, the script prompts for credentials of a site administrator and uses those to connect.
  • Get-PnPList to check if the target list already exists and Remove-PnPList to remove the list if found.
  • New-PnPList to create the target list.
  • Add-PnPField to define the set of fields used to store directory information.
  • Remove-PnPField to remove the standard Notes field inherited from the Links template. Here’s how the script creates the list and the fields used to store Teams directory information:

New-PnpList -Title $ListName -Template Links -EnableVersioning -Connection $Connection | Out-Null
# Add fields
Add-PnpField -List $ListName -DisplayName 'Team Name' -Internalname TeamName -Type Text -AddToDefaultView | Out-Null
Add-PnpField -List $ListName -DisplayName 'Description' -Internalname Description -Type Text -AddToDefaultView | Out-Null
Add-PnpField -List $ListName -DisplayName 'Owner' -Internalname Owner -Type Text -AddToDefaultView | Out-Null
Add-PnpField -List $ListName -DisplayName 'Owner SMTP Address' -Internalname OwnerSMTP -Type Text -AddToDefaultView | Out-Null
Add-PnpField -List $ListName -DisplayName 'Member count' -Internalname MemberCount -Type Number -AddToDefaultView | Out-Null
Add-PnpField -List $ListName -DisplayName 'External count' -Internalname ExternalCount -Type Number -AddToDefaultView | Out-Null
Add-PnpField -List $ListName -DisplayName 'Access' -Internalname AccessMode -Type Text -AddToDefaultView | Out-Null
# Remove the Notes field inherited from the Links template
Remove-PnPField -List $ListName -Identity Notes -Force
  • Add-PnPListItem to populate the list with items imported from the CSV file. Here’s how the script populates the list:
[array]$TeamsData = Import-CSV -Path $CSVFile
[int]$i = 0
ForEach ($Team in $TeamsData) {
    $i++
    Write-Host ("Adding record for team {0} {1}/{2}" -f $Team.Team, $i, $TeamsData.count)
    Add-PnPListItem -List $ListName -Values @{
        "URL" = $($Team.Deeplink);
        "TeamName" = $($Team.Team);
        "Description" = $($Team.Description);
        "Owner" = $($Team.Owner);
        "OwnerSMTP" = $($Team.OwnerSMTP);
        "MemberCount" = $($Team.Members);
        "ExternalCount" = $($Team.ExternalGuests);
        "AccessMode" = $($Team.Access);
    } | Out-Null
}

The original version of the Teams Directory script generates a directory record for each team including a clickable deeplink to allow users to open Teams in the selected team. They can then join the team (public teams) or request the team owner to join (private teams). The deeplink generated by the script is formatted to make it clickable when exported to a HTML report. I updated the script to include a simple deeplink because SharePoint list entries don’t need the formatting.

Figure 1 shows the Teams directory records in a SharePoint Online list. I’m sure that the visual appearance of the list could be improved by tweaking the columns, but what’s here is sufficient to demonstrate the principles behind creating and populating a list.

The Teams Directory in a SharePoint Online list

Create SharePoint list using Pnp.PowerShell
Figure 1: The Teams Directory in a SharePoint Online list

You can download a copy of the full script from GitHub.

Lots to Explore in Lists

The SharePoint community understands and takes full advantage of lists (here’s an example). Others in the Microsoft 365 world might not. Perhaps this example of extracting information from one area of Microsoft to create a SharePoint list and populate the list with Teams directory information might get your creative juices flowing.


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/2023/10/24/create-sharepoint-list-pnp/feed/ 1 62079
Teams and Microsoft Exchange PowerShell Modules Clash Over Required DLL https://office365itpros.com/2023/10/20/powershell-module-clash-exo/?utm_source=rss&utm_medium=rss&utm_campaign=powershell-module-clash-exo https://office365itpros.com/2023/10/20/powershell-module-clash-exo/#comments Fri, 20 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=62024

PowerShell Module Clash After Recent Microsoft 365 Module Updates

Updated 27-March-2024

When Teams and Exchange PowerShell modules clash

There’s been a bunch of updates for PowerShell modules used to manage Microsoft 365 tenants lately. The Microsoft Graph PowerShell SDK reached version 2.7.0 and then retreated to version 2.6.1 because of a problem with the Restore-MgDirectoryDeletedItem cmdlet. The Microsoft Teams module is now at version 5.7.1 and the Exchange Online module has reached version 3.4. With so much updating to do, I’m glad that I have scripts to update the Microsoft 365 modules on my PC and update the Microsoft 365 modules used as resources in Azure Automation accounts.

After a frenzy of updating, I spun up a new PowerShell session (itself updated to version 7.3.8) and ran the Connect-MicrosoftTeams cmdlet to connect to Teams followed by Connect-Exchange Online. Teams connected but Exchange Online barfed because of an issue loading the Microsoft.Identity.Client DLL. The PowerShell module clash looked like this:

PowerShell 7.3.8
Connect-MicrosoftTeams

Account                            Environment Tenant                               TenantId
-------                            ----------- ------                               --------
Tony.Redmond@office365itpros.com   AzureCloud  a562313f-14fc-43a2-9a7a-d2e27f4f3478 a662313f-14fc-43a2-9a7a-d2e27f4f34…

Connect-ExchangeOnline
OperationStopped: Could not load file or assembly 'Microsoft.Identity.Client, Version=4.44.0.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae'.

Different Versions and Dependencies Cause PowerShell Module Clash

A dynamic link library (DLL) is a hunk of sharable code that applications can use. In this case, the two modules call functions in Microsoft.IdentityClient.Dll to authenticate against Entra ID. The problem is that Teams loads version 4.299 of the DLL and when Exchange Online comes along, it has a declared dependency on version 4.44 of the same DLL. PowerShell 7 runs on .NET core and only allows one version of a DLL to load at any time, so the request made by Exchange Online to load version 4.44 is blocked by version 4.299 loaded by Teams. Exchange Online isn’t happy to use a lower version of the module because it might be dependent on something in version 4.44, which leads us to the barf because the assembly couldn’t load.

It’s all perfectly logical and if you reverse the process and connect to Exchange Online first in a session, it loads version 4.44. If you then connect to Teams, the versioning rules for .NET Core allows the Teams module to load a higher version of the module, which is already present. The Teams module is therefore happy to use version 4.44 instead of 4.299. The only workaround to the problem is to close the PowerShell session and restart, loading the Exchange Online module first before loading the Teams module.

Update: The problem persists with version 3.4 of the Exchange Online management and version 6.0 of the Microsoft Teams PowerShell modules.

Lack of Coordination within Microsoft

Logical as the explanation is (and better than the all-too-often instances when we can’t understand why software fails), it’s disappointing that two Microsoft engineering groups working in the Microsoft 365 ecosystem cannot agree on which version of a critical DLL to use.

Exchange Online has done a lot of work recently to remove basic authentication from email connection protocols. Recently, they also removed support for Remote PowerShell. It’s understandable that their code base is in a state of change. By comparison, since the deprecation of the Skype for Business Connector, the degree of major change for the Teams PowerShell module has been less evident. Sure, Microsoft has modernized cmdlets, fixed bugs, added properties, and so on, but they haven’t ripped the guts out of their authentication stack. It’s plausible that the Teams developers were happy with the older version of the DLL because not much recent change happened (for them) in authentication and this might have allowed a gap to open in DLL versions.

I don’t know if this is what happened. It’s plausible based on observation, but that’s about all. Only Microsoft can say exactly why the two engineering groups arrived in a state of conflict. In any case, because modules like Teams and Exchange are often used together in scripts and interactive sessions, it would be nice if the development groups that produce PowerShell modules used in Microsoft 365 tested for clashes before releasing new versions of their modules.

It’s worth making the point that a dependency clash can happen for any module. I’ve experienced the same problem recently with the Pnp.PowerShell module (here’s a known issue). For instance, in a PowerShell 7 session, run Connect-MgGraph to connect to the Graph and then run Connect-PnpOnline to see another barf:

Connect-PnPOnline: Could not load file or assembly 'Microsoft.Identity.Client, Version=4.50.0.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae'. Could not find or load a specific file. (0x80131621)

The truth is that once software has a dependency on something, things can go wrong when the underlying dependency changes. Pnp.PowerShell is a community initiative, so it can’t be blamed for something like this, but Microsoft engineering groups…

Stay Calm and Keep on Updating Your Modules

Microsoft knows about the problem and I believe work is under way to straighten things out. I’m not sure when the results of that activity will be available.

I still believe in updating PowerShell modules soon after new modules become available. It’s better to take advantage of fixes for reported problems than run old modules and find known bugs. But it’s still wise to test updated modules just in case something weird happens, like a version mismatch that causes a PowerShell module clash.


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/10/20/powershell-module-clash-exo/feed/ 3 62024
How to Control the Creation of Microsoft 365 Groups with the Microsoft Graph PowerShell SDK https://office365itpros.com/2023/10/18/control-group-creation-sdk/?utm_source=rss&utm_medium=rss&utm_campaign=control-group-creation-sdk https://office365itpros.com/2023/10/18/control-group-creation-sdk/#comments Wed, 18 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61987

Control Group Creation to Avoid Group Sprawl

Microsoft’s documentation covering the topic of “Manage who can create Microsoft 365 Groups” begins with: “By default, all users can create Microsoft 365 groups. This is the recommended approach because it allows users to start collaborating without requiring assistance from IT.”

I can’t say how strongly I disagree with this perspective. All it does is result in group sprawl, or more likely, teams sprawl. We learned the lesson with Exchange Server public folders in 1996 when users created new folders with abandon. Organizations are still clearing up the mess today, which is one of the reasons for the persistence of public folders in Exchange Online. The same need will arise to clean up unused and unwanted teams if organizations follow Microsoft’s advice to allow group creation by any and all. Microsoft promised to develop functionality to help with group sprawl in 2021. So far, there’s little sign of progress in this space, unless you include the ownerless group policy (2022) and the group expiration policy (available since 2020).

Group Creation Using the Microsoft Graph PowerShell SDK

The Microsoft documentation explains how to restrict group creation by running PowerShell to configure the Entra ID groups policy. Unhappily, the current version of the documentation uses cmdlets from the Azure AD Preview module, which is due for deprecation in March 2024, The same work can be done using cmdlets from the Microsoft Graph PowerShell SDK, which is what I cover here.

The basic approach is:

  • Create a security group to control group creation. The members of this group will be allowed to create new Microsoft 365 groups via user applications like Outlook and Teams. Accounts holding roles like Global administrator, Teams service administrator, Groups administrator, SharePoint administrator, User administrator, and Exchange administrator can always use administrative interfaces like PowerShell or the Microsoft 365 admin center to create new groups. The members of this group need Entra ID Premium P1 licenses.
  • Update the Entra ID groups policy to block group creation by anyone except the members of the security group.

I have no idea why Microsoft doesn’t make control over Microsoft 365 group creation available through an option in the Microsoft 365 admin center. My cynical side says that this is because they don’t want tenants to control group creation, so they force administrators to use PowerShell.

Create a Security Group to Control Group Creation

A simple security group is sufficient to define the set of accounts allowed to create new Microsoft 365 groups (Figure 1). You can either create a new group or use an existing group. Creating a new group is probably best because you can give the group an appropriate name and description and be sure that the group will only be used to control group creation.

A security group created to control group creation
Figure 1: A security group created to control group creation

Create a Groups Policy Object

Microsoft 365 uses a directory setting object to hold the settings to control creation and other aspects of Microsoft 365 groups. By default, tenants use default settings. To change these settings, you must create a copy of the template directory settings object and modify it. Here’s how to create a new directory settings object by retrieving the identifier of the default object and creating a new object for the tenant:

Connect-MgGraph -Scopes Directory.ReadWrite.All
$PolicyId = (Get-MgBetaDirectorySettingTemplate | Where-Object {$_.DisplayName -eq "Group.Unified"}).Id 
New-MgBetaDirectorySetting -TemplateId $PolicyId

The New-MgBetaDirectorySetting cmdlet fails if a tenant-specific directory settings object already exists.

Updating the Groups Policy to Limit Creation

With a groups policy object in place, we can update the settings. You can see the default settings by running:

Get-MgBetaDirectorySetting | Where-Object {$_.DisplayName -eq "Group.Unified"} | ForEach Values

To control group creation, two settings are updated:

  • EnableGroupCreation: This setting controls if users can create new groups. The default is true. We update it to false.
  • GroupCreationAllowedGroupId: This setting holds the identifier for the group whose members are allowed to create new groups.

The setting names are case-sensitive and should be passed exactly as shown.

To update the settings, fetch the identifier for the group (or have it available). Then populate an array with the current settings before updating the two settings described above. Finally, update the directory settings object with the new policy settings. Here’s the code:

$GroupId = (Get-MgGroup -Filter "displayName eq 'GroupCreationEnabled'").Id
$TenantSettings = Get-MgBetaDirectorySetting | Where-Object {$_.DisplayName -eq "Group.Unified"}
[array]$Values = $TenantSettings.Values
($Values | Where-Object Name -eq 'EnableGroupCreation').Value = "false"
($Values | Where-Object Name -eq 'GroupCreationAllowedGroupId').Value = $GroupId
Update-MgBetaDirectorySetting -DirectorySettingId $TenantSettings.Id -Values $Values

Figure 2 shows these commands being run.

Running the PowerShell code to control group creation
Figure 2: Running the PowerShell code to control group creation

Updating the group policy settings (for instance, to switch the group defining who can create new groups) uses the same approach: find values, update values, update the directory setting object.

If you make a mess of the Groups policy, you can start over by removing the directory settings object and creating a new policy. Here’s how to remove the policy:

$PolicyId = (Get-MgBetaDirectorySetting | Where-Object {$_.DisplayName -eq "Group.Unified"}).Id
Remove-MgBetaDirectorySetting -DirectorySettingId $PolicyId

Keeping Groups Under Control

Even if you decide to limit group creation, it’s a good idea to keep a close eye on what groups and teams are in active use and trim (or archive) those that don’t meet usage thresholds. The Teams and Groups activity report script can help with this process. Another point to consider is that Teams doesn’t come with any form of directory to allow users check if a team already exists for a topic. It’s possible to create such a directory, but making people check the list is a different challenge.

Another example of using directory objects to control groups is to block guest access for individual groups and teams. You can do this with sensitivity labels or by updating the directory setting for individual Microsoft 365 groups with PowerShell.


]]>
https://office365itpros.com/2023/10/18/control-group-creation-sdk/feed/ 3 61987
How to Execute Bulk Updates of Primary SMTP Address for Distribution Lists https://office365itpros.com/2023/10/17/distribution-list-proxy-address/?utm_source=rss&utm_medium=rss&utm_campaign=distribution-list-proxy-address https://office365itpros.com/2023/10/17/distribution-list-proxy-address/#respond Tue, 17 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61972

Updating Distribution List Proxy Addresses is a Good Example of PowerShell in Action

A question in the Microsoft Technical Community asks how to perform a bulk update of all distribution lists whose primary SMTP address uses the tenant service domain and replace this address with one from a “vanity” domain owned by the tenant. For example, replace onmicrosoft.contoso.com with contoso.com. This sometimes happens when a tenant begins operations and forgets to assign an address from a vanity domain when creating new distribution lists (Figure 1). It can also occur if an organization decides to use a different domain for whatever reason.

Assigning a distribution list proxy address during object creation. This becomes the DL's primary SMTP address.
Figure 1: The proxy address assigned during creation becomes a distribution list’s primary SMTP address

Updating primary SMTP addresses for any mail-enabled object is a great example of how PowerShell is so useful to tenant administrators. Let’s figure out what you might do.

Find Distribution Lists to Update

The first step is to extract the set of domains known for the tenant and find the domain marked as the default. We’ll use that domain to create new primary SMTP addresses:

[array]$Domains = Get-AcceptedDomain
$PreferredDomain = $Domains | Where-Object {$_.Default -eq $True} | Select-Object -ExpandProperty DomainName
If (!($PreferredDomain)) { Write-Host "Can't find the default domain" ; break }

You can use any of the accepted domain defined for the tenant, so if you want to use a different domain, amend the script to insert the desired domain in the $PreferredDomain variable.

Find the Target Distribution Lists

Now let’s find the set of distribution lists that use the service domain for their primary SMTP address:

[array]$DLs = Get-DistributionGroup | Where-Object {$_.PrimarySMTPAddress -like "*onmicrosoft.com*"}
If (!($DLs)) {
   Write-Host "No distribution lists use the service domain for their primary SMTP address" ; break
} Else {
   Write-Host ("{0} distribution lists found to update" -f $DLs.count)
}

To check details of the distribution lists, run this command:

$DLs | Format-Table DisplayName, PrimarySMTPAddress

Update the Distribution Lists with a New Primary SMTP Address

If you’re happy to go ahead, this code uses the Set-DistributionGroup cmdlet to update the primary SMTP address for the distribution lists. The code builds the new primary SMTP address for a list from its alias and the preferred domain:

ForEach ($DL in $DLs) {
  $NewSMTPAddress = $DL.Alias + "@" + $PreferredDomain
  Write-Host ("Updating distribution list {0} with new address {1}..." -f $DL.DisplayName, $NewSMTPAddress )
  Set-DistributionGroup -Identity $DL.Alias -PrimarySMTPAddress $NewSMTPAddress
}

Exchange Online keeps all previous SMTP proxy addresses (including the prior primary SMTP address) to make sure that it can correctly handle messages sent to the distribution lists using those addresses. You can see this by running the Get-DistributionGroup cmdlet to examine the email addresses:

Get-DistributionGroup -Identity 'San Francisco Rooms' | Select-Object -ExpandProperty EmailAddresses
SMTP:SanFranciscoRooms@Office365itpros.com
smtp:SanFranciscoRooms@office365itpros.onmicrosoft.com

Alternatively, you can see the proxy addresses by examining the distribution list properties in the Exchange admin center.

The primary SMTP address is only one distribution list proxy address. Like any Exchange Online recipient, a distribution list can have up to 300 proxy addresses (depending on the length of the addresses), so there’s usually plenty of room to store proxy addresses created for different reasons.

Distribution Lists Still Have a Place

Despite Microsoft’s ongoing efforts to persuade customers that Microsoft 365 groups are better than distribution lists, there’s no doubt that these objects still have a place in any tenant. A Microsoft 365 group is a great choice when you want to use Teams or Outlook groups for collaboration, but scenarios still exist where a simple distribution list is the best way to communicate, especially when you want to include external email addresses (guest accounts can also be members of distribution lists).

Here’s a script to report distribution list membership, just in case you’re looking for another project after fixing distribution list proxy addresses and making sure that the right primary SMTP address is in place.


Learn about using Exchange Online and the rest of Office 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/2023/10/17/distribution-list-proxy-address/feed/ 0 61972
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
How to Block User Access to Microsoft 365 PowerShell Modules https://office365itpros.com/2023/10/12/block-powershell-m365/?utm_source=rss&utm_medium=rss&utm_campaign=block-powershell-m365 https://office365itpros.com/2023/10/12/block-powershell-m365/#comments Thu, 12 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61922

Use Enterprise Applications to Block PowerShell Modules

A question arose about the best way to block Microsoft 365 user accounts from being able to run PowerShell. It seemed like a worthy problem to consider. In some cases an obvious answer exists, like stopping Exchange Online users from accessing PowerShell, but that’s a technique that only works for Exchange, and the block needs to be imposed for every new mailbox. We need something more generic that works across Microsoft 365.

Microsoft documents a process to block access to PowerShell for EDU tenants. The script to block PowerShell uses cmdlets from the Azure AD module, which Microsoft is deprecating with retirement scheduled for March 30, 2024. A replacement script using Microsoft Graph PowerShell SDK cmdlets is needed. Fortunately, I’ve been down this path with an article covering secure access to the SDK and can reuse many of the concepts explained there.

Update August 23, 2024: Microsoft now documents how to use the EXOModuleEnabled setting for accounts to control access to Exchange Online PowerShell. The technique described here works for other PowerShell modules.

Key Steps to Block PowerShell Modules

Every application that authenticates against Entra ID is known to the directory. Some applications are created within a tenant (registered apps). Others are created by companies like Microsoft as multi-tenant applications that can run anywhere. These are enterprise applications. The PowerShell modules that connect to Microsoft 365 endpoints like Exchange or Teams authenticate using enterprise applications created by Microsoft. The Microsoft Graph PowerShell SDK is the most obvious of these applications, but other applications exist for the Exchange Online management module, SharePoint Online management module, and the Microsoft Teams module.

Most administrators are unaware that these PowerShell enterprise applications exist. The applications don’t show up in the Entra ID admin center because normally they do not have a service principal. Applications use service principals to store permissions, like the Graph permissions used by the Microsoft Graph PowerShell SDK. Applications without service principals use roles instead.

For instance, when you run the Connect-ExchangeOnline cmdlet to connect to Exchange Online, the ability to work with Exchange data is gated by the roles possessed by the signed-in user account. If the account holds the Exchange administrator or Global administrator role, they can manage all aspects of Exchange Online (this also applies to Azure Automation accounts). If not, they can manage their own mailbox.

The key steps to restrict access to a PowerShell module are:

  • Find the application identifier for the module. We’ll get to doing that in a minute.
  • Create a service principal for the application.
  • Update the service principal so that it uses application role assignments.
  • Create a security group to manage assignments of permission to use the module.
  • Add the security group as an assignment to the service principal.

Finding Application Identifiers for PowerShell Modules

The first step is to find the application identifiers. The easiest way to do this is to check the Entra ID sign-in logs for events when people connect using a PowerShell module. Figure 1 shows an example of a sign-in event logged when an administrator connected with the SharePoint Online management module. We can see that the application identifier is 9bc3ab49-b65d-410a-85ad-de819febfddc.

Finding the application identifier for a PowerShell module from an Entra ID sign-in event

Block PowerShell access
Figure 1: Finding the application identifier for a PowerShell module from an Entra ID sign-in event

Application identifiers for other modules include:

  • Exchange Online management: fb78d390-0c51-40cd-8e17-fdbfab77341b (covers both regular Exchange and the compliance endpoint).
  • Microsoft Teams: 12128f48-ec9e-42f0-b203-ea49fb6af367
  • Azure: 1950a258-227b-4e31-a9cf-717495945fc2
  • Microsoft Graph PowerShell SDK: 14d82eec-204b-4c2f-b7e8-296a70dab67e

Example: Block Access to Exchange Online PowerShell

Now that we know the application identifiers, we can go ahead and create the service principal for the modules to block. Here are the PowerShell commands to connect an interactive Graph session and create a block for Exchange Online:

# Connect to the Grph
Connect-MgGraph -Scopes Directory.ReadWrite.All, Group.ReadWrite.All, Application.ReadWrite.All

# Create security group to control access to Exchange Online PowerShell
$Group = New-MgGroup -DisplayName "Allow access to EXO PowerShell" -MailEnabled:$False -SecurityEnabled:$True -MailNickName 'EXO.PowerShell'

# Create the service principal for the Exchange Online PowerShell app
$ServicePrincipal = New-MgServicePrincipal -Appid 'fb78d390-0c51-40cd-8e17-fdbfab77341b'

# Check that the Service Principal exists
Get-MgServicePrincipal -ServicePrincipalId $ServicePrincipal.Id | Format-Table DisplayName, Id, AppId

DisplayName                                  Id                                   AppId
-----------                                  --                                   -----
Microsoft Exchange REST API Based PowerShell 8d32ebd2-7295-4236-a3da-7c45be69a0b3 fb78d390-0c51-40cd-8e17-fdbfab77341b

# Update the Service Principal so that it requires application role assignments
Update-MgServicePrincipal -ServicePrincipalId $ServicePrincipal.Id -AppRoleAssignmentRequired:$True

# Add the security group as an assignment to the service principal
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $ServicePrincipal.Id -AppRoleId ([Guid]::Empty.ToString()) -ResourceId $ServicePrincipal.Id -PrincipalId $Group.Id

After running these commands, no one can run the Connect-ExchangeOnline cmdlet to connect to Exchange unless they are added to the security group (Figure 2).

Members of the security group permitted to run Exchange Online PowerShell
Figure 2: Members of the security group permitted to run Exchange Online PowerShell

Entra ID rejects connection attempts from unauthorized accounts with an AADSTS50105 error (Figure 3). The “Microsoft Exchange REST API Based PowerShell” name is assigned to the enterprise application by Microsoft.

Error when attempting to run the Exchange Online PowerShell module
Figure 3: Error when attempting to run the Exchange Online PowerShell module

Discovering Who Accesses PowerShell

Often it’s simple to know who should be allowed to be members of the security group controlling access to a module. The tenant administrator, any administrators for a workload (like Teams service administrators), break glass accounts, service accounts such as those used by Azure Automation, and so on. But to be definite, we should review the Entra ID sign-in logs to see who uses a module.

This command retrieves the last 5,000 sign-in records and filters them for any sign-in for the Exchange Online application:

[array]$AuditRecords = Get-MgAuditLogSignIn -Top 5000 -Sort "createdDateTime DESC" -Filter "AppId eq 'fb78d390-0c51-40cd-8e17-fdbfab77341b'"

A simple Group-Object command gives the answer:

$AuditRecords | Group-Object UserPrincipalName -NoElement | Sort-Object Count -Descending| Select-Object Name, Count

Name                               Count
----                               -----
tony.redmond@office365itpros.com      10
EXOAdmin@office365itpros.com           7
James.Atkinson@office365itpros.com     3

You can then decide if any or all of the people who have accessed the module should be added to the security group. To check another module, replace the application identifier in the Get-MgAuditLogSignIn command.

Should My Tenant Block PowerShell?

The factors driving the decision to block PowerShell access for user accounts will differ from organization to organization. At least now you know the best way to block the most common PowerShell modules used with Microsoft 365 and how to find out who’s using the modules.


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/10/12/block-powershell-m365/feed/ 16 61922
How to Remove Licenses From Disabled Accounts with PowerShell https://office365itpros.com/2023/10/11/disabled-accounts-licenses/?utm_source=rss&utm_medium=rss&utm_campaign=disabled-accounts-licenses https://office365itpros.com/2023/10/11/disabled-accounts-licenses/#comments Wed, 11 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61870

The Reasons for Disabled Accounts

Many reasons exist why organizations disable user accounts, including when employees go on sabbaticals, take time off due to illness, or have leave following childbirth. A less innocuous explanation is when employees are suspended for some reason. In all cases, accounts might remain in a disabled state for long periods.

Disabling an account means that Entra ID won’t let the user sign into their account. Data remains online and accessible for corporate purposes such as eDiscovery. Here’s how to disable an account using the Update-MgUser cmdlet from the Microsoft Graph PowerShell SDK:

Update-MgUser -UserId Andy.Ruth@office365itpros.com -AccountEnabled:$False

When the user returns, run Update-MgUser again to restore access by setting the AccountEnabled property to $True. To find the set of disabled accounts, run the Get-MgUser cmdlet like this:

Get-MgUser -Filter "accountEnabled eq false" -Property AccountEnabled, Id, DisplayName -All

Licensing of Disabled Accounts

Because accounts might be disabled for a long time, thoughts turn to the monthly license charges levied by Microsoft. If someone’s away for six months, should the organization pay for six months’ of charges. If the account has a Microsoft 365 E3 license and perhaps an add-on license (like SharePoint-Syntex advanced management) and a Teams calling plan, the costs could mount to $300 or thereabouts while the user is away.

One or two accounts incurring charges without use might not be a big deal. Interest about controlling license costs mounts as the number of disabled accounts mount. Twenty disabled accounts means $6,000 over six months. At that point, it might be worthwhile taking action to remove licenses from disabled accounts until their owners return to work.

Removing Exchange Online Licenses Leads to Disabled Mailboxes

Before rushing to remove all licenses from disabled accounts, let me sound a note of caution about removing products that include Exchange Online. An Exchange Online service plan is included in many Office 365 and Microsoft 365 products. For instance, Exchange Online Plan 2 (necessary for option such as archive mailboxes) is part of the Office 365 E3 and Office 365 E5 products. If you remove disable the Exchange Online service plan or remove the license for a product that includes Exchange Online from an account, the mailbox goes into a disabled state. One way to find mailboxes without licenses is to use the Get-EXOMailbox cmdlet to check if mailboxes have a valid SKU (product license):

Get-EXOMailbox -Filter {SkuAssigned -eq $True} | Format-Table DisplayName, UserPrincipalName, ExternalDirectoryObjectId

Exchange Online permanently removes disabled mailboxes after 30 days. To move from the disabled state, the owner’s account must be assigned a license that includes an Exchange Online service plan.

When removing licenses from disabled accounts, it’s important to check for Exchange Online to make sure that a removal doesn’t lead to potential data loss. Two options are available:

  • Retain assigned licenses that include Exchange Online for disabled accounts.
  • Replace the assigned license with a lower-cost license that includes Exchange Online. For example, you could assign inexpensive Office 365 E1 or F3 licenses to keep account mailboxes in a healthy state.

Exchange Online supports license stacking, meaning that it’s possible to assign multiple licenses to accounts that include an Exchange Online service plan. When this happens, Exchange Online uses the most functional plan.

Scripting License Removal

This article covers the basics of license management with the Microsoft Graph PowerShell SDK. The outline of a script to find and remove licenses from disabled accounts might include the following steps:

  • Connect to the Graph.
  • Define exclusions for licenses that should not be removed from accounts (those with Exchange Online).
  • Find disabled accounts.
  • Loop through each account to examine the assigned licenses and decide if any can be removed.
  • Run the Set-MgUserLicense cmdlet to remove the licenses.
  • Report the actions taken.

If an organization uses group-based licensing, Set-MgUserLicense cannot remove licenses assigned using this mechanism. Instead, the correct approach is to remove the account from the group used by Entra ID to control license assignments.

My version of a script to process license removals for disabled accounts can be downloaded from GitHub. It includes code to exclude licenses containing Exchange Online service plans. As mentioned earlier, the alternative is to replace licenses with a cheaper version. The code to do this would be simple to add. The script excludes licenses assigned through group-based licenses. Again, it would be easy to add code to remove accounts from the groups used to assign licenses. Figure 1 shows the script in action.

Removing licenses from disabled accounts
Figure 1: Removing licenses from disabled accounts

The Shared Mailbox Approach

Another way to handle the question of what to do with mailboxes belonging to long-term absentees is to turn them into shared mailboxes for the duration of their owner’s absence. When the owner returns, revert the shared mailbox to make it a regular mailbox again. This technique preserves the mailbox because shared mailboxes don’t need licenses. Here’s what you do:

  1. Convert the mailbox into a shared mailbox.
  2. Disable the account and change the password.
  3. Remove all licenses.
  4. (Optional) Hide the shared mailbox from Exchange address lists.
  5. (Optional) Remove the shared mailbox from distribution lists so that mail doesn’t pile up in the mailbox during the owner’s absence.

When the user returns:

  1. Convert the shared mailbox to a regular mailbox.
  2. Enable the account and assign a new password.
  3. Assign licenses to the account.
  4. Unhide (if necessary) the mailbox.
  5. Restore distribution list membership.

Check and Verify Before Use

Remember that the script illustrates the principles behind license removal for disabled accounts. It is not a production-ready solution. Like any code downloaded from the internet, you should verify and test the script and adapt it to meet your needs (especially because it removes licenses from accounts). The nice thing is that everything’s done in PowerShell, so please go ahead and modify the code as you wish.


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/10/11/disabled-accounts-licenses/feed/ 1 61870
How to Create Dynamic Microsoft 365 Groups (and Teams) for Departments https://office365itpros.com/2023/10/10/dynamic-microsoft-365-groups/?utm_source=rss&utm_medium=rss&utm_campaign=dynamic-microsoft-365-groups https://office365itpros.com/2023/10/10/dynamic-microsoft-365-groups/#respond Tue, 10 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61844

Create Dynamic Microsoft 365 Groups and Teams with PowerShell

No sooner had I published the article about creating dynamic administrative units with PowerShell, the first email arrived asking if the same was possible for dynamic Microsoft 365 groups. The answer is “of course,” but with the caveat that it’s not just a matter of some minor updates to the script.

That being said, the outline for the script to create dynamic groups is broadly the same:

  • Find the licensed users in the tenant and extract a list of departments. The departments should be accurate and some care should be taken to eliminate inconsistencies. For instance, some people might be listed as belonging to IT while others belong to the Information Technology department. Decide on one value and apply it to all.
  • You might not want to create groups for all departments. The script defines an array of excluded departments that are removed from the set to process.
  • Find the set of dynamic Microsoft 365 groups. We need this information to check if a dynamic group already exists for a department.
  • For each department, check if a group already exists. If not, define some parameters for the new group, including the membership rule that Entra ID uses to calculate the group members, and run the New-MgGroup cmdlet to create the group.
  • Following a successful creation, proceed to team-enable the new group by running the New-MgTeam cmdlet. This is an optional step, but seeing that Teams is the heaviest workload for Microsoft 365 groups, it seemed like a good thing to include.

Let’s examine some of the steps.

Scripting the Creation of a Dynamic Microsoft 365 Group

Here’s an example of creating a new dynamic Microsoft 365 group for the department whose name is stored in the $Dept variable:

Write-Host ("Checking groups for department {0}" -f $Dept)
$Description = ("Dynamic Microsoft 365 group created for the {0} department on {1}" -f $Dept, (Get-Date))
$DisplayName = ("{0} Dynamic group" -f $Dept)
$MailNickName = ("Dynamic.{0}.Group" -f ($Dept -replace " ",""))
$MembershipRule = '(User.Department -eq "' + $Dept +'")'

If ($DisplayName -in $Groups.DisplayName) {
   Write-Host ("Group already exists for {0}" -f $Dept) -ForegroundColor Red
} Else {
# Create the new dynamic Microsoft 365 Group
   $NewGroup = New-MgGroup -DisplayName $DisplayName -Description $Description ` 
   -MailEnabled:$True -SecurityEnabled:$False `
   -MailNickname $MailNickName -GroupTypes "DynamicMembership", "Unified" `
   -MembershipRule $MembershipRule -MembershipRuleProcessingState "On"
}

Wait Before Progressing to Teams

Flushed with the successful creation, you might want to rush to team-enable the new group. However, it’s best to wait 10-15 seconds before proceeding to allow Teams to learn about the new group from Entra ID. If you attempt to team-enable a group immediately after creation, you’ll probably see an error like this:

Failed to execute Templates backend request CreateTeamFromGroupWithTemplateRequest. Request Url: https://teams.microsoft.com/fabric/emea/templates/api/groups/bab7a3a8-2e30-4996-9405-48ca395b99c6/team, Request Method: PUT, Response Status Code: NotFound, Response Headers: Strict-Transport-Security: max-age=2592000
x-operationid: a228258204c3466dbd64c4d88373a416
x-telemetryid: 00-a228258204c3466dbd64c4d88373a416-82a9b5015f332574-01
X-MSEdge-Ref: Ref A: FC01DAADBD0D4A1A9ECBB9826707CC17 Ref B: DB3EDGE2518 Ref C: 2023-10-04T15:00:51Z
Date: Wed, 04 Oct 2023 15:00:52 GMT
ErrorMessage : {"errors":[{"message":"Failed to execute GetGroupMembersMezzoCountAsync.","errorCode":"Unknown"}],"operationId":"a228258204c3466dbd64c4d88373a416"}

Team-Enabling a Group

To team-enable a group, run the New-MgTeam cmdlet and provide a hash table containing information to allow Teams to find the new group (the Graph URI for the group) plus the Teams template to use. This code does the trick.

$GroupUri = "https://graph.microsoft.com/v1.0/groups('" + $NewGroup.Id + "')"
$NewTeamParams = @{
   "template@odata.bind"="https://graph.microsoft.com/v1.0/teamsTemplates('standard')"
   "group@odata.bind"="$($GroupUri)"
}
$NewTeam = New-MgTeam -BodyParameter $NewTeamParams
If ($NewTeam) {
   Write-Host ("Successfully team-enabled the {0}" -f $NewGroup.DisplayName)
}

Checking Groups Post-Creation

Figure 1 shows some of the dynamic Microsoft 365 groups created in my tenant. Note the groups for “Information Technology” and the “IT Department.” Obviously my checking of user departments was deficient prior to running the script. The fix is easy though. Decide on which department name to use and update user accounts to have that. Then remove the now-obsolete group. Entra ID will make sure that the accounts with reassigned departments show up in the correct group membership.

Dynamic Microsoft 365 groups created for departments
Figure 1: Dynamic Microsoft 365 groups created for departments

In this case, only one account had “IT Department,” so I quickly updated its department property with:

Update-MgUser -UserId Jack.Smith@office365itpros.com -Department "Information Technology"

I then removed the IT Department dynamic group:

$Group = Get-MgGroup -Filter "displayName eq 'IT Department Dynamic Group'"
Remove-MgGroup -GroupId $Group.Id

Soon afterwards, the membership of the Information Department Dynamic group was correct (Figure 2) and all was well.

Membership of a dynamic Microsoft 365 group for a department
Figure 2: Membership of a dynamic Microsoft 365 group for a department

You can download the complete script from GitHub. It would be easy to adapt the code to run as an Azure Automation runbook to scan for new departments and create groups as necessary.

Simple PowerShell Results in Big Benefits

Scripting the creation of dynamic Microsoft 365 groups for each department in a tenant isn’t too difficult. The membership rule is simple but could be expanded to include different criteria. Once the groups are created, they should be self-maintaining. That is, if you make sure that the department property for user accounts is accurate.


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 like dynamic Microsoft 365 groups work.

]]>
https://office365itpros.com/2023/10/10/dynamic-microsoft-365-groups/feed/ 0 61844
Microsoft Removes Exchange Online User Photo Cmdlets https://office365itpros.com/2023/10/09/user-photo-cmdlets/?utm_source=rss&utm_medium=rss&utm_campaign=user-photo-cmdlets https://office365itpros.com/2023/10/09/user-photo-cmdlets/#comments Mon, 09 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61833

Use Graph SDK Cmdlets to Manage User and Group Photos

According to message center notification MC678855 (2 October), effective November 30, 2023, Microsoft will retire the Exchange Online management cmdlets to manipulate photos for mailboxes (Get-, Set-, and Remove-UserPhoto). This is part of the work to improve the way Microsoft 365 manages and displays user photos and moves the photo storage location away from Exchange Online to Entra ID. Microsoft says that this will create “a coherent user profile image experience by retiring legacy profile photo services.

Basically, this effort resolves the inconsistencies that crept into user photo handling through Exchange and SharePoint doing their own thing, largely because of their on-premises roots. Delve attempted to fix the problem in 2015 but never really went anywhere. After that, Microsoft started to use Exchange Online to host photos and synchronize from there, but it’s a better idea to use Entra ID and have all workloads come to a common place for photo data.

Replacement User Photo Cmdlets

The replacement cmdlets for user photo management are in the Microsoft Graph PowerShell SDK:

  • Set-MgUserPhotoContent: Add a photo to an Entra ID account. You can add JPEG or PNG files of up to 4 MB. Entra ID can store photos with a large pixel count. I have commonly uploaded photos sized at 8256 x 5504 pixels. When applications fetch photos to use, they can specify what sized photo they wish Entra ID to provide ranging from a thumbnail (48 x 48 pixels) to a high-definition photo as used in Teams meetings.
  • Get-MgUserPhoto: Check if an account has photo data in the profilePhoto property.
  • Update-MgUserPhoto: According to the documentation, this cmdlet “updates the navigation photo in users.” That doesn’t make much sense, so I asked the SDK development group to ask what the text really means. As it turns out, this cmdlet is a duplicate of Set-MgUserPhotoContent, so you can ignore it.
  • Remove-MgUserPhoto: Remove user photo information from an account.

For example:

Set-MgUserPhotoContent -Userid Jim.Smith@office365itpros.com -Infile "c:\temp\Jim.Smith.jpg"

 A user photo updated in Entra ID
Figure 1: A user photo updated in Entra ID

Updating Scripts

From an administrator perspective, the impact of the change is a need to review scripts that call the old cmdlets to replace them with the SDK cmdlets. The changes to the script are likely to involve:

  • Call the Connect-MgGraph cmdlet to connect to the SDK.
  • Find target user accounts instead of mailboxes.
  • Remove the references to Get-UserPhoto and Set-UserPhoto.
  • Use the Get-MgUserPhoto cmdlet to find if a target mailbox has a photo and the Set-MgUserPhotoContent cmdlet to update the photo if necessary (and a suitable file is available).

To provide a working example, I updated the script mentioned in this article. You can download the full script from GitHub. Remember that Graph permissions work differently to the permissions granted when an account holds the Exchange administrator or Global administrator roles for a tenant. Using the SDK in an interactive session to update photos will only work if the signed in account holds one of the two roles mentioned above and consent is granted for the SDK app to use the Directory.ReadWrite.All permission.

Group Photos

Because it’s a mailbox cmdlet and supports the GroupMailbox switch, the Set-UserPhoto cmdlet can set photos for Microsoft 365 groups. The Set-MgUserPhotoContent cmdlet only handles user accounts. To update the photos for Microsoft 365 groups, it’s necessary to use the Set-MgGroupPhotoContent cmdlet. Alternatively, for team-enabled groups, you can use the Set-TeamPicture cmdlet from the Microsoft Teams module.

I wrote an article describing how to update photos for Microsoft 365 groups. Updating the associated script wasn’t quite as simple because the Get-MgGroupPhoto cmdlet doesn’t return a thumbnail identifier. The foundation of the original script is that the thumbnail identifier could tell the script if the group already had a photo. This is now not possible, so the updated script (available from GitHub) is a rewritten and simplified version of the original.

Another Example of Change

This transition is yet another example of recent change in the Microsoft 365 PowerShell space. Exchange Online has just turned off Remote PowerShell and we’re on the final stretch of deprecation for the Microsoft Online Services module (the cmdlets that deal with license assignment have already stopped working). Keeping up to date with cmdlet changes can take some time but it’s an essential task.


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/10/09/user-photo-cmdlets/feed/ 2 61833
How to Convert Custom Background Images for Teams 2.1 https://office365itpros.com/2023/10/04/custom-background-image-teams21/?utm_source=rss&utm_medium=rss&utm_campaign=custom-background-image-teams21 https://office365itpros.com/2023/10/04/custom-background-image-teams21/#comments Wed, 04 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61781

Tracking the Evolution of Custom Background Images for Teams Meetings

I’ve written about using custom background effects in Teams meetings since their introduction in early 2020, including how to fetch and use the Bing daily images as custom background inages for Teams meetings.

Recently, I’ve used the Teams 2.1 client as my daily driver. Microsoft expected that the new Teams client would become the default client in late September. The switchover is very close and the new client is in good shape. It’s as stable as its Electron-based predecessor and performs better, so using Teams 2.1 is a reasonable option for someone like me. The task of deploying the new client across a large enterprise will take some planning and coordination.

No Custom Background Images After Switch to Teams 2.1

As part of my personal move, I noticed that the custom background images I use for Teams meetings are unavailable in Teams 2.1. Some investigation revealed that Teams 2.1 uses a different folder to store custom images. Here are the folders used by the two clients.

  • Teams 1: C:\Users\userx\AppData\Roaming\Microsoft\Teams\Backgrounds\Uploads
  • Teams 2.1: C:\Users\userx\AppData\Local\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Backgrounds\Uploads

Switching to Teams 2.1 doesn’t transfer custom background images to the new location, possibly because Microsoft uses a new naming scheme for the images. Instead of regular names like My favorite beach scene.jpg, Teams 2.1 uses names like 42b9a5ad-460e-46df-8c1b-f4d7c42dffc0.jpg. Each image exists in a high-resolution (at least 1920 x 1080 pixels) version and a lower-resolution thumbnail (220 x 158 pixels). The high-resolution version is the image that Teams loads as a meeting background. The thumbnails are displayed in the gallery of available background images.

Transferring Custom Background Images for Teams 2.1

Because of the naming scheme used by Teams 2.1, it’s not enough to simply copy custom background images from the Teams 1 folder because Teams 2.1 ignores any files that don’t follow its naming convention (with or without a thumbnail).

The obvious fix for the problem is to upload custom background images into the Teams 2.1 client. That’s acceptable when only a couple of files are involved. Things get boring thereafter, which is why I wrote a PowerShell script (downloadable from GitHub). The script:

  • Loads a Resize-Image function to resize images to the desired sizes (originally written by Christopher Walker and found in GitHub).
  • Defines the folders used by Teams 1 and Teams 2.1.
  • Finds JPG files in the Teams 1 folder. The script ignores any thumbnails and only processes high-resolution images.
  • Call the Resize-Image function to generate new high-resolution files sized at 1920 x 1080 and thumbnails. The file names follow the Teams 2.1 scheme.
  • Copy the generated files to the Teams 2.1 folder and remove them from Teams 1 folder.
  • Lists the JPG files now in the Teams 2.1 folder.

It’s not a particularly complex script, but it worked. The acid test is that I can select my custom background images from the gallery when in a meeting with the Teams 2.1 client (Figure 1).

Custom background images in the gallery of the Teams 2.1 client
Figure 1: Custom background images in the gallery of the Teams 2.1 client

Bumps in the Changeover

The changeover from Teams 1 to Teams 2.1 will reveal some flaws. The architecture and internal functions of the two clients are very different. Using a different folder to store custom background images is a very small indicator of the kind of change involved in the transition. I have no idea why Microsoft decided to switch folders as the Teams 1 folder seemed perfectly acceptable, but they did. Now you know how to transfer custom background images to the new location, the issue goes away.

I don’t see any reason why the same technique could not be used to distribute a set of organization images to workstations. If you can access the folder, you can copy custom background images to it and make those images available for Teams meetings.


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/10/04/custom-background-image-teams21/feed/ 22 61781
How to Update Shared Mailbox Owners About Quota Usage https://office365itpros.com/2023/10/03/shared-mailbox-quota-report/?utm_source=rss&utm_medium=rss&utm_campaign=shared-mailbox-quota-report https://office365itpros.com/2023/10/03/shared-mailbox-quota-report/#comments Tue, 03 Oct 2023 01:00:00 +0000 https://office365itpros.com/?p=61729

Shared Mailbox Quota Report a Take on an Old Script

In September 2019, I wrote about using PowerShell to generate an Exchange Online mailbox quota report. The idea was to allow administrators to identify mailboxes that surpassed a certain quota threshold (say 85%) so that they could proactively manage the situation and prevent users from exceeding quota. It’s never good for someone to be unable to send and receive email because of a blown quota.

The 2019 article came to mind when I was asked about writing a script to report quota usage for shared mailboxes. These mailboxes don’t have formal owners, but the idea was to regard anyone with full access to the mailbox as an owner. The purpose of the script is to capture details of quota usage and email that information to the mailbox owners.

Stitching Bits Together to Create a New Script

One of the nice things about PowerShell is that it’s easy to reuse code from scripts to form a new solution. In this case, I used the following:

Reusing code saves time, which is one of the prime benefits cited for GitHub Copilot. Why write code from scratch when you can find it on the internet (always test this code first) or on your workstation?

Script Code Flow to Create and Email Shared Mailbox Quota Reports

The major steps in the script are:

  • Define settings such as the account used to send email, the account that will serve as the default recipient if no accounts with full access are found for a mailbox, app and tenant identifiers, and the certificate thumbprint to use for authentication.
  • Sign into Exchange Online to use cmdlets like Get-ExoMailboxStatistics.
  • Sign into the Graph using an app and certificate.
  • Find the set of shared mailboxes with Get-ExoMailbox.
  • For each mailbox, find the set of accounts with full access rights. This set might include security groups, so some processing is needed to identify groups and extract their membership.
  • Check if mailboxes are assigned a product license containing the Exchange Online Plan 2 service plan. If so, their quota is higher (100 GB) than the default (50 GB). Some unlicensed mailboxes have the higher quota, but that’s only because Microsoft hasn’t reduced those quotas (yet).
  • Fetch the current mailbox statistics.
  • Compute the percentage of quota used.
  • Write data about each shared mailbox into a list.

After processing the shared mailboxes, a second step loops through the list to create and send messages to the mailbox owners to tell them how much quota is used. Figure 1 shows an example of a quota message generated by the script.

Email notification for shared mailbox quota usage
Figure 1: Email notification for shared mailbox quota usage

The message is sparse and lots of possibilities exist for including other information in it, such as pointers to tell recipients what to do if the percentage quota used is more than a certain threshold. You’re only limited by your imagination!

You can download the full script from GitHub.

PowerShell Fills the Gaps

This is yet another example of how to use PowerShell to fill in the gaps in Microsoft 365 tenant administration. Some people might not care too much about shared mailbox quotas, other will be very concerned. PowerShell gives you the ability to code your own solution if you think it’s necessary.


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/10/03/shared-mailbox-quota-report/feed/ 1 61729
How to Monitor New Members Added to Teams https://office365itpros.com/2023/09/27/monitor-new-teams-members/?utm_source=rss&utm_medium=rss&utm_campaign=monitor-new-teams-members https://office365itpros.com/2023/09/27/monitor-new-teams-members/#respond Wed, 27 Sep 2023 01:00:00 +0000 https://office365itpros.com/?p=61747

Monitor New Teams Members and Remove Undesired Members Automatically

A question in the Office 365 Technical Discussions Facebook group asked about a script to monitor member additions to Teams. If the new member comes from specific departments, they should be removed. The script should then post details of new members that pass the check to a team channel.

On the surface, this seems to be an ideal situation for Information Barriers, a Microsoft Purview solution designed to keep designated groups of users from communicating with each other. However, Information Barriers require Office 365 E5 or above and it’s a solution that’s best suited when hard barriers must be enforced.

Sketching Out a Solution to Monitor New Teams Members

A custom solution isn’t too difficult to design. The essential steps are:

  • Periodically search the unified audit log to find events captured when a new member joins a group. The example script searches for events occurring within the last three hours.
  • Check if the group is one of the monitored set (defined in an array of group identifiers). If it is, record details of the group and the user. Clearly, you could use whatever criteria you wanted to check new team members.
  • For some reason, searching the unified audit log can return multiple instances of add member events. This might be part of the problems the audit log has suffered recently. To remove duplicates, the script sorts the list of detected events.
  • Loop through the deduplicated events and check the department for each added member. If the department is on the banned list, remove the user from the group. If not, post a message to a designated channel in the team to announce their arrival.

Teams also posts notices about new users to the information pane (Figure 1). The advantage of doing it this way is the ability to remove members plus do whatever other processing is desired.

Notification of new members in the information pane
Figure 1: Notification of new members in the information pane

Posting Teams Channel Messages

As covered in this article, several methods exist to post messages to Teams channels. Briefly:

  • The Submit-PnpTeamsChannelMessage cmdlet. A connection to PnP must be established first and the signed in account must be a member of the target team.
  • The New-MgTeamChannelMessage from the Microsoft Graph PowerShell SDK. This cmdlet only supports delegate permissions (Channel.Send.Message), meaning that the signed-in account must be a member of the target team.
  • Connect the Incoming Webhook connector to the target channel and post a JSON-format message to the connector. This method works without authentication.

To illustrate the principles behind the solution, I choose to use the SDK method because the script already used the Get-MgUser cmdlet to fetch details of user departments.

Diving Into the Code to Monitor New Teams Members

Here’s the code used to search for audit records and extract information from records of interest:

$StartDate = (Get-Date).AddHours(-3)
$EndDate = (Get-Date).AddHours(1)

Write-Host "Searching for audit records..."
[array]$Records = Search-UnifiedAuditLog -Start $StartDate -End $EndDate -Operations "Add member to group" -Formatted -ResultSize 500
If (!($Records)) { Write-Host "No member additions to groups to check" ; break }

Write-Host "Processing audit records..."
$MembersReport = [System.Collections.Generic.List[Object]]::new() 
ForEach ($Rec in $Records) {
  $AuditData = $Rec.AuditData | ConvertFrom-Json
  $GroupId = $AuditData.ModifiedProperties | Where-Object {$_.Name -eq 'Group.ObjectID'} | Select-Object -ExpandProperty NewValue
  $GroupName = $AuditData.ModifiedProperties | Where-Object {$_.Name -eq 'Group.DisplayName'} | Select-Object -ExpandProperty NewValue
  $UserAdded = $AuditData.ObjectId
  $Actor = $Rec.UserIds
  If ($GroupId -in $GroupsToCheck) {
    $UserData = Get-MgUser -UserId $UserAdded -Property Id, displayName, department
    $ReportLine = [PSCustomObject]@{
      Team       = $GroupName
      User       = $UserAdded
      UserName   = $UserData.displayName
      UserId     = $UserData.Id
      Addedby    = $Actor
      Timestamp  = $Rec.CreationDate
      Department = $UserData.Department
      GroupId    = $GroupId
      Id         = ("{0}_{1}_{2}" -f $GroupName, $UserAdded, $Rec.CreationDate) }
    $MembersReport.Add($Reportline)
  }
}

And here’s how the script processes member removals and posting notifications for approved new members:

ForEach ($R in $MembersReport) {
  If ($R.Department -in $ExcludedDepartments) {
     Write-Host ("User {0} with department {1} will be removed from team" -f $R.User, $R.Department) -ForegroundColor Red
     Remove-MgGroupMemberByRef -DirectoryObjectId $R.UserId -GroupId $R.GroupId 
  } Else {
    Write-Host ("Sending channel message about new team member {0}" -f $R.UserName) -ForegroundColor Yellow
    [string]$UserName = $R.UserName
    $HtmlContent = "<h1>New User Has Joined Our Team</h1>
               <h2>$UserName has joined this team</h2><p></p>
               <p>Please welcome <b>$UserName</b> to the team. They will bring great joy to all of us!</p>"
    $Message = (New-MgTeamChannelMessage -TeamId $TargetTeamId -ChannelId $TargetTeamChannelId -Body @{Content = $HTMLContent; ContentType = "html"} -Subject "New User Join Report" -Importance "High")
  }
}

Figure 2 shows an example of the message posted to Teams. The content of the message is very simple HTML and could easily be enhanced to communicate whatever sentiments are desired.

New member notification posted to a Teams channel

Monitor new teams members
Figure 2: New member notification posted to a Teams channel

Improve the Solution to Monitor New Teams Members

I don’t pretend that this script is a complete solution. It would be more effective to run as a scheduled Azure Automation runbook (here’s an example of a runbook that monitors audit events). At a pinch, it could run as a scheduled task on a workstation, but I prefer Azure Automation to Windows Scheduler for several reasons. In any case, the principle is proven and now it’s up to you to take the code forward and make it work the way you want. You can download the sample script from GitHub.


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/09/27/monitor-new-teams-members/feed/ 0 61747
How to Analyze User Email Traffic by Internal or External Destination https://office365itpros.com/2023/09/26/message-trace-user-analysis/?utm_source=rss&utm_medium=rss&utm_campaign=message-trace-user-analysis https://office365itpros.com/2023/09/26/message-trace-user-analysis/#comments Tue, 26 Sep 2023 01:00:00 +0000 https://office365itpros.com/?p=61708

Use PowerShell to Analyze Message Trace Data to Find Out Who’s Sending External Email

Updated 8 November 2023

In an August 2023 article, I explain how to use PowerShell to analyze message trace data to report the volume of traffic going to external domains. A follow-up question about that article asked if it was possible to create a report showing the volume of external and internal email sent by each user. It seemed like this should be a straightforward thing to do, and that’s what I explain how to do here.

Message trace data captures the sender and recipient for each message. If the recipient address doesn’t belong to one of the domains registered for the tenant, we can conclude that it’s external email. The previous article explains how to fetch message data for the last ten days from Exchange Online and construct a list of messages suitable for analysis. This script uses exactly the same code to fetch message trace data. The difference is what we do with that data.

Finding Messages for Users

The first thing is to find the set of mailboxes we’re interested in reporting. The example script processed both user and shared mailboxes.

[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox, SharedMailbox -ResultSize Unlimited

Now the script loops through each mailbox and finds if any message trace transactions are available for the mailbox. If true, the script counts internal and external messages and calculates the percentage of the overall total for each category. The script also records to which external domains a mailbox sends messages before capturing the data in a PowerShell list:

ForEach ($User in $Mbx) {
  Write-Host ("Processing email for {0}" -f $User.DisplayName)
  # Get messages sent by the user
  [array]$UserMessages = $Messages| Where-Object {$_.Sender -eq $User.PrimarySmtpAddress}
  If ($UserMessages) {
  # We’ve found some messages to process, so let’s do that
  [int]$ExternalEmail = 0; [int]$InternalEmail = 0; [array]$ExternalDomains = $Null
  ForEach ($M in $UserMessages) {
    $MsgRecipientDomain = $M.RecipientAddress.Split('@')[1]    
        If ($MsgRecipientDomain -in $Domains) {
            $InternalEmail++ 
        } Else {
            $ExternalEmail++
            $ExternalDomains += $MsgRecipientDomain
        }
  }
  $ExternalDomains = $ExternalDomains | Sort-Object -Unique
  $PercentInternal = "N/A"; $PercentExternal = "N/A"
  If ($InternalEmail -gt 0) {
     $PercentInternal = ($InternalEmail/($UserMessages.count)).toString("P") }
  If ($ExternalEmail -gt 0) {
     $PercentExternal = ($ExternalEmail/($UserMessages.count)).toString("P") }

  $ReportLine = [PSCustomObject]@{
    User          = $User.UserPrincipalName
    Name          = $User.DisplayName
    Internal      = $InternalEmail
    "% Internal"  = $PercentInternal
    External      = $ExternalEmail 
    "% External"  = $PercentExternal
    "Ext.Domains" = $ExternalDomains -Join ", "
    “Mbx Type”    = $User.RecipientTypeDetails }
  $MessageReport.Add($ReportLine)
 } # End if user
} # End ForEach mailboxes

An example of the information reported for a mailbox is shown below:

User        : Tony.Redmond@office365itpros.com
Name        : Tony Redmond (User)
Internal    : 115
% Internal  : 64.97%
External    : 62
% External  : 35.03%
Ext.Domains : bermingham.com, codetwo.com, eastman.com, eightwone.com, microsoft.com, nordan.ie, o365maestro.onmicrosoft.com, office365.microsoft.com, ravenswoodtechnology.com, sharepointeurope.com, thecluelessguy.de

Generating an Analysis Report

After generating the report file, the script creates two output files: a CSV file that might be used for further analysis with Excel or another tool (perhaps it might be interesting to visualize the data in Power BI) and a HTML report (Figure 1).

Mail Traffic User Analysis report

Message trace data analysis
Figure 1: Mail Traffic User Analysis report

An “N/A” value in either of the field reporting the percentage of email sent internally or externally means that the user sent no messages of this type during the reporting period (last 10 days). If a mailbox doesn’t send any email during that time, the script doesn’t include it in the report.

You can download the full script from GitHub.

It’s PowerShell

The point is that none of what the script does is magic. The message trace data is easily accessible and available for analysis. All you need to do is slice and dice the data as you wish, using PowerShell to sort, refine, or otherwise process the information. Learning how to use PowerShell is such a fundamental part of working with tenant data that it always surprises me when I meet tenant administrators who seem unwilling to master the shell. Oh well, at least it gives me topics to write about!


Learn about maximizing the use of Exchange Online data 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/2023/09/26/message-trace-user-analysis/feed/ 4 61708
How to Create Dynamic Administrative Units with PowerShell https://office365itpros.com/2023/09/25/dynamic-administrative-units-ps/?utm_source=rss&utm_medium=rss&utm_campaign=dynamic-administrative-units-ps https://office365itpros.com/2023/09/25/dynamic-administrative-units-ps/#comments Mon, 25 Sep 2023 01:00:00 +0000 https://office365itpros.com/?p=61642

Creating a Dynamic Administrative Unit Per Department

I wrote about using dynamic Entra ID administrative units earlier this year. Not much has changed since then as the feature remains in preview, but an interesting question asked about creating dynamic administrative units with PowerShell. I could have referred the questioner to Microsoft’s documentation, but its examples feature cmdlets from the soon-to-be-deprecated Azure AD module. An example using the Microsoft Graph PowerShell SDK seems like a better idea, so that’s what I cover here.

The question asked about using a CSV file containing department names with the idea of creating a separate dynamic administrative unit for each department. Using CSV files is an effective way of driving scripts, but if the tenant directory is accurate and maintained, it’s easy to extract a list of departments from user accounts.

Scripting the Creation of Dynamic Administrative Units

The steps in a script to create a dynamic administrative unit per department are as follows:

  • Run the Get-MgUser cmdlet to fetch the set of licensed Entra ID member accounts in the tenant. It’s important to fetch licensed accounts to exclude accounts used with shared mailboxes, room mailboxes, and member accounts created through synchronization for multi-tenant organizations.
  • Create an array of departments from user accounts.
  • Create an array of existing administrative units that we can check against to avoid creating duplicate administrative units.
  • For each department, run the New-MgBetaAdministrativeUnit cmdlet to create a new administrative unit (the beta module of the Microsoft Graph PowerShell SDK is needed because the feature is in preview).
  • Calculate the membership rule to find accounts belonging to the department.
  • Run the Update-MgBetaAdministrativeUnit to transform the administrative unit to use dynamic membership.

Here’s the code used to create a new administrative unit:

$Description = ("Dynamic administrative unit created for the {0} department created {1}" -f $Department, (Get-Date))
    $DisplayName = ("{0} dynamic administrative unit" -f $Department)

    If ($DisplayName -in $CurrentAUs.DisplayName) {
        Write-Host ("Administrative unit already exists for {0}" -f $DisplayName)
    } Else {
    # Create the new AU
    $NewAUParameters = @{
        displayName = $DisplayName
        description = $Description
        isMemberManagementRestricted = $false
       }
       $NewAdminUnit = (New-MgBetaAdministrativeUnit -BodyParameter $NewAUParameters)
    }

And here’s the code to transform it into a dynamic administrative unit:

$MembershipRule = '(user.department -eq "' + $Department + '" -and user.usertype -eq "member")'
       # Create hash table with the parameters
       $UpdateAUParameters = @{
	      membershipType = "Dynamic"
	      membershipRuleProcessingState = "On"
	      membershipRule = $MembershipRule
        }
        Try {
            Update-MgBetaAdministrativeUnit -AdministrativeUnitId $NewAdminUnit.Id -BodyParameter $UpdateAUParameters
        } Catch {
            Write-Host ("Error updating {0} with dynamie properties" -f $NewAdminUnit.DisplayName )
        }
        Write-Host ("Created dynamic administrative unit for the {0} department called {1}" -f $Department, $NewAdminUnit.DisplayName)
        

Figure 1 shows the properties of a dynamic administrative unit created by the script, which you can download from GitHub.

Properties of a dynamic administrative unit
Figure 1: Properties of a dynamic administrative unit

Membership Rules Glitches

The membership rule determines the membership of a dynamic administrative unit. Although you can construct filters to use with the Get-MgUser cmdlet to find licensed user accounts belonging to a department, the same flexibility doesn’t exist for the rules used to interrogate Entra ID to find members for a dynamic administrative unit (or dynamic Microsoft 365 group).

The problem is that membership rules don’t allow you to mix properties of different types. For instance, the rule can find user accounts belonging to a department (a string property), but it can’t combine that clause with a check against the assignedLicenses property to make sure that the account is licensed. That’s because assignedLicenses is a multi-value property and the rule can’t mix checks against strings with checks against multi-value properties. If you try, Entra ID signals a “mixed use of properties from different types of object” error. In effect, because we want to create dynamic administrative units based on department, the membership rule is limited to string properties.

Finding the Right Cmdlet to Do the Job

I bet some folks reading this article ask the question “how do I find out what cmdlets to use to interact with Entra ID objects?” It’s a fair question. The SDK modules contain hundreds of cmdlets, some of which have extraordinarily long and complex names. My answer is to use the Graph X-ray add-on to gain insight into what the Entra ID admin center does to manipulate objects. If a method is good enough for the Entra ID admin center, it’s probably good enough for you.


Learn about using Entra ID, 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/2023/09/25/dynamic-administrative-units-ps/feed/ 4 61642
Chasing Performance When Reporting Teams SharePoint Site URLs https://office365itpros.com/2023/09/21/teams-sharepoint-url/?utm_source=rss&utm_medium=rss&utm_campaign=teams-sharepoint-url https://office365itpros.com/2023/09/21/teams-sharepoint-url/#comments Thu, 21 Sep 2023 01:00:00 +0000 https://office365itpros.com/?p=61654

Improving the Speed of reporting Teams SharePoint URLs by Replacing the Get-UnifiedGroup Cmdlet

Last week, following a response to a reader question, I updated an article describing how to create a report of Teams and the URLs for the SharePoint Online sites used to store shared files. The only real improvement I made to the script was to use the Get-ExoRecipient cmdlet to resolve the members of the ManagedBy property to output display names instead of mailbox names. This change is necessary since Exchange Online moved to using the External Directory Object ID (EDOID) as the mailbox name to ensure uniqueness. Not everyone can recognize a mailbox GUID and know what mailbox it refers to.

The script uses the Get-UnifiedGroup cmdlet to find team-enabled groups. After reviewing the code, I wondered if it was possible to speed up processing by replacing the Exchange Online cmdlets with Microsoft Graph PowerShell SDK cmdlets or API requests. It’s always been true that the Get-UnifiedGroup cmdlet is relatively slow. This situation is explainable because the cmdlet fetches a lot of data about a Microsoft 365 group from multiple workloads. Microsoft has improved the performance of Get-UnifiedGroup over the years, but it’s still not the most rapid cmdlet you’ll ever use.

Converting to Graph SDK Cmdlets

Converting the script to use Microsoft Graph PowerShell SDK cmdlets isn’t very difficult. Here’s the code:

# Check that we are connected to Exchange Online
$ModulesLoaded = Get-Module | Select-Object -ExpandProperty Name
If (!($ModulesLoaded -match "ExchangeOnlineManagement")) {Write-Host "Please connect to the Exchange Online Management module and then restart the script"; break}
 
Connect-MgGraph -NoWelcome -Scopes Group.Read.All, Sites.Read.All
Write-Host "Finding Teams..."
[array]$Teams = Get-MgGroup -Filter "resourceProvisioningOptions/any(x:x eq 'Team')" -All
     
If (!($Teams)) {
   Write-Host "Can't find any Teams for some reason..."
} Else {
  Write-Host ("Processing {0} Teams..." -f $Teams.count)
  $TeamsList = [System.Collections.Generic.List[Object]]::new()    
  ForEach ($Team in $Teams) { 
   $SPOSiteURL = (Get-UnifiedGroup -Identity $Team.Id).SharePointSiteURL  [array]$Channels = Get-MgTeamChannel -TeamId $Team.Id
   [array]$Owners = (Get-MgGroupOwner -GroupId $Team.Id).AdditionalProperties.displayName
   $DisplayNames = $Owners -join ", "
   $TeamLine = [PSCustomObject][Ordered]@{
      Team      = $Team.DisplayName
      SPOSite   = $SPOSiteURL
      Owners    = $DisplayNames  }
   $TeamsList.Add($TeamLine)
  }
  $TeamsList | Out-GridView
  $TeamsList | Export-CSV -NoTypeInformation c:\temp\TeamsSPOList.CSV
}

Figure 1 shows the result.

Reporting the URLs for SharePoint Online sites used by Teams
Figure 1: Reporting the URLs for SharePoint Online sites used by Teams

You’ll notice that I still use the Get-UnifiedGroup cmdlet to fetch the Teams SharePoint URL. It’s possible to retrieve this information using the Graph with code like:

   $Uri = ("https://graph.microsoft.com/v1.0/groups/{0}/drive/root/webUrl" -f $Team.Id)
   $SPOData = Invoke-MgGraphRequest -Uri $Uri -Method Get
   [string]$SPODocLib = $SPOData.Value
   $SPOSiteUrl = $SPODocLib.SubString(0, $SPODocLib.LastIndexOf("/"))

Or:

   $Uri = ("https://graph.microsoft.com/v1.0/groups/{0}/sites/root" -f $Team.Id)
   $SPOData = Invoke-MgGraphRequest -URI $Uri -Method Get
   $SPOSiteUrl = $SPOData.WebURL

The Problem with Permissions when Fetching Teams SharePoint URLs

In both cases, the code works. However, the code fails for some teams due to the restriction placed on interactive use of the Graph SDK. When you connect an interactive session to the Graph, you’re restricted to using delegate permissions. The only data that the Graph SDK cmdlets can access is whatever the signed-in user can access. This is very different to the permissions model used by modules like the Exchange Online management module, which allow access to data based on RBAC controls, meaning that a tenant administrator can access everything.

The restriction disappears when running the SDK cmdlets using a registered app or an Azure Automation runbook. Now the cmdlets can use application permissions, so they can access any data permitted by the Graph permissions assigned to the service principal of the app.

Using either version of the code shown above works perfectly and returns the SharePoint site URL, but only for sites accessible to the signed-in user. Attempts to access any other site returns a 403 forbidden error.

I even tried using the Teams Graph cmdlets:

[array]$Channels = Get-MgTeamChannel -TeamId $Team.Id
$Files = (Get-MgTeamChannelFileFolder -TeamId $Team.Id -ChannelId $Channels[0].Id).WebURL
$SPOSiteUrl =  $Files.SubString(0,$Files.IndexOf("sites/")) + "sites/" + $Team.MailNickName

Again, this approach works for teams that the signed-in user is a member of, but not for other teams.

Going Back to Pure Exchange Cmdlets to Report Teams SharePoint URLs

The problem with permissions meant that I had to use a hybrid of Graph SDK cmdlets to get everything except the SharePoint site URL. And while this approach works, it’s slower than the original implementation using only Exchange Online cmdlets. In several runs against 88 teams the hybrid version took an average of 42 seconds to finish. The Exchange version required an average of 31 seconds.

The learning here is that Graph SDK cmdlets aren’t always the best choice for speed, no matter what you read on the internet. It’s always worth testing to find which approach is the most functional and fastest. Sometimes both boxes are ticked, and that’s a result.


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/09/21/teams-sharepoint-url/feed/ 2 61654
Use Message Trace Data to Analyze Email Traffic https://office365itpros.com/2023/08/23/message-trace-analysis/?utm_source=rss&utm_medium=rss&utm_campaign=message-trace-analysis https://office365itpros.com/2023/08/23/message-trace-analysis/#respond Wed, 23 Aug 2023 01:00:00 +0000 https://office365itpros.com/?p=61275

Analyze Traffic for Inbound and Outbound Domains Over the Last Ten Days

I’ve covered how to use the Exchange Online message trace facility several times in the past to handle tasks like analyzing email sent to external domains. A reader asked if it’s possible to summarize the top inbound and outbound domains using the same data. The answer is that it’s certainly possible to extract this information, but only for the last ten days because that’s how long Exchange Online keeps message trace data online.

Figure 1 shows the output of the script I wrote to demonstrate the principles of the solution. You can download the script from GitHub and make whatever improvements you like.

Top 10 outbound and inbound domains computed from message trace data
Figure 1: Top 10 outbound and inbound domains computed from message trace data

Fetching Message Trace Data

After connecting to Exchange Online, the first task is to retrieve message trace data for analysis. The Get-MessageTrace cmdlet fetches message trace events in pages of up to 5,000 objects. To fetch all available data, the script retrieves information page-by-page until there’s nothing left. This code does the job with a While loop:

[int]$i = 1
$MoreMessages = $True
[array]$Messages = $Null
$StartDate = (Get-Date).AddDays(-10)
$EndDate = (Get-Date).AddDays(1)

Write-Host ("Message trace data will be analyzed between {0} and {1}" -f $StartDate, $EndDate)
While ($MoreMessages -eq $True) {
    Write-Host ("Fetching message trace data to analyze - Page {0}" -f $i)
    [array]$MessagePage = Get-MessageTrace -StartDate $StartDate -EndDate $EndDate -PageSize 1000 -Page $i -Status "Delivered"
    If ($MessagePage)  {
        $i++
        $Messages += $MessagePage
    } Else {
        $MoreMessages = $False
    }
}

My tenant includes public folders. Public folder mailboxes synchronize hierarchy data between each other to make sure that users can connect and access public folders no matter which public folder mailbox they select. The synchronization messages aren’t very interesting, so the script removes them:

# Remove Exchange Online public folder hierarchy synchronization messages
$Messages = $Messages | Where-Object {$_.Subject -NotLike "*HierarchySync*"}

Creating Data to Analyze

Next, the script fetches the set of accepted domains and extracts the domain names into an array. When the script analyzes messages, it uses the domain names to decide if a message is inbound or outbound based on the sender’s email address:

[array]$Domains = Get-AcceptedDomain | Select-Object -ExpandProperty DomainName

The script then loops through the message trace records to create a list with the sender domain extracted and the direction (inbound or outbound) determined:

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

ForEach ($M in $Messages) {
   $Direction = "Inbound"
   $SenderDomain = $M.SenderAddress.Split("@")[1]
   $RecipientDomain = $M.RecipientAddress.Split("@")[1]
   If ($SenderDomain -in $Domains) {
      $Direction = "Outbound" 
   }
   $ReportLine = [PSCustomObject]@{
     TimeStamp       = $M.Received
     Sender          = $M.SenderAddress
     Recipient       = $M.RecipientAddress
     Subject         = $M.Subject
     Status          = $M.Status
     Direction       = $Direction
     SenderDomain    = $SenderDomain
     RecipientDomain = $RecipientDomain
    }
    $Report.Add($ReportLine)

}

After that, it’s simply a matter of splitting the data into separate arrays containing inbound and outbound messages and piping the results to the Group-Object cmdlet to count the number of times domains appear in the set. We then display the top 10 domains for inbound traffic and the same for outbound traffic, which is what you see in Figure 1. For example, here’s the code to display the top ten outbound domains:

$OutboundMessages | Group-Object RecipientDomain -NoElement | Sort-Object Count -Descending | Select-Object -First 10 | Format-Table Name, Count -AutoSize

Traffic Sent to Groups

One thing to be aware of for inbound traffic is that entries for a message delivered to a Microsoft 365 group or distribution list appears in the message trace data for each recipient. This is logical because Exchange Online needs to track the progress of a message to its final destination. However, it does amplify the number of messages that an external domain appears to send to your tenant.

Use PowerShell to Supplement Standard Reports

The Reports section of the Exchange admin center features a top domain mail flow status report with tabs for inbound and outbound traffic. On the surface, these reports seem like they do the same job. They don’t because these reports are focused on different factors (read the documentation for details). Between what Microsoft provide and what you can create using PowerShell, you’ll have a pretty good idea of what’s happening for email traffic to and from your tenant.


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/2023/08/23/message-trace-analysis/feed/ 0 61275
Monitor and Report Additions to Teams Membership https://office365itpros.com/2023/08/22/teams-membership-monitoring/?utm_source=rss&utm_medium=rss&utm_campaign=teams-membership-monitoring https://office365itpros.com/2023/08/22/teams-membership-monitoring/#respond Tue, 22 Aug 2023 01:00:00 +0000 https://office365itpros.com/?p=61294

Use PowerShell and the Audit Log to Find Targeted Accounts in Teams Memberships

A request came into the Office 365 Technical Discussions Facebook group for a way to monitor member additions to teams. The idea is that if a team owner adds an account with a specific attribute in the display name, something picks up the addition and notifies someone that the action happened.

PowerShell is the normal way to answer questions of this nature. That is, if you can get at the data. In this instance, the unified audit log captures events for team membership additions, so the raw data exists, even if a little manipulation is necessary to extract the information we need (thankfully, the needed manipulation is less than in other scenarios, such as tracking updates for properties of user accounts).

Specifying User Accounts to Monitor in Teams Memberships

The first thing to do is identify the set of users to check for. The original request didn’t specify what kind of attribute to look for in the display name, so the solution outlined here assumes that it’s a string after the combination of first name and last name. For instance, “Tom Smith (Project Management).”

Identifying the accounts to monitor is a key part of the solution. Here’s the code to use the Get-MgUser cmdlet with the Search parameter to find licensed member accounts that include “Project” in the display name.

[array]$Users = Get-MgUser -Search "displayName:Project" -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel Eventual
If (!($Users)) { 
    Throw "No users found"
}

There might be many user accounts that need to be monitored. To speed things up when we check audit records, the script creates a hash table composed of the user principal name and display name.

$UserLookup = @{}
ForEach ($User in $Users) {
   $UserLookup.Add($User.UserPrincipalName, $User.DisplayName)
}

Searching the Audit Log for Additions to Teams Memberships

Next, the script calls the Search-UnifiedAuditLog cmdlet to look for MemberAdded events generated by Teams over the past seven days:

$StartDate = (Get-Date).AddDays(-7)
$EndDate = (Get-Date).AddDays(1)
[array]$Records = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Formatted -ResultSize 5000 -RecordType MicrosoftTeams -Operations MemberAdded -SessionCommand ReturnLargeSet
$Records = $Records | Sort-Object Identity -Unique

To check the audit events, the script converts the AuditData property for each event from JSON and examines what’s stored in the Members property (an array). For each item in Members, the script looks up the hash table to see if the account is monitored, and if so, captures details of the event in a list:

$Report = [System.Collections.Generic.List[Object]]::new()  
ForEach ($Rec in $Records) {
    $Role = $Null
    $AuditData = $Rec.AuditData | ConvertFrom-Json
    # Check the members noted as added to a group
    ForEach ($Member in $AuditData.Members) {
        If ($UserLookup[$Member.Upn]) {
           Write-Host ("User {0} added to team {1}" -f $Member.DisplayName, $AuditData.TeamName) 
           Switch ($Member.Role) {
            "1"  { $Role = "Member" }
            "2"  { $Role = "Owner"}
            "3"  { $Role = "Guest" }
           }
           $ReportLine = [PSCustomObject]@{
             Date = $AuditData.CreationTime
             User = $Member.Upn   
             Name = $Member.DisplayName
             Team = $AuditData.TeamName
             Role = $Role
             AddedBy = $AuditData.UserId
           }
          $Report.Add($ReportLine)
        }
    }
}

Here’s an example of the output:

Date    : 20/08/2023 12:12:55
User    : Hans.Geering@office365itpros.com
Name    : Hans Geering (Project Management)
Team    : Office 365 Adoption
Role    : Member
AddedBy : Tony.Redmond@office365itpros.com

Sharing the Results

To share the results, we send email from a shared mailbox. This action requires the Mail.Send.Shared Graph permission and uses the Send-MgUsermail cmdlet using a variety of the code explained in this article. Figure 1 shows an example of an email sent to the designated recipient (which should probably be a distribution list in production) to report results.

Email to report additions made to Teams memberships
Figure 1: Email to report additions made to Teams memberships

Posting the information to a Teams channel is another way to share details about new membership additions. Another option is to upload the file to a SharePoint Online document library, a topic explored in this article when Azure Automation runs a script to create content like a report. Monitoring for changes in a Microsoft 365 tenant is the kind of task that is well suited to Azure Automation, and it’s the way that I would go in production.

You can download the sample script from GitHub. Feel free to change (hopefully improve) the code.


Learn about using the Graph SDK, the unified audit log, and the rest of Office 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/2023/08/22/teams-membership-monitoring/feed/ 0 61294
Entra ID Guest Accounts Can Now Have Sponsors https://office365itpros.com/2023/08/17/guest-account-sponsors/?utm_source=rss&utm_medium=rss&utm_campaign=guest-account-sponsors https://office365itpros.com/2023/08/17/guest-account-sponsors/#comments Thu, 17 Aug 2023 01:00:00 +0000 https://office365itpros.com/?p=61219

Defining Guest Account Sponsors with GUI and PowerShell

In July 2023, Microsoft added a new preview feature to allow organizations to assign ‘sponsors’ for Entra ID guest accounts. The idea is that an organization should be able to assign people or groups to be the sponsor of guest accounts. The sponsor should be “a responsible individual,” meaning someone who understand why a guest account is present in the directory, how that guest account is used, and what access they have to data. A sponsor can be an individual account or a group, and a guest account can have up to five sponsors (a mixture of accounts and groups).

When the time comes to review guest accounts and decide to keep or remove the account, sponsors can justify the retention of the guest account or ask for its removal. For instance, if a group owner uses a tool like Entra ID Access Review to conduct a periodic review of the membership of a group (team) and doesn’t recognize a guest account, they can contact the sponsor for more information. Whether or not the group owner gets anything useful from the sponsor is another matter.

Defining Entra ID Guest Account Sponsors

According to Microsoft’s documentation, “If you don’t specify a sponsor, the inviter will be added as a sponsor.” They then go on to explain how to invite an external user and add a sponsor to the new Entra ID guest account (Figure 1).

Adding sponsor information for a new guest account
Figure 1: Adding sponsor information for a new guest account

However, if you don’t add a sponsor to the new external account, the sponsor information is not filled in with the identifier of the account used to create and send the invitation. Maybe my tenant is missing some bits, which is entirely possible.

Sponsor information isn’t filled in either if you add a guest account by adding an external user to a team or sharing a document with them. This isn’t surprising because the sponsors feature is in preview and it takes time for applications like Teams, Outlook, SharePoint Online, and OneDrive for Business to catch up and populate new guest account properties.

In summary, if you want to update the sponsor for a guest account using a GUI, the only way is to edit the account properties in the Entra ID admin center.

Programmatic Updates for Guest Account Sponsors

A beta Graph API is available to list, update, and remove guest account sponsors. As usual, the Graph Explorer is an invaluable tool to help understand how a Graph API works (Figure 2).

Getting sponsor information for a guest account with the Graph Explorer
Figure 2: Getting sponsor information for a guest account with the Graph Explorer

The Get-MgBetaUser cmdlet from the beta module of the Microsoft Graph PowerShell SDK (now at V2.3) can fetch information about sponsors. For example, this code fetches information about a guest account including the sponsors. It then uses the Get-MgUser cmdlet to resolve the set of user identifiers into display names.

$User = Get-MgBetaUser -UserId 7bfd3f83-be63-4a5a-bbf8-c821e2836920 -Property Id, displayName, Sponsors -ExpandProperty Sponsors
ForEach ($Id in $User.Sponsors.Id) { Get-MgUser -UserId $Id | Select-Object DisplayName }

Of course, the code doesn’t handle the situation where a sponsor is a group, but that’s easily added if needed.

If you wanted to scan all guest accounts that don’t have sponsors defined and add a default sponsor, you could do something like this. The code:

  • Defines an account to be the default sponsor.
  • Builds a payload to use when updating the guest accounts.
  • Finds guest accounts in the tenant.
  • Checks each guest account for sponsors. If none are found, the script applies the default sponsor.

Connect-MgGraph -Scopes User.ReadWrite.All

$DefaultSponsorId = (Get-MgUser -UserId James.Ryan@office365itpros.com).Id
$Body = '{"@odata.id": "https://graph.microsoft.com/beta/users/' + $DefaultSponsorId + '"}'

[array]$Guests = Get-MgBetaUser -Filter "userType eq 'Guest'" -All -Property Id, displayName, Sponsors -ExpandProperty Sponsors | Sort-Object displayName
If ($Guests) {
    Write-Host "Scanning for sponsors"
    ForEach ($Guest in $Guests) {
      If ($Null -eq $Guest.Sponsors.Id) {
         Write-Host ("Guest {0} has no sponsors - updating with default sponsor" -f $Guest.displayName) 
         $Uri = ("https://graph.microsoft.com/beta/users/{0}/sponsors/`$ref" -f $Guest.Id)
         Invoke-MgGraphRequest -Uri $Uri -Method Post -Body $Body
      }
    }
}

Auditing Updates to Guest Account Sponsors

Last week I wrote about the way that Entra ID auditing does not capture details of changes to the usage location property for user accounts. As it turns out, updating a guest account with sponsor information creates an audit record without details of the change. Again, this could be a matter of timing and an update is coming to make sure that audit log events for account updates capture sponsor information correctly.

Tracking Guest Additions

Since Azure B2B Collaboration introduced guest accounts in summer 2016, administrators have been tracking the creation of guest accounts in different ways (for instance, here’s how to track the addition of guest accounts to teams). In many cases, the reason for doing so was to know who was responsible for the creation of a guest account. With sponsors, that need might go away, or at least it might be easier to retrieve the “who created that account information” by using the sponsor information stored for accounts. That is, once the apps record sponsors.


Learn about using Entra ID, PowerShell, the Microsoft Graph, and the rest of Office 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/2023/08/17/guest-account-sponsors/feed/ 2 61219
Filtering Against the Entra ID Employee Hire Date Property https://office365itpros.com/2023/08/10/entra-id-employee-hire-date/?utm_source=rss&utm_medium=rss&utm_campaign=entra-id-employee-hire-date https://office365itpros.com/2023/08/10/entra-id-employee-hire-date/#comments Thu, 10 Aug 2023 01:00:00 +0000 https://office365itpros.com/?p=61146

Two Filters Available for the Entra ID Employee Hire Date Property

In an article published earlier this year about different ways to find Entra ID (Azure AD) user accounts with PowerShell, I commented that the Get-MgUser cmdlet could not apply a server-side filter against the Entra ID employee hire date property. For instance, to find accounts with an employee hire date, you must use Get-MgUser to fetch accounts and then apply a client-side filter to find the target objects. For instance, this code finds accounts with an employee hire date later than 1 January 2023:

[array]$Employees = Get-MgUser -filter "userType eq 'Member' and EmployeeId ge ' '" -Property Id, displayname, userprincipalname, employeeid, employeehiredate, employeetype
$CheckDate = Get-Date “8-Jul-2023”
$Employees | Where-Object {$CheckDate -as [datetime] -lt $_.EmployeeHireDate} | Sort-Object {$_.EmployeeHireDate -as [datetime]} -Descending | Format-Table DisplayName, userPrincipalName, employeeHireDate -AutoSize

DisplayName   UserPrincipalName                 EmployeeHireDate
-----------   -----------------                 ----------------
Michael King  Michael.King@office365itpros.com  01/08/2023 23:00:00
Terry Hegarty Terry.Hegarty@office365itpros.com 01/08/2023 23:00:00
Hans Geering  Hans.Geering@office365itpros.com  31/07/2023 23:00:00
Chris Bishop  Chris.Bishop@office365itpros.com  31/07/2023 23:00:00

The problem persists in the latest version of the Microsoft Graph PowerShell SDK using both the Get-MgUser and Get-MgBetaUser cmdlets.

Dynamic Groups Support for Employee Hire Date

All of which brings me to news that membership rules for Entra ID dynamic groups support the PowerShell le and ge operators against the employee hire date property. This capability is a preview for now.

In a nutshell, the new feature supports the creation of dynamic groups (which require Entra ID Premium P1 licenses) based on a filter against the EmployeeHireDate property. Two kinds of date filters are available. The first performs a simple comparison to test if the employee hire date is greater than or equal to or less than or equal to a specified date. For example, this command creates a dynamic Microsoft 365 group with a membership rule that finds all member accounts with an employee hire date greater or equal to 1 January 2023:

$Group = New-MgGroup -DisplayName "New Employees (Dynamic)" -Description "Dynamic group containing new employees (2023)" -MailEnabled:$True -SecurityEnabled:$False -MailNickname New.Employees.2023 -GroupTypes "DynamicMembership", "Unified" -MembershipRule "(user.employeehiredate -ge ""2023-01-01T00:00:00Z"" -and (user.usertype eq ""Member"")" -MembershipRuleProcessingState "On"

Dates must be passed in the sortable format rather than a more human-friendly type. For PowerShell, use Get-Date to set the date and format the output as follows:

$DateForFilter = (((Get-Date).AddDays(-365)) | Get-Date -format 'yyyy-MM-ddThh:mm:ssZ')

The second filter tests the employee hire date against a calculated date based on the current date. This example creates a dynamic Microsoft 365 group with a membership rule that looks for employees with hire dates within the last 31 days (system.now is the current date):

$Group = New-MgGroup -DisplayName "New Employees (Last Month)" -Description "Dynamic group containing employees hired in the last month" -MailEnabled:$True -SecurityEnabled:$False -MailNickname New.Employees.LastMonth -GroupTypes "DynamicMembership", "Unified" -MembershipRule "(user.employeehiredate -ge system.now -minus p31d ) -and (user.usertype eq ""Member"")" -MembershipRuleProcessingState "On"

It looks like only day intervals are supported. Entra ID rule validation rejects values like p4w (4 weeks) or p1m (1 month).

Validating the Filter Against the Entra ID Employee Hire Date Property

It’s easy to check the effectiveness of the membership rule. Let Entra ID calculate the membership for the dynamic group and note who’s present (Figure 1):

Viewing members of a dynamic group using a membership rule using the Entra ID employee hire date property

Azure AD employee hire date property
Figure 1: Viewing members of a dynamic group using a membership rule using the Entra ID employee hire date property

Then run the Get-MgUser example shown at the top of the article with an appropriate value inserted into the $CheckDate variable (use this code to set the variable to 31 days from the current date).

$CheckDate = (Get-Date).AddDays(-31)

Check the results generated by PowerShell against the set shown in the Entra ID admin center. The two should match. If they don’t, wait for 30 minutes or so to be sure that Entra ID has had time to process any recent updates and try again.

Time Updates All Cmdlets

It takes time for the Graph SDK cmdlets to catch up with new developments and preview features. Now that the Entra ID developers have enabled date filtering against the employee hire date property, it can’t be long before server-side filters work with Get-MgUser too. And if they don’t, there is a workaround – fetch the membership of the dynamic group with Get-MgGroupMember and use that information instead of running Get-MgUser. That’s the kind of lateral thinking we’re looking for in the great PowerShell script-off competition at TEC 2023 next month!


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/08/10/entra-id-employee-hire-date/feed/ 1 61146
Managing Assigned Licenses for Deleted User Accounts https://office365itpros.com/2023/08/07/deleted-user-account-licenses/?utm_source=rss&utm_medium=rss&utm_campaign=deleted-user-account-licenses https://office365itpros.com/2023/08/07/deleted-user-account-licenses/#comments Mon, 07 Aug 2023 01:00:00 +0000 https://office365itpros.com/?p=61072

Why Some Deleted User Accounts Store License Assignment Information And Some Do Not

A reader asks why the Microsoft 365 admin center displays a license for a deleted user account (Figure 1). The follow-up question is how they can remove the license and reassign it to another user.

Deleted user account with license assignment information
Figure 1: Deleted user account with license assignment information

The answer is that they don’t need to do anything. When an administrator removes a user account, Entra ID moves the account into its deleted items container (aka the wastebasket). The deleted account remains there for 30 days, during which time an administrator can restore the account (see the big blue button in Figure 1). The ideal situation is for a restored account to come back with all its settings intact, including assigned licenses. Entra ID tracks the licenses that the deleted account once had so that it can reassign the licenses to the newly-restored account.

Any licenses assigned to a deleted user account become available following the account’s deletion. This includes accounts used for shared mailboxes where assigned licenses exist to enable features like archiving. No one wants to keep expensive licenses on ice pending account restores, so often the licenses end up being assigned to other accounts.

It Depends on How User Accounts Are Deleted

The interesting thing is that the presence of assigned licenses for deleted accounts depends on the method used to delete the account. When an administrator deletes an account through the Microsoft 365 admin center, the process removes license assignments before removing the account, which means that if you examine the properties of the deleted account afterward, no licenses are present (Figure 2).

Deleted user account with no license assignment information
Figure 2: Deleted user account with no license assignment information

However, if you use PowerShell or the Microsoft Entra admin center to remove an account, the deleted account object retains license information. The licenses are not assigned, but the license information is present in the properties of the deleted user object. This is why Figure 1 shows that a deleted account has a license.

The reason why the Microsoft 365 admin center removes licenses and other administrative interfaces do not is due to the multi-phase process the Microsoft 365 admin center uses for account removal. The process includes steps such giving another user access to the user’s OneDrive for Business account (Figure 3) to allow for the recovery of any important information before the permanent removal of the user account.

Steps in the Microsoft 365 admin center account deletion process
Figure 3: Steps in the Microsoft 365 admin center account deletion process

PowerShell and the Microsoft Entra admin center only concern themselves with the removal of the user account object, and that’s why some deleted user accounts have license assignment information and others do not.

Care Needed When Restoring Deleted Accounts

The Microsoft 365 admin center user restore process warns administrators to:

  • Assign licenses after restoring the account.
  • Change the account password.

A user account has no access to Microsoft 365 services after it is restored until these steps are complete.

By comparison, if you restore a deleted account through the Microsoft Entra admin center or PowerShell, the license assignments noted in the account properties become active again. This can lead to an over-assignment condition where too many user accounts have licenses for specific products, like Office 365 E3. In this situation, administrators must buy additional licenses or remove licenses from other accounts (or delete other accounts).

To check if the properties of any deleted accounts include license assignments, you can run these Microsoft Graph PowerShell SDK commands to fetch details of deleted accounts and report if any license data exists:

Connect-MgGraph -Scope Directory.Read.All
[array]$DeletedUsers = Get-MgDirectoryDeletedItemAsUser -Property DeletedDateTime, Id, displayName, userPrincipalName, assignedlicenses | Sort-Object DeletedDateTime -Descending
ForEach ($User in $DeletedUsers) {
  If ($User.assignedLicenses) {
     $Licenses = $User | Select-Object -ExpandProperty assignedLicenses
     [string]$Skus = $Licenses.SkuID -Join ", "
     Write-Host ("Deleted user {0} has license information noted in their account properties {1}" -f $User.displayName, $Skus ) }
}

If you use PowerShell to script the recovery of user accounts, you should check for license assignments and validate that available licenses are available before recovering the account. This article explains how to fetch subscription information using the Get-MgSubscribedSku cmdlet and the subscriptions API, including the count of assigned and available licenses. It’s easy to check if a license for a SKU is available before assigning it to a recovered account.

Alternatively, go ahead and recover the account and fix the licensing problem later through the Microsoft 365 admin center.

Processing Differences Exist

This discussion reveals a difference in behavior between the raw processing performed by Graph APIs and the wrapper around the APIs implemented in the Microsoft 365 admin center. Sometimes the differences bubble up to the surface and the reasons for the differences aren’t immediately clear until you poke around to discover why things happen the way that they do. Isn’t that often the case in 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/2023/08/07/deleted-user-account-licenses/feed/ 5 61072
Reporting Retention Tags for Exchange Online Mailbox Folders https://office365itpros.com/2023/08/03/exchange-retention-tags-report/?utm_source=rss&utm_medium=rss&utm_campaign=exchange-retention-tags-report https://office365itpros.com/2023/08/03/exchange-retention-tags-report/#comments Thu, 03 Aug 2023 01:00:00 +0000 https://office365itpros.com/?p=61024

List Exchange Retention Tags Assigned to Folders

A reader wondered how they could create a report of folders in an Exchange Online mailbox to include the retention tag assigned to folders. Retention tags mean both mailbox records management (MRM) retention tags (Exchange legacy retention) and Microsoft 365 retention labels. The Managed Folder Assistant (MFA), the component responsible for retention processing of mailboxes, treats both types equally.

Although Microsoft would like customers to transition from Exchange MRM, the older implementation of retention tags still offers significant value that isn’t available in Microsoft 365. The major gaps in Microsoft 365 retention are the ability to move mailbox items to Exchange archive mailboxes and folder-level retention processing (using default folder tags or personal retention tags). You can transition most retention processing to Microsoft 365, but some elements of MRM retention are still required to use these two features. Microsoft tweaks Exchange MRM to make it behave more like Microsoft 365 retention, but the gap remains.

Getting Exchange Retention Tags from Folder Statistics

Our reader used the Get-ExoMailboxFolderStatistics cmdlet to retrieve details of mailbox folders. The DeletePolicy property is one of the properties fetched for each folder. This property stores the name of the retention tag (folder or personal) assigned to the folder. However, the property is blank if the folder is governed by the default delete and default archive tags defined in the MRM policy assigned to the mailbox. An MRM retention policy can have one default (move to) archive tag and one default delete tag. A policy doesn’t have to include default tags.

Script Steps to Report Exchange Retention Tags

To create a complete picture, I did the following:

Run Get-ExoMailboxFolderStatistics to fetch details of the default mailbox folders (like Inbox and Sent Items) plus user created folders (those likely to be exposed in a client for users to apply retention tags to).

$User = Read-Host "Enter name of user mailbox to examine"
$User = Get-ExoMailbox -Identity $User -ErrorAction SilentlyContinue -Properties RetentionPolicy
If (!($User)) { Write-Host ("Can't find mailbox for {0}" -f $User) ; break }
Write-Host ("Checking mailbox folders for {0}" -f $User.DisplayName)
[array]$MailboxFolders = Get-ExoMailboxFolderStatistics -Identity $User.UserPrincipalName | Where-Object {$_.FolderType -eq 'User created' -or $_.FolderType -eq 'Inbox' `
  -or $_.FolderType -eq 'SentItems' -or $_FolderType -eq 'DeletedItems' -or $_.FolderType -eq 'JunkEMail' -or $_.FolderType -eq 'Contacts'} | Sort-Object Name

Unfortunately, Exchange Online mailboxes contain a heap of system-generated folders that are marked as user created. I remove these from the folder set. This is the lazy way to remove the folders.

$MailboxFolders = $MailboxFolders | Where-Object {$_.Name -ne 'Social Activity Notifications'}
$MailboxFolders = $MailboxFolders | Where-Object {$_.Name -ne 'Clutter'}
$MailboxFolders = $MailboxFolders | Where-Object {$_.Name -ne 'Quick Step Settings'}
$MailboxFolders = $MailboxFolders | Where-Object {$_.Name -ne 'Suggested Contacts'}

The script then finds the MRM retention policy assigned to the mailbox and check if the policy contains any default delete or archive tags.

[array]$Tags = Get-RetentionPolicy $User.RetentionPolicy |Select-Object -ExpandProperty RetentionPolicyTagLinks

[array]$DefaultTags = $Null
ForEach ($Tag in $Tags) {
    If ((Get-RetentionPolicyTag -Identity $Tag | Select-Object -ExpandProperty Type) -eq 'All') {
    $DefaultTags += $Tag }
}

After that, it’s a matter of running down through the folder set to find if the folder has a tag noted. If it does, we report that. If not, we report the default tags. Figure 1 shows the result.

Exchange mailbox folders and MRM retention tags

Exchange retention tags
Figure 1: Exchange mailbox folders and MRM retention tags

You can download the script from GitHub.

Get Retention Tags for Individual Messages

There’s no obvious way to get the retention tag for individual messages with PowerShell. I asked Glen Scales, an MVP with long experience of developing against Exchange with EWS and the Graph, and he pointed me to a property called Single Value Extended Properties where Exchange stores the retention tag data for messages. Here’s some code to fetch the top 10 messages from the Inbox folder in a mailbox, including the retention data:

$Uri = "https://graph.microsoft.com/v1.0/users('tony.redmond@office365itpros.com')/MailFolders/Inbox/messages/?`$select=ReceivedDateTime,Sender,Subject,IsRead,InternetMessageId,parentFolderId,hasAttachments&`$Top=10&`$expand=SingleValueExtendedProperties(`$filter=(Id%20eq%20'String%20%7B403FC56B-CD30-47C5-86F8-EDE9E35A022B%7D%20Name%20ComplianceTag'))"
$Data = Invoke-MgGraphRequest -Uri $Uri -Method Get

The “normal” properties are obvious in the output:

$data.value[0]
Name                           Value
----                           -----
@odata.etag                    W/"CQAAABYAAAA3tTkMTDKYRI6zB9VW59QNAAaLOoml"
singleValueExtendedProperties  {System.Collections.Hashtable}
sender                         {emailAddress}
parentFolderId                 AAMkADAzNzBmMzU0LTI3NTItNDQzNy04NzhkLWNmMGU1MzEwYThkNAAuAAAAAAB_7ILpFNx8TrktaK8VYWerAQBe9CuwLc2fTK7W4... 
isRead                         True
id                             AAMkADAzNzBmMzU0LTI3NTItNDQzNy04NzhkLWNmMGU1MzEwYThkNABGAAAAAAB_7ILpFNx8TrktaK8VYWerBwBe9CuwLc2fTK7W4... 
receivedDateTime               27/07/2023 19:43:43
hasAttachments                 False
subject                        Delivery estimate update for your Amazon.co.uk order #026-5997568-1550717
internetMessageId              <0102018998e0cc4a-0fef8181-323f-4bb1-b22f-951a6840abe4-000000@eu-west-1.amazonses.com>

The retention tag is in a hash table in Single Value Extended Properties. We can see that the name of the retention tag is “Inbox 7 Year.”

$data.value[0].singleValueExtendedProperties

Name                           Value
----                           -----
value                          Inbox 7 Year
id                             String {403fc56b-cd30-47c5-86f8-ede9e35a022b} Name ComplianceTag

Note: retention tag information is only present when an item has been stamped with a tag. Items under the control of a default retention tag (for deletion or archival) don’t have the retention information in their properties. When Managed Folder Assistant processes mailbox items, it applies the settings from default tags to items when if a more specific tag (folder or personal) is absent.

It is possible to fetch the information for every message in a mailbox and report its retention tag. Given the sheer number of messages in mailboxes, I’m not sure if the exercise would be useful in any way, but at least you know it can be done.


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/2023/08/03/exchange-retention-tags-report/feed/ 3 61024
Retrieving Azure AD (Entra ID) Privileged Identity Management Role Assignments https://office365itpros.com/2023/07/12/privileged-identity-management-ps/?utm_source=rss&utm_medium=rss&utm_campaign=privileged-identity-management-ps https://office365itpros.com/2023/07/12/privileged-identity-management-ps/#comments Wed, 12 Jul 2023 01:00:00 +0000 https://office365itpros.com/?p=60809

Taking Account of PIM When Blocking User Access to Exchange Online PowerShell

Updated 15 August 2024

In May, I wrote a Practical365.com article about disabling PowerShell access to Exchange Online for all but administrative accounts. Given the happiness of attackers to use PowerShell to attack Exchange (mostly against Exchange Server, but certainly also Exchange Online), it makes sense to remove the ability of “normal” users to run Exchange cmdlets.

In any case, the example script I use in the article demonstrates how to use the Get-MgDirectoryRoleMember cmdlet to find holders of the Exchange administrator and Global administrator roles. These are the people who need to run PowerShell against Exchange Online, so the script leaves their accounts intact. For anyone else, the script calls the Set-User cmdlet to disable PowerShell access. I suggest that the script is a good candidate for Azure Automation to make sure that new accounts can’t use PowerShell.

Privileged Identity Management

Everything works for most tenants. The problem is that some tenants use Azure AD Privileged Identity Management (PIM), an optional service that requires Azure AD Premium P2 licenses. PIM is most commonly used by large enterprises to control access to resources. Unlike normal open-ended permanent assignments to privileged roles like Exchange administrator, PIM allows the assignments to be time-limited on an on-demand basis.

To do this, PIM differentiates between eligible and active role assignments. An eligible role assignment is not currently effective. If needed, an administrator can activate the assignment to allow its holder to use the permissions available to active role holders. Assignments can be time-limited and expire after a certain period. A comment for the original article pointed out that it didn’t handle PIM assignments and the script is therefore unusable in tenants that use PIM.

If you look at role assignments through the Privileged Identity Management section of the Microsoft Entra admin center, you can see those with eligible, active, and expired assignments for the different roles used in the tenant. Figure 1 shows the active assignments for the Exchange administrator and Global administrator roles. You can see that some service principals are in the set of Exchange administrators. Azure Automation uses these service principals to allow managed identities to sign into Exchange Online and run cmdlets as an administrator.

 PIM assignments for the Exchange administrator and Global administrator roles
Figure 1: PIM assignments for the Exchange administrator and Global administrator roles

The problem is that the Get-MgDirectoryRoleMember cmdlet only reports active role assignments. The assignments eligible for activation are ignored. For the purposes of this exercise, tenants using PIM must include accounts with eligible assignments when determining what accounts can access PowerShell.

Privileged Identity Management APIs

After some searching, I found a script written by Paul Contreras that explains how to get PIM role assignments for Azure AD. The script uses the Get-AzureADMSPrivilegedRoleAssignment cmdlet from the AzureADPreview module to retrieve assignments.

Given that the AzureADPreview module is due for deprecation in March 2024, I looked for an equivalent Microsoft Graph PowerShell SDK cmdlet. Microsoft’s cmdlet map to help developers move from the Azure AD and MSOL modules to the SDK didn’t help. I had great hope for the Get-MgBetaRoleManagementDirectoryRoleAssignment cmdlet but the cmdlet appears to only return “normal” role assignments.

One complication is that the current (beta) Graph API for governance role assignments is due for deprecation. Its documentation points to “Privileged Identity Management iteration 2 APIs.” Obviously, the underlying APIs are in a state of change, so the lack of SDK support isn’t surprising.

Amending the Role Assignment Script for PIM (Updated)

I amended the original script to use the Get-AzureADMSPrivilegedRoleAssignment cmdlet to fetch the assignments known for the Global administrator and Exchange administrator roles. This was fine until the retirement of the AzureAD module. V2.0 of the script replaces the AzureAD cmdlet with the Get-MgBetaRoleManagementDirectoryRoleAssignmentSchedule cmdlet from the Microsoft Graph PowerShell SDK (V2.22).

Write-Output "Retrieving assignment information from Privileged Identity Management..."                    
# Get PIM assignments for accounts holding Exchange administrator or Global administrator roles
[array]$ActiveAssignments = Get-MgBetaRoleManagementDirectoryRoleAssignmentSchedule -Filter "(RoleDefinitionId eq '$($ExoAdminRoleId)') or (RoleDefinitionId eq '$($GlobalAdminRoleId)')" -ExpandProperty RoleDefinition, Principal, DirectoryScope -All

# Filter out the Exchange administrators
[array]$ExoRoleMembers = $ActiveAssignments | Where-Object {$_.RoleDefinitionId -eq $ExoAdminRoleId} | Select-Object RoleDefinitionId, Principal, MemberType   
If (!($ExoRoleMembers)) { Write-Output "Can't find any Exchange administrators! Exiting..." ; break }                                                                                                

# Do the same for global administrators
[array]$GARoleMembers = $ActiveAssignments | Where-Object {$_.RoleDefinitionId -eq $GlobalAdminRoleId} | Select-Object RoleDefinitionId, Principal, MemberType
If (!($GARoleMembers)) { Write-Output "Can't find any global administrators! Exiting..." ; break }

The script then loops through the arrays of assignments to fetch details of user account (with Get-MgUser) and members of groups used for PIM (with Get-MgGroupMember). The script stores information about the assignments that we can report (Figure 2).

Reporting PIM role assignments

Privileged Identity Management
Figure 2: Reporting PIM role assignments

The next step is to create an array of administrator user principal names to check against Exchange mailboxes. Basically, if a mailbox belongs to an administrator, we allow PowerShell access. If it doesn’t, we block PowerShell access.

[array]$ExoMailboxes = Get-ExoMailbox -Filter {CustomAttribute5 -eq $Null} -ResultSize Unlimited -RecipientTypeDetails UserMailbox -Properties CustomAttribute5
ForEach ($Mbx in $ExoMailboxes) {
   # If not an admin holder, go ahead and block PowerShell
   If ($Mbx.userPrincipalName -notin $AdminAccounts) {
     Write-Output ("Blocking PowerShell access for mailbox {0}..." -f $Mbx.displayName)
     Try {
         Set-User -Identity $Mbx.userPrincipalName -RemotePowerShellEnabled $False -Confirm:$False
         $MessageText = "PowerShell disabled on " + (Get-Date -format s)
         Set-Mailbox -Identity $Mbx.userPrincipalName -CustomAttribute5 $MessageText
     }
     Catch {
         Write-Output ("Error disabling PowerShell for mailbox {0}" -f $Mbx.userPrincipalNane )
     }
   }
} # End ForEach

An improvement to the original script is that the final step is to check that administrator accounts have PowerShell access. This is to pick up new administrators that receive individual PIM assignments or join a group with a PIM assignment.

Write-Output "Checking administrator mailboxes to make sure that they have PowerShell access..."
ForEach ($Mbx in $AdminAccounts) {
   [string]$mbx = $mbx
   $PSEnabled = (Get-User -Identity $Mbx  -ErrorAction SilentlyContinue).RemotePowerShellEnabled
   If (!($PsEnabled)) {
        Write-Output ("Resetting PowerShell access for admin account {0}" -f $Mbx)
        Set-User -Identity $Mbx -RemotePowerShellEnabled $True -Confirm:$False 
   }
}

The full script is available from GitHub.

Always Learning

The nice thing about working with Microsoft 365 is that there’s always something to learn. Authors learn from the comments posted for our articles. The comments force us to research before we can answer questions posed by readers. That’s a good thing.


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/07/12/privileged-identity-management-ps/feed/ 4 60809
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
How to Report Renewal Dates for Microsoft 365 Subscriptions https://office365itpros.com/2023/07/06/microsoft-365-subscriptions-ps/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-subscriptions-ps https://office365itpros.com/2023/07/06/microsoft-365-subscriptions-ps/#comments Thu, 06 Jul 2023 01:00:00 +0000 https://office365itpros.com/?p=60708

New Method to Retrieve Renewal Dates for Microsoft 365 Subscriptions

As part of my campaign to help people move off the old MSOL and AzureAD PowerShell modules to use the Microsoft Graph PowerShell SDK before Microsoft deprecates the modules, I wrote a script to demonstrate how to use the Graph SDK to create a licensing report for a tenant. One of the replies to the article observed that the output of the Get-MgSubscribedSku cmdlet didn’t provide the same information as the old Get-MsolSubscription cmdlet. Specifically, the SDK cmdlet doesn’t tell you the renewal date for a product (SKU).

Relief is now available, but not yet in an SDK cmdlet. Instead, you can fetch the renewal information using a new beta Graph subscriptions endpoint described in Vasil’s blog. This is different to the SubscribedSku API, which is what I think is the base for the Get-MgSubscribedSku cmdlet.

Practical Example of Displaying Renewal Dates for Microsoft 365 Subscriptions

As an example of how you might use the information, I took the output generated by the Get-MgSubscribedSku cmdlet and reformatted it so that it looks like the output from the Get-MsolSubscription cmdlet. The cmdlet lists the SKU part number, active units (available units), warning units (licenses that have expired or have another problem), and consumed units (licenses assigned to user accounts). I wanted to add the renewal date and number of days until the renewal date.

To fetch the renewal dates, I then use the Invoke-MgGraphRequest cmdlet to query the https://graph.microsoft.com/V1.0/directory/subscriptions endpoint. If a SKU has a renewal date, it is in the nextLifecycleDateTime property. Some SKUs that don’t expire (like Power BI standard) don’t have renewal dates. Here’s an example of the information for a Viva Topics subscription that has a renewal date.

Name                           Value
----                           -----
skuId                          4016f256-b063-4864-816e-d818aad600c9
skuPartNumber                  TOPIC_EXPERIENCES
createdDateTime                05/02/2021 18:09:21
totalLicenses                  25
id                             de6eac24-b4b7-4f7e-abeb-9e4f10b36883
serviceStatus                  {System.Collections.Hashtable, System.Collections.Hashtable, System.Collections.Hasht...
ocpSubscriptionId              eeda0292-642e-4901-9825-aa7dfc9b0efc
isTrial                        True
status                         Warning
nextLifecycleDateTime          30/07/2023 14:53:22

To make it easy to lookup the renewal data for a SKU, I created a hash table to store SKU identifiers and renewal dates. The final step is to loop through the SKU information and add the renewal date. Here’s the code:

Connect-MgGraph -Scopes Directory.Read.All -NoWelcome
# Get the basic information about tenant subscriptions
[array]$Skus = Get-MgSubscribedSku
$SkuReport = [System.Collections.Generic.List[Object]]::new()
ForEach ($Sku in $Skus) {
 $DataLine = [PSCustomObject][Ordered]@{
   SkuPartNumber = $Sku.SkuPartNumber
   SkuId         = $Sku.SkuId
   ActiveUnits   = $Sku.PrepaidUnits.Enabled
   WarningUnits  = $Sku.PrepaidUnits.Warning
   ConsumedUnits = $Sku.ConsumedUnits }
 $SkuReport.Add($Dataline)
}

# Get the renewal data
$Uri = "https://graph.microsoft.com/V1.0/directory/subscriptions"
[array]$SkuData = Invoke-MgGraphRequest -Uri $Uri -Method Get
# Put the renewal information into a hash table
$SkuHash = @{}
ForEach ($Sku in $SkuData.Value) { $SkuHash.Add($Sku.SkuId,$Sku.nextLifecycleDateTime) }

# Update the report with the renewal information
ForEach ($R in $SkuReport) {
  $DaysToRenew = $Null
  $SkuRenewalDate = $SkuHash[$R.SkuId]
  $R | Add-Member -NotePropertyName "Renewal date" -NotePropertyValue $SkuRenewalDate -Force 
  If ($SkuRenewalDate) {
   $DaysToRenew = -(New-TimeSpan $SkuRenewalDate).Days
   $R | Add-Member -NotePropertyName "Days to renewal" -NotePropertyValue $DaysToRenew -Force 
 }
}

$SkuReport | Format-Table SkuPartNumber, ActiveUnits, WarningUnits, ConsumedUnits, "Renewal date", "Days to renewal" -AutoSize

Figure 1 shows the output.

Reporting Microsoft 365 subscriptions with renewal dates.
Figure 1: Reporting Microsoft 365 subscriptions with renewal dates

Future SDK Cmdlet Will Probably Come

Obviously, it would be much better if an SDK cmdlet exposed renewal dates for Microsoft 365 subscriptions. Given that the subscriptions endpoint is new, it’s likely that a new SDK will appear after Microsoft’s AutoRest process runs to process the metadata for the endpoint. I’d expect this to happen sometime in the next few weeks.

In the interim, if access to subscription renewal dates is holding up the migration of some old MSOL or AzureAD scripts, a solution is available.


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/06/microsoft-365-subscriptions-ps/feed/ 2 60708
Assigning OneDrive Storage Quotas Based on Group Membership https://office365itpros.com/2023/06/15/onedrive-storage-quota-manage/?utm_source=rss&utm_medium=rss&utm_campaign=onedrive-storage-quota-manage https://office365itpros.com/2023/06/15/onedrive-storage-quota-manage/#comments Thu, 15 Jun 2023 01:00:00 +0000 https://office365itpros.com/?p=60478

Managing OneDrive Storage Quotas Through Groups

A reader asked if it is possible to control the assignment of OneDrive for Business storage quotas using groups using a mechanism like group-based license management. The simple answer is that Microsoft 365 doesn’t support such a feature, but like many administrative operations, it’s relatively easy to automate with PowerShell.

Another article covers the basics of reporting and assigning OneDrive storage. OneDrive for Business accounts are personal SharePoint Online sites. Assigning a new storage quota to a user’s OneDrive account is done using the Set-SPOSite cmdlet from the SharePoint Online administration module. This is one of the Microsoft 365 modules that receives frequent updates, so make sure that you use the most recent version. It’s a good idea to check for updates monthly, either manually or using a PowerShell script to process the Microsoft 365 modules typically used by tenant administrators.

Creating a Script to Update OneDrive Storage Quotas

The steps required in the script to update OneDrive storage quotas based on group membership are:

  • Connect to SharePoint Online and the Microsoft Graph PowerShell SDK.
  • Read information about the target OneDrive storage allocations from some source. I used a CSV file with columns for the group name, group identifier, and storage allocation in megbytes, The names of the columns are group, groupid, and allocation.
  • Figure out the service domain for the tenant to calculate the root of OneDrive account URLs. This will be something like: https://office365itpros-my.sharepoint.com/personal/. Later, we combine a modified version of user principal names (replacing dot and @ characters with underscores) to form the URL for each account. An example is https://office365itpros-my.sharepoint.com/personal/James_Ryan_office365itpros_com.
  • For each group, get the group members. For each member, figure out the user’s OneDrive account URL and run the Get-SPOSite cmdlet to check its current storage quota. You can use any of the group types supported by Entra ID including dynamic Microsoft 365 groups. With some adjustments to the code, it would also be possible to use an Exchange Online dynamic distribution list.
  • If the assigned quota is less than the desired quota, run the Set-SPOSite cmdlet to increase the quota.
  • Create a report about what happened (Figure 1).

Reporting adjustments made to OneDrive for Business storage quotas

OneDrive storage quota
Figure 1: Reporting adjustments made to OneDrive storage quotas

The script includes nothing complicated in terms of code. You can download the script I wrote from GitHub. Remember that the script is not bulletproof in terms of error handling. Its intention is to prove the principle of what is possible. The script should run without a problem if you sign in with a tenant administrator account. I have not tested the code in an Azure Automation runbook (to run the script on a schedule), but I think that adapting the code for Azure Automation would not be difficult.

Use Azure AD Administrative Units Instead of Groups

Azure AD administrative units are the current flavor of the month in Microsoft Purview with many solutions, including Data loss prevention (DLP) and Data lifecycle management (retention) supporting the use of administrative units to scope policies. If you have the necessary Azure AD Premium licenses, you could use administrative units as the basis for storage assignment.

This article explains how to use PowerShell to retrieve information from administrative units. Instead of fetching a set of user principal names for group members, you’d fetch the same information for the members of an administrative unit, like this:

[array]$GroupMemberUPN = (Get-MgBetaAdministrativeUnitMember -AdministrativeUnitId 150dccad-f8b8-4e54-9246-89834b8b5a25).AdditionalProperties.userPrincipalName

PowerShell Automation Scores Again

It would be nice if Microsoft included group-based OneDrive storage management in SharePoint Online. However, this functionality is probably not high on their priority list for new development. This is yet another example of how PowerShell fills in the cracks and gaps left in Microsoft 365 management and underscores why tenant administrators should have the ability to perform at least simple tasks with PowerShell.


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/15/onedrive-storage-quota-manage/feed/ 10 60478
How Administrators Can Remove Meetings On Behalf Of Users https://office365itpros.com/2023/06/09/remove-calendarevents/?utm_source=rss&utm_medium=rss&utm_campaign=remove-calendarevents https://office365itpros.com/2023/06/09/remove-calendarevents/#comments Fri, 09 Jun 2023 01:00:00 +0000 https://office365itpros.com/?p=60381

Use Remove-CalendarEvents to Cancel Teams or Regular Meetings Owned By a Specific User

The question arose about how an administrator can cancel Teams meetings scheduled by a user who is currently unavailable. Because cancelling meetings on behalf of a user is not a day-to-day administrative operation, no option is available for this purpose in the Teams admin center or the Microsoft 365 admin center. However, the Remove-CalendarEvents cmdlet in the Exchange Online management module will do the job (the cmdlet is also available for Exchange 2019).

Normally, administrators don’t interfere with meeting arrangements. Those who create meetings (the organizers) take care of scheduling and maintaining meeting settings, whether they own the calendar or are a calendar delegate. Reasons why an administrator might intervene include:

  • The organizer is unavailable for a sustained period and the need exists to rearrange the meeting.
  • The organizer has left the company.
  • The organizer is due to leave the company and part of the offboarding process involves cancellation of all their future meetings.

Mailbox Access Required

In all cases, Remove-CalendarEvents can only work if access is still available to the user’s mailbox. Exchange Online must be able to connect to the mailbox to access the organizer’s calendar to remove events. For this reason, if you plan to cancel a user’s meetings when they leave the company, the correct procedure is:

  • Sign in as with an account holding the Exchange administrator or global administrator role.
  • Revoke access to their account.
  • Run Remove-CalendarEvents to cancel all future meetings in the user’s mailbox where they are the meeting organizer and the meeting has one or more attendees (including resources like meeting rooms). Cancellations cover both normal meetings and online meetings (using Teams or another online provider). Events without attendees (appointments) are left untouched. The mailbox sends meeting cancelations to meeting participants.
  • Proceed with the remainder of the account removal process. If holds exist on the mailbox, it becomes inactive. However, you can’t cancel meetings from an inactive mailbox. If you forget to cancel meetings, restore the inactive mailbox, cancel the meetings, and then delete the mailbox again.

Running Remove-CalendarEvents

The Remove-CalendarEvents cmdlet can cancel meetings up to 1825 days (five years) in the future, which should be more than sufficient in most cases. You can perform a test run beforehand to see what meetings it will remove by including the PreviewOnly parameter. For example, this command previews what will happen if the cmdlet removes meetings from the current date up to 365 days in the future:

Remove-CalendarEvents –Identity chris.bishop@office365itpros.com -CancelOrganizedMeetings -QueryStartDate (Get-Date) -QueryWindowInDays 365 -PreviewOnly

Confirm
Are you sure you want to perform this action?
The meeting(s) will be canceled and removed from the calendar. This action cannot be undone.
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): y
The meeting with subject "Glastonbury Meeting" and start date "09/06/2023" has been queued for cancellation.
The meeting with subject "Project X" and start date "09/06/2023" has been queued for cancellation.
The meeting with subject "Project Mercury" and start date "17/06/2023" has been queued for cancellation.
The meeting with subject "Project Botha" and start date "21/06/2023" has been queued for cancellation.
The meeting with subject "Project Botha" and start date "28/06/2023" has been queued for cancellation.

When ready to proceed, remove the PreviewOnly parameter and run the command again. Exchange Online connects to the mailbox, finds the relevant meetings, and cancels them.  Cancellation occurs immediately. Meeting participants receive a cancellation notice as normal (Figure 1).

Cancellation notice for a meeting deleted by the Remove-CalendarEvents cmdlet
Figure 1: Cancellation notice for a meeting deleted by the Remove-CalendarEvents cmdlet

Graph Delete Event API

If you need a programmatic method to remove calendar events, the Graph includes a Delete Event API. An app with the Calendars.ReadWrite application permission can connect to a mailbox, find relevant events in the calendar, and delete them.


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/09/remove-calendarevents/feed/ 12 60381
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
Azure AD Access Token Lifetimes and Long-running PowerShell Scripts https://office365itpros.com/2023/05/29/azure-ad-access-token-lifetime/?utm_source=rss&utm_medium=rss&utm_campaign=azure-ad-access-token-lifetime https://office365itpros.com/2023/05/29/azure-ad-access-token-lifetime/#respond Mon, 29 May 2023 01:00:00 +0000 https://office365itpros.com/?p=60194

Sometimes Scripts Need Extended Azure AD Access Token Lifetimes

A recent issue where Microsoft limited the page size for the Graph List Users API when retrieving sign-in activity sparked a request from a reader who had problems with a script. They reported that roughly an hour into the script, it failed with a 401 Unauthorized error. The reason is that the access token granted to the app to allow it to run Graph requests to fetch data expired, meaning that the next time the app tried to request data, the Graph refused.

The default Azure AD access token lifetime varies between 60 and 90 minutes (75 minutes on average). The variation exists on purpose to avoid cyclical spikes in demand. Exceptions to the rule do exist. For example, applications like SharePoint Online and OWA that support continuous access evaluation (CAE) can use tokens that last up to 28 hours. These apps support a feature known as claim challenge that is unlikely to be found in apps that execute Graph requests through PowerShell.

Apps can retrieve access tokens from Azure AD using different OAuth 2.0 authentication flows, including password, device code, and authorization code. Azure AD registered apps usually use the client credentials authentication flow. The app authenticates using its own credentials instead of trying to impersonate a user. Valid app credentials include a secret known to the app, a certificate, or a certificate thumbprint.

The client credentials authentication flow does not include the issuance of a refresh token. The lack of a refresh token, which allows apps to silently renew access tokens, means that if you want to keep a script running, you must either:

  • Configure the tenant with a longer access token lifetime.
  • Include code in the script to fetch a new access token before the current one expires.

Configurable Azure AD Access Token Lifetimes

Azure AD supports configurable token lifetimes. This is a preview feature that can set a longer lifetime for an access token. However, the current implementation supports setting token lifetimes for all apps in an organization or for multi-tenant applications. For instance, this code creates a new token lifetime policy that sets a default two-hour token lifetime. Note the organization default setting is True, so this policy applies to all apps in the organization.

$PolicySettings = @{
    "definition"= @("{'TokenLifetimePolicy':{'Version': 1, 'AccessTokenLifetime': '2:00:00'}}")
    "displayName"= "Org-wide 2 Hr AccessTokenPolicy"
    "IsOrganizationDefault" = $True
} 
 
New-MgPolicyTokenLifetimePolicy -BodyParameter $PolicySettings

To test the policy, use an app to request an access token. Here is some PowerShell code to get an access token using the client credentials authentication flow. In this case, the credential is a client secret stored in the app.

$AppId = “de0d7a5d-982a-49e2-8c52-f4596f32b437”
$TenantId = “a662313f-14fc-43a2-9a7a-d2e27f4f3478”
$AppSecret = “3il8Q~Yx4_DOJZxHAxvp7akxW5TQxXdSzhsGpdme”
$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"
}
# Get OAuth 2.0 Token
$TokenRequest = Invoke-WebRequest -Method Post -Uri $Uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
# Unpack Access Token
$Token = ($tokenRequest.Content | ConvertFrom-Json).access_token

Write-Host ("Retrieved new access token at {0}" -f (Get-Date)) -foregroundcolor red

Take the access token stored in the $Token variable and examine its contents. For example, Figure 1 shows how jwt.io displays the settings in an access token. To verify that the token lifetime works is expected, compare the time of issuance with the expiration time. In this instance, the timespan should be two hours.

Checking the expiration time for an Azure AD access token

Azure AD access token lifetime
Figure 1: Checking the expiration time for an Azure AD access token

Although creating a token lifetime policy with a new default lifetime for the organization works, increasing token lifetime in this manner is not something to do on a whim. It would be better to be able to assign a token lifetime policy only to the apps that need to use extended token lifetimes.

An organization can support multiple token lifetime policies. It would be nice to be able to apply suitable policies to apps as needed but this doesn’t seem to be possible currently. The Microsoft PowerShell Graph SDK includes the New-MgApplicationTokenLifetimePolicyByRef cmdlet, and you can use the cmdlet assign a token lifetime policy to an application. Alas, this has no effect on the access tokens issued by Azure AD to the app. I’ve been discussing this point with Microsoft and investigations continue.

Tracking Azure AD Access Token Lifetime in Scripts

The alternative is to incorporate code into scripts to track the lifetime of an access token so that the script can retrieve a new token before the old one expires. The script for the Microsoft 365 Groups and Teams Activity Report uses this technique. A function checks if the current time is greater than the calculated token expiration time. If it is, the script requests a new token:

Function Check-AccessToken {
# Function to check if the access token needs to be refreshed. If it does, request a new token
# This often needs to happen when the script processes more than a few thousands groups
$TimeNow = (Get-Date)
if($TimeNow -ge $TokenExpiredDate) {
  $Global:Token = GetAccessToken
  $Global:TokenExpiredDate = (Get-Date).AddMinutes($TimeToRefreshToken) 
#  Write-Host "Requested new access token - expiration at" $TokenExpiredDate 
}
Return $Token
}

The function can then be called whenever necessary within the script.

$Global:Token = Check-AccessToken

This is a relatively unsophisticated mechanism, but it allows the script to process tens of thousands of groups. Variations on the theme can handle other situations.

Only for Special Scripts

The default lifetime for an access token is sufficient for most scripts. Even scripts that run dozens of Graph requests can usually complete processing in a few minutes. It is scripts that must retrieve tens of thousands of items (or even hundreds of thousands of items) that usually deal with inadequate Azure AD access token lifetimes. In those cases, you’ll be glad that methods exist to avoid the dreaded 401 Unauthorized error.


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/29/azure-ad-access-token-lifetime/feed/ 0 60194
Reporting the Assignment of Container Management Labels https://office365itpros.com/2023/05/09/container-management-labels/?utm_source=rss&utm_medium=rss&utm_campaign=container-management-labels https://office365itpros.com/2023/05/09/container-management-labels/#comments Tue, 09 May 2023 01:00:00 +0000 https://office365itpros.com/?p=60051

Managing Microsoft 365 Group Settings with Container Management Labels

Container management labels are sensitivity labels configured with management controls that Microsoft 365 groups and their associated SharePoint Online sites and teams inherit from labels assigned on the creation of new groups or by administrators afterward. Important settings include external sharing of SharePoint content (Figure 1) and whether guest members are allowed in groups.

Container management settings for a sensitivity label
Figure 1: Container management settings for a sensitivity label

The settings inherited from assigned labels cannot by changed by group or team owners. However, a group owner can change the effective settings for a site by choosing to apply a different container management label. An organization can monitor for label assignments to groups and reserve those changes if necessary. However, this isn’t standard functionality and must be enabled using a tool like PowerShell (here’s an example).

Container Management Labels

Sensitivity labels are available in tenants licensed with Office 365 E3 or above. If you’re in this position, container management labels are an excellent way to ensure consistency in group settings. An organization can deploy and use container management labels even if they don’t use sensitivity labels for information protection and document marking. In fact, even though it’s possible to use the same set of sensitivity labels for both purposes, it’s a good idea to maintain two sets of labels: one for container management and the other for information protection.

Checking Container Management Label Assignments

Two PowerShell methods are available to check group information:

  • The Get-UnifiedGroup cmdlet from the Exchange Online management module.
  • The Get-MgGroup cmdlet from the Microsoft Graph PowerShell SDK.

Because it must fetch information for many aspects of a Microsoft 365 group, Get-UnifiedGroup is a “heavy” cmdlet. The cmdlet will find all groups, but it will be slow. Get-MgGroup is faster because it retrieves fewer properties for each group. The downside is that Get-MgGroup doesn’t include sensitivity labels in its set of properties. A separate Graph call is required to fetch the label assigned to a group.

To illustrate the point, this code finds all Microsoft 365 groups in a tenant and highlights any group that doesn’t have an assigned sensitivity label.

Connect-MgGraph -Scopes Directory.Read.All

Write-Host "Finding Microsoft 365 Groups to process..."
[array]$Groups = Get-MgGroup -Filter "groupTypes/any(c:c eq 'unified')" -All
If (!($Groups)) { Write-Host "Whoops - can't find any Microsoft 365 Groups" ; break }

ForEach ($Group in $Groups) {
 $LabelId = $Null; $LabelName = $Null

 $Uri = ("https://graph.microsoft.com/v1.0/groups/{0}?`$select=assignedLabels" -f $Group.Id)
 $LabelData = Invoke-GraphRequest -Uri $Uri
 $LabelName = $LabelData.assignedLabels.displayName
 $LabelId   = $LabelData.assignedLabels.labelId
 [array]$GroupOwners = Get-MgGroupOwner -GroupId $Group.Id
 $GroupOwnerNames = $GroupOwners.additionalProperties.displayName -join ", "
 If (!($LabelName)) {
    Write-Host ("The {0} group has no label. Owner(s) {1}" -f $Group.displayName, $GroupOwnerNames) -foregroundcolor Red
 } }
}

By contrast, Get-UnifiedGroup includes sensitivity label data in its properties. However, the property holds the GUID for an assigned sensitivity label instead of its display name. Some additional effort is required to resolve the label GUID to a display name by first fetching the set of sensitivity labels in the tenant (with the Get-Label cmdlet) and building a table of GUIDs and display names to lookup.

This code illustrates how to use the Get-UnifiedGroup cmdlet to accomplish the same goal:

Write-Host "Finding Microsoft 365 Groups to process..."
[array]$Groups = Get-UnifiedGroup -ResultSize Unlimited
If (!($Groups)) { Write-Host "Whoops - can't find any Microsoft 365 Groups" ; break }

ForEach ($Group in $Groups) {
 [array]$GroupOwnerNames = $Null; $LabelId = $Null; $LabelName = $Null

 $LabelId = $Group.SensitivityLabel.Guid
 If ($LabelId) {
   $LabelName = $LabelHash[$LabelId] }

 [array]$GroupOwners = $Group.ManagedBy
 ForEach ($Owner in $GroupOwners) {
    [string]$Owner = $Owner
    $GroupOwnerNames += (Get-Mailbox -Identity $Owner -ErrorAction SilentlyContinue).DisplayName }
 [string]$GroupOwnerNames = $GroupOwnerNames -join ", "
 If (!($LabelName)) {
    Write-Host ("The {0} group has no label. Owner(s) {1}" -f $Group.displayName, $GroupOwnerNames) -foregroundcolor Red
 }
}

The code doesn’t include the commands to connect to Exchange Online and the compliance endpoint (to get label information), nor does it include the code to build the hash table used for label lookups. These commands add about ten seconds of overhead. This isn’t usually a problem.

Testing against 250 groups, the Get-MgGroup method took 1 minute 12 seconds while Get-UnifiedGroup took 1 minute 28 seconds. Generally speaking, Graph-based cmdlets are always faster than the more complex cmdlets used by workloads like Exchange, especially when running at scale against thousands of objects.

Completing the Job

After deciding which approach to use, to finish the job, we put the results of the scan into a PowerShell list and generate a report. The final result is a little more complicated than the processing described above to format the output (Figure 2) and include some additional sections like listing groups that don’t have a label, groups that don’t have any owners, and summary data like how many assignments to groups for each label.

Reporting Container Management labels for Microsoft 365 groups
Figure 2: Reporting Container Management labels for Microsoft 365 groups

The final script is available from GitHub. Feel free to improve the output!


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/2023/05/09/container-management-labels/feed/ 7 60051
The Right Way to Revoke Access from Entra ID User Accounts with PowerShell https://office365itpros.com/2023/04/18/revoke-access-for-entra-id-accounts/?utm_source=rss&utm_medium=rss&utm_campaign=revoke-access-for-entra-id-accounts https://office365itpros.com/2023/04/18/revoke-access-for-entra-id-accounts/#comments Tue, 18 Apr 2023 01:00:00 +0000 https://office365itpros.com/?p=59843

Use the Revoke-MgUserSignInSession cmdlet to Revoke Access for Entra ID Accounts

Updated 24 January 2024

Microsoft’s documentation for how to revoke access to an Entra ID user account describes the use of the Revoke-AzureADUserAllRefreshToken cmdlet from the Azure AD PowerShell module. That’s unfortunate because of the upcoming deprecation of that module. If we consult Microsoft’s cmdlet map to find the appropriate replacement cmdlet from the Microsoft Graph PowerShell SDK, it turns out to be Invoke-MgInvalidateUserRefreshToken, which “Invalidates all of the user’s refresh tokens issued to applications (as well as session cookies in a user’s browser), by resetting the refreshTokensValidFromDateTime user property to the current date-time.”

The guidance could not be clearer. Any script using the Revoke-AzureADUserAllRefreshToken should replace it with the Invoke-MgInvalidateUserRefreshToken cmdlet. Except when you discover that the SDK also includes the Revoke-MgUserSignInSession cmdlet. This cmdlet is in beta and its documentation is less than perfect (or totally inadequate), but the salient fact is that it performs the same task. These two commands have the same effect:

$RevokeStatus = Revoke-MgUserSignInSession -UserId $UserId
$InvalidateStatus = Invoke-MgInvalidateUserRefreshToken -UserId $UserId

Up to now, the Office 365 for IT Pros eBook (chapter 5) documented how to use the Invoke-MgInvalidateUserRefreshToken cmdlet to block an Entra ID user account. Finding the alternative cmdlet used in a Microsoft example provoked a query to ask why two cmdlets did the same thing.

Microsoft’s response is that they built Invoke-MgInvalidateUserRefreshToken for a specific purpose. The cmdlet still works and has the significant benefit of being part of the production (V1.0) module. However, Microsoft’s recommendation is to use Revoke-MgUserSignInSession in the future, even if it is in the beta module.

Revoking Access for an Entra ID Account is the Start

Of course, revoking access for an Entra ID user account might just be the first step in the process of securing the account. Revoking access will force the user to reauthenticate, but if you want to stop further access to the account, you must:

Disabling the account and changing the password are both critical events that force Entra ID to signal applications that support continuous access evaluation (CAE) to terminate sessions. Many of the important Microsoft 365 apps like Outlook and SharePoint Online support CAE (see current list).

This PowerShell code does the necessary. The account signed into the Microsoft Graph PowerShell SDK must have the user administrator role to update account details and the Cloud Device Administrator role to update the device. Alternatively, the Global administrator role will do the job:

Connect-MgGraph -Scopes Directory.AccessAsUser.All
$Account = Read-Host "Enter the User Principal Name of the account to block"
$User = (Get-MgUser -UserId $Account -ErrorAction SilentlyContinue)
If (!($User)) { Write-Host ("Can't find an Entra ID user account for {0}" -f $Account); break }
Write-Host ("Revoking access and changing password for account {0}" -f $User.DisplayName)  
# Disable the account
Update-MgUser -UserId $User.Id -AccountEnabled:$False
# Create a password profile with details of a new password
$NewPassword = @{}
$NewPassword["Password"]= "!NewYorkCity2022?"
$NewPassword["ForceChangePasswordNextSignIn"] = $True
Update-MgUser -UserId $User.Id -PasswordProfile $NewPassword
# Revoke signed in sessions and refresh tokens
$RevokeStatus = Revoke-MgUserSignInSession -UserId $User.Id
If ($RevokeStatus.Value -eq $true) {
   Write-Host ("Access revoked for user {0}" -f $User.DisplayName)
}
# Disable registered devices
[array]$UserDevices = Get-MgUserRegisteredDevice -UserId $User.Id
If ($UserDevices) {
ForEach ($Device in $UserDevices) {
    Update-MgDevice -DeviceId $Device.Id -AccountEnabled:$False}
}

Figure 1 shows that after running the script, the user account is disabled and the SignInSessionsValidFromDateTime property (referred to as refreshTokensValidFromDateTime above) is set to the time when the Revoke-MgUserSignInSession cmdlet ran.

Running PowerShell to revoke access for an Entra ID account.
Figure 1: Running PowerShell to revoke access for an Entra ID user account

Consequences of Disabling an Entra ID User Account

In a scenario like a departing employee, losing access to some teams might not be important. If it is, or in situations where it’s necessary to preserve the account in full working order, an alternative to disabling an account is to change its password and revoke access. The account remains active but is inaccessible unless those attempting to sign-in know the new password.

Example of Knowledge Gap

In July 2022, I wrote about the opening of a knowledge gap as tenants transitioned from the depreciated Azure AD and Microsoft Online Services (MSOL) modules. Having two cmdlets that revoke user access to pick from is one too many. It doesn’t help people migrate scripts to use the Microsoft Graph PowerShell SDK. But at least the recommendation is clear: use Revoke-MgUserSignInSession.


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/18/revoke-access-for-entra-id-accounts/feed/ 7 59843
Microsoft Removes Remote PowerShell for Compliance Sessions https://office365itpros.com/2023/04/17/compliance-endpoint-powershell/?utm_source=rss&utm_medium=rss&utm_campaign=compliance-endpoint-powershell https://office365itpros.com/2023/04/17/compliance-endpoint-powershell/#comments Mon, 17 Apr 2023 01:00:00 +0000 https://office365itpros.com/?p=59861

No Remote PowerShell Connections for Compliance Endpoint

Following the December 15, 2022 announcement to deprecate Remote PowerShell connections to Exchange Online, the news in MC541649 (April 14) that the connection to the compliance endpoint with Connect-IPPSSession cmdlet will follow suit is no surprise. The only surprise is that the text of the announcement is quite so confusing.

Connect-IPPSSession establishes a PowerShell connection to what used to be called the Security and Compliance endpoint (SCC). Microsoft also refers to EOP in the announcement because some cmdlets loaded (like Get-PhishSimOverridePolicy) are associated with Exchange Online Protection.

Today, the endpoint might be called the Microsoft Purview endpoint or compliance endpoint because the cmdlets loaded after establishing the connection allow access to objects like retention labels, sensitivity labels, and their respective publishing policies.

REST Rather than Remote PowerShell

Microsoft says that “in line with our vision to enhance the security of our cloud,” the compliance cmdlets will now use a REST API instead of the traditional (established in Exchange 2010) Remote PowerShell approach. Once you install V3.2 of the Exchange Online management module (apparently available on May 1, 2023), the REST-base cmdlets are available and Remote PowerShell is no longer required. You won’t see this kind of message when connecting to the endpoint:

WARNING: Your connection has been redirected to the following URI:
"https://eur01b.ps.compliance.protection.outlook.com/Powershell-LiveId?BasicAuthToOAuthConversion=true;PSVersion=5.1.22621.963"

Existing scripts don’t need to be updated. As Microsoft says “Simply using the new module will ensure REST is used rather than RPS.”

Part of the confusion in this announcement is the need to use a version of the Exchange Online management module that is currently unavailable. The current version doesn’t support the UseRPSSession parameter mentioned by Microsoft in their text:

Connect-IPPSSsession -UseRPSSession:$false

Microsoft says that Remote PowerShell connections to the compliance endpoint will not be available after July 15, 2023. This is a tad ahead of the announced schedule for the depreciation of Remote PowerShell for the main Exchange module (due on October 1, 2023).

Using a REST API instead of Remote PowerShell should make cmdlets more reliable and better performing. Remote PowerShell is very much a mechanism rooted in a period when Microsoft needed to support management of Exchange servers from workstations without the need to log into the servers. It worked well for Exchange 2010 and 2013 but its deficiencies are obvious with cloud services when connecting to a service is more important than connecting to a server.

More to Do

Welcome as it is to see the compliance cmdlets transition to a REST-based endpoint, there’s still more to do to fully modernize these cmdlets. Adding support for Azure managed identifies is a big step that needs to happen. It can be argued that the compliance cmdlets are less heavily accessed than those in the main Exchange module, but this ignores the fact that many of the tasks that you might want to run on a scheduled basis using an Azure Automation runbook might need to access compliance elements, like the list of sensitivity labels defined in a tenant (Figure 1).

 Listing sensitivity labels after connecting to the compliance endpoint
Figure 1: Listing sensitivity labels after connecting to the compliance endpoint

Good Change

There’s no doubt that moving the compliance endpoint away from a dependency on Remote PowerShell is a good thing. Throwing away the baggage of on-premises implementations to make things work smoother in the cloud is always positive for those who need to automate Microsoft 365 operations. This is especially so when discussing compliance because the range of compliance functionality available in Microsoft 365 is so much wider and deeper than in the on-premises servers.

At this point, we don’t have the V3.2 release of the Exchange Online management module available so it’s hard to verify Microsoft’s assertion that nothing needs to be done to move the compliance cmdlets from Remote PowerShell to REST-based APIs. However, given the progress seen in the main Exchange Online management module, Microsoft is progressing down a well-known path and the change should be smooth. At least. I hope it will be.

]]>
https://office365itpros.com/2023/04/17/compliance-endpoint-powershell/feed/ 5 59861
Microsoft Releases Cmdlet to Retrieve Disposition Review Items https://office365itpros.com/2023/04/10/disposition-review-items-export/?utm_source=rss&utm_medium=rss&utm_campaign=disposition-review-items-export https://office365itpros.com/2023/04/10/disposition-review-items-export/#respond Mon, 10 Apr 2023 01:00:00 +0000 https://office365itpros.com/?p=59787

Export Details of Disposition Review Items

Message Center notification MC521457 (Microsoft 365 roadmap item 106102) might have passed you buy on February 27 when Microsoft announced a new PowerShell cmdlet for disposition review. Relatively few people are concerned with Microsoft Purview Data Lifecycle Management to care that a new cmdlet is available to export (not just “to support”) disposition review items, so it’s entirely natural that you might have gone on to read about other announcements occurring around the same time, like Exchange Online’s improved message recall feature.

Roll-out of the new Get-ReviewItems cmdlet is now complete. The cmdlet is available after loading the latest version of the Exchange Online management module.

Disposition Items

Microsoft 365 retention labels often result in the deletion of items after the lapse of their retention periods. This is enough for most organizations, but those that want oversight over the final processing of selected items can configure retention labels to invoke a disposition review, part of the Microsoft Purview records management solution. Disposition reviews are often used to retain messages and documentations such as those for project documentation until the organization is absolutely sure that it’s safe to remove individual items.

Using a disposition review with retention labels requires advanced licenses, like Office 365 E5. An organization can put items through a single-stage or multi-stage review (Figure1) leading to final deletion, retention for another period, or assignment of a new retention label. The reviewers who decide on the disposition of content are selected by the organization because they have the expertise and experience to know if items are still needed or can progress to final disposition. It’s also possible to configure a custom automated disposition process using Power Automate.

Viewing disposition review items for a retention label
Figure 1: Viewing disposition review items for a retention label

Exporting Disposition Review Items

The Get-ReviewItems cmdlet doesn’t affect disposition outcomes. It’s a utility cmdlet to export details of disposition review items for a specific retention label in a pending or disposed (processed) state. The reason why the cmdlet exists is that the Purview GUI (Figure 1) supports export of up to 50,000 items. Although it’s unlikely that an organization will have more than 50,000 items awaiting disposition review, it is possible that they might have more than 50,000 disposed (processed) items. The Get-ReviewItems cmdlet can export details of all those items.

Microsoft’s documentation for Get-ReviewItems includes examples of using the cmdlet. One in particular is noteworthy because it explains how to fetch pages of review items until all items have been recovered. Fetching pages of data is common practice in the Graph API world and it’s done to reduce the strain on the service imposed if administrators requested very large numbers of items at one time.

I expanded the example to create a report of all disposition review items for a tenant (all items for all retention labels with a disposition review). Here’s the code:

Connect-IPPSSession

[array]$ReviewTags = Get-ComplianceTag | Where-Object {$_.IsReviewTag -eq $True} | Sort-Object Name
If (!($ReviewTags)) { Write-Host "No retention tags with manual disposition found - exiting"; break }

Write-Host ("Looking for Review Items for {0} retention tags: {1}" -f $ReviewTags.count, ($ReviewTags.Name -join ", "))

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

[array]$ItemsForReport = $Null
ForEach ($ReviewTag in $ReviewTags) {
 Write-Host ("Processing disposition items for the {0} label" -f $ReviewTag.Name)
 [array]$ItemDetails = $Null; [array]$ItemDetailsExport = $Null
 # Fetch first page of review items for the tag and extract the items to an array
 [array]$ReviewItems = Get-ReviewItems -TargetLabelId $ReviewTag.ImmutableId -IncludeHeaders $True -Disposed $False  
 $ItemDetails += $ReviewItems.ExportItems
 # If more pages of data are available, fetch them and add to the Item details array
 While (![string]::IsNullOrEmpty($ReviewItems.PaginationCookie))
 {
    $ReviewItems = Get-ReviewItems -TargetLabelId $ReviewTag.ImmutableId -IncludeHeaders $True -PagingCookie $ReviewItems.PaginationCookie
    $ItemDetails += $ReviewItems.ExportItems
 }
 # Convert data from CSV
 If ($ItemDetails) {
   [array]$ItemDetailsExport = $ItemDetails | ConvertFrom-Csv -Header $ReviewItems.Headers 
   ForEach ($Item in $ItemDetailsExport) {
     # Sometimes the data doesn't include the label name, so we add the label name to be sure
     $Item | Add-Member -NotePropertyName Label -NotePropertyValue $ReviewTag.Name }
   $ItemsForReport += $ItemDetailsExport
 }
}

ForEach ($Record in $ItemsForReport) {
  If ($Record.ItemCreationTime) {
   $RecordCreationDate =  Get-Date($Record.ItemCreationTime) -format g 
  } Else {
   $RecordCreationDate = "Unknown" }
 
   $DataLine  = [PSCustomObject] @{
     TimeStamp       = $RecordCreationDate
     Subject         = $Record.Subject
     Label           = $Record.Label
     AppliedBy       = $Record.LabelAppliedBy
     RecordType      = $Record.RecordType
     'Last Reviewed' = Get-Date($Record.ItemLastModifiedTime) -format g
     'Review Action' = $Record.ReviewAction
     Comment         = $Record.Comment
     'Deleted Date'  = $Record.DeletedDate
     Author          = $Record.Author
     Link            = $Record.InternetMessageId
     Location        = $Record.Location
   } 
   $Report.Add($DataLine)
}

Everything works – until you meet an item with a comma in its subject or the comment captured when a reviewer decides upon a disposition outcome. After discussing the issue with Microsoft, its root cause is that the export is in CSV format and the comma in these fields causes problems when converting from CSV format. Microsoft is working on a fix which might be present as you read this.

The Lesson of Export

The Get-ReviewItems cmdlet will be a useful tool for those involved in disposition processing. They can extract details of items and report that information in whatever way they wish. The comma issue proves that documentation is not always perfect. It’s important to test examples to make sure that they work as they should.


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/10/disposition-review-items-export/feed/ 0 59787
Generate a HTML Report of Managers and Direct Reports with the Graph SDK https://office365itpros.com/2023/04/06/entra-id-manager-direct-reports/?utm_source=rss&utm_medium=rss&utm_campaign=entra-id-manager-direct-reports https://office365itpros.com/2023/04/06/entra-id-manager-direct-reports/#comments Thu, 06 Apr 2023 01:00:00 +0000 https://office365itpros.com/?p=59707

Creating a Report From Entra ID Manager and Direct Reports Data with PowerShell

It’s always good to be able to build on the knowledge contributed by someone else. This brings me to a post by Vasil Michev, the esteemed technical editor for the Office 365 for IT Pros eBook. The post covers how to Create an All Managers group in Microsoft 365 and covers how to do this in different ways for different types of group. It brought back some memories of Microsoft’s initiative in April 2017 to auto-generate a Microsoft 365 group for every manager with its membership populated with the manager’s direct report.

Retrieving Entra ID Managers and Direct Reports

In any case, Vasil discussed how the Get-Recipient (but not Get­ExoRecipient) and Get-User cmdlets have a filter to find accounts that have direct reports using the backlink from users to their managers. By definition, these accounts are managers, so you can use the commands as the basis to control the membership of distribution lists, dynamic distribution lists, or Microsoft 365 groups.

Get-Recipient -Filter {DirectReports -ne $Null}
Get-User -Filter {DirectReports -ne $Null}

The only problem is that the output of the two cmdlets is imperfect. The cmdlets find accounts with direct reports, but their results include some accounts that don’t have any direct reports. In my tenant, I found that the cmdlets found three accounts with no direct reports. I believe that these accounts had direct reports at some point in the past, but they don’t now. For instance, when I queried the accounts to see the set of direct reports reported by Get-User, I see a blank:

Get-User -Identity Ben.Owens | Select-Object Name, Manager, DirectReports

Name      Manager      DirectReports
----      -------      -------------
Ben Owens tony.redmond {}

The same is true when viewing details of the account through Exchange address lists, the organization chart in Teams, or the Outlook Org Explorer (Figure 1).

Outlook Org Explorer lists no direct reports for a manager
Figure 1: Outlook Org Explorer lists no direct reports for a manager

According to message center notification MC492902 (updated 7 February 2023), the Outlook Org Explorer is only available to users with the “Microsoft Viva Suite” or “Microsoft Viva Suite with Glint” licenses, which is why you might not be seeing it. Originally, Microsoft said that the Org Explorer would be available to accounts with Microsoft 365 E3/E5 or Microsoft 365 Business licenses, but they decided to align this feature with the Viva initiative. The Org Explorer is not available for OWA.

My conclusion is that synchronization betweenEntra ID and Exchange Online leaves some vestige behind in the DirectReports property following the removal of the last direct report for a manager. It’s enough to stop the filter working accurately.

Reporting Entra ID Managers and Direct Reports

Which brings me back to considering how to report the links between managers and employees using the information stored in Entra ID. I covered this ground in an article two years ago, but I didn’t realize the flaw in Get-User at the time, so the script I wrote (available from GitHub) can produce incorrect results. A different approach is needed.

Given that Entra ID is the source of the information, it makes sense to use Graph APIs to retrieve data. I chose to use the Microsoft Graph PowerShell SDK to avoid the necessity to create a registered app.

The new script (also available from GitHub) does the following:

  • Finds user accounts with at least one assigned license. This step filters out accounts created for purposes like room and shared mailboxes.
  • Use the Get-MgUserManager cmdlet to check each account to see if it has a manager. If not, note this fact.
  • Use the Get-MgUserDirectReport cmdlet to see if the account has direct reports. If it does, record the details of the manager’s reports.
  • Create an HTML report detailing each manager and their reports.
  • At the end of the report, add a section detailing accounts without managers.
  • Output the HTML file and a CSV file containing details of managers and reports.

Figure 2 shows some example output. Because the code is PowerShell, it’s easy to tweak it to include other information about each employee.

Reporting managers and their direct reports

Azure AD Managers
Figure 2: Reporting managers and their direct reports

Go to the Source to Find Managers and Direct Reports

It’s never nice to discover that a technique you thought worked well is no longer fit for purpose and it’s necessary to rework a script. The Get-User and Get-Recipient cmdlets return accurate information about managers and direct reports, but only if managers always have at least one report. I guess that’s possible, but it’s better to make sure by using Graph APIs to retrieve data about managers and their direct reports. At least then you’ll know that your reports show the same reporting relationships that surface elsewhere in Microsoft 365.


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/04/06/entra-id-manager-direct-reports/feed/ 6 59707
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
Time Running Out for AzureAD and MSOL PowerShell Modules https://office365itpros.com/2023/04/04/azuread-powershell-retirement/?utm_source=rss&utm_medium=rss&utm_campaign=azuread-powershell-retirement https://office365itpros.com/2023/04/04/azuread-powershell-retirement/#comments Tue, 04 Apr 2023 01:00:00 +0000 https://office365itpros.com/?p=59646

Last Gasp for AzureAD PowerShell Retirement as Deadline Approaches

Updated 2 April 2024

Microsoft’s original announcement about the deprecation of the AzureAD and Microsoft Online Services (MSOL) PowerShell modules goes back to 26 August, 2021. At that time, Microsoft wanted to have the retirement done by June 30, 2022. Customer pushback duly ensued and Microsoft decided to push the dates out another year to allow customers more time to upgrade their scripts. They subsequently deferred the deprecation for a further nine months to March 30, 2024. The time for the AzureAD PowerShell retirement is now rapidly approaching.

This was the only sensible course of action. The Graph APIs for dealing with many Entra ID user account interactions, especially license assignments, were sadly undocumented. The suggestion of using cmdlets from the Microsoft Graph PowerShell SDK ran into difficulties because some of the cmdlets didn’t work as expected. Allied to that, the documentation for the SDK cmdlets remains poor and inscrutable at times. These issues have largely been addressed.

Microsoft Graph PowerShell SDK Improved Over Time

Time is a great healer and allows for improvements to be made. The Graph Explorer works better and the Graph X-Ray tool reveals details about how Microsoft uses Graph calls in places like the Microsoft Entra admin center.

In addition, Microsoft developed documentation to help people migrate scripts, including a cmdlet map to translate old cmdlets to new. The important thing to realize here is that automatic translation from one set of cmdlets to the other is difficult. People code in PowerShell in different ways and it’s not always clear how to translate code to a new cmdlet. Some community-based projects do exist (here’s a new one that is spinning up), but any attempt to covert to SDK cmdlets must take the SDK foibles into consideration, like its fundamental disregard for the PowerShell pipeline.

But mostly time allowed people to share their knowledge about how to use SDK cmdlets to automate administrative tasks like user and group management. For instance, here’s a writeup I did about license management for Entra ID user accounts using the SDK, and here’s another covering how to create a license report for Entra ID user accounts.

What Will Happen Between Now and the Final AzureAD PowerShell Retirement

But time eventually runs out and we are now at the point where Microsoft will soon retire the AzureAD and MSOL modules. Here’s my understanding of the situation:

  • The licensing cmdlets from the AzureAD and MSOL modules do not work for tenants created after November 1, 2022. These tenants must use Graph APIs or SDK cmdlets to manage license assignments for Entra ID user accounts.
  • For all tenants, March 30, 2024 is the official deprecation date for the licensing cmdlets in the AzureAD and MSOL modules.
  • Apart from the cmdlets that assign or work with licenses, deprecation doesn’t mean “stop working.” Microsoft blocks the cmdlets that to Entra ID user accounts. This is in line with the warning posted on July 29, 2022, that “Customers may notice performance delays as we approach the retirement deadline,” The affected cmdlets are:
    • Set-MsolUserLicenseSet-AzureADUserLicense
    • New-MsolUser (where the creation of an account includes a license assignment)
The Set-AzureADUserLicense cmdlet stopped  working on June 30, 2023

AzureAD PowerShell retirement
Figure 1: The Set-AzureADUserLicense cmdlet stopped working on June 30, 2023
  • After March 30, 2024, the AzureAD and MSOL modules are deprecated and unsupported apart from security fixes. With the notable exception of the licensing cmdlets, Microsoft says that the modules will continue to function until March 30, 2025. At that point, Microsoft will retire the AzureAD and MSOL modules. Cmdlets from the two modules might still run, but no guarantees exist that they will be successful. In other words, scripts might fail without warning.

The Bottom Line About the AzureAD PowerShell Retirement

The AzureAD and MSOL modules are now on borrowed time. If you haven’t already started to upgrade scripts to use the Graph APIs or the Microsoft Graph PowerShell SDK, scripts that use these modules could encounter an unpleasant failure very soon. It’s time to get busy to make sure that all scripts are migrated to run using the SDK cmdlets before March 30, 2024.


The Office 365 for IT Pros eBook includes hundreds of examples of working with Microsoft 365 through PowerShell. We explain how to run Microsoft Graph API queries through PowerShell and how to use the cmdlets from the Microsoft Graph PowerShell SDK in a very practical and approachable manner.

]]>
https://office365itpros.com/2023/04/04/azuread-powershell-retirement/feed/ 18 59646
Microsoft Releases Version 5 of the Microsoft Teams PowerShell Module https://office365itpros.com/2023/03/06/get-csonlineuser-teams-v5/?utm_source=rss&utm_medium=rss&utm_campaign=get-csonlineuser-teams-v5 https://office365itpros.com/2023/03/06/get-csonlineuser-teams-v5/#respond Mon, 06 Mar 2023 01:00:00 +0000 https://office365itpros.com/?p=59333

Major Update for the Get-CsOnlineUser Cmdlet

I don’t normally write about a new version of the Microsoft Teams PowerShell module and confine myself to updating the post covering recent module updates. However, the release of a major version is worth comment, which is the case with V5.0 of the Teams module, now available from the PowerShell Gallery (Figure 1).

V5.0 of the Microsoft Teams PowerShell module

Get-CsOnlineUser
Figure 1: V5.0 of the Microsoft Teams PowerShell module

Over the past few releases, Microsoft concentrated on “modernizing” the policy management cmdlets that Teams inherited from the Skype for Business connector. Modernization is a term to describe updating the cmdlets to recent standards to make them more reliable and robust. The Get-CsOnlineUser cmdlet is the focus for the V5.0 release.

The Use of Get-CsOnlineUser

Get-CsOnlineUser fetches details of user accounts enabled for Teams. I only use this cmdlet when I need to view details of the Teams policies assigned to accounts as I prefer using the Get-MgUser cmdlet to retrieve information about user accounts. The Get-CsOnlineUser cmdlet can return details of the Teams service plans assigned to an account (like the MCO_VIRTUAL_APPT and TEAMS_WEBINAR service plans assigned to accounts with the Teams Premium license), but these are also retrievable with Get-MgUser.

In the past, Get-CsOnlineUser hasn’t been very performant or flexible when retrieving accounts. Microsoft says that they’ve improved performance, especially when using filters to find accounts. In addition, a set of new filterable properties are available (Alias, City, CompanyName, CompanyName, HostingProvider, UserValidationErrors, OnPremEnterpriseVoiceEnabled, OnPremHostingProvider, OnPremLineURI, OnPremSIPEnabled, SipAddress, SoftDeletionTimestamp, State, Street, TeamsOwnersPolicy, WhenChanged, WhenCreated, FeatureTypes, PreferredDataLocation, andLastName).

Changes to Filtering

Another improvement is in the support of filtering operators to bring the cmdlet in line with other cmdlets that fetch user information like Get-ExoMailbox. This is server-side filtering, meaning that the server only returns items that match the filter. It’s faster to retrieve data with a server-side filter than it is to fetch items and then apply a filter on the workstation (client-side filtering).

For instance, this use of the like operator now works:

Get-CsOnlineUser -Filter {City -like "*York*"} | Format-Table DisplayName, City

DisplayName   City
-----------   ----
Terry Hegarty New York

Previous versions of the module generate the error: Get-CsOnlineUser : The filter attribute ‘city’ is not supported.

Get-CsOnlineUser now supports use of the gt (greater than), lt (less than), and le (less than or equal to) operators to filter against string properties. For instance, this works:

Get-CsOnlineUser -Filter {DisplayName -gt "James"} | Sort-Object DisplayName | Format-Table DisplayName, City

DisplayName                             City
-----------                             ----
James Abrahams                          Foxrock
James Ryan                              Foxrock
Jane Sixsmith                           Dublin

The contains operator now supports properties that contain arrays. For instance, this command returns the set of accounts enabled for Teams:

Get-CsOnlineUser -Filter {FeatureTypes -contains "Teams"} | Format-Table DisplayName

The ge operator supports filters against Teams policies (previous versions only support the eq and ne operators):

Get-CsOnlineUser -Filter {TeamsFilesPolicy -ge "*NoSP*"} | Format-Table DisplayName, TeamsFilesPolicy

My attempts to use the cmdlet to filter against the Teams Channel policy failed. I also saw inconsistent results when filtering against other policies. For instance, this returns no accounts:

Get-CsOnlineUser -Filter {TeamsMessagingPolicy -ge "B"}

Adding wildcards generates some results, but it’s hard to accept that a policy called “Advanced” has a name greater or equal to “B”:

Get-CsOnlineUser -Filter {TeamsMessagingPolicy -ge "*B*"} | Format-Table DisplayName, TeamsMessagingPolicy

DisplayName                 TeamsMessagingPolicy
-----------                 --------------------
Jane Sixsmith               Advanced
Marc Vigneau                Advanced

Interestingly, a client-side filter has problems too:

$Users = Get-CsOnlineUser | Where-Object {$_.TeamsMessagingPolicy -ge "B"} | Format-Table DisplayName
Where-Object : Cannot compare "Advanced" because it is not IComparable.
At line:1 char:29

I might be doing things in a way unanticipated by the Teams PowerShell developers, but I have been around PowerShell long enough to know when things don’t work quite the way they should. Some tweaks might still be necessary to make sure that filters work against all Teams policies in the same way.

Soft Deleted Users

Apart from the filtering changes, Get-CsOnlineUser now returns details of unlicensed users for 30 days after license removal and indicates soft-deleted users (accounts in the Azure AD recycle bin awaiting permanent removal) by showing the date and time of deletion in the SoftDeletionTimestamp property. You can find the soft-deleted users with:

Get-CsOnlineUser -Filter {SoftDeletionTimestamp -ne $Null} | Format-Table DisplayName, SoftDeletionTimestamp

DisplayName SoftDeletionTimestamp
----------- ---------------------
Ben James   04/03/2023 23:11:41

Work Still to Do

Get-CsOnlineUser is an important cmdlet used in many scripts to automate administrative processes. It’s good that Microsoft invested effort to make the Get-CsOnlineUser cmdlet work better, even if some issues still exist. Crack out the update procedure you use to refresh Microsoft 365 modules (or use my script, which handles Exchange Online, SharePoint Online, and the Microsoft Graph PowerShell SDK too) and upgrade to V5.0 of the Microsoft Teams module.


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/03/06/get-csonlineuser-teams-v5/feed/ 0 59333
How to Disable the Viva Engage Core Service Plan https://office365itpros.com/2023/02/20/viva-engage-core-service-plan/?utm_source=rss&utm_medium=rss&utm_campaign=viva-engage-core-service-plan https://office365itpros.com/2023/02/20/viva-engage-core-service-plan/#comments Mon, 20 Feb 2023 01:00:00 +0000 https://office365itpros.com/?p=59136

Viva Engage Core Service Plan for Continuity and New Features

Along with their announcements that Yammer is becoming Viva Engage, Microsoft blogged about a new Viva Engage admin center (just what we need – another admin portals). The admin blog contained the news of new service plans that Microsoft has added to user account license assignments. For example, accounts with Office 365 E3 and E1 licenses now have the Viva Engage Core and Viva Learning Seeded service plans (Figure 1).

Viva Engage Core listed in the apps (service plans) available to a user account
Figure 1: Viva Engage Core listed in the apps (service plans) available to a user account

I don’t see any trace of the Viva Engage Core service plan in Office 365 E5 licenses. This might be because some accounts have Viva Suite licenses.

Good Intentions but Bad Outcome

Microsoft added the Viva Engage Core service plan to make sure that Viva users could continue to use Yammer services (like Q&A) after the switchover, saying “The service plans have been enabled for all users to provide a smooth and easily controlled feature roll out process.” The Viva Engage Code service plan will control new features and Microsoft wanted to put the service plan in place so that no one would miss out.

That’s a laudable intention, but they missed one very important point. Microsoft failed to disable the Viva Engage Core service plan for accounts where administrators had previously disabled the Yammer Enterprise service plan. Because the Viva Engage Core service plan enables Yammer services, the newly enabled license option means that people who previously couldn’t use Yammer can now do so.

Disabling the Viva Engage Core Service Plan

Most users won’t realize that they can go to yammer.com and launch Yammer with a URL like https://web.yammer.com/main/org/office365itpros.com. Anyway, if they did, they probably wouldn’t find much because the organization obviously doesn’t want to use Yammer. Considering those facts, you might think that little damage is done, but workers councils and unions might not take the same view.

Some PowerShell can fix the damage. Many organizations have a general-purpose script to remove service plans from Microsoft 365 licenses (here’s my version – make sure that you use the Graph-based script). In this case, I repurposed a script that I wrote to remove the Kaizala service plan from licenses, if only because it’s more recent work and includes logging of license updates.

To check user accounts for disabled service plans, we need to know what to look for. In this instance, the script must check accounts to see if the Yammer Enterprise service plan (7547a3fe-08ee-4ccb-b430-5077c5041653) is disabled and if so, disable the Viva Engage Core service plan (a82fbf69-b4d7-49f4-83a6-915b2cf354f4). The source for this information is Microsoft’s Azure AD license reference page.

The outline of the script is:

  • Find licensed user accounts.
  • For each account, check if it has an Office 365 license.
  • If so, check if Yammer Enterprise is disabled.
  • If so, disable Viva Engage Core.

You can download a copy of the full script from GitHub. I know the script will remove Viva Core Engage from Office 365 E3 licenses, but I don’t know how Microsoft assigned the service plan to other licenses. Because the code is PowerShell, it should be easy to amend to handle other license conditions.

Evolving License Management with PowerShell

PowerShell is a great way to automate license management operations if you don’t have something more sophisticated to help, like Entra ID group-based licensing. But remember that Microsoft will retire the license management cmdlets from the Azure AD and MSOL modules on March 31, 2023. Make sure that any PowerShell you write to work with user licenses uses Graph API requests or cmdlets from the Microsoft Graph PowerShell SDK.

P.S. Microsoft’s graphic to support the rebranding announcement in tweets and other social media was really quite clever. (Figure 1), even if it hid what must have been a bruising transition for some.

Yammer and Viva Engage
Figure 2: Yammer and Viva Engage

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. We don’t like when Microsoft rebrands software products because it means that we’ve then got to update references in the book. There were 298 mentions of Yammer in the February 2023 update for the Office 365 for IT Pros eBook. March will see that number drop dramatically…

]]>
https://office365itpros.com/2023/02/20/viva-engage-core-service-plan/feed/ 8 59136
Mastering the Foibles of the Microsoft Graph PowerShell SDK https://office365itpros.com/2023/02/13/microsoft-graph-powershell-sdk-prob/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-powershell-sdk-prob https://office365itpros.com/2023/02/13/microsoft-graph-powershell-sdk-prob/#comments Mon, 13 Feb 2023 01:00:00 +0000 https://office365itpros.com/?p=59070
He looks happy, but he hasn't hit some of the Microsoft Graph PowerShell SDK foibles yet...
He looks happy, but he hasn’t hit some of the SDK foibles yet…

Translating Graph API Requests to PowerShell Cmdlets Sometimes Doesn’t Go So Well

The longer you work with a technology, the more you come to know about its strengths and weaknesses. I’ve been working with the Microsoft Graph PowerShell SDK for about two years now. I like the way that the SDK makes Graph APIs more accessible to people accustomed to developing PowerShell scripts, but I hate some of the SDK’s foibles.

This article describes the Microsoft Graph PowerShell SDK idiosyncrasies that cause me most heartburn. All are things to look out for when converting scripts from the Azure AD and MSOL modules before their deprecation (speaking of which, here’s an interesting tool that might help with this work).

No Respect for $Null

Sometimes you just don’t want to write something into a property and that’s what PowerShell’s $Null variable is for. But the Microsoft Graph PowerShell SDK cmdlets don’t like it when you use $Null. For example, let’s assume you want to create a new Azure AD user account. This code creates a hash table with the properties of the new account and then runs the New-MgUser cmdlet.

$NewUserProperties = @{
    GivenName = $FirstName
    Surname = $LastName
    DisplayName = $DisplayName
    JobTitle = $JobTitle
    Department = $Null
    MailNickname = $NickName
    Mail = $PrimarySmtpAddress
    UserPrincipalName = $UPN
    Country = $Country
    PasswordProfile = $NewPasswordProfile
    AccountEnabled = $true }
$NewGuestAccount = New-MgUser @NewUserProperties

New-MgUser fails because of an invalid value for the department property, even though $Null is a valid PowerShell value.

New-MgUser : Invalid value specified for property 'department' of resource 'User'.
At line:1 char:2
+  $NewGuestAccount = New-MgUser @NewUserProperties
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: ({ body = Micros...oftGraphUser1 }:<>f__AnonymousType64`1) [New-MgUser
   _CreateExpanded], RestException`1
    + FullyQualifiedErrorId : Request_BadRequest,Microsoft.Graph.PowerShell.Cmdlets.NewMgUser_CreateExpanded

One solution is to use a variable that holds a single space. Another is to pass $Null by running the equivalent Graph request using the Invoke-MgGraphRequest cmdlet. Neither are good answers to what should not happen (and we haven’t even mentioned the inability to filter on null values).

Ignoring the Pipeline

The pipeline is a fundamental building block of PowerShell. It allows objects retrieve by a cmdlet to pass to another cmdlet for processing. But despite the usefulness of the pipeline, the SDK cmdlets don’t support it and the pipeline stops stone dead whenever an SDK cmdlet is asked to process incoming objects. For example:

Get-MgUser -Filter "userType eq 'Guest'" -All | Update-MgUser -Department "Guest Accounts"
Update-MgUser : The pipeline has been stopped

Why does this happen? The cmdlet that receives objects must be able to distinguish between the different objects before it can work on them. In this instance, Get-MgUser delivers a set of guest accounts, but the Update-MgUser cmdlet does not know how to process each object because it identifies an object is through the UserId parameter whereas the inbound objects offer an identity in the Id property.

The workaround is to store the set of objects in an array and then process the objects with a ForEach loop.

Property Casing and Fetching Data

I’ve used DisplayName to refer to the display name of objects since I started to use PowerShell with Exchange Server 2007. I never had a problem with uppercasing the D and N in the property name until the Microsoft Graph PowerShell SDK came along only to find that sometimes SDK cmdlets insist on a specific form of casing for property names. Fail to comply, and you don’t get your data.

What’s irritating is that the restriction is inconsistent. For instance, both these commands work:

Get-MgGroup -Filter "DisplayName eq 'Ultra Fans'"
Get-MgGroup -Filter "displayName eq 'Ultra Fans'"

But let’s say that I want to find the group members with the Get-MgGroupMember cmdlet:

[array]$GroupMembers = Get-MgGroupMember -GroupId (Get-MgGroup -Filter "DisplayName eq 'Ultra Fans'" | Select-Object -ExpandProperty Id)

This works, but I end up with a set of identifiers pointing to individual group members. Then I remember from experience gained from building scripts to report group membership that Get-MgGroupMember (like other cmdlets dealing with membership like Get-MgAdministrationUnitMember) returns a property called AdditionalProperties holding extra information about members. So I try:

$GroupMembers.AdditionalProperties.DisplayName

Nope! But if I change the formatting to displayName, I get the member names:

$GroupMembers.AdditionalProperties.displayName
Tony Redmond
Kim Akers
James Ryan
Ben James
John C. Adams
Chris Bishop

Talk about frustrating confusion! It’s not just display names. Reference to any property in AdditionalProperties must use the same casing as used the output, like userPrincipalName and assignedLicenses.

Another example is when looking for sign-in logs. This command works because the format of the user principal name is the same way as stored in the sign-in log data:

[array]$Logs = Get-MgAuditLogSignIn -Filter "UserPrincipalName eq 'james.ryan@office365itpros.com'" -All

Uppercasing part of the user principal name causes the command to return zero hits:

[array]$Logs = Get-MgAuditLogSignIn -Filter "UserPrincipalName eq 'James.Ryan@office365itpros.com'" -All

Two SDK foibles are on show here. First, the way that cmdlets return sets of identifiers and stuff information into AdditionalProperties (something often overlooked by developers who don’t expect this to be the case). Second, the inconsistent insistence by cmdlets on exact matching for property casing.

I’m told that this is all due to the way Graph APIs work. My response is that it’s not beyond the ability of software engineering to hide complexities from end users by ironing out these kinds of issues.

GUIDs and User Principal Names

Object identification for Graph requests depends on globally unique identifiers (GUIDs). Everything has a GUID. Both Graph requests and SDK cmdlets use GUIDs to find information. But some SDK cmdlets can pass user principal names instead of GUIDs when looking for user accounts. For instance, this works:

Get-MgUser -UserId Tony.Redmond@office365itpros.com

Unless you want to include the latest sign-in activity date for the account.

Get-MgUser -UserId Tony.Redmond@office365itpros.com -Property signInActivity
Get-MgUser :
{"@odata.context":"http://reportingservice.activedirectory.windowsazure.com/$metadata#Edm.String","value":"Get By Key
only supports UserId and the key has to be a valid Guid"}

The reason is that the sign-in data comes from a different source which requires a GUID to lookup the sign-in activity for the account, so we must pass the object identifier for the account for the command to work:

Get-MgUser -UserId "eff4cd58-1bb8-4899-94de-795f656b4a18" -Property signInActivity

It’s safer to use GUIDs everywhere. Don’t depend on user principal names because a cmdlet might object – and user principal names can change.

No Fix for Problems in V2 of the Microsoft Graph PowerShell SDK

V2.0 of the Microsoft Graph PowerShell SDK is now in preview. The good news is that V2.0 delivers some nice advances. The bad news is that it does nothing to cure the weaknesses outlined here. I’ve expressed a strong opinion that Microsoft should fix the fundamental problems in the SDK before doing anything else.

I’m told that the root cause of many of the issues is the AutoRest process Microsoft uses to generate the Microsoft Graph PowerShell SDK cmdlets from Graph API metadata. It looks like we’re stuck between a rock and a hard place. We benefit enormously by having the SDK cmdlets but the process that makes the cmdlets available introduces its own issues. Let’s hope that Microsoft gets to fix (or replace) AutoRest and deliver an SDK that’s better aligned with PowerShell standards before our remaining hair falls out due to the frustration of dealing with unpredictable cmdlet behavior.


Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.

]]>
https://office365itpros.com/2023/02/13/microsoft-graph-powershell-sdk-prob/feed/ 10 59070
Reporting Exchange Online Meeting Room Usage Patterns https://office365itpros.com/2023/02/10/room-mailboxes-usage-pattern/?utm_source=rss&utm_medium=rss&utm_campaign=room-mailboxes-usage-pattern https://office365itpros.com/2023/02/10/room-mailboxes-usage-pattern/#comments Fri, 10 Feb 2023 01:00:00 +0000 https://office365itpros.com/?p=59048

Calculating Statistics for Room Mailboxes

A Practical365.com article I wrote explaining how to extract and report statistics for room mailboxes is quite popular. The script uses Microsoft Graph API requests to fetch data about events from the calendars of the meeting rooms and analyzes the data. Apparently, many people need this data for one reason or another.

As I noted last week, when you publish a PowerShell script and make it available publicly, you’re likely to get requests for enhancements. Most of the time I don’t mind people sharing their ideas with me because I like hearing what others think and the issues they grapple with. Being forced to respond to questions also encourages research to find the right answers, and that’s a good way to acquire more knowledge.

In a minority of cases, I wonder why the person making a request doesn’t simply amend the code to do what they want. It could be that they don’t feel too confident with PowerShell or don’t know how to make a change. Basic familiarity with PowerShell and the modules used with Microsoft 365 is a core competency for administrators. At least, it is if you want to automate administrative operations.

Report Daily Usage Patterns for Room Mailboxes

In any case, this week a request came in to report the most popular days for meetings. Given that we already have the data about meetings and report statistics like the total events for a room, total minutes booked, average event duration, average attendees, and so on, it’s logical to ask when is a meeting room popular.

The information recorded for each meeting has a start and end date, so finding out the day of the week that the meeting occurred on is easily done with the PowerShell Get-Date cmdlet:

$Day = (Get-Date($MeetingStart)).DayOfWeek

Storing the day of the week for each event allows the script to analyze the information when it generates the other statistics. The basic approach I took is:

  • Count the total events for each day.
  • Compute the percentage of the overall events for each day.
  • Build a very basic chart element for the day. The idea is to build a simple bar chart where the larger the bar, the higher the daily room usage is. I’ve no doubt that those with more artistic minds than mine can come up with a much nicer solution.
  • Store the information.

After processing all room mailboxes, the script generates summary information, including the daily usage pattern for all rooms (Figure 1).

Daily usage pattern for room mailboxes included with the other report statistics
Figure 1: Daily usage pattern for room mailboxes included with the other report statistics

The daily usage data is stored for each room mailbox and the script outputs the same kind of chart for the individual rooms (Figure 2).

Figure 2: Daily usage patterns for individual room mailboxes

After I published the updated script, I was asked how the script aligns the bars. That’s simple. The script inserts a tab character when creating the output. That’s another old PowerShell trick. If the tab character wasn’t there, the bar chart wouldn’t line up properly.

Download Script from GitHub – But Check Article Comments

If you have issues running the script (downloadable from GitHub), check out my article about the most common errors people encounter when running PowerShell with Graph queries. Many of these issues are debated and resolved in the comments for the original article. Remember, it’s PowerShell, so the code is there to be amended. Enjoy!


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/02/10/room-mailboxes-usage-pattern/feed/ 1 59048
Cleaning up Teams Premium Trial Licenses https://office365itpros.com/2023/02/09/remove-teams-premium-license/?utm_source=rss&utm_medium=rss&utm_campaign=remove-teams-premium-license https://office365itpros.com/2023/02/09/remove-teams-premium-license/#comments Thu, 09 Feb 2023 01:00:00 +0000 https://office365itpros.com/?p=59022

Remove Teams Premium Licenses from Azure AD User Accounts After 30-Day Trial Finishes

Microsoft makes a Teams Premium trial license to allow customers test whether the functionality available in Teams Premium is worth the $10/user/month cost. Some of the features, like meeting templates, might be less obviously worth the money. Others, like the advanced webinar functionality (like having a waitlist for webinar participants) might just be what you need. The trial allows you to try before you buy by testing all the features with up to 25 users for 30 days.

Once the 30-day period finishes, Microsoft automatically terminates the license validity and users lose access to the premium features. Even if you decide to go ahead with Teams Premium, it’s a good idea to clean up by removing the licenses from the user accounts that participated in the trial. This is easily done in the Microsoft 365 admin center by selecting the license, selecting all accounts holding the license and choosing Unassign licenses (Figure 1).

Removing the Teams Premium license from user accounts in the Microsoft 365 admin center

Remove Teams Premium licenses from Azure AD accounts
Figure 1: Removing the Teams Premium license from user accounts in the Microsoft 365 admin center

Remove Teams Premium Licenses with PowerShell

Given that we’re all learning how to manage licenses with the Microsoft Graph because of the imminent retirement of the Azure AD and MSOL modules, it’s good to know how to remove licenses. Let’s examine what’s needed to remove the Teams Premium trial licenses.

First, we must know the SKU identifier for the license. To do this, run the Get-MgSubscribedSku cmdlet and look through the set of licenses known to the tenant to find Teams Premium:

Get-MgSubscribedSku | Format-List SkuId, SkuPartNumber, ServicePlans

SkuId         : 36a0f3b3-adb5-49ea-bf66-762134cf063a

SkuPartNumber : Microsoft_Teams_Premium

ServicePlans  : {MCO_VIRTUAL_APPT, MICROSOFT_ECDN, TEAMSPRO_VIRTUALAPPT, TEAMSPRO_CUST...}

According to the Azure AD list of licenses and identifiers, the SKU identifier for Teams Premium is 989a1621-93bc-4be0-835c-fe30171d6463 rather than the 36a0f3b3-adb5-49ea-bf66-762134cf063a shown here. This is because the first value is for the paid license. The second is for the trial license. Both SKUs have the same part number and display name (which is why the license shown in Figure 1 is called Microsoft Teams Premium). It would be nice if Microsoft added a trial suffix for its trial licenses.

In any case, both SKUs include seven separate service plans. A service plan is a license for a piece of functionality that cannot be bought. Instead, it’s bundled into a product (SKU) like Teams Premium. Service plans allow administrators to selectively disable functionality enabled by a license. For instance, you could disable advanced virtual appointments without affecting the other elements in Teams Premium. Table 1 lists the service plans covered by Teams Premium.

Service plan identifierService plan nameDisplay name
85704d55-2e73-47ee-93b4-4b8ea14db92bMICROSOFT_ECDNMicrosoft Content Delivery Network
0504111f-feb8-4a3c-992a-70280f9a2869TEAMSPRO_MGMTMicrosoft Teams Premium Management
cc8c0802-a325-43df-8cba-995d0c6cb373TEAMSPRO_CUSTMicrosoft Teams Premium Branded Meetings
f8b44f54-18bb-46a3-9658-44ab58712968TEAMSPRO_PROTECTIONMicrosoft Teams Premium Advanced Meeting Protection
9104f592-f2a7-4f77-904c-ca5a5715883fTEAMSPRO_VIRTUALAPPTMicrosoft Teams Premium Virtual Appointment
711413d0-b36e-4cd4-93db-0a50a4ab7ea3MCO_VIRTUAL_APPTMicrosoft Teams Premium Virtual Appointments
78b58230-ec7e-4309-913c-93a45cc4735bTEAMSPRO_WEBINARMicrosoft Teams Premium Webinar
Table 1: Teams Premium service plans

PowerShell Code to Remove Teams Premium Licenses from Azure AD Accounts

Now that we know the SKU identifier, we can run some PowerShell to:

  • Find user accounts with the Teams Premium license. This is done using a lambda filter against the assignedLicenses property of each account.
  • Remove the license from those accounts.

Connect-MgGraph -Scope User.ReadWrite.All
Select-MgProfile Beta
# Populate identifier for target product (SKU)
$TeamsPremiumSku = "36a0f3b3-adb5-49ea-bf66-762134cf063a"
[array]$Users = Get-MgUser -filter "assignedLicenses/any(s:s/skuId eq $TeamsPremiumSku)" -All
If (!($Users)) { Write-Host "No Teams Premium Trial licenses found - exiting" ; break }
Write-Host ("Removing {0} Teams trial licenses from {1}..." -f $Users.count, ($Users.displayName -join ", "))

ForEach($User in $Users) {
  Try {
    $Status = Set-MgUserLicense -UserId $User.Id -RemoveLicenses $TeamsPremiumSku -AddLicenses @{}  }
  Catch {
    Write-Host "Error removing Teams Premium Trial license from {0}" -f $User.displayName }
}

Updated with an appropriate SKU identifier, the code will remove licenses for other Microsoft 365 products.

Remove Teams Premium Licenses to Avoid Confusion

It doesn’t matter if you leave expired licenses in place. They won’t affect how people use Microsoft 365. However, given that the paid-for and trial versions of the Teams Premium licenses have the same display name, it’s best to remove trial licenses to avoid potential future confusion.


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/02/09/remove-teams-premium-license/feed/ 1 59022
How to Purge Guest Accounts with Unredeemed Invitations from Entra ID https://office365itpros.com/2023/02/07/entra-id-guest-accounts-unredeemed/?utm_source=rss&utm_medium=rss&utm_campaign=entra-id-guest-accounts-unredeemed https://office365itpros.com/2023/02/07/entra-id-guest-accounts-unredeemed/#comments Tue, 07 Feb 2023 01:00:00 +0000 https://office365itpros.com/?p=58988

Use PowerShell to Find and Remove Entra ID Guest Accounts Who Don’t Want to Join Your Party

Updated 5 September 2023

A January 30 post by Microsoft’s Jef Kazimer about using Azure Automation with Managed Identities to remove unredeemed guests from Entra ID (Azure AD) promised to be a good read. Jef is a Principal Program Manager in the Microsoft Entra organization. Apart from using Azure Automation (something that every tenant administrator should master), highlighting the Microsoft Graph PowerShell SDK V2.0 (currently in early preview) gave me another reason to read the article.

I have expressed some concerns about Microsoft’s plans for the V2.0 of the Microsoft Graph PowerShell SDK. Leaving those concerns aside, it’s always good to learn how others approach a problem, especially as I’ve recently covered similar ground in terms of how to decide to remove guest accounts using the SDK. The differences between the two methods of reviewing guest accounts is that Jef looks for instances where guest accounts never went through the invitation redemption process to fully validate their accounts. On the other hand, my script looks at how long it’s been since a guest signed into the tenant and the number of groups the account is a member of to determine “staleness.” Let’s consider how to review guest accounts based on unredeemed invitations.

Outlining the Process

On paper, the steps involved to find and remove guest accounts with unredeemed invitations are straightforward:

  • Find guest accounts that have not redeemed the invitations received to join the tenant.
  • Remove the accounts from Entra ID.

Jef’s article suggests that this should be a regular process executed by an Azure Automation job using a managed identity to sign into the Graph and run the necessary PowerShell commands. I agree and think this is a good way to make sure to clear out unwanted guest accounts periodically.

Where I disagree is the detail of how to find the guests. Let’s discuss.

The Need for Administrative Units

Jef uses a dynamic administrative unit (currently a preview feature) to manage guest accounts. While it’s certainly convenient to create a dynamic administrative unit and assign the user management role for the administrative unit to the managed identity, this approach is optional and creates a potential requirement for Entra ID Premium P1 licenses. If your organization has those licenses, using a dynamic administrative unit offers the advantage of reducing the scope for the managed identity to process Entra ID accounts.

In some organizations, using administrative units (both the standard and dynamic variants) could be overkill because user management is a task performed by one or two administrators. In larger organizations, granularity in user management can be a desirable aspect, which is why administrative units exist.

Finding Entra ID Guest Accounts with Unredeemed Invitations

The first step is to find the target set of guest accounts. The simplest way is to run the Get-MgUser cmdlet and filter accounts to look for guests:

Connect-MgGraph -Scope Directory.ReadWrite.All
Select-MgProfile Beta
[array]$Guests = Get-MgUser -Filter "userType eq 'Guest'" -All

The guest accounts we want are those that have the ExternalUserState property set to “PendingAcceptance.” In other words, Entra ID issued an invitation to the guest’s email address, but the guest never followed up to redeem their invitation. This amended call to Get-MgUser fetches the set of guest accounts with unredeemed invitations:

[array]$Guests = Get-MgUser -Filter "userType eq 'Guest' and ExternalUserState eq 'PendingAcceptance'" -All

Jef’s version uses the Get-MsIDUnredeemedInviteUser cmdlet from the MSIdentityTools module to find guest accounts with unredeemed invitations. It’s certainly worth considering using the MSIdentityTools module to manage Entra ID, but it’s also worth understanding how to do a job with the basic tools, which is what I do here.

Determining the Age of an Unredeemed Invitation

It would be unwise to remove any Entra ID guest accounts without giving their owners a little time to respond. Taking vacation periods into account, 45 days seem sufficient time for anyone to make their minds up. The loop to remove unredeemed guest accounts needs to check how long it’s been since Entra ID issued the invitation and only process the accounts that exceed the age threshold.

Our script can check when Entra ID created an invitation by checking the ExternalUserStateChangeDateTime property, which holds a timestamp for the last time the state of the account changed. The only state change for the accounts we’re interested in occurred when Entra ID created the invitations to join the tenant, so we can use the property to measure how long it’s been since a guest received their invitation.

This code shows how to loop through the set of guests with unredeemed invitations, check if their invitation is more than 45 days old, and remove the account that satisfy the test. To keep a record of what it does, the script logs the deletions.

[datetime]$Deadline = (Get-Date).AddDays(-45)
$Report = [System.Collections.Generic.List[Object]]::new()
ForEach ($Guest in $Guests) {
  # Check Date
  [datetime]$InvitationSent = $Guest.ExternalUserStateChangeDateTime
  If ($InvitationSent -le $Deadline) {
     $DateInvitation = Get-Date($InvitationSent) -format g
     $DaysOld = (New-TimeSpan ($InvitationSent)).Days
     Try { 
        Remove-MgUser -UserId $Guest.Id
        $ReportLine = [PSCustomObject][Ordered]@{  
          Date        = Get-Date
          User        = $Guest.displayName
          UPN         = $Guest.UserPrincipalName
          Invited     = $DateInvitation
          "Days old"  = $DaysOld }
        $Report.Add($ReportLine)
      }
      Catch {
        Write-Error $_
      }
   } #End if
} #End Foreach Guest
Write-Host "Guest Accounts removed for" ($Report.User -Join ", ")

Figure 1 shows some data from the report generated for the deletions. In an Azure Automation scenario, you could create a report in SharePoint Online, send email to administrators, or post a message to a Teams channel to advise people about the removed accounts.

Old Entra ID guest accounts with unredeemed invitations
Figure 1: Old guest accounts with unredeemed invitations

Caveats Before Removing Entra ID Guest Accounts

The code works and stale guest account disappear to the Entra ID recycle bin. However, the danger exists that some of the accounts might be in active use. Take guest accounts created to represent the email addresses of Teams channels. These email addresses represent a connector to import messages into Teams channels. No one can sign into these non-existent mailboxes so no one  will ever redeem the guest invitations. However, the mail user objects created by Exchange Online for these guest accounts allow them to be included in distribution lists, added to address lists, and so on.

Another example is when a guest joins an Outlook group (a Microsoft 365 group whose membership communicates via email). Guest members of these groups do not need to redeem their invitation unless they intend to sign into the tenant to access Teams or SharePoint Online or another application that supports Azure B2B Collaboration. If you remove these guest accounts based on their invitation redemption status, some important email-based communication might fail, and that would be a bad thing.

One way around the issue is to mark Entra ID guest accounts used for these purposes by writing a value into an appropriate property. For instance, set the department to EMAIL. Here’s how to mark the set of guest accounts used to route email to Teams channels:

[array]$MailGuests = $Guests | Where-Object {$_.Mail -Like "*teams.ms*"}  
ForEach ($MG in $MailGuests) { Update-MgUser -UserId $MG.Id -Department "EMAIL" }

And here’s how to mark the guest members for an Outlook group using cmdlets from the Exchange Online management module:

[array]$Members = Get-UnifiedGroupLinks -Identity 'Exchange Grumpy Alumni' -LinkType Member
ForEach ($Member in $Members) { 
  If ($Member.RecipientType -eq "MailUser")  { Set-User -Identity $Member.Name -Department "EMAIL" -Confirm:$False }
}

After marking some guest accounts as exceptions, we can find the set of guest accounts to process with:

[array]$Guests = Get-MgUser -Filter "userType eq 'Guest'" -All | Where-Object {$_.ExternalUserState -eq "PendingAcceptance" -and $_.Department -ne "EMAIL"}

All of this goes to prove that setting out to automate what appears to be a straightforward administrative task might lead to unforeseen consequences if you don’t think through the different ways applications use the objects.

Using SDK V2.0

Coming back to using V2.0 of the Microsoft Graph PowerShell SDK, nothing done so far needs V2.0. The only mention of a V2.0-specific feature is the support for a managed identity when connecting to the Graph. The code used to connect is:

Connect-MgGraph -Identity

A one-liner is certainly convenient, but it’s possible to connect to a managed identity with the Graph SDK with code that is just a little more complicated. Here’s what I do:

Connect-AzAccount -Identity
$AccessToken = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com"
Connect-MgGraph -AccessToken $AccessToken.Token

Going from three lines to one is probably not a huge benefit!


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/2023/02/07/entra-id-guest-accounts-unredeemed/feed/ 3 58988
Microsoft Retires Azure Automation Run As Accounts in September 2023 https://office365itpros.com/2023/02/06/run-as-account-retirement/?utm_source=rss&utm_medium=rss&utm_campaign=run-as-account-retirement https://office365itpros.com/2023/02/06/run-as-account-retirement/#respond Mon, 06 Feb 2023 01:00:00 +0000 https://office365itpros.com/?p=58977

Azure Automation for IT Pros

I’ve spent a lot of time working with Azure Automation over the last few years. It’s an extremely useful facility for tenant administrators who want to run PowerShell scripts using a more modern mechanism than offered by Windows Scheduler. This is especially true so in large tenants where processing hundreds or thousands of objects is common, which is why I started to use Run As accounts with Azure Automation.

Converting scripts to run on Azure Automation isn’t too difficult, once you understand the headless nature of the beast and that PowerShell runs on virtual machines spun up for the purpose. The biggest issue often faced when moving scripts from running interactively to being an Azure Automation runbook is how to create output from scripts, but it’s possible to send email, post to Teams channels, and create files in SharePoint document libraries.

Microsoft seems to communicate with developers and administrators (aka IT Pros) in different ways. For instance, the news about the retirement of Azure Automation Run As accounts on September 30, 2023, didn’t appear in any notification in the Microsoft 365 admin center. In fact, apart from the notices posted in Azure Automation documentation (like that shown in Figure 1), I can’t find a formal announcement from Microsoft.

Microsoft notice about the retirement of Run As accounts
Figure 1: Microsoft notice about the retirement of Run As accounts

Informing the Technical Community About the Run As Retirement

The possibility exists that I might not be looking hard enough. Normally, I am reasonably proficient with search (Google), but the first hit I find is a 27 September 2022 Microsoft Answers post saying “On 30 September 2023, we’ll retire the Azure Automation Run As account that you use for Runbook authentication.” I can find an earlier “plan for change” note for July 2022 in the What’s new in Azure Automation page. Apart from that, Microsoft seems to have updated the documentation on 18 October 2022 (here’s the FAQ).

I suppose that it’s reasonable to expect people to learn about developments from documentation. In this instance, I think Microsoft dropped the ball and didn’t do a great job of telling people what’s going to happen when Run As accounts retire.

Managed Identities Are a Better Solution

The logic for retiring Run As accounts is undeniable. A better and more secure solution (managed identities) exists. Run As accounts authenticate using a self-signed certificate that needs to be renewed yearly. Microsoft has removed the ability to renew these certificates from the Azure portal, meaning that Run As accounts are counting down to a time when they won’t be able to authenticate. Microsoft has a script to renew certificates for Run As accounts and the script will run after September 30, 2023. However, Run As accounts will then be unsupported, which isn’t a great situation for production components.

The nice thing about managed identities from an Office 365 perspective is that the important PowerShell modules used for automation support managed identities. Some do so very smoothly (like the latest Exchange Online management module, where even the latest RBAC for applications feature supports managed identities) and some do it with a little extra work. For example, V1.0 of the Microsoft Graph PowerShell SDK needs to get an access token from the Azure Automation account that owns a managed identity while V2.0 will be able to sign in using a managed identity. Here’s an example of a simple runbook that:

  • Connects to the Azure Automation account using a managed identity.
  • Gets an access token from Azure AD.
  • Uses the access token to connect to the Graph with Connect-MgGraph.
  • Retrieves the service domain (like office365itpros.onmicrosoft.com) using the Get-MgOrganization cmdlet.
  • Uses the service domain and a managed identity to connect to Exchange Online.
  • Lists details of user mailboxes.
# Connect to Microsoft Graph with Azure Automation
Connect-AzAccount -Identity
$AccessToken = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com"
Connect-MgGraph -AccessToken $AccessToken.Token
# Get Tenant service domain using Get-MgOrganization
$TenantName = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.IsInitial -eq $True} | Select-Object -ExpandProperty Name
# Connect to Exchange Online
Connect-ExchangeOnline -ManagedIdentity -Organization $TenantName 
Get-ExoMailbox -RecipientTypeDetails UserMailbox | Format-Table DisplayName, UserPrincipalName

When V2.0 of the Microsoft Graph PowerShell SDK is available, you’ll be able to replace the first three lines of code with a simple Connect-MgGraph -Identity.

Another example of using a managed identity with Exchange Online is to monitor events gathered in the audit log to detect and report events that might indicate potential tenant compromise. Running the script on an Azure Automation schedule makes sure that audit events are checked without human intervention.

Time to Move Forward

Apart from the poor communication, I don’t have any problem with Microsoft’s decision to retire Run As accounts. They worked as a mechanism to connect resources to Azure Automation. We’re just moving on to adopt a new approach. Microsoft documents the migration steps to move from a Run As account to use managed identities. It’s a manual process, but not onerous.

]]>
https://office365itpros.com/2023/02/06/run-as-account-retirement/feed/ 0 58977
Tweaking the Teams and Groups Report Script https://office365itpros.com/2023/02/03/powershell-tricks-groups-report/?utm_source=rss&utm_medium=rss&utm_campaign=powershell-tricks-groups-report https://office365itpros.com/2023/02/03/powershell-tricks-groups-report/#comments Fri, 03 Feb 2023 01:00:00 +0000 https://office365itpros.com/?p=58902

PowerShell Tricks Help Get Real Work Done

As dedicated readers of this blog might have noticed, I write quite a few PowerShell scripts. Notice that I use the word “write” instead of “develop.” That’s because my time of professional development lie in the days VAX BASIC and VAX COBOL in the 1980s. Today, I write PowerShell for fun to illustrate concepts and principals. I don’t write code that is bulletproof or a thing of beauty. In fact, I represent the archetype hacker (in its original meaning).

Upgrading Scripts

All of which means that I am constantly discovering techniques when I go looking for solutions to problems. Some would say that I am always on the lookout for new PowerShell tricks, but that stretches the point.

Take the Microsoft 365 Groups and Teams report script (Figure 1). I have worked on many iterations of this script since 2016 (the latest version is 5.10). The code evolved from using Exchange Online cmdlets to interrogate group mailboxes to the current version that’s mostly based on Graph API requests and is much faster. Some people have run the report for over 20,000 groups.

HTML output of the Microsoft 365 Groups and Teams Report script

PowerShell tricks
Figure 1: HTML output of the Microsoft 365 Groups and Teams Report script

In some cases, the changes I make are driven by need. For example, the Get-ExoMailboxFolderStatistics and Get-MailboxFolderStatistics cmdlets from V3.01 of the Exchange Online management module have an annoying habit of returning system objects (arrays) for the date of last received email and the number of items in the inbox folder of group mailboxes. In some cases, the cmdlets report that a group mailbox has two dates for the last received email. This is crazy stuff because the Get-MailboxFolderStatistics cmdlet has been around since 2006 and Microsoft really shouldn’t be screwing it up at this point. The solution isn’t based on any PowerShell tricks. Instead, the script now includes code to handle the erratic behavior of Exchange Online cmdlets.

Outputting Email Addresses on Separate Lines

In other cases, I make changes in response to requests. One recent request was to include the email address of a group owner, which I did in V5.8. Then someone asked if they could have the email addresses of all group owners output in the report. Easy, I said, edit your copy of the script to get the owner display names for each group and join the names together into a string. But that didn’t work because they needed each email address to be on a separate line to import the data into Power BI.

Some search brought me to a May 2006 blog written by Jeffrey Snover, who was then the architect evangelizing PowerShell within Microsoft. Jeffrey subsequently became a Microsoft Fellow before leaving to go to Google in the Fall of 2022, just after he wrote the foreword for the 2023 edition of the Office 365 for IT Pros eBook.

Jeffrey’s blog covered what he called the “Ouptut Field Sperator,” or OFS, defined as a special variable containing the string used as a separator when PowerShell converts an array into a string. By default, OFS is a space, but you can change it. This knowledge and some searching brought me to another blog that explained how to use escape characters in the OFS. New line is an escape character, so presto, a solution appeared.

Take this example where we fetch the owners of a group and store the data in an array:

$GroupId = (Get-MgGroup -Filter "displayname eq 'Ultra Fans'").Id
[array]$GroupData = Get-MgGroupOwner -GroupId $GroupId
[array]$GroupOwners = $GroupData.AdditionalProperties.mail

The email addresses of the group owners are now in the array:

Tony.Redmond@office365itpros.com
Ben.James@Office365itpros.com
Chris.Bishop@office365itpros.com

If I convert the array to a string, the output is a line of names separated by spaces:

[string]$Owners = $GroupOwners
$Owners
Tony.Redmond@office365itpros.com Ben.James@Office365itpros.com Chris.Bishop@office365itpros.com

But if I define the special $OFS variable to be a new line character, I get this:

$OFS="`n`n"
[string]$ContactEmail = $GroupOwners
$ContactEmail
Tony.Redmond@office365itpros.com

Ben.James@Office365itpros.com

Chris.Bishop@office365itpros.com

Problem solved and the output has email addresses on separate lines. In some cases, a carriage return and new line might be better for the output. To do this, set $OFS to “`r`n” (see this post). Either way, being able to change the output space character is a nice example of the kind of PowerShell tricks and techniques that you can find on the internet.

Overcoming Export-CSV Limitations

I often use the Export-CSV cmdlet to export report data from PowerShell to a CSV file where people can open and work on the data using tools like Excel. Recently, a French MVP reported that the Teams and Groups Activity report script worked great, but the CSV output dropped the accented characters used in French (like é) from essential information like group names.

It’s not at all surprising that this should happen. CSV files are comma delimited plain-text ASCII files and by default, the cmdlet generates 7-bit ASCII output (other encoding schemes are available). If you want to more precise control over formatting and extended characters, you need something more sophisticated, which is where the ImportExcel module comes in. Exporting to an XLSX file preserves the formatting and group names appear in all their glory. The lesson learned here is that Export-CSV does a good but limited job. If you work with non-ASCII data, seek another solution.

Time to write some code and discover a few more PowerShell tricks to investigate!

]]>
https://office365itpros.com/2023/02/03/powershell-tricks-groups-report/feed/ 5 58902
Reporting Operating System Versions for Registered Devices https://office365itpros.com/2023/01/31/entra-id-registered-devices-os/?utm_source=rss&utm_medium=rss&utm_campaign=entra-id-registered-devices-os https://office365itpros.com/2023/01/31/entra-id-registered-devices-os/#comments Tue, 31 Jan 2023 01:00:00 +0000 https://office365itpros.com/?p=58916

Know What Operating System Used by Entra ID Registered Devices

After reading an article about populating extension attributes for registered devices, a reader asked me how easy it would be to create a report about the operating systems used for registered devices. Microsoft puts a lot of effort into encouraging customers to upgrade to Windows 11 and it’s a good idea to know what’s the device inventory. Of course, products like Intune have the ability to report this kind of information, but it’s more fun (and often more flexible) when you can extract the information yourself.

As it turns out, reporting the operating systems used by registered devices is very easy because the Microsoft Graph reports this information in the set of properties retrieved by the Get-MgDevice cmdlet from the Microsoft Graph PowerShell SDK.

PowerShell Script to Report Entra ID Registered Devices

The script described below creates a report of all registered devices and sorts the output by the last sign in date. Microsoft calls this property ApproximateLastSignInDateTime. As the name indicates, the property stores the approximate date for the last sign in. Entra ID doesn’t update the property every time someone uses the device to connect. I don’t have a good rule for when property updates occur. It’s enough (and approximate) that the date is somewhat accurate for the purpose of identifying if a device is in use, which is why the script sorts devices by that date.

Any Windows device that hasn’t been used to sign into Entra ID in the last six months is likely not active. This isn’t true for mobile phones because they seem to sign in once and never appear again. The report generated for my tenant still has a record for a Windows Phone which last signed in on 2 December 2015. I think I can conclude that it’s safe to remove this device from my inventory.

Figuring Out Device Owners

In the last script I wrote using the Get-MgDevice cmdlet, I figured out the owner of the device by extracting the user identifier from the PhysicalIds property. While this approach works, it’s complicated. A much better approach is to use the Get-MgDeviceRegisteredOwner cmdlet which returns the user identifier for the user account of the registered owner. With this identifier, we can retrieve any account property that makes sense, such as the display name, user principal name, department, city, and country. You could easily add other properties that make sense to your organization. See this article for more information about using the Get-MgUser cmdlet to interact with user accounts.

The Big Caveat About Operating System Information

The problem that exists in using registered devices to report operating system information is that it’s not accurate. The operating system details noted for a device are accurate at the point of registration but degrade over time. If you want to generate accurate reports, you need to use the Microsoft Graph API for Intune.

With that caveat in mind, here’s the code to report the operating system information for Entra ID registered devices:

Connect-MgGraph -Scope User.Read.All, Directory.Read.All

Write-Host "Finding registered devices"
[array]$Devices = Get-MgDevice -All -PageSize 999
If (!($Devices)) { Write-Host "No registered devices found - exiting" ; break }
Write-Host ("Processing details for {0} devices" -f $Devices.count)
$Report = [System.Collections.Generic.List[Object]]::new() 
$i = 0
ForEach ($Device in $Devices) {
  $i++
  Write-Host ("Reporting device {0} ({1}/{2})" -f $Device.DisplayName, $i, $Devices.count)
  $DeviceOwner = $Null
  Try {
    [array]$OwnerIds = Get-MgDeviceRegisteredOwner -DeviceId $Device.Id
    $DeviceOwner = Get-MgUser -UserId $OwnerIds[0].Id -Property Id, displayName, Department, OfficeLocation, City, Country, UserPrincipalName} 
 } Catch {}

  $ReportLine = [PSCustomObject][Ordered]@{
   Device             = $Device.DisplayName
   Id                 = $Device.Id
   LastSignIn         = $Device.ApproximateLastSignInDateTime
   Owner              = $DeviceOwner.DisplayName
   OwnerUPN           = $DeviceOwner.UserPrincipalName
   Department         = $DeviceOwner.Department
   Office             = $DeviceOwner.OfficeLocation
   City               = $DeviceOwner.City
   Country            = $DeviceOwner.Country
   "Operating System" = $Device.OperatingSystem
   "O/S Version"      = $Device.OperatingSystemVersion
   Registered         = $Device.RegistrationDateTime
   "Account Enabled"  = $Device.AccountEnabled
   DeviceId           = $Device.DeviceId
   TrustType          = $Device.TrustType }
  $Report.Add($ReportLine)

} #End Foreach Device

# Sort in order of last signed in date
$Report = $Report | Sort-Object {$_.LastSignIn -as [datetime]} -Descending

$Report | Out-GridView

Figure 1 is an example of the report as viewed through the Out-GridView cmdlet.

Reporting operating system information for Entra ID registered devices
Figure 1: Reporting operating system information for Entra ID registered devices

You can download the latest version of the script from GitHub.

An Incomplete Help

I’ve no idea whether this script will help anyone. It’s an incomplete answer to a question. However, even an incomplete answer can be useful in the right circumstances. After all, it’s just PowerShell, so use the code as you like.


Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.

]]>
https://office365itpros.com/2023/01/31/entra-id-registered-devices-os/feed/ 1 58916
Fetching Group Membership Information for an Entra ID User Account https://office365itpros.com/2023/01/30/group-membership-with-the-graph/?utm_source=rss&utm_medium=rss&utm_campaign=group-membership-with-the-graph https://office365itpros.com/2023/01/30/group-membership-with-the-graph/#comments Mon, 30 Jan 2023 01:00:00 +0000 https://office365itpros.com/?p=58839

Discover Group Membership with the Graph SDK

Group membership with the Graph SDK

I’ve updated some scripts recently to remove dependencies on the Azure AD and Microsoft Online Services (MSOL) modules, which are due for deprecation on June 30, 2023 (retirement happens at the end of March for the license management cmdlets). In most cases, the natural replacement is cmdlets from the Microsoft Graph PowerShell SDK.

One example is when retrieving the groups a user account belongs to. This is an easy task when dealing with the membership of individual groups using cmdlets like:

  • Get-DistributionGroupMember (fetch distribution list members).
  • Get-DynamicDistributionGroupMember (fetch dynamic distribution group members).
  • Get-UnifiedGroupLinks (fetch members of a Microsoft 365 group).
  • Get-MgGroupMember (fetch members of an Entra ID group).

Things are a little more complex when answering a question like “find all the groups that Sean Landy belongs to.” Let’s see how we can answer the request.

The Exchange Online Approach

One method of attacking the problem often found in Exchange scripts is to use the Get-Recipient cmdlet with a filter based on the distinguished name of the mailbox belonging to an account: For example, this code reports a user’s membership of Microsoft 365 groups:

$User = Get-EXOMailbox -Identity Sean.Landy
$DN = $User.DistinguishedName
$Groups = (Get-Recipient -ResultSize Unlimited -RecipientTypeDetails GroupMailbox -Filter "Members -eq '$DN'" )
Write-Host (“User is a member of {0} groups” -f $Groups.count)

The method works if the distinguished name doesn’t include special characters like apostrophes for users with names like Linda O’Shea. In these cases, extra escaping is required to make PowerShell handle the name correctly. This problem will reduce when Microsoft switches the naming mechanism for Exchange Online objects to be based on the object identifier instead of mailbox display name. However, there’s still many objects out there with distinguished names based on display names.

The Graph API Request

As I go through scripts, I check if I can remove cmdlets from other modules to make future maintenance easier. Using Get-Recipient means that a script must connect to the Exchange Online management module, so let’s remove that need by using a Graph API request. Here’s what we can do, using the Invoke-MgGraphRequest cmdlet to run the request:

$UserId = $User.ExternalDirectoryObjectId
$Uri = ("https://graph.microsoft.com/V1.0/users/{0}/memberOf/microsoft.graph.group?`$filter=groupTypes/any(a:a eq 'unified')&`$top=200&$`orderby=displayName&`$count=true" -f $UserId)
[array]$Data = Invoke-MgGraphRequest -Uri $Uri
[array]$Groups = $Data.Value
Write-Host (“User is a member of {0} groups” -f $Groups.count) 

We get the same result (always good) and the Graph request runs about twice as fast as Get-Recipient does.

Because the call is limited to Microsoft 365 groups, I don’t have to worry about transitive membership. If I did, then I’d use the group transitive memberOf API.

Using the SDK Get-MgUserMemberOf Cmdlet

The Microsoft Graph PowerShell SDK contains cmdlets based on Graph requests. The equivalent cmdlet is Get-MgUserMemberOf. This returns memberships of all group types known to Entra ID, so it includes distribution lists and security groups. To return the set of Microsoft 365 groups, apply a filter after retrieving the group information from the Graph.

[array]$Groups = Get-MgUserMemberOf -UserId $UserId -All | Where-Object {$_.AdditionalProperties["groupTypes"] -eq "Unified"}
Write-Host (“User is a member of {0} groups” -f $Groups.count) 

Notice that the filter looks for a specific type of group in a value in the AdditionalProperties property of each group. If you run Get-MgUserMemberOf without any other processing. the cmdlet appears to return a simple list of group identifiers. For example:

$Groups

Id                                   DeletedDateTime
--                                   ---------------
b62b4985-bcc3-42a6-98b6-8205279a0383
64d314bb-ea0c-46de-9044-ae8a61612a6a
87b6079d-ddd4-496f-bff6-28c8d02e9f8e
82ae842d-61a6-4776-b60d-e131e2d5749c

However, the AdditionalProperties property is also available for each group. This property contains a hash table holding other group properties that can be interrogated. For instance, here’s how to find out whether the group supports private or public access:

$Groups[0].AdditionalProperties['visibility']
Private

When looking up a property in the hash table, remember to use the exact form of the key. For instance, this works to find the display name of a group:

$Groups[0].AdditionalProperties['displayName']

But this doesn’t because the uppercase D creates a value not found in the hash table:

$Groups[0].AdditionalProperties['DisplayName']

People starting with the Microsoft Graph PowerShell SDK are often confused when they see just the group identifiers apparently returned by cmdlets like Get-MgUserMemberOf, Get-MgGroup, and Get-MgGroupMember because they don’t see or grasp the importance of the AdditionalProperties property. It literally contains the additional properties for the group excepting the group identifier.

Here’s another example of using information from AdditionalProperties. The details provided for a group don’t include its owners. To fetch the owner information for a group, run the Get-MgGroupOwner cmdlet like this:

$Group = $Groups[15]
[array]$Owners = Get-MgGroupOwner -GroupId $Group.Id | Select-Object -ExpandProperty AdditionalProperties
$OwnersOutput = $Owners.displayName -join ", "
Write-Host (“The owners of the {0} group are {1}” -f $Group.AdditionalProperties[‘displayName’], $OwnersOutput)

If necessary, use the Get-MgGroupTransitiveMember cmdlet to fetch transitive memberships of groups.

The Graph SDK Should be More Intelligent

It would be nice if the Microsoft Graph PowerShell SDK didn’t hide so much valuable information in AdditionalProperties and wasn’t quite so picky about the exact format of property names. Apparently, the SDK cmdlets behave in this manner because it’s how Graph API requests work when they return sets of objects. That assertion might well be true, but it would be nice if the SDK applied some extra intelligence in the way it handles data.


Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.

]]>
https://office365itpros.com/2023/01/30/group-membership-with-the-graph/feed/ 4 58839
Upgrading the Microsoft 365 Groups and Teams Membership Report Script https://office365itpros.com/2023/01/19/microsoft-365-groups-report-v2/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-365-groups-report-v2 https://office365itpros.com/2023/01/19/microsoft-365-groups-report-v2/#comments Thu, 19 Jan 2023 01:00:00 +0000 https://office365itpros.com/?p=58775

Moving the Microsoft 365 Groups Report Script from Azure AD to the Graph SDK

Two years ago, I wrote a script to report the membership of Microsoft 365 groups and teams. The script processes user accounts to find accounts they are members of and generates detailed and summary reports.

As it turned out, I ended up writing two versions of the script: one using standard PowerShell cmdlets from the Exchange Online PowerShell and Azure AD modules, the other using Graph API requests. The Graph version is faster but some people don’t like Graph-based scripts because of the requirement to register an Azure AD app, consent to permissions, and so on.

Time and technology march on and it’s time to review any script that uses the Azure AD module because of its imminent deprecation in June 2023. Imminent sounds like a strange word to use about something that will happen in five and a half months but time slips away and there’s always something different to be done. I had the time and was already committed to upgrading the script to report “stale” guest accounts, so it seemed like a good idea to plunge into the code and replace the Azure AD and Exchange Online cmdlets with the Microsoft Graph PowerShell SDK.

Scripts to Process Azure AD Accounts and Groups

I’ve come to the view that it’s now best to use the SDK for anything to do with Azure AD accounts and groups. Because the Exchange Online management module contains cmdlets that operate against Microsoft 365 groups, I could have used those cmdlets in the script, but it’s easier when a script uses just the one module.

The two versions of the scripts are available from GitHub:

Changes to Upgrade to the SDK

Among the changes made to upgrade the script were:

  • Connect to the Graph with Connect-MgGraph, setting appropriate permissions and selecting the beta endpoint.
  • Replace the Exchange Get-Organization cmdlet with SDK Get-MgOrganization to fetch tenant name.
  • Replace Get-AzureADUser with Get-MgUser. The filter used with Get-MgUser fetches only licensed accounts (excludes guests and accounts used for room and resource mailboxes). Replacing Get-AzureADUser is one of the more common changes that people will make as they upgrade scripts. See this article for more information.
  • Replace Get-UnifiedGroup with Get-MgTeam to fetch a list of team-enabled groups.
  • Replace Get-Recipient with the Graph MemberOf API to find the set of groups a user is a member of. The Invoke-MgGraphRequest cmdlet runs the Graph query to remove the need to register an app.
  • Use Get-MgGroupOwner to return group owners instead of fetching this information from the ManagedBy property available with Get-UnifiedGroup.
  • Other miscellaneous changes of the type that you find you make when reviewing code.

The code generates the same reports as before (HTML report  – Figure 1 – and two CSV files). All the change is in the plumbing. Nothing is different above water.

HTML version of the Microsoft 365 Groups and Teams report

Microsoft 365 Groups Report
Figure 1: HTML version of the Microsoft 365 Groups and Teams report

Unpredictable Upgrade Effort

It’s hard to estimate how long it will take to upgrade a script to use the Microsoft Graph PowerShell SDK. Factors include:

  • The number of lines of code in the script.
  • The number of Azure AD cmdlets to replace.
  • How easy it is to replace a cmdlet. Microsoft publishes a cmdlet map to guide developers. The caveat is that sometimes the suggested SDK cmdlet generates different output to its Azure AD counterpart, meaning that some additional processing is necessary. Dealing with group members and owners are examples where changes are likely.

One thing’s for sure. The sooner an organization starts to inventory and upgrade its scripts, the sooner the work will be done and the less likely the effort will run into a time crunch when Microsoft deprecates the Azure AD and MSOL modules. Deprecation doesn’t mean that cmdlets necessarily stop working (some will, like the license management cmdlets). Support ceases and no further development of the deprecated modules happen, and that’s not a state you want for operational scripts. Time’s ebbing away…


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 changes to PowerShell modules mean for your tenant.

]]>
https://office365itpros.com/2023/01/19/microsoft-365-groups-report-v2/feed/ 1 58775
Reporting Group Membership for Azure AD Guest Accounts with the Microsoft Graph PowerShell SDK https://office365itpros.com/2023/01/18/old-azure-ad-guest-accounts/?utm_source=rss&utm_medium=rss&utm_campaign=old-azure-ad-guest-accounts https://office365itpros.com/2023/01/18/old-azure-ad-guest-accounts/#comments Wed, 18 Jan 2023 01:00:00 +0000 https://office365itpros.com/?p=58742

Finding Azure AD Guest Accounts in Microsoft 365 Groups

The article explaining how to report old guest accounts and their membership of Microsoft 365 Groups (and teams) in a tenant is very popular and many people use its accompanying script. The idea is to find guest accounts above a certain age (365 days – configurable in the script) and report the groups these guests are members of. Any old guest accounts that aren’t in any groups are candidates for removal.

The script uses an old technique featuring the distinguished name of guest accounts to scan for group memberships using the Get-Recipient cmdlet. The approach works, but the variation of values that can exist in distinguished names due to the inclusion of characters like apostrophes and vertical lines means that some special processing is needed to make sure that lookups work. Achieving consistency in distinguished names might be one of the reasons for Microsoft’s plan to make Exchange Online mailbox identification more effective.

In any case, time moves on and code degrades. I wanted to investigate how to use the Microsoft Graph PowerShell SDK to replace Get-Recipient. The script already uses the SDK to find Azure AD guest accounts with the Get-MgUser cmdlet.

The Graph Foundation

Graph APIs provide the foundation for all SDK cmdlets. Graph APIs provide the foundation for all SDK cmdlets. The first thing to find is an appropriate API to find group membership. I started off with getMemberGroups. The PowerShell example for the API suggests that the Get-MgDirectoryObjectMemberGroup cmdlet is the one to use. For example:

$UserId = (Get-MgUser -UserId Terry.Hegarty@Office365itpros.com).id 
[array]$Groups = Get-MgDirectoryObjectMemberGroup  -DirectoryObjectId $UserId -SecurityEnabledOnly:$False

The cmdlet works and returns a list of group identifiers that can be used to retrieve information about the groups that the user belongs to. For example:

Get-MgGroup -GroupId $Groups[0] | Format-Table DisplayName, Id, GroupTypes

DisplayName                     Id                                   GroupTypes
-----------                     --                                   ----------
All Tenant Member User Accounts 05ecf033-b39a-422c-8d30-0605965e29da {DynamicMembership, Unified}

However, because Get-MgDirectoryObjectMemberGroup returns a simple list of group identifiers, the developer must do extra work to call Get-MgGroup for each group to retrieve group properties. Not only is this extra work, calling Get-MgGroup repeatedly becomes very inefficient as the number of guests and their membership in groups increase.

Looking Behind the Scenes with Graph X-Ray

The Azure AD admin center (and the Entra admin center) both list the groups that user accounts (tenant and guests) belong to. Performance is snappy and it seemed unlikely that the code used was making multiple calls to retrieve the properties for each group. Many of the sections in these admin centers use Graph API requests to fetch information, and the Graph X-Ray tool reveals those requests. Looking at the output, it’s interesting to see that the admin center uses the beta Graph endpoint with the groups memberOf API (Figure 1).

Using the Graph X-Ray tool to find the Graph API for group membership

Azure AD Guest Accounts
Figure 1: Using the Graph X-Ray tool to find the Graph API for group membership

We can reuse the call used by the Azure AD center to create the query (containing the object identifier for the user account) and run the query using the SDK Invoke-MgGraphRequest cmdlet. One change made to the command is to include a filter to select only Microsoft 365 groups. If you omit the filter, the Graph returns all the groups a user belongs to, including security groups and distribution lists. The group information is in an array that’s in the Value property returned by the Graph request. For convenience, we put the data into a separate array.

$Uri = ("https://graph.microsoft.com/beta/users/{0}/memberOf/microsoft.graph.group?`$filter=groupTypes/any(a:a eq 'unified')&`$top=200&$`orderby=displayName&`$count=true" -f $Guest.Id)
[array]$Data = Invoke-MgGraphRequest -Uri $Uri
[array]$GuestGroups = $Data.Value

Using the Get-MgUserMemberOf Cmdlet

The equivalent SDK cmdlet is Get-MgUserMemberOf. To return the set of groups an account belongs to, the command is:

[array]$Data = Get-MgUserMemberOf -UserId $Guest.Id -All
[array]$GuestGroups = $Data.AdditionalProperties

The format of returned data marks a big difference between the SDK cmdlet and the Graph API request. The cmdlet returns group information in a hash table in the AdditionalProperties array while the Graph API request returns a simple array called Value. To retrieve group properties from the hash table, we must enumerate through its values. For instance, to return the names of the Microsoft 365 groups in the hash table, we do something like this:

[Array]$GroupNames = $Null
ForEach ($Item in $GuestGroups.GetEnumerator() ) {
   If ($Item.groupTypes -eq "unified") { $GroupNames+= $Item.displayName }
}
$GroupNames= $GroupNames -join ", "

SDK cmdlets can be inconsistent in how they return data. It’s just one of the charms of working with cmdlets that are automatically generated from code. Hopefully, Microsoft will do a better job of ironing out inconsistencies when they release V2.0 of the SDK sometime later in 2023.

A Get-MgUserTransitiveMemberOf cmdlet is also available to return the membership of nested groups. We don’t need to do this because we’re only interested in Microsoft 365 groups, which don’t support nesting. The cmdlet works in much the same way:

[array]$TransitiveData = Get-MgUserTransitiveMemberOf -UserId Kim.Akers@office365itpros.com -All

The Script Based on the SDK

Because of the extra complexity in accessing group properties, I decided to use a modified version of the Graph API request from the Azure AD admin center. It’s executed using the Invoke-MgGraphRequest cmdlet, so I think the decision is justified.

When revising the script, I made some other improvements, including adding a basic assessment of whether a guest account is stale or very stale. The assessment is intended to highlight if I should consider removing these accounts because they’re obviously not being used. Figure 2 shows the output of the report.

Report highlighting potentially obsolete guest accounts
Figure 2: Report highlighting potentially obsolete Azure AD guest accounts

You can download a copy of the script from GitHub.

Cleaning up Obsolete Azure AD Guest Accounts

Reporting obsolete Azure AD guest accounts is nice. Cleaning up old junk from Azure AD is even better. The script generates a PowerShell list with details of all guests over a certain age and the groups they belong to. To generate a list of the very stale guest accounts, filter the list:

[array]$DeleteAccounts = $Report | Where-Object {$_.StaleNess -eq "Very Stale"}

To complete the job and remove the obsolete guest accounts, a simple loop to call Remove-MgUser to process each account:

ForEach ($Account in $DeleteAccounts) {
   Write-Host ("Removing guest account for {0} with UPN {1}" -f $Account.Name, $Account.UPN) 
   Remove-MgUser -UserId $Account.Id }

Obsolete or stale guest accounts are not harmful, but their presence slows down processing like PowerShell scripts. For that reason, it’s a good idea to clean out unwanted guests periodically.


Learn about mastering the Microsoft Graph PowerShell SDK and the Microsoft 365 PowerShell modules by subscribing to the Office 365 for IT Pros eBook. Use our experience to understand what’s important and how best to protect your tenant.

]]>
https://office365itpros.com/2023/01/18/old-azure-ad-guest-accounts/feed/ 2 58742
How to Enable Exchange Online Mailbox Archives Based on Mailbox Size https://office365itpros.com/2023/01/03/archive-mailboxes-based-size/?utm_source=rss&utm_medium=rss&utm_campaign=archive-mailboxes-based-size https://office365itpros.com/2023/01/03/archive-mailboxes-based-size/#comments Tue, 03 Jan 2023 01:00:00 +0000 https://office365itpros.com/?p=58373

Automatically Enable Archive Mailboxes Once the Primary Mailbox Exceeds a Threshold

A question following my article about how to transition from Exchange Online mailbox retention policies to Microsoft Purview retention policies asked:

Is there a way in legacy or M365 online archiving policies , that it can be enabled based on primary mailbox data size ,say for example mailbox size crosses 40 gb , it’s online archive gets enabled automatically and older data gets move to online archive to keep primary mailbox at 40 gb limit.”

It’s a reasonable request. Essentially, the organization wants users to keep all email in their primary mailboxes until the mailboxes get to 40GB. Once that point is reached, the organization wants to enable archives for those mailboxes and start to move old email from the primary mailboxes to the archives to keep the size of the primary under 40 GB.

Archive Mailboxes and Sizing

These are proper archive mailboxes and not Outlook’s archive folder. Real archive mailboxes can grow to up to 1.5 TB using the Exchange Online auto-expanding mechanism. Note: if you enable auto-expanding archives, you cannot move those archive mailboxes back to an on-premises Exchange server.

Exchange Online enterprise mailboxes have quotas of between 50 GB and 100 GB based on the license assigned to the account, so the 40 GB threshold is a tad arbitrary. It might be that keeping under this size assures reasonable performance for the OST file. If so, that’s a good thing because you don’t want the OST to become so large that it impacts PC performance.

Assigning Archives Based on Mailbox Size

The outline of the solution is:

  1. Find mailboxes that are not archive-enabled.
  2. Check the mailbox size.
  3. If the mailbox size exceeds the threshold, enable the archive mailbox, and assign an Exchange Online mailbox retention policy to instruct the Mailbox Folder Assistant to move items from the primary to the archive mailbox after they reach a certain age.

Exchange Online mailbox retention policies are the only way to move items into an archive mailbox. Microsoft Purview retention policies can keep or remove items, but they cannot move mailbox items.

To prepare, I created an Exchange Online mailbox retention policy with a single default move to archive tag (Figure 1). The policy can contain other retention tags to handle processing of default folders like the Inbox and Sent Items, or to allow users to mark items for retention. However, all that we need is the default move to archive tag. In this instance, the tag instructs the MFA to move items from the primary to the archive mailbox once they reach 730 days (2 years) old.

Configuring an Exchange Online mailbox retention policy to move items into archive mailboxes
Figure 1: Configuring an Exchange Online mailbox retention policy to move items into archive mailboxes

Now we need some PowerShell to check for and process mailboxes. Here’s the script that I came up with:

# Define archive threshold
$ArchiveThreshold = 40GB

# Find mailboxes without an archive
Write-Host "Looking for mailboxes that are not archive-enabled..."
[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox -Filter {ArchiveState -ne "Local"} -ResultSize Unlimited
If (!($Mbx)) { Write-Host "No mailboxes found without archives - exiting!" ; break }

Write-Host ("Checking {0} mailboxes" -f $Mbx.count); $MbxUpdated = 0
ForEach ($M in $Mbx) {
   
   $Stats = Get-ExoMailboxstatistics -Identity $M.ExternalDirectoryObjectId
   If ($Stats.TotalItemSize.Value -gt $ArchiveThreshold) { # Mailbox size is larger than the threshold
      Write-Host ("Enabling archive for mailbox {0}..." -f $M.UserPrincipalName)
      Enable-Mailbox -Archive -Identity $M.ExternalDirectoryObjectId
      Set-Mailbox -Identity $M.ExternalDirectoryObjectId -RetentionPolicy "Mailbox Two-Year Archive Policy"
      $MbxUpdated++
   } #End if
} #End ForEach Mbx

Write-Host ("All done. {0} mailboxes were processed and {1} were archive-enabled" -f $Mbx.Count, $MbxUpdated)

Wrapping Things Up

To complete the solution, we should arrange for the script to be run periodically to be sure that mailboxes receive archives once they exceed the threshold. The scheduler in Azure Automation is a great way to run scripts like this and the cost to execute scripts is very reasonable. V3.0 of the Exchange Online management module introduced support for Azure Automation managed identities so there’s no danger of compromise due to leaked credentials. Which is exactly how it should be.


Learn about exploiting Exchange Online and the rest of Office 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/2023/01/03/archive-mailboxes-based-size/feed/ 9 58373
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
Exchange Online to Stop Support for Remote PowerShell Connections in September 2023 https://office365itpros.com/2022/12/19/remote-powershell-deprecation/?utm_source=rss&utm_medium=rss&utm_campaign=remote-powershell-deprecation https://office365itpros.com/2022/12/19/remote-powershell-deprecation/#comments Mon, 19 Dec 2022 01:00:00 +0000 https://office365itpros.com/?p=58416

Part of the Effort to Move Exchange Online to Modern Authentication

Updated March 27, 2023

Microsoft’s December 15 announcement of the deprecation of Remote PowerShell (RPS) for Exchange Online was predictable but regrettable. Not that I want to keep RPS. Microsoft built RPS to allow administrators to manage Exchange 2010 on-premises servers from local workstations. But time moves on and RPS started down the slippery slope to oblivion when Microsoft began to modernize Exchange Online PowerShell with the introduction of the REST-based cmdlets in 2019. That process came to a head with the launch of V3.0 of the Exchange Online management module in September 2022.

Update: Microsoft issued message center notification MC488586 (20 Dec 2022) for this change.

Update 2: Microsoft has stretched things out to allow customers some extra time to prepare for the change. Remote PowerShell will work in tenants where it’s used today until the end of September, 2023. After that, no more Remote PowerShell. An opt-out tool is available for tenants to request the extra time.

Heading to the V3 Module

What’s happening is part of a phased approach to force Exchange Online tenants to use the V3 module.

  • Usage of the V1 module will cease when Microsoft finally blocks basic authentication for connectivity protocols on January 1, 2023. This is a good thing because all clients, including PowerShell, should use modern authentication.
  • Usage of the V2 module (the version that originally launched the REST cmdlets) will stop with the deprecation of this module on July 1, 2023. Although the V2 module supports modern authentication, many of its cmdlets are not modernized and therefore still have some dependencies on components like basic authentication via WinRM.
  • Microsoft will stop all RPS connections from October 1, 2023. This means that any script that connects to Exchange Online using the New-PSSession cmdlet or by specifying the –UseRPSSession parameter with the Connect-ExchangeOnline cmdlet will fail and you’ll see errors like that shown in Figure 1.

A remote PowerShell session fails to connect
Figure 1: A remote PowerShell session fails to connect

With the Exchange Online management V3 module available for over two months and a deprecation date set six months away (June 30, 2023), why would anyone be upset that Microsoft has chosen to proceed to retire RPS?

Easy Change to Remove Remote PowerShell

Making the change to modern authentication without Remote PowerShell for Exchange Online is easy. First, make sure that all workstations run V3 of the Exchange Online management module. If you use Azure Automation to run Exchange Online scripts, make sure to update the Azure accounts with the Exchange Online V3 module. I use script to periodically check and update modules on local workstations and Azure Automation.

Next, find all the scripts that connect to Exchange Online and look for instances of:

New-PSSession -ConfigurationName Microsoft-Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid

Editorial note: There are many internet articles that inform readers that this is the way to connect to Exchange Online PowerShell. Many of the blogs are quite old, but I found some published in 2022 (here’s an example).

Other scripts might use the Connect-ExchangeOnline cmdlet with the –UseRPSSession parameter. I think these scripts will be less common. My concern is with old scripts that no one has looked at in a while.

Once you find the scripts, you can modify their code to use Connect-ExchangeOnline. Be sure to test the scripts afterward. Apart from the connection, no changes are necessary to cmdlets.

The compliances cmdlets contained in the Exchange Online management module continue to have a dependency on remote PowerShell. Microsoft plans to remove that dependency in the future but hasn’t provided a firm date for the change.

The Azure AD Conundrum

Microsoft wants to eliminate RPS by the end of June 2023, which is the same deadline chosen for the deprecation of the Azure AD and Microsoft Online Services (MSOL) PowerShell modules (license management cmdlets stop working after March 31, 2023). The deprecation of these modules has been delayed multiple times, but as the date approaches tenant administrators know that they must upgrade scripts to use cmdlets from the Microsoft Graph PowerShell SDK or Graph API requests. No automatic tool is available to upgrade scripts. It’s a manual process to review code, decide what SDK cmdlet might be an appropriate alternative, make the change, and then test. This is time consuming work.

For the Exchange development group to choose the same date to deprecate RPS shows an unfortunate and unhappy lack of awareness of what’s happening in the Microsoft 365 ecosystem. It’s possible that an assumption exists that different developers deal with Azure AD and Exchange Online. That assumption might be correct on-premises where the lines between Active Directory and Exchange Server are more distinct. Inside Office 365, the need for close interconnection between Azure AD and Exchange Online is obvious. Even Microsoft acknowledged this when they introduced the dual-write mechanism to update Azure AD and the Exchange Online directory some years ago.

Overall, it would be better if Microsoft pushed the date out a little to give tenant administrators and developers time to finish the Azure AD transition before needing to deal with RPS.

New Year Might Bring Relief

No doubt the Exchange developers will let us know more details about the strategy they’re pursuing to eliminate RPS over time. For now, it seems like we’re heading for an unfortunate and avoidable clash of PowerShell update exercises. That’s bad news. Let’s hope that something changes to ease the problem in 2023.


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/12/19/remote-powershell-deprecation/feed/ 2 58416
Checking the Release of Quarantined Messages https://office365itpros.com/2022/12/08/quarantined-message-report/?utm_source=rss&utm_medium=rss&utm_campaign=quarantined-message-report https://office365itpros.com/2022/12/08/quarantined-message-report/#comments Thu, 08 Dec 2022 01:00:00 +0000 https://office365itpros.com/?p=58275

Report Who Released a Quarantined Message

The question was asked on Twitter about whether it is possible to notify end users when administrators release outbound messages from the quarantine. Most of the time, email ends up in quarantine when Exchange Online Protection decides that inbound messages contain spam or malware, but it’s possible to direct outbound email to quarantine using mail flow rules or actions invoked by Microsoft Purview DLP policies. Exchange Online can certainly quarantine problematic messages but as far as end users are concerned, outbound messages intercepted in this way go into a black hole.

Some good suggestions resulted. My initial response was to use the Get-QuarantineMessage cmdlet to periodically check messages in quarantine and detect released items on that basis. Michel de Rooij came up with a better solution to use a mail flow rule to look for the X-MS-TrafficTypeDiagnostic or X-MS-Exchange-Generated-Message-Source email headers to see if they were related to quarantine releases. That’s quite an innovative approach. However, in both cases, the problem exists that you don’t have all the information about a quarantined message following its release.

Check the Audit Log

Which brings me to the unified audit log. Exchange Online generates audit events for most operations, including when an administrator releases a message from quarantine. Administrators can search the unified audit log by running the Search-UnifiedAuditLog cmdlet to look for QuarantineReleaseMessage events. For example:

$StartDate = (Get-Date).AddDays(-90)
$EndDate = (Get-Date).AddDays(1)

[array]$Records = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Formatted -ResultSize 1000 -Operations QuarantineReleaseMessage
If (!($Records)) { Write-Host "No audit records found for quarantine release - exiting" ; break }

This search finds all events logged over the last 90 days when someone released a message from quarantine. The problem is that the information captured in audit log records tells us who released a message but doesn’t tell us anything about the message. For instance, the audit record doesn’t capture the direction of the message (inbound or outbound), the sender, its recipients, and the message subject.

That information is available in the data recorded for quarantined messages. It is therefore possible to capture information about quarantined messages periodically and store the data in a repository that can be checked to retrieve message details. To prove the point, I created a PowerShell list and populated it with details of quarantined messages. Here’s the code I used:

[array]$QM = Get-QuarantineMessage
$QMData = [System.Collections.Generic.List[Object]]::new() 
ForEach ($Item in $QM) {
  $DataLine = [PSCustomObject] @{
   Received     = $Item.ReceivedTime
   MessageId    = $Item.MessageId
   Direction    = $Item.Direction
   Sender       = $Item.SenderAddress
   Recipients   = $Item.RecipientAddress -Join ", "
   Subject      = $Item.Subject
   Type         = $Item.Type
   Expires      = $Item.Expires
   Identity     = $Item.Identity 
   Id           = $Item.Identity.Split("\")[0]}

  $QMData.Add($DataLine)
} # End ForEach Item

Creating a Composite View of Quarantine Message Release

Now that we have data for quarantined messages, let’s use it to create the information needed to communicate with users. This code creates another PowerShell list and then loops through the audit records retrieved earlier. The code checks each audit record against the data for quarantined messages to see if a match exists. If it does, we grab the information about the message and combine it with the information from the audit record to generate a composite view about the release from quarantine.

$QMInfo = [System.Collections.Generic.List[Object]]::new() 
ForEach ($Rec in $Records) {
  $AuditData = $Rec.AuditData | ConvertFrom-Json
  [array]$QMFound = $QMData | Where-Object {$_.Id -eq $AuditData.NetworkMessageId}
  If ($QMFound) {
     ForEach ($Item in $QMFound) {
       $DataLine = [PSCustomObject] @{
         MessageId  = $AuditData.NetworkMessageId
         Received   = $Item.Received
         Sender     = $Item.Sender
         Recipients = $Item.Recipients
         Subject    = $Item.Subject
         Type       = $Item.Type
         Expires    = $Item.Expires
         Releasedby = $AuditData.UserId
         ReleasedAt = $Rec.CreationDate }
       $QMInfo.Add($DataLine)
     } # End ForEach $QMFound
    } # End If
} # End ForEach $Records

Figure 1 shows examples of the composite records generated by the code.

Audit data for messages released from quarantine
Figure 1: Audit data for messages released from quarantine

After generating the composite data, it’s then a matter of deciding how to notify end users.

A Directional Oddity

One oddity I noticed is that PowerShell reported a quarantined message as “Outbound” (going out of the tenant) while the Microsoft 365 Defender admin center was certain that the message was “Inbound” (coming into the tenant). Figure 1 shows what Defender reports.

Details of a quarantined message shown by the Microsoft 365 Defender portal
Figure 2: Details of a quarantined message shown by the Microsoft 365 Defender portal

And here’s what Get-QuarantineMessage reported. The other message properties indicate that the message is definitely inbound, so I have no idea why PowerShell thinks otherwise.

Identity                   : 2a008698-201e-497f-3dee-08dad2e835e2\7129d58d-ca5e-7e32-a4f8-676d082ba9af
ReceivedTime               : 30/11/2022 15:33:20
Organization               : a662313f-14fc-43a2-9a7a-d2e27f4f3478
MessageId                  : <PA4PR06MB7264B28C1D73C9EB547DDC5AB8159@PA4PR06MB7264.eurprd06.prod.outlook.com>
SenderAddress              : missf0rtune@hotmail.co.uk
RecipientAddress           : {tony.redmond@xxx.com}
Subject                    : Document 49KB (tony.redmond@xxx.com)
Size                       : 93651
Type                       : High Confidence Phish
PolicyType                 : HostedContentFilterPolicy
PolicyName                 : Default
TagName                    : AdminOnlyAccessPolicy
PermissionToBlockSender    : True
PermissionToDelete         : True
PermissionToPreview        : True
PermissionToRelease        : True
PermissionToRequestRelease : False
PermissionToViewHeader     : False
PermissionToDownload       : True
Released                   : False
ReleaseStatus              : NOTRELEASED
SystemReleased             : False
RecipientCount             : 1
QuarantineTypes            : HighConfPhish
Expires                    : 15/12/2022 15:33:20
RecipientTag               : {Priority Account}
DeletedForRecipients       : {}
QuarantinedUser            : {}
ReleasedUser               : {}
Reported                   : False
Direction                  : Outbound

Looking Everywhere for Data

Often people become dismayed when they look for information and discover that a source doesn’t deliver all the detail they need. It’s often the case inside Microsoft 365 that you can combine data from different sources to come up with an answer. It would be nice if Microsoft captured all the relevant message for a quarantined message release in the audit records, but at least we can find the data.


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/12/08/quarantined-message-report/feed/ 3 58275
Running Exchange Online Historical Message Traces for Sets of Mailboxes https://office365itpros.com/2022/12/07/historical-message-trace-shared-mbx/?utm_source=rss&utm_medium=rss&utm_campaign=historical-message-trace-shared-mbx https://office365itpros.com/2022/12/07/historical-message-trace-shared-mbx/#respond Wed, 07 Dec 2022 01:00:00 +0000 https://office365itpros.com/?p=58251

Use a Historical Message Trace to Find Inbound Email Delivered to Shared Mailboxes

Updated 24-Oct-2023

A question in the Facebook group for Office 365 Technical Discussions (no YouTube videos or marketing posts accepted) asked how to check shared mailboxes for email received from external senders over the past sixty days. The check should look for email received from a specific domain and report details of those messages.

Given the number of shared mailboxes that might be used in a tenant and the volume of email that these mailboxes might receive, running a manual check is not feasible. You would have to sign into each mailbox and review their content. This is a tiresome process that wouldn’t detect messages received from the specific domain that users subsequently deleted (or messages removed by a retention policy).

Exchange Historical Message Traces

Exchange Online historical message traces can go back a maximum of 90 days, so they can be used to search the data logged by Exchange Online when it delivers messages to mailboxes. A single historical message trace can cover up to 100 sender or recipient addresses. If a tenant wants to check email related to a larger number of addresses, they can split the check across multiple searches and combine the results.

It all sounds so easy to script. Run the Start-HistoricalSearch cmdlet to submit the message trace. Check the output. Find and report problem messages. Easy. But as is so often the case, some complexity lurks under the surface.

Submit a Historical Message Trace and Wait

The PowerShell code to automate the check must be split into two scripts. The first creates and submits the historical message trace job. The second analyzes the results of the trace. The two cannot be connected because Exchange Online runs historical message trace jobs in the background as service resources allow. If you’re lucky, a message trace might complete in less than twenty minutes. More often, it will take an hour or so.

Here’s the code I used to submit the job. It finds the set of shared mailboxes, sets the search period, and creates the parameters for the Start-HistoricalSearch cmdlet to process. As noted above, a historical message trace can process up to 100 mailboxes, so a check is there to make sure that we don’t attempt to schedule a job for more than this number of mailboxes.

# Find all shared mailboxes
[array]$SharedMailboxes = Get-ExoMailbox -RecipientTypeDetails SharedMailbox 
If ($SharedMailboxes.Count -gt 100) { 
   Write-Host ("Too many shared mailboxes found - we can't do a message trace for {0} mailboxes" -f $SharedMailboxes.Count) ; break 
}
[array]$RecipientAddresses = $SharedMailboxes.PrimarySmtpAddress

# Submit historical search (maximum of 250 per day)
Start-HistoricalSearch -RecipientAddress $RecipientAddresses -StartDate (Get-Date).AddDays(-60) -EndDate (Get-Date) -ReportType MessageTrace -ReportTitle ("Report Shared Mailbox {0}" -f (Get-Date))

Although you could code a loop to use the Get-HistoricalSearch cmdlet to check the progress of the search job and resume when the job completes, a further complication is that Exchange Online stores the message trace results in Azure storage. There’s no way for PowerShell to download the data for processing. Instead, an Exchange administrator goes to the Mail flow section of the Exchange admin center to view the status of historical message trace jobs and download the results if the job to scan for shared mailbox traffic is complete (Figure 1).

Downloading the report for a historical message trace
Figure 1: Downloading the report for a historical message trace

Processing Historical Message Trace Results

Exchange Online downloads the message trace results using a URL like:

https://admin.protection.outlook.com/ExtendedReport/Download?Type=OnDemandReport&RequestID=044439ab-614e-4ec6-b4d9-a095c92befbe

The result is a CSV file in the Downloads folder with a name with a “MTSummary_Report” prefix followed by the historical message trace name and an identifier. For instance:

MTSummary_Report Shared Mailbox Scan 12062022 184532_044439ab-614e-4ec6-b4d9-a095c92befbe

Occasionally, the data generated by Exchange Online doesn’t import properly into PowerShell using the Import-CSV cmdlet. To make sure that everything works, I open the downloaded file with Excel and save it to a known location, like c:\temp\MessageTraceResults.csv. The save seems to cure any lingering data formatting problems.

We can now process the data by first searching the records to find if any originated from the domain of interest. For the purpose of this exercise, I’ll search for messages originating from Practical365.com:

[array]$MessageData = Import-CSV c:\temp\MessageTraceResults.CSV
[array]$ProblemItems = $MessageData | Where-Object {$_.Sender_Address -like "*practical365.com"}
If (!($ProblemItems)) { Write-Host "No email found from Practical365.com - exiting" ; break }

Creating a report from the discovered items is simple:

$ProblemInfo = [System.Collections.Generic.List[Object]]::new() 
ForEach ($Item in $ProblemItems) {
  $DataLine = [PSCustomObject] @{
   Timestamp = Get-Date($Item.origin_timestamp_utc) -format g
   Sender    = $Item.Sender_Address
   Subject   = $Item.Message_Subject
   Recipient = $Item.Recipient_Status.Split("##")[0] }
  $ProblemInfo.Add($DataLine)
} # End ForEach Item

Figure 2 shows the report of the messages received from Practical365.com.

Messages from a domain found by a historical message trace
Figure 2: Messages from a domain found by a historical message trace

Getting the Job Done

Some organizations extract and move message trace data to external repositories like Splunk to make it easier to perform this kind of tracing. An external repository usually allows for long-term storage and is more flexible in terms of its search capabilities. However, the basic tools built into Exchange Online can do the job, even if the PowerShell processing is split into two tasks. It would be nice if Microsoft allowed tenants to download the message trace data with PowerShell to avoid the messing around with CSV files, but that’s just a small complaint.


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/12/07/historical-message-trace-shared-mbx/feed/ 0 58251
Reporting Distribution List Membership with the Microsoft Graph PowerShell SDK https://office365itpros.com/2022/12/06/distribution-list-membership-sdk/?utm_source=rss&utm_medium=rss&utm_campaign=distribution-list-membership-sdk https://office365itpros.com/2022/12/06/distribution-list-membership-sdk/#comments Tue, 06 Dec 2022 01:00:00 +0000 https://office365itpros.com/?p=58218

A New Take on an Old Favorite Script

In the past, I’ve written several times about using PowerShell to report the membership of Exchange Online distribution lists. Support of multiple mail-enabled objects, including nested groups, makes the extraction of full distribution list membership trickier than simply running the Get-DistributionGroupMember cmdlet and a variety of techniques have been used over the years to expand and report all members using Exchange Online and Azure AD cmdlets and Microsoft Graph API requests.

Normally, I don’t return to the same topic again and again. The reason why I’m back here for a third bite at the cherry is that Microsoft will deprecate the Azure AD PowerShell module on June 30, 2023. Although it’s possible to use Microsoft Graph API requests to report distribution list membership (with a caveat), some would prefer to convert their scripts to another PowerShell module rather than going full-blown Graph. I guess the Microsoft Graph PowerShell SDK is that half-way stop, so here goes.

Using the Graph SDK with Group Memberships

It’s important to understand that the Microsoft Graph PowerShell SDK interacts with Azure AD groups. Distribution lists are Exchange Online objects that synchronize to appear as groups in Azure AD. However, although distribution lists support membership of mail-enabled objects that are unique to Exchange, like mail-enabled public folders, these objects don’t show up in membership reported by Azure AD. The reason is simple: the objects don’t exist in Azure AD. What does show up are the objects supported by Azure AD: user accounts (including guests), contacts, and groups. That’s what you see when you run the Get-MgGroupMember cmdlet to retrieve group membership.

Because distribution groups support nested groups, we need a way to expand the members of nested groups and resolve duplicate member entries that might exist. This can be done using a Graph query to fetch transitive members. The transitive query does all the work to expand nested groups and return a unified set of members.

Because a Graph API request exists to fetch transitive members, an equivalent cmdlet is available in the Microsoft Graph PowerShell SDK. That cmdlet is Get-MgGroupTransitiveMember. For example, this call fetches all the members in the group pointed to by the variable $DL.ExternalDirectoryObjectId.

[array]$Members = Get-MgGroupTransitiveMember -GroupId $DL.ExternalDirectoryObjectId

Objects synchronized from Exchange Online to Azure AD store their Azure AD identifier (GUID) in the ExternalDirectoryObjectId property. For instance, a mailbox stores the identifier for its owning Azure AD user account in the property. Azure AD treats a distribution list like any other group, and so it has a group identifier that’s stored in the property. That identifier is the one we use to extract distribution list membership with Get-MgGroupTransitiveMember.

Get-MgGroupTransitiveMember returns a list of identifiers. In earlier versions of the Microsoft Graph PowerShell SDK, you had to resolve the identifiers into useful information, like the display names of individual group members. Now, the group cmdlets return the information in an array of member details stored in the AdditionalProperties property, which means that we can find what we want by extracting it from the array. For convenience, I usually extract the array into a separate variable:

[array]$MemberData = $Members.AdditionalProperties

You might ask why Microsoft decided to update the groupcmdlets to output the member data in a separate property instead of changing the default to output the list of members (which is how cmdlets like Get-AzureADGroupMember work). One explanation is that changing the output of a cmdlet will break existing scripts. In that context, it’s understandable to include a new property.

Parsing Distribution List Membership

After fetching the transitive membership for a distribution list, the remaining task is to figure out how many members of the different categories are in the set (members, contacts, and groups). This is easily done by counting the items in the set. After it gathers this basic information about the group, the script updates a PowerShell list with the data.

You can drive some other processing from the list. For instance, you might decide to convert any distribution list with over 100 members to a team (use the same kind of approach as described here to covert a dynamic distribution list to a team). An easier decision might be to remove any distribution list found with zero members on the basis that they’re no longer in use. This is easily done with:

$Report | Where-Object {$_.Members -eq 0} | Remove-DistributionGroup

To be safe, I left the confirmation prompt in place so that you’re asked to confirm the deletion of each distribution list. You can suppress the prompt by adding -Confirm:$False to the command.

Reporting Distribution List Membership

The final stage is to generate output, which the script does in the form of a CSV file and HTML file (Figure 1). This ground is well-known and there’s no mystery in the code needed to generate the files.

Output of the distribution list membership report
Figure 1: Output of the distribution list membership report

Converting from Azure AD cmdlets to Microsoft Graph PowerShell SDK cmdlets is not challenging – once you understand how the Graph SDK works. The trick is to make no assumptions about the input parameters or the output a cmdlet produces. You might expect things to work in a certain way, but the chances are that they won’t, so go into the conversion in the spirit of a voyage of discovery and you won’t be disappointed. To help, here’s the script to report distribution list members using the Microsoft Graph PowerShell SDK.


Learn about exploiting Exchange Online and the rest of Office 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/2022/12/06/distribution-list-membership-sdk/feed/ 4 58218
Adding New Azure AD Users to Groups Automatically https://office365itpros.com/2022/12/05/dynamic-group-membership/?utm_source=rss&utm_medium=rss&utm_campaign=dynamic-group-membership https://office365itpros.com/2022/12/05/dynamic-group-membership/#comments Mon, 05 Dec 2022 01:00:00 +0000 https://office365itpros.com/?p=58175

Dynamic Group Membership is the Obvious But Not the Only Option

A member of the Microsoft Technical Community asks if it’s possible to automatically add newly-created accounts to an existing group. The initial response offered by the community focused on dynamic groups – either dynamic distribution lists or dynamic Azure AD groups.

It’s a reasonable suggestion. Dynamic distribution groups are part of base Exchange Online functionality and don’t require any additional licenses. Dynamic Azure AD groups require Azure AD Premium P1 licenses for every account covered by dynamic membership. In both cases, the trick is to make sure that the query used by Exchange Online or Azure AD to determine group membership finds the new account.

Dynamic Group Membership for Exchange Online Mailboxes

It’s possible to create a dynamic distribution group based on a simple query like “all mailboxes” that will automatically include new accounts (if they have mailboxes). Figure 1 shows the UX in the Exchange admin center (EAC) to define the membership of a new dynamic distribution list.

Figure 1: Dynamic membership settings for all mailboxes

The list works and email sent to it arrives in the inbox of every mailbox in the tenant, including shared mailboxes. This is because the recipient filter generated by Exchange Online for the dynamic distribution group selects all mail-enabled objects with a recipient type of ‘UserMailbox’ and only filters out some system mailboxes.

A dynamic distribution list like this is said to use a “canned” recipient filter because Exchange Online generates the filter based on the choices the administrator makes when they create the new list. You can only edit canned filters through the EAC. Exchange Online gives greater flexibility through the support of custom recipient filters. These filters can only be created using PowerShell, but they’re much more flexible in terms of selecting the set of mail-enabled objects to address through the list. A simple custom recipient filter to find just user mailboxes is shown below together with a test with the Get-Recipient cmdlet to prove that the filter works.

$Filter = "{RecipientTypeDetails -eq 'UserMailbox'}"
Get-Recipient -RecipientPreviewFilter $Filter

Dynamic Group Membership for Azure AD User Accounts

Dynamic Azure AD groups can be used with Microsoft 365 groups and Teams. These groups use different membership filters (query rules) to find the set of target objects. Instead of mail-enabled objects like mailboxes, the query against Azure AD focuses on user accounts rather than mailboxes. However, the same capability exists in that it’s possible to create a dynamic Azure AD group that includes all user accounts, including those newly created.

Again, the key is to construct a query rule that finds all user accounts – of the right type. When Azure AD is used for a Microsoft 365 tenant, there are many non-interactive user accounts created to give identities to objects such as shared mailboxes and room mailboxes. These are all considered “member” accounts and it’s easy to build a rule to find all member accounts. However, you probably want a more refined version that finds just the accounts used by humans.

Azure AD doesn’t have a human filter, so we need to construct something that Azure AD can use to find matching accounts in its directory. One approach is to use licenses for the check. You could look for accounts assigned Office 365 E3 licenses but would have to check for accounts with F1 or E5 licenses too. An easy change is to look for accounts that have any license that has at least one enabled service. For instance, accounts with Office 365 E3 or E5 licenses with the Exchange Online, Teams, Planner, or SharePoint Online service would all match. Figure 2 shows a test of the rule against a “real” user account and some other user accounts belonging to room and shared mailboxes. You can see that the real account passes the validation test while the others do not.

Testing the membership rule for a dynamic Azure AD group to find all user accounts
Figure 2: Testing the membership rule for a dynamic Azure AD group to find all user accounts

Azure AD accounts used by shared mailboxes must be assigned licenses when they need more than 50 GB of mailbox storage or an online archive. These accounts satisfy the membership rule, but that’s perhaps not important. If it is, some tweaking of the membership rule is necessary to remove the shared mailbox accounts.

Dynamic Group Membership of Org-Wide Teams

If your organization is smaller than 10,000 accounts, new Azure AD accounts automatically join the org-wide teams in the tenant (a tenant can support up to five org-wide teams). Org-wide teams are a special form of dynamic Microsoft 365 group whose membership is controlled by Teams rather than Azure AD, so Azure AD Premium P1 license are not required.

The PowerShell Alternative to Manage Dynamic Group Membership

If you don’t want to use a dynamic object, it’s certainly possible to use standard distribution lists or Microsoft 35 groups. In this scenario, the tenant takes the responsibility for maintaining group membership. Usually, PowerShell is used to add new accounts to group membership. You don’t have to worry about removing deleted accounts from the group as this happens automatically following an account deletion.

To add a new user to a distribution list, use the Add-DistributionGroupMember cmdlet:

Add-DistributionGroupMember -Identity "All Tenant Mailboxes" -Member Lotte.Vetler@office365itpros.com

To add a new user account to a Microsoft 365 group, either run the Add-UnifiedGroupLinks cmdlet (from the Exchange Online management module) or the New-MgGroupMember cmdlet (from the Microsoft Graph PowerShell SDK):

Add-UnifiedGroupLinks -Identity "All Tenant Accounts" -LinkType Member -Links Lotte.Vetler@office365itpros.com

New-MgGroupMember -GroupId "107fe4dd-809c-4ec9-a3a1-ab88c96e0a5e" -DirectoryObjectId (Get-MgUser -UserId Lotte.Vetler@office365itpros.com).Id

If the tenant creates user accounts programmatically with PowerShell, these commands can be added to that script. If not, a background scheduled job could find accounts that don’t exist in group membership and add them. See this article for more information about group management with the Microsoft Graph PowerShell SDK.

Many Possibilities to Ponder

A simple question required a long answer. That’s because the questioner didn’t specify what type of group that they wanted to add new accounts to. In any case, it’s nice to be able to debate the possibilities and then settle on the best course of action to take.


Insight about the various options to manage dynamic group membership for new accounts 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/12/05/dynamic-group-membership/feed/ 3 58175