Microsoft Graph PowerShell SDK – Office 365 for IT Pros https://office365itpros.com Mastering Office 365 and Microsoft 365 Wed, 07 Aug 2024 09:41:42 +0000 en-US hourly 1 https://i0.wp.com/office365itpros.com/wp-content/uploads/2024/06/cropped-Office-365-for-IT-Pros-2025-Edition-500-px.jpg?fit=32%2C32&ssl=1 Microsoft Graph PowerShell SDK – Office 365 for IT Pros https://office365itpros.com 32 32 150103932 Finding Managers of Users with the Microsoft Graph PowerShell SDK https://office365itpros.com/2024/07/29/find-manager-for-entra-id-account/?utm_source=rss&utm_medium=rss&utm_campaign=find-manager-for-entra-id-account https://office365itpros.com/2024/07/29/find-manager-for-entra-id-account/#comments Mon, 29 Jul 2024 07:00:00 +0000 https://office365itpros.com/?p=65761

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

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

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

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

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

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

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

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

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

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

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

Find the Managers for Multiple Users

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

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

The error is not terribly helpful:

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

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

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

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

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

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

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

Looking for User Accounts without Managers

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

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

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

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

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

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

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

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

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

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

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

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

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

Entra ID Should Mark Utility Accounts

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


Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.

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

Counting Fetched Objects is a Hard Computer Problem

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

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

An apparently simple question for the Get-MgUser cmdlet

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

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

Pagination and Page Size

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

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

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

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

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

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





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

Get-MgGroup-Top 7 -PageSize 3

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

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

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

Frustrating Paging and Display

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

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

Selecting Properties to Use

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

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

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

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

More Handcrafting Required for the Microsoft Graph PowerShell SDK

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

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


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

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

Updating Old Code to Use the Microsoft Graph PowerShell SDK

Teams and Groups activity report

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

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

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

Microsoft Graph PowerShell SDK Advantages

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

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

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

Upgrading Script Code

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

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

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

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

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

Some Hiccups

Like all coding projects, some hiccups occurred.

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

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

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

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

Get-MgGroup_List: Sorting not supported for current query.

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

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

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

A Successful Conversion

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


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

]]>
https://office365itpros.com/2024/07/15/teams-and-groups-activity-6/feed/ 1 65597
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
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 Create a Password Expiration Report https://office365itpros.com/2024/04/17/password-expiration-report/?utm_source=rss&utm_medium=rss&utm_campaign=password-expiration-report https://office365itpros.com/2024/04/17/password-expiration-report/#comments Wed, 17 Apr 2024 08:00:00 +0000 https://office365itpros.com/?p=64505

But Will a Password Expiration Report be Obsolete Soon?

The advice not to force users to change passwords regularly comes from both Microsoft and independent security agencies. Forcing people to change passwords creates friction for people without delivering better security. The consensus is that better security is attained by moving away from passwords to protect accounts with stronger authentication methods like multifactor authentication or passkeys. Evidence of progress in this direction is Microsoft’s recent announcement of support in Entra ID for device-bound passkeys based on the Authenticator app.

The direction of travel seems clear, but progress is slow. The percentage of Entra ID connections using multifactor authentication reached 38% in early 2024. It takes time to change, which is why I still receive requests for how to create a report showing when Entra ID accounts last updated passwords and details of when the next password change is scheduled.

Setting the Password Expiration Policy

My tenant doesn’t force password changes. The password expiration policy for the tenant is set to never expire. This is easily done through the Org settings section of Microsoft 365 admin center (Figure 1).

Setting the password expiration policy for a Microsoft 365 tenant.
Figure 1: Setting the password expiration policy for a Microsoft 365 tenant

The accounts in the tenant are not a great test case for reporting password changes. I’m more concerned about how to report the multifactor authentication status for accounts. With that thought in mind, let’s examine how to approach creating a report with PowerShell.

Steps to Create a Password Expiration Report

Generating a password expiration report is straightforward. In this discussion, I used the Microsoft Graph PowerShell SDK to create a script to:

  • Connect to the Graph endpoint by running the Connect-MgGraph cmdlet. Three permissions are needed (If you wish, Directory.Read.All is a higher privileged permission that can be used instead of the first three permissions).
    • Domain.Read.All to read the domain information.
    • User.Read.All to read account information.
    • Organization.Read.All to read information about the tenant (fetch the display name).
    • AuditLog.Read.All to read the sign-in activity information for user accounts.
  • Find the password expiration policy for the tenant. This can be done by using the Get-MgDomain cmdlet to fetch details of the default domain and retrieving the password validity period from it. If the value is 2147483647, the tenant does not expire passwords. Date calculations won’t work with 2147483647, so the script adjusts the value to 20000 to calculate a notional password expiration date.
  • Find the set of licensed member accounts in the tenant. It’s important to use a server-side filter here to maximize performance. Running a command like Get-MgUser -All fetches all the known accounts in a tenant, but a client-side filter will be necessary to remove guest accounts and unlicensed member accounts such as those used for room and shared mailboxes. Master the art of filtering to make sure that scripts that work with Entra ID accounts perform well. I’ll cover filtering in some depth during my Microsoft Graph PowerShell SDK session at the M365 Conference in Orlando.
  • For each account, retrieve details like the date and time of the last password change, the password profile for the account, and to compute a date when the password should be renewed. In tenants that don’t force password renewal, this date will be somewhere long after you retire.
  • Generate a report.

A good case exists for using the beta version of the Get-MgUser cmdlet in the script. Apart from fetching a wider set of properties by default, the Get-MgBetaUser cmdlet returns an additional timestamp for the last successful interactive sign-in (which might be different than the last sign-in).

Figure 2 shows a sample password expiration report generated by the script. In this case, the tenant password expiration policy sets password to never expire, so the reported expiration dates are years into the future and no warnings about impending expiration appear in the status column.

An example of a password expiration report for a Microsoft 365 tenant.
Figure 2: An example of a password expiration report for a Microsoft 365 tenant

You can download the script from GitHub. Remember, the code is intended to illustrate a principle. Use it as you see fit.

Onward to a Passwordless Future

I don’t think there is any doubt but that the time will come when passwords disappear, and we will use more phishing-resistant technologies to prove our identities and sign into applications. Until then, perhaps some will want to report password expiration, and now you have a script to do the job.


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. The PowerShell chapter includes hundreds of examples of using the Microsoft Graph PowerShell SDK.

]]>
https://office365itpros.com/2024/04/17/password-expiration-report/feed/ 3 64505
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
Teams Retires Client Ability to Load Websites from Channel Tabs https://office365itpros.com/2024/01/23/website-channel-tab-teams/?utm_source=rss&utm_medium=rss&utm_campaign=website-channel-tab-teams https://office365itpros.com/2024/01/23/website-channel-tab-teams/#comments Tue, 23 Jan 2024 01:00:00 +0000 https://office365itpros.com/?p=63358

Website Channel Tabs Will Open New Browser Tab

In message center notification MC708500 (20 January 2024), Microsoft announces that from early April 2024 (following the retirement of the classic Teams client), Teams will no longer load websites inside the client when called from a website channel tab. Figure 1 shows an example of a channel tab targeting https://office365itpros.com. When a user accesses the tab, Teams opens the site and displays its content inside the client. The client also highlights the direction it is moving in and offers the option to open the page inside a browser.

The Teams desktop client opens a website channel tab.
Figure 1: The Teams desktop client opens a website channel tab

Microsoft says that the change “is to better align with emerging best practices in web security and privacy while also improving the reliability of websites opened through this feature.” They also say that the change applies only to commercial and government tenants and not educational tenants. Perhaps because Microsoft’s telemetry shows that accessing websites via Teams channels is most common in Edu tenants, they won’t go ahead with the change in Edu tenants until Microsoft finds a “non-disruptive way to access websites.” One wonders why the same care isn’t paid not to disrupt other tenants.

When the change reaches a tenant, all website channel tabs accessed through the Teams desktop and browser clients will open the target site in a new browser tab. The Teams mobile app is not affected by the change.

Not a Bad Idea

I don’t think the change is bad. Many web sites require authentication and quite a few use a form of multifactor authentication. Attempting to open these sites in the Teams client usually fails. I’ve experienced this issue several times, and opening a site in a browser tab is an effective solution.

The change doesn’t affect pages in SharePoint Online sites. These pages are accessed through the SharePoint app tab rather than the website tab and can therefore use the integrated authentication available for Microsoft 365 apps.

Identifying Affected Website App Links

The altered behavior might come as a surprise to users so it’s a good idea to understand how many channel tabs are affected. We’ve been down this path before when Microsoft deprecated the use of the wiki tab in favor of OneNote. At the time, I created a PowerShell script to report channels with active wikis.

Much the same approach can be used to find websites. The same basic script structure applies:

  • Connect to the Microsoft Graph PowerShell SDK with the necessary permissions (scopes). The signed in account should hold the global administrator or Teams administrator role.
  • Find the set of teams in the tenant.
  • For each team, find its channels.
  • For each channel, find if its tabs include a website tab (the app id associated with the tab is “com.microsoft.teamspace.tab.web.” A team can support up to 1,000 channels, so this process can take time!
  • Extract the details, including a decoded version of the URL (Teams stores the URL in a fashion that makes it possible to load the page in the client).
  • Report what’s found.

When I ran the script in my tenant, I was surprised to discover that many of the website tabs had URLs pointing to SharePoint sites. This is probably because the current SharePoint channel tab was not as functional beforehand – or I made a mistake and used the website tab instead. In either case, this demonstrates the value of reviewing website tabs to figure out if website tabs should be converted to SharePoint tabs.

Interestingly, despite their deprecation, wiki tabs remain registered to channels. If you list the tabs in a channel, you are likely to find a wiki tab in the returned set. The Teams clients ignore the wiki tab and never displays it in the set of tabs shown for a channel. If you write code against Teams, remember to ignore the wiki tab in the same manner.

Figure 2 shows what I found after running the script on my tenant. You can download the script from GitHub.

The website channel tab report.
Figure 2: The website channel tab report

Not the Last Change to Channel Tabs

Discovering the set of website channel tabs exist in a Microsoft 365 tenant is a good example of the value tools like the Microsoft Graph PowerShell SDK add for tenant administrators. This is unlikely to be the last time that Microsoft makes a change to how channel tabs work. At least we know how to find out what the effect of a change might be.


Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.

]]>
https://office365itpros.com/2024/01/23/website-channel-tab-teams/feed/ 7 63358
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
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
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
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
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
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
Creating a Teams Directory with PowerShell https://office365itpros.com/2022/11/10/teams-directory-powershell/?utm_source=rss&utm_medium=rss&utm_campaign=teams-directory-powershell https://office365itpros.com/2022/11/10/teams-directory-powershell/#comments Thu, 10 Nov 2022 01:00:00 +0000 https://office365itpros.com/?p=57818

Teams Directory Lets Users Know What’s Available

Updated 17 October 2023

Blog posts, articles, and books are hostage to the passing of time and the evolution of technology. I’ve known this truth for years, but it does keep on recurring at regular intervals. For instance, this week I’ve been working on my session for the European SharePoint Conference (ESPC), which takes place in Copenhagen from November 28 to December 1.

My session is about managing Teams for success, a topic that’s impossible to cover in the allotted hour. Focus is therefore necessary, and I’ve decided to discuss issues like Teams sprawl and automation. While preparing my slides, I looked at a Petri.com article I wrote in April 2019 covering the lack of a Teams directory and how to generate such a directory. A Teams directory helps those who create teams know if a suitable team is already available. It also helps users find teams that they might want to join.

Finding Areas for Improvement

The code in the script I wrote then is not what I would write now. This is natural. We all accumulate knowledge over time and that knowledge guides us on how we solve problems. New technology and updates become available too, and the combination of new technology and new knowledge makes it inevitable that the solution to a problem will probably explore new approaches. There was nothing I could do but start reviewing the code to find areas for improvement.

After the review, I found one technology gap and a couple of ways to improve the script:

  • Teams has changed the format of the internal identifier used to create the deeplink to a team. Recently-created teams use the new format while older teams have the old format. Obviously, the code needs to cope with both formats.
  • The old script uses cmdlets from the Microsoft Teams and Exchange Online management PowerShell modules. Although Microsoft has improved both modules since 2019, using the Microsoft Graph PowerShell SDK should aid performance, especially when the number of teams in a tenant grows into the thousands. At least, that’s the theory.
  • The output (an HTML file and a CSV file) could be improved. For instance, the ImportExcel module supports the generation of nicely-formatted Excel worksheets instead of plain CSV files.

Rewriting Code

Replacing the Get-Team and Get-UnifiedGroup cmdlets with the Microsoft Graph PowerShell SDK is straightforward. The Get-MgTeam cmdlet does the job for both. Thus, to find the set of teams in a tenant, we run:

[Array]$Teams = Get-MgTeam -All

This is equivalent to running the Get-MgGroup cmdlet with a filter to find team-enabled groups:

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

Afterward, it’s a matter of looping through the set of teams to retrieve information about each team. This is when I hit a problem in that the data returned for a team by Get-MgTeam doesn’t include the internal identifier (a value like 19:BxPfo_xgaoGDQMvyuDzztvrQjLWNRvY6sGoLWGLriHA1@thread.tacv2). This is true even if you specify that the cmdlet should return the InternalId property by specifying it in the Property parameter.

The internal identifier is necessary to form the deeplink for a team. A full deeplink contains the internal identifier, the group identifier for the team, and the tenant identifier. A user can click a deeplink to go direct to a team and apply to join the team if it interests them. Here’s what a complete deeplink looks like:

https://teams.microsoft.com/l/team/19:0eb0233aca6640bf84b2ccddd8fc227e@thread.skype/conversations?groupId=2856abd3-85f5-4cac-ba5f-6f8c2e7047c6&tenantId=a262313f-14fc-43a2-9a7a-d2e27f4f3478

The workaround is to run Get-MgTeam to process each team in the loop. For example, this is how the code fetches the InternalId:

$InternalId = Get-MgTeam -TeamId $T.Id | Select-Object -ExpandProperty InternalId

Although this solution works (and it’s what I use), it’s neither elegant nor satisfactory. The extra calls to Get-MgTeam slow down the script and remove some of the benefit of using Graph API requests instead of the Teams and Exchange Online cmdlets. Not returning the internal identifier for multiple teams might not happen for the sake of performance (the identifier might require retrieval from a separate data store), but it seems like a bug when the cmdlet fails to retrieve the property when explicitly requested. I used version 2.07 of the Microsoft Graph PowerShell SDK and it’s possible that Microsoft will fix the problem in a future version.

The Value of a General-Purpose Cmdlet

The previous iteration of the script uses the Get-UnifiedGroup cmdlet. One of the big advantages of this cmdlet is that it returns interesting information like the group owners and counts for members and external members. You don’t have to request this information. The cmdlet returns it automatically.

However, switching to the Microsoft Graph PowerShell SDK means that the script must include code to retrieve this information. This isn’t very difficult (this article covers the basics), but it’s another thing to do.

Improving Output

As noted above, I used the ImportExcel module to output an Excel worksheet instead of a CSV file. I also wanted to improve the effectiveness of the HTML page by making the deeplink clickable. Playing around with adding the HTML <a href=> tag didn’t work so well until I formatted the output with these commands:

Add-Type -AssemblyName System.Web
[System.Web.HttpUtility]::HtmlDecode($htmlreport) | Out-File $ReportFile -Encoding UTF8

Figure 1 shows the HTML output. Obviously, other team properties could feature in the directory. The intention here is to explore what’s possible and demonstrate the technique rather than to set a standard for Teams directories.

Teams Directory in a HTML file
Figure 1: Teams Directory in a HTML file

For instance, the directory includes all teams. You might decide that it’s better to include just public teams because users can join these teams without owner approval.

Making the Directory Available to Teams Users

Several ways exist to make the Teams Directory available to users:

  • Publish the page on a corporate web site.
  • Import the Excel worksheet into a SharePoint Online list and make the list available to users (for example, as a channel tab).
  • Use the Print function from a browser to create a PDF. Upload the PDF to SharePoint Online and create a channel tab pointing to the PDF. Remember to hold down the CTRL key when clicking the links in the PDF.
  • Import the HTML and XLSX files into SharePoint Online and allow users to download the files from there.

Figure 2 shows the how the Teams viewer displays a PDF created from the web page that was imported into SharePoint Online and accessed through a channel tab.

Teams Directory viewed as a PDF file accessed through a channel tab
Figure 2: Teams Directory viewed as a PDF file accessed through a channel tab

Ongoing Maintenance is the Hard Part

Creating a Teams Directory isn’t particularly difficult. In this case, the only technical difficulty was figuring out how to create the deeplink. The issues involved in the ongoing maintenance of the directory are more prosaic, such as:

  • How often do you refresh the directory.
  • What mechanism to use to refresh the directory (An Azure Automation runbook is a good way to generate files periodically).
  • What’s the best way to make the directory available to users?

The updated script is available on GitHub. If you find another way to generate a Teams Directory, please let me know with a comment.


]]>
https://office365itpros.com/2022/11/10/teams-directory-powershell/feed/ 6 57818
Report SSPR Status for Azure AD Accounts https://office365itpros.com/2022/10/20/azure-ad-sspr-accounts-not-enabled/?utm_source=rss&utm_medium=rss&utm_campaign=azure-ad-sspr-accounts-not-enabled https://office365itpros.com/2022/10/20/azure-ad-sspr-accounts-not-enabled/#comments Thu, 20 Oct 2022 01:00:00 +0000 https://office365itpros.com/?p=57562

Use Microsoft Graph PowerShell SDK Cmdlets to Report Accounts Not Yet Set Up for SSPR

A tweet by Nathan McNulty about the Get-MgReportAuthenticationMethodUserRegistrationDetail cmdlet attracted my attention. The cmdlet generates a report about the registered authentication methods for Azure AD accounts. Nathan used the cmdlet to identify accounts that aren’t set up for self-service password reset (SSPR) by filtering the results to find only member accounts where the IsSSPRCapable property is set to False. SSPR is a premium Azure AD feature.

Get-MgReportAuthenticationMethodUserRegistrationDetail outputs filtered results, but two problems exist before the data is really usable. First, the default output for the cmdlet is user identifiers (GUIDs) instead of human-friendly display names for each account. Second, while the filter can isolate member accounts, it can’t refine the query further to drop accounts created for shared mailboxes, resource mailboxes, and other purposes. The first issue is resolved by explicitly selecting the userPrincipalName and userDisplayName properties for output; the second takes more work.

Exploring a Solution

One potential solution is illustrated below. The script uses the Get-MgUser cmdlet to find all accounts with at least one assigned license (the set of returned accounts can include those used for shared mailboxes). Information about account identifiers and display names are loaded into a hash table to make it possible to lookup an identifier very quickly. We can then loop through the set returned by Get-MgReportAuthenticationMethodUserRegistrationDetail and check each account against the hash table. If a match occurs, we know that we have a licensed account that isn’t currently enabled for self-service password result and can report that fact.

Although the Get-MgReportAuthenticationMethodUserRegistrationDetail cmdlet can output user principal name and user display name properties, looking up the user account details against a table created by Get-MgUser allows us to drop the non-user accounts and lay the foundation for retrieving other data, as explained below. Here’s the code:

Connect-MgGraph -Scope Directory.Read.All, UserAuthenticationMethod.Read.All, AuditLog.Read.All
Select-MgProfile Beta

Write-Host "Finding licensed Azure AD accounts"
[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable Records -All
# Populate a hash table with the details
$UserTable = @{}
$Users.ForEach( { $UserTable.Add([String]$_.Id, $_.DisplayName) } )
Write-Host "Finding user accounts not capable of Self-Service Password Reset (SSPR)"
[array]$SSPRUsers = Get-MgReportAuthenticationMethodUserRegistrationDetail | Where-Object {$_.userType -eq 'member' -and $_.IsSSPRCapable -eq $False} | Select-Object Id, userDisplayName, userPrincipalName, DefaultMfaMethod, IsAdmin, IsMfaCapable, IsMfaRegistered, IsPasswordlessCapable, IsSSPRCapable         
Write-Host "Cross-checking against licensed users..."
[array]$NonSSPR = $Null
ForEach ($S in $SSPRUsers) {
  $DisplayName = $UserTable.Item($S.Id) 
  If ($DisplayName) {
     $NonSSPR += $DisplayName }
}
$PNonSSPR = ($NonSSPR.count/$Users.Count).toString("P")
Write-Host ("{0} out of {1} licensed accounts ({2}) are not enabled for Self-Service Password Reset" -f $NonSSPR.count, $Users.count, $PNonSSPR )
Write-Host ($NonSSPR -join ", ")

Only a list of account display names is output. When I ran the script in my tenant, the following output was generated:

Finding licensed Azure AD accounts
Finding user accounts not capable of Self-Service Password Reset (SSPR)
Cross-checking against licensed users...
23 out of 32 licensed accounts (71.88%) are not enabled for Self-Service Password Reset
Andy Ruth (Director), Ben James, Ben Owens (DCPG), Bruno Satlier, Chris Bishop, , Jackson Hoare, James Abrahams, Jeff Guillet, John C. Adams, Ken Bowers, Lotte Vetler, Marc Vigneau, Michael King, Paul Howett, Peter Bridges, Rene Artois, Sean Landy, Terry Hegarty, Tony Redmond (Office 365 for IT Pros), Vasil Michev (Technical Guru)…

Improving the Output

We can improve the output by including more information in the lookup table. A hash table is fast, but it’s limited to a key and a value, but the value can any PowerShell object. The hash table can then hold more information about each user. For example:

$UserTable = @{}
ForEach ($U in $Users) {
    $ReportLine  = [PSCustomObject] @{          
     Id                  = $U.Id
     DisplayName         = $U.DisplayName
     Department          = $U.Department
     Office              = $U.OfficeLocation  
     Country             = $U.Country
     }
    $UserTable.Add([String]$U.Id, $ReportLine) 
}

I’ve selected five properties for a user account. It’s easy to add more as necessary. With the hash table populated like this, we can grab the information from the PowerShell object in the value when a match occurs for an account and use it to build a nicer report.

ForEach ($S in $SSPRUsers) {
  $Data = $UserTable.Item($S.Id) 
  If ($Data) { # We found a match
     $ReportLine  = [PSCustomObject] @{  
       Id = $Data.Id
       DisplayName = $Data.DisplayName
       Department  = $Data.Department
       Office      = $Data.Office
       Country     = $Data.Country }
     $NonSSPRUsers.Add($ReportLine) }
}

Figure 1 shows the output of the report file.

Azure AD accounts that are not enabled for SSPR
Figure 1: A list of user accounts that don’t use SSPR

Checking Accounts Regularly

This is exactly the kind of check against user accounts that tenants might want to run regularly. A scheduled runbook executed by Azure Automation is a good way to process these kinds of operations and the code discussed here would move over easily to a runbook. In the interim, here’s the link to the full script in GitHub for you to improve and enhance it 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/2022/10/20/azure-ad-sspr-accounts-not-enabled/feed/ 7 57562
Deep Dive into Entra ID Authentication Methods https://office365itpros.com/2022/10/07/authentication-methods-scripts/?utm_source=rss&utm_medium=rss&utm_campaign=authentication-methods-scripts https://office365itpros.com/2022/10/07/authentication-methods-scripts/#respond Fri, 07 Oct 2022 01:00:00 +0000 https://office365itpros.com/?p=57366

Managing Authentication Methods for an Entra ID User Account with PowerShell

Microsoft product manager Merill Fernando (of Graph X-Ray fame) posted an interesting tweet about a script he wrote to remove all the authentication methods from a user account. Entra ID supports a wide range of authentication methods (Figure 1) ranging from the classic username/password combination to using the Microsoft Authenticator app.

Entra ID Authentication Methods (source: Microsoft).
Figure 1:Entra ID Authentication Methods (source: Microsoft)

At the recent TEC conference, Microsoft VP for Identity Security Alex Weinert made a passionate plea for more Microsoft 365 tenants to secure their accounts with MFA. It’s shocking that only 26.64% of all user accounts use MFA. The figure for accounts holding an administrative role is higher at 34.15%, but that’s still poor. We need to do a better job of moving accounts to the right-hand methods shown in Figure 1.

Scripting Authentication Methods

Merill acknowledges that the script “is not pretty” because the Microsoft Graph does not currently support a way to find the default authentication method for an account. In short, the script attempts to delete an authentication method and if it fails it assumes that the method (like the Microsoft Authenticator app) is the default and leaves it to the last. You can only remove the default authentication method from an account if it’s the last and only method.

In any case, it’s a good script to have around just in case you need to reset an account. I’m not sure how often you’d want to do this, but I guess you might. All contributions to the admin toolbox are gratefully received.

Authentication Methods and the Microsoft Graph PowerShell SDK

Merill’s script uses cmdlets from the Microsoft Graph PowerShell SDK. I like the PowerShell SDK a lot, but sometimes it goes overboard in terms of the number of cmdlets it uses. I think this is due to the way that Microsoft generates the SDK modules and cmdlets from Graph APIs using a process called AutoRest. It’s nice to have a way to generate code automatically, but sometimes human intelligence could do better. Usually, Microsoft generates a new version of the SDK monthly, but sometimes errors creep in and several versions appear in a month (this just happened when versions 1.12 had several minor updates (current version is 1.12.3).

For instance, every authentication method has a separate cmdlet to add (New), update, and remove it from an account. The set of cmdlets used to remove methods in Merill’s script is:

  • Remove-MgUserAuthenticationFido2Method
  • Remove-MgUserAuthenticationEmailMethod
  • Remove-MgUserAuthenticationMicrosoftAuthenticatorMethod
  • Remove-MgUserAuthenticationPhoneMethod
  • Remove-MgUserAuthenticationSoftwareOathMethod
  • Remove-MgUserAuthenticationTemporaryAccessPassMetho
  • Remove-MgUserAuthenticationWindowHelloForBusinessMethod

Seven different cmdlets (you can’t remove the classic password method with one of these cmdlets), or 21 when you add the others for adding and updating methods. It would be simpler all round if the SDK consolidated everything so that we had one cmdlet to add, one to update, and one to remove authentication methods. However, I suspect that because separate API requests exist for each method, we are condemned to work with a confusing mass of cmdlets.

Reporting Authentication Methods

I decided that it would be a good idea to find out what authentication methods are in use. Microsoft makes this information available in the Entra ID admin center, but it’s no fun to simply accept what Microsoft wants to deliver in an admin portal. Instead, if we understand how the technology works, we can adapt it for our own purposes. For instance, I want to focus on tenant accounts rather than including guest accounts in the mix, and I want to extract some information about each authentication method to include in the report.

I already have a script to create an Authentication Method Report for Entra ID and another script to report administrator accounts that aren’t protected with MFA, but there’s always room for another (and this version extracts a little more information about each authentication method, like the phone number used for SMS challenges). Here are the important bits of the code (the full script is available from GitHub):

Write-Host "Finding licensed user accounts"
[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable Records -All
If (!($Users)) { Write-Host "No licensed users found... exiting!"; break }

$i = 0
$Report = [System.Collections.Generic.List[Object]]::new()
ForEach ($User in $Users) {
 $i++
 Write-Host ("Processing user {0} {1}/{2}." -f $User.DisplayName, $i, $Users.Count)
 $AuthMethods = Get-MgUserAuthenticationMethod -UserId $User.Id
 ForEach ($AuthMethod in $AuthMethods) {
  $P1 = $Null; $P2 = $Null
  $Method = $AuthMethod.AdditionalProperties['@odata.type']
  Switch ($Method) {
     "#microsoft.graph.passwordAuthenticationMethod" {
       $DisplayMethod = "Password"
       $P1 = "Traditional password"
     }
     "#microsoft.graph.microsoftAuthenticatorAuthenticationMethod" {
       $DisplayMethod = "Authenticator" 
       $P1 = $AuthMethod.AdditionalProperties['displayName']
       $P2 = $AuthMethod.AdditionalProperties['deviceTag'] + " " + $AuthMethod.AdditionalProperties['phoneAppVersion'] 
     }
     "#microsoft.graph.fido2AuthenticationMethod" {
       $DisplayMethod = "Fido 2 Key"
       $P1 = $AuthMethod.AdditionalProperties['displayName']
       $P2 = Get-Date($AuthMethod.AdditionalProperties['createdDateTime']) -format g
     }
     "#microsoft.graph.phoneAuthenticationMethod" {
       $DisplayMethod = "Phone" 
       $P1 = "Number: " + $AuthMethod.AdditionalProperties['phoneNumber']
       $P2 = "Type: " + $AuthMethod.AdditionalProperties['phoneType']
     }
    "#microsoft.graph.emailAuthenticationMethod" {
      $DisplayMethod = "Email"
      $P1 = "Address: " + $AuthMethod.AdditionalProperties['emailAddress']
     }
    "#microsoft.graph.passwordlessMicrosoftAuthenticatorAuthenticationMethod" {
      $DisplayMethod = "Passwordless"
      $P1 = $AuthMethod.AdditionalProperties['displayName']
      $P2 = Get-Date($AuthMethod.AdditionalProperties['createdDateTime']) -format g
    }
  }
  
  $ReportLine   = [PSCustomObject] @{ 
     User   = $User.DisplayName
     Method = $DisplayMethod
     Id     = $AuthMethod.Id
     P1     = $P1
     P2     = $P2 
     UserId = $User.Id }
  $Report.Add($ReportLine)
 } #End ForEach Authentication Method
} #End ForEach User

The code doesn’t include choices for every possible authentication method because examples aren’t available in my tenant. It’s easy to update the code to handle a method like the temporary pass. Figure 2 shows the output generated by the script.

Listing authentication methods found for Entra ID user accounts.
Figure 2: Listing authentication methods found for Entra ID user accounts

One thing that puzzles me is why my account has multiple methods listed for the Microsoft Authenticator app. Both relate to my iPhone 11, but Entra ID might have created the second record after I renamed the phone. It’s something to look at when the time is available.

You can analyze the data to get further insights. For instance:

Write-Host ""
Write-Host "Authentication Methods found"
Write-Host "----------------------------"
Write-Host ""
$Report | Group-Object Method | Sort-Object Count -Descending | Select Name, Count
Authentication Methods found
----------------------------

Name          Count
----          -----
Password         33
Phone            21
Email            11
Authenticator     5
Fido 2 Key        2
Passwordless      1

The other scripts show how to deal with other aspects of reporting that might be important to you, like checking accounts for administrative roles, date of last sign-in, and so on. The nice thing about PowerShell is its flexibility. Cut and paste from different scripts to create a new take and meet your requirements. That’s a great capability to have.


Learn more about how Entra ID and 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/2022/10/07/authentication-methods-scripts/feed/ 0 57366
Use the Debug Parameter for Microsoft Graph PowerShell SDK Cmdlets to Expose Graph API Requests https://office365itpros.com/2022/07/11/debug-microsoft-graph-powershell-sdk/?utm_source=rss&utm_medium=rss&utm_campaign=debug-microsoft-graph-powershell-sdk https://office365itpros.com/2022/07/11/debug-microsoft-graph-powershell-sdk/#comments Mon, 11 Jul 2022 01:00:00 +0000 https://office365itpros.com/?p=56010

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

A comment for my article about recent enhancements for the Microsoft Graph Explorer noted that while it was great to see the Graph Explorer generate PowerShell code snippets for requests it executes, it would be even nicer if the Graph Explorer supported “round tripping.” In this instance, that means users could send PowerShell commands to the Graph Explorer, which would then interpret the commands and generate the appropriate Graph API requests. It sounds like a great idea.

I contacted some of the folks working on the Microsoft Graph to see if this is possible. Although they couldn’t commit on such an implementation appearing in the future, I was told about a nice feature in the Microsoft Graph PowerShell SDK cmdlets. It doesn’t address the round-tripping request, but it’s a good thing to know none the less, especially if you’re grappling to understand how the SDK cmdlets work for tasks like user, group, or license management.

In a nutshell, if you add the –Debug parameter to any Microsoft Graph PowerShell SDK cmdlet, you’ll see exactly what the cmdlet does, including the Graph API request it runs to execute the command. This is a great way to gain insight into how these cmdlets work and also understand how to leverage Graph API requests.

Because many of its cmdlets are built on Graph APIs, the Debug parameter also works in the same manner for cmdlets from the Microsoft Teams PowerShell module. However, the Teams cmdlets do not output details about permissions and the older policy cmdlets that originated from the Skype for Business Online connector do not display Graph API URIs when they run.

Running with the Debug Parameter

Let’s take a basic example and run the Get-MgUser cmdlet to fetch details of all user accounts in a tenant:

Get-MgUser -All -Debug -Filter "userType eq 'Member'"

When the cmdlet starts, it shows the context it will run under, including whether it’s an interactive session and the scopes (permissions) available to the command. You can get the same information by running the Get-MgContext cmdlet, but this is useful up-front knowledge.

In Figure 1 you can see that the service principal used by the Microsoft Graph PowerShell SDK has many permissions. This is the result of permission creep, the tendency of the service principal to accrue permissions over time due to testing different cmdlets. The existence of so many permissions makes it a bad idea to use the Microsoft Graph PowerShell SDK cmdlets interactively unless you know what you’re doing. In production, it’s best to use certificate-based authentication and a registered Azure AD app to limit the permissions available.

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

The Graph API request is now displayed. We can see that it looks for the top 100 matching items that satisfy the filter. In other words, return the first 100 Azure AD member accounts (Figure 2).

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

As you can see, running with Debug set, the cmdlet halts frequently to allow you to read what’s happened and understand if the command has any problems. If you want to see the cmdlet run as normal but with the diagnostic information, set the SDebugPreference variable from its default (SilentlyContinue) to Continue.

$DebugPreference="Continue"

To revert to normal operation, set $DebugPreference back to SilentlyContinue.

Pagination

Pagination is a concept that doesn’t really exist in PowerShell. Some cmdlets have a ResultSize parameter to control the number of items retrieved by a command, and some have an All parameter to tell the command to fetch everything. The Get-MgUser and Get-MgGroup cmdlets are examples of cmdlets that support an -All parameter.

Graph API requests limit the retrieval of data (usually to 100 or 200 items) to avoid issues caused by requests that might mistakenly look for tens of thousands of items. If more items exist, the application must make additional requests to fetch more pages of data until it has fetched all available items. Applications do this by following a nextlink (or skiptoken) link.

In Figure 3, we see a nextlink for the cmdlet to run to retrieve the next page of data. In this instance, I ran the Get-MgUser cmdlet with no filter, so more than 100 accounts are available, and this is what caused the Graph to respond with the first 100 accounts and the nextlink. In debug mode, you can pause after each page to see the results retrieved from the Graph.

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

Another Thing for the Administrator Toolbox

Facilities like the Debug parameter and the Graph X-ray tool help people to understand how the Graph APIs work. Knowing how the Graph functions is invaluable. Having an insight into how cmdlets work helps people develop better code and hopefully avoid bugs. At least, that’s the theory. Try out the Debug parameter with some Microsoft Graph PowerShell SDK cmdlets and see what you think.


Learn how to exploit the data available to Microsoft 365 tenant administrators like how to debug Microsoft Graph PowerShell SDK cmdlets through the Office 365 for IT Pros eBook. We love figuring out how things work.

]]>
https://office365itpros.com/2022/07/11/debug-microsoft-graph-powershell-sdk/feed/ 2 56010
Imminent Deprecation of Azure AD PowerShell Modules Creates Knowledge Gap in Documentation https://office365itpros.com/2022/07/05/azure-ad-powershell-knowledge/?utm_source=rss&utm_medium=rss&utm_campaign=azure-ad-powershell-knowledge https://office365itpros.com/2022/07/05/azure-ad-powershell-knowledge/#comments Tue, 05 Jul 2022 01:00:00 +0000 https://office365itpros.com/?p=55878

Microsoft and Other Documentation Needs Updating

Microsoft announced their intention to retire the Azure AD Authentication Library (ADAL) and Azure AD Graph API in June 2020. A consequence of this decision meant the retirement of the Azure AD PowerShell modules, including the now very old Microsoft Online Services (MSOL) module. Even though Microsoft has pushed the retirement date out to June 30, 2023, the writing is on the wall for these modules.

Tenants will feel the first effect on March 31, 2023, when Microsoft 365 moves to a new license management platform. At that time, the PowerShell cmdlets for license assignment will stop working. Microsoft’s guidelines for converting from the old cmdlets to the Microsoft Graph PowerShell SDK include the blunt phrase that “there is currently no tool to automatically convert scripts.” In other words, it’s time to get coding, testing, and preparing to switchover. Good documentation is at a premium during conversions, and that’s an issue right now.

With two years’ warning, we can hardly plead a lack of awareness when the change happens., but with time running out, I don’t detect any great pressure. Maybe people will change when license management scripts and products stop working, or users report strange errors. It’s odd.

The Problem Lurking in Microsoft Documentation

One thing that might not be helping is the continued use of Azure AD and MSOL PowerShell examples in Microsoft documentation. After all, if the official documentation gives implicit approval to these modules, why bother changing? And to be fair, apart from license management, all the cmdlets will continue to work past the retirement date. There just won’t be any support or future updates.

Take the documentation to delete or restore user mailboxes in Exchange Online. This includes four references to the old Remove-MsolUser cmdlet as the text explains to readers how to remove an Azure AD user account permanently from the Azure AD recycle bin (Figure 1).

Microsoft documentation recommends the use of the obsolete Remove-MsolUser cmdlet

Azure AD PowerShell
Figure 1: Microsoft documentation recommends the use of the obsolete Remove-MsolUser cmdlet

It would be more impactful if the documentation used Microsoft Graph PowerShell SDK cmdlets or Graph API requests. For example, here’s how to use a Graph API request to retrieve the list of deleted Azure AD accounts in the recycle bin:

$Uri = "https://graph.microsoft.com/V1.0/directory/deletedItems/microsoft.graph.user"
[array]$DeletedUsers = Invoke-MgGraphRequest -Uri $Uri -Method Get
ForEach ($Account in $DeletedUsers.Value) { Write-Host $Account.displayname, $Account.id }
… select a user and then
Remove-MgDirectoryDeletedItem -DirectoryObjectId <Guid for deleted account>

This is an example where using a Graph API request is easier than the equivalent Microsoft Graph PowerShell SDK cmdlet (Get-MgDirectoryDeletedItem – see this example). The point is that if Microsoft documentation featured methods that customers can use in the future instead of the soon-to-be-deprecated modules, it would encourage tenant administrators to adopt those methods.

To be fair to Microsoft, they have tons of pages to update with new examples. However, it’s something that I think should happen sooner rather than later to nudge people along the path to where they should go.

The Problem with Azure AD Examples Across the Wider Internet

Much the same issue exists across the wider internet. Think of the average administrator who wants to find how to accomplish a task using the Azure AD PowerShell cmdlets. They’ll probably start by searching the internet to see how others did the same thing, and they’ll find many examples of old code. There’s always a big caveat about code you find on the internet: don’t trust anything until after fully testing the code. This advice still holds, but now we’re in a situation where the code that someone finds on August 20 to perform automated license assignment with PowerShell won’t work a week later.

I accept that the other cmdlets in the Azure AD and MSOL modules will continue to work after Microsoft retires the modules, but my point is still valid: examples based on soon-to-be-deprecated modules have a short shelf life.

You can’t expect blog writers to go back and revise every post with a reference to an Azure AD or MSOL cmdlet. Some might update some posts, but in general, it won’t happen. Nor should it. A blog post is a snippet of knowledge captured at a point in time rather than a definite statement about how something works that remains current. What’s different about this situation is that many posts face accelerated degeneration to a point where their content is no more than an interesting look back into the past.

Things Will Improve Over Time

Time heals many things. In this case, as time progresses and old blog posts decay, people will post new information and insights to renew the corpus of knowledge available on the internet. The Office 365 for IT Pros team is keenly aware of the issue and has generated several articles to help, including:

And of course, we’ve just released the 2023 edition of the Office 365 for IT Pros eBook containing over 250 examples of the Microsoft Graph PowerShell SDK cmdlets in action. In fact, we replaced every instance of using the Azure AD PowerShell cmdlets with the except of connecting to another tenant to update your guest account photo. The permissions model used by the Graph is completely different to the permissions granted when someone connects to Azure AD with Connect-AzureAD, and that’s why we had to leave that example. Over time, we hope to find a workaround and be able to remove the last vestige of Azure AD PowerShell.

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

Upgrading Scripts to Use New Cmdlets

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

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

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

It’s Good to Use Photos with Azure AD Accounts

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

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

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

Experimenting with the Microsoft Graph PowerShell SDK

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

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

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

The Service Principal Question

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

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

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

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

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

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

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

That Permissioned Service Principal

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

]]>
https://office365itpros.com/2022/06/07/azure-ad-guest-account-photo/feed/ 0 55373
Track User Access to Teams Shared Channels with Entra ID Sign-In Logs https://office365itpros.com/2022/03/31/teams-shared-channels-access/?utm_source=rss&utm_medium=rss&utm_campaign=teams-shared-channels-access https://office365itpros.com/2022/03/31/teams-shared-channels-access/#comments Thu, 31 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=54326

Know Who’s Collaborating in Teams Shared Channels From Outside Your Tenant

Updated 4 March 2024

When Microsoft launched Teams shared channels into public preview (according to MC390413, shared channels will GA in mid-July 2022) the rubber hit the road as tenant administrators tried to figure out the complexities of managing shared channels in production use. It’s true that Microsoft conducted a long private preview with many customers to get shared channels to the point where they squashed obvious bugs and delivered usable software. However, once software is exposed to the kind of examination that an application with 270 million monthly active users can create, other questions bubble to the surface.

Which brings me to the topic of controlling user access to shared channels. The cross-tenant access settings in the External identities section of the Entra admin center control which tenants your organization can access using Entra ID B2B Direct Connect. This is the underlying authentication mechanism for Teams shared channels. It allows users to authenticate in their home tenant and use that authentication, including MFA and device state claims, to access resources in other tenants, if permitted by other tenants.

Entra ID Sign-Ins Track Cross-Tenant Access

Microsoft’s guidance for cross-tenant access settings advises that you can use Entra ID sign in logs to figure out user access to other tenants. It’s true that you can use the PowerShell snippet provided there, but I think we can do better.

The code uses the Get-MgBetaAuditLogSignIn cmdlet from the Microsoft Graph PowerShell SDK to look for sign in records where the resource tenant identifier (the organization delivering a resource like Teams) is not the same as the home tenant identifier (the organization hosting the sign in logs).

$TenantId = (Get-MgOrganization).Id
Get-MgBetaAuditLogSignIn -Filter "ResourceTenantId ne '$TenantId'" -All:$True

The code works (the All switch doesn’t need $True), but the result of the query is a set of sign-in records for both Entra ID B2B Collaboration (guest accounts) and Entra ID B2B Direct Connect. This is a better filter if you want to focus on access to Teams shared channels:

Get-MgBetaAuditLogSignIn -Filter "ResourceTenantId ne '$TenantId' and CrossTenantAccessType eq 'b2bDirectConnect'" -All

Next, although you might recognize the identifier for your tenant, it’s unlikely that you’ll know the identifiers for other tenants (like 22e90715-3da6-4a78-9ec6-b3282389492b). To translate these identifiers into human-friendly tenant names, we need another method.

We’re already connected to the Microsoft Graph, so we can use a Graph query to resolve the identifier into a tenant name.

Finding Tenant Names

Fortunately, a beta query called findTenantInformationByTenantId does the trick. There’s little documentation available, but by running it through the Invoke-MgGraphRequest cmdlet (runs any Graph query when an SDK cmdlet is unavailable), we can retrieve tenant data:

$ExternalTenantId = $Record.ResourceTenantId
$Uri = "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$ExternalTenantId')"
$ExternalTenantData = Invoke-MgGraphRequest -Uri $Uri -Method Get

The tenant information returned is:

Name                           Value
----                           -----
@odata.context                 https://graph.microsoft.com/beta/$metadata#microsoft.graph.tenantInformation
tenantId                       22e90715-3da6-4a78-9ec6-b3282389492b
displayName                    o365maestros
federationBrandName
defaultDomainName              o365maestros.onmicrosoft.com

I assume this web site, which can return the identifier of any Microsoft 365 tenant, uses a similar API.

Flow of the Script

The flow of the PowerShell script to analyze sign-in data is therefore:

  • Find sign-in records for Entra ID Direct Connect activity. If you want to process records for Azure B2B Collaboration, change the filter to remove the check against the CrossTenantAccessType property.
  • Extract data from each record, including resolving external tenant identifiers to tenant names.
  • Report.

In normal circumstances, the sign-in data will feature just a few tenants. It would be slow to run a query to resolve the tenant identifier for every record. To ensure performance, the script resolves a tenant name the first time it is encountered and stores the tenant name identifier and name in a hash table. When the script processes subsequent records for the same tenant, it reads the information from the hash table.

You can download the script from GitHub. Normal warnings apply: use at your peril, etc. and please fix my bugs…

Script Outputs

The output of the script is a PowerShell list containing details of sign-ins which use cross-tenant access to connect to Teams shared channels in external tenants (Figure 1).

Viewing information about user connects to Teams shared channels
Figure 1: Viewing information about user connects to Teams shared channels

The data can be parsed to reveal statistics like which tenants use cross-tenant access:

$Report | Group TenantName | Sort Count -Descending | Format-Table Name, Count

Or to reveal the names of the users who connect to external tenants:

$Report | Group User | Sort Count -Descending | Format-Table Name, Count

Name       Count
----       -----
Sean Landy     4
James Ryan     3
Ken Bowers     3

And so on. I’m sure you’ll find other ways to use the information to track what’s happening with Teams shared channels. The point is that the data is there if you need it. All that’s required is a little massaging of the information.


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/03/31/teams-shared-channels-access/feed/ 10 54326
Assign Azure AD Roles to User Accounts with the Microsoft Graph PowerShell SDK https://office365itpros.com/2022/03/30/azure-ad-role-assignments/?utm_source=rss&utm_medium=rss&utm_campaign=azure-ad-role-assignments https://office365itpros.com/2022/03/30/azure-ad-role-assignments/#respond Wed, 30 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=54307

So Many Roles to Work With

A bunch of built-in Azure AD roles exist to help manage resources. Microsoft adds more to the list over time. Not all the roles exist in Microsoft 365 tenants, but you can expect to find at least twenty roles, including:

  • Global administrator.
  • Exchange administrator.
  • SharePoint administrator.
  • Teams administrator.
  • Compliance administrator.
  • Reports reader.
  • User administrator.
  • Helpdesk administrator.

These are Azure AD roles. Another set of compliance roles exist like “Fraud Investigators” and “Disposition Processors,” used to grant the ability to perform information governance and protection tasks. As explained in this post, the Exchange Online Get-RoleGroup cmdlet reveals both types, but for the purpose of this article, I focus on the assignable Azure AD roles.

Finding the List of Roles

In my tenant, there are 31 Azure AD roles in use. I know this because I ran the Get-MgDirectoryRole cmdlet. Here’s what I see (list trimmed for space):

[array]$DirectoryRoles = Get-MgDirectoryRole | Sort DisplayName
$DirectoryRoles | Format-Table DisplayName, Id

DisplayName                                Id
-----------                                --
Attack Simulation Administrator            f28eae22-7444-492d-bb0a-d3e41f8f9d4f
Attribute Assignment Administrator         b1042b1d-c84a-448e-9ca8-e6ad85e4fb65
Attribute Definition Administrator         327df668-5ea8-4ad8-b153-75d326b3c7d1
Azure AD Joined Device Local Administrator 268030c9-556f-47a6-a167-5970cb734558
Billing Administrator                      07308ce7-381b-4fb1-b31e-398b8a66c946
Compliance Administrator                   88b6939a-ef4b-4e8e-9aba-00f4f8447e66
Compliance Data Administrator              fdc30bad-3a16-4c24-ac89-79b73ad9468d
Customer LockBox Access Approver           1402c923-f478-4a9c-82b1-0511726c43bd

This is functionally equivalent to running the Get-AzureADDirectoryRole cmdlet from the soon-to-deprecated Azure AD module. Other Azure AD administrative roles exist but haven’t been used. You can see these roles by running the Get-MgDirectoryRoleTemplate cmdlet.

The role identifier is the important piece of information because we need this to assign a role to a user account. To store the role identifier in a variable to make it easier to use, we filter it from the set of roles. For instance, these commands return the role identifier for the Global administrator and Teams administrator roles:

$GlobalAdminRoleId = $DirectoryRoles | ? {$_.DisplayName -eq "Global administrator"} | Select -ExpandProperty Id
$TeamsAdminRoleId  = $DirectoryRoles | ? {$_.DisplayName -eq "Teams administrator"} | Select -ExpandProperty Id

Assigning Azure AD Roles to User Accounts

The New-MgDirectoryRoleMemberByRef cmdlet assign an Azure AD role to a user account. This cmdlet works like the New-MgGroupOwnerByRef cmdlet used to assign a new user to an Azure AD group (see this post about group management with the Microsoft Graph PowerShell SDK).

In this example, we first fetch the identifier for a user account and the set of current holders of the role. We then check the user identifier against the set of current role holders and if the account is not in the list, we assign the role using New-MgDirectoryRoleByRef:

$User = Get-MgUser -UserId Ken.Bowers@Office365itpros.com
$RoleMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $TeamsAdminRoleId
If ($User.Id -notin $RoleMembers.Id) {
  Write-Host ("Adding user {0} to the Teams administrator role" -f $User.DisplayName)
  New-MgDirectoryRoleMemberByRef -DirectoryRoleId $TeamsAdminRoleId -BodyParameter @{"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($user.Id)"}
}

By comparison, the equivalent Azure AD cmdlet is simpler:

Add-AzureADDirectoryRoleMember -ObjectId $TeamsAdminRoleId -RefObjectId $User.Id

To check that the correct account was added to the Azure AD role, fetch the set of updated role members and loop through the set to retrieve the display name of each role holder:

$RoleMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $TeamsAdminRoleId
ForEach ($RoleMember in $RoleMembers) {
    Write-Host $RoleMember.AdditionalProperties["displayName"]}

Alternatively, check the assignments through Azure AD admin center (Figure 1):

Checking Azure AD role assignments in the Azure AD admin center
Figure 1: Checking Azure AD role assignments in the Azure AD admin center

There doesn’t appear to be a cmdlet available currently to remove a role assignment comparable to the Remove-AzureADDirectoryRoleMember cmdlet. This might be because Microsoft hasn’t yet added such a cmdlet to the SDK. Until they do, run the Azure AD cmdlet or use a graph API query to remove role assignments – or remove the assignments from the Azure AD admin center.

More Learning Required

Like many topics related to the Microsoft Graph PowerShell SDK, some more work is required to fully understand the intricacies of role assignment management, especially in deleting assignments. The good news is that this area is under active development. We can but wait for progress.


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/03/30/azure-ad-role-assignments/feed/ 0 54307
Basic Entra ID Group Management with the Microsoft Graph PowerShell SDK https://office365itpros.com/2022/03/29/create-entra-id-group/?utm_source=rss&utm_medium=rss&utm_campaign=create-entra-id-group https://office365itpros.com/2022/03/29/create-entra-id-group/#comments Tue, 29 Mar 2022 01:00:00 +0000 https://office365itpros.com/?p=54278

Create Entra ID Groups with SDK Cmdlets

Updated 28 December 2023

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

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

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

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

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

Creating Different Kinds of Entra ID Groups

Entra ID groups include:

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

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

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

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

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

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

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

Creating a Dynamic Microsoft 365 Group

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

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

Update Group Properties

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

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

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

Updating Group Membership

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

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

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

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

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

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

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

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

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

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

Removing Groups

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

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

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

Restore Deleted Groups

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

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

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

Finding Group Objects

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

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

You can also search using the StartsWith filter:

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

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

[array]$Groups = Get-MgGroup -All

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

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

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

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

Find all mail-enabled groups:

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

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

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

To find the set of dynamic Microsoft 365 Groups:

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

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

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

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

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

Filter Speed

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

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

Complex Queries Against Entra ID Groups

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

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

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

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

An Evolving Story

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


Keep up to date with developments like the migration from the cmdlets in the Azure AD module to the Microsoft Graph SDK for PowerShell by subscribing to the Office 365 for IT Pros eBook. Our monthly updates make sure that our subscribers understand the most important changes happening across Office 365.

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

Preparing to Migrate Away from Old AzureAD cmdlets

Updated: 15 March, 2023

Manage Entra ID user accounts

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

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

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

Connections

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

Scopes

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

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

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

Welcome To Microsoft Graph!

Updating Properties for Entra ID User Accounts

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

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

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

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

Updating Email Properties for an Account

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

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

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

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

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

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

Updating a User’s Manager

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

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

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

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

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

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

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

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

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

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

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

Delete a User Account

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

Remove-MgUser -UserId $UserId

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

Restore-MgUser -UserId $UserId

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

Finding User Accounts

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

[array]$Users = Get-MgUser -All

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

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

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

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

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

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

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

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

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

You can combine properties in a filter. For example:

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

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

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

Support for SDK Problems via GitHub

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

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

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

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

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

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

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

Viewing deleted Entra ID user accounts via the Graph Explorer

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

Using Old Azure AD Cmdlets

MC344406 features two cmdlets from the Azure AD Preview module:

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

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

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

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

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

Soft-Delete an Entra ID User Account

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

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

List Soft-Deleted Entra ID User Accounts

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

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

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

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

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

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

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

Remove Soft-Deleted Entra ID User Account

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

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

Time of Transition

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


Keep up to date with developments like the Microsoft Graph PowerShell SDK by subscribing to the Office 365 for IT Pros eBook. Our monthly updates make sure that our subscribers understand the most important changes happening across Office 365.

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

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

Azure AD PowerShell retirement

Microsoft has recently been beating the drum about the retirement of the Azure AD PowerShell module and its older Microsoft Online Services (MSOL) counterpart. On March 3, the Azure AD team posted in the Microsoft Technical Community to say that they had listened to customer feedback and pushed the termination of support out from the end of June to the end of 2022. On September 30, Microsoft set a new retirement date for the Azure AD and MSOL modules for June 30, 2023. Things tend to happen around the end of June to align with the end of Microsoft’s financial year and allow everyone to start the new year afresh.

The salient points in message center notification MC281145 are:

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

Shifting Dates

The deprecation date for the Azure AD and MSOL modules is shifting. Originally, this was June 2022, then the end of 2022, and now it’s June 2023. Clearly, customer feedback has told Microsoft that it’s going to be difficult to update PowerShell scripts before Microsoft wants to retire these modules. ISV products which use the modules or the Azure AD Graph API must also be updated before the axe descends. See Microsoft’s FAQ for help in identifying other applications which use the Azure AD Graph API.

Update (July 29): Microsoft has pushed out the retirement of the Azure AD and MSOL license management cmdlets to 31 March 2023.

No matter which way you turn, the basic fact is that Microsoft will eventually retire the Azure AD and MSOL modules. It’s time to update scripts now, with the priority order being:

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

Microsoft Documents Its Migration Approach

To help, Microsoft has created some documentation for steps to migrate scripts. The most important statement is “There is currently no tool to automatically converts scripts in Azure AD PowerShell to Microsoft Graph PowerShell.” I doubt that any automatic script migration tool will appear. There are just too many variations in how people code with PowerShell to guarantee that a tool could handle even moderately complex scripts. The potential to create a support nightmare is one reason why I think Microsoft won’t produce a migration tool.

Which leaves us with Microsoft’s simple three-step approach to script migration:

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

Testing might be a good fourth step to add. And before you start, you need to create an inventory of scripts which use Azure AD or MSOL cmdlets.

Migration Tips

At first glance, the process seems straightforward. In many cases, it is, and you won’t have huge difficulty in converting Get-AzureADUser with Get-MgUser. Microsoft notes some limitations, to which I add:

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

We Feel Your Pain

The Office 365 for IT Pros eBook writers are busy converting script examples to use the Microsoft Graph PowerShell SDK. We plan to have everything done over the next few months. On one level, it’s a pain to be forced to find and upgrade scripts. On another, it’s an opportunity to revamp scripts to make them work better. Perhaps you might even consider moving some of your long-running scripts to Azure Automation?


So much change, all the time. It’s a challenge to stay abreast of all the updates Microsoft makes across Office 365. Subscribe to the Office 365 for IT Pros eBook to receive monthly insights into what happens, why it happens, and what new features and capabilities mean for your tenant.

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

Targeted Communications for Teams

Updated: 21-Feb-2024

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

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

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

Update: Microsoft is deprecating suggested tags.

How Much is Tagging Used?

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

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

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

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

Graph Permissions

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

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

The Code

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

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

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

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

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

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

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

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

Reporting Teams tags
Figure 2: Reporting Teams tags

Teams Tagging for All

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


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

]]>
https://office365itpros.com/2022/03/16/teams-tags-report/feed/ 1 54045
Microsoft Flags Need to Upgrade PowerShell Scripts to Use TLS 1.2 https://office365itpros.com/2021/11/12/microsoft-flags-need-upgrade-powershell-scripts-use-tls-12/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-flags-need-upgrade-powershell-scripts-use-tls-12 https://office365itpros.com/2021/11/12/microsoft-flags-need-upgrade-powershell-scripts-use-tls-12/#respond Fri, 12 Nov 2021 01:00:00 +0000 https://office365itpros.com/?p=52336

Failure Will Break Ability to Send Email via Exchange Online

Message center notification MC297438 arrived in the Microsoft 365 admin center on November 10 to inform me that Microsoft was about to enforce version 1.2 of the Transport Layer Security (TLS) for Direct Routing SIP interfaces. I have no problem with this proposal. It seems perfectly splendid to enforce TLS 1.2 for all manner of communications.

The note then said: “You are receiving this message because our reporting indicates that your organization is still connecting using SMTP Auth client submission via smtp.office365.com with TLS1.0 or TLS1.1 to connect to Exchange Online.”

Deprecating Old TLS

The problem here is that PowerShell uses the system default for TLS unless you specify otherwise. Although Microsoft is excluding SMTP AUTH from the set of connection protocols they will block for basic authentication in all tenants in October 2022, this doesn’t mean that SMTP AUTH is immune from other efforts within Microsoft 365 to remove older, less secure protocols. As Microsoft notes in MC297438, they communicated their intention to remove TLS 1.0 and 1.1 from Microsoft 365 as far back as December 2017, so this development shouldn’t come as a shock to anyone.

I covered this topic in January 2021 and noted that script developers who use the Send-MailMessage cmdlet to send email via Exchange Online should include a line in their scripts to force PowerShell to use TLS 1.2. If you don’t, the deprecation of TLS 1.0 and 1.1 in Exchange Online will prevent scripts being able to send messages.

For the record, the command to force TLS 1.2 connections from PowerShell is:

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Hopefully, components like multi-function devices which use basic authentication with SMTP AUTH today can use TLS 1.2 connections. If they can’t, those connections will stop working even while basic authentication for SMTP AUTH persists.

Moving to the Graph

Forcing PowerShell to use TLS 1.2 is effective, but it’s a short-term fix. Microsoft will come back to the topic of SMTP AUTH once the dust settles after the removal of basic authentication for the other connection protocols next year. The time will come when Exchange Online ceases support for basic authentication with SMTP AUTH connections.

Microsoft’s preferred method for sending secure email with Exchange Online is to use the Graph APIs. You can do this in two ways by upgrading scripts to replace calls to the Send-MailMessage cmdlet with:

Graph APIs use modern authentication, so the basic authentication issue doesn’t arise.

It’s time to inventory the scripts in your tenant which send email via Exchange Online to know what needs to be done, make sure that TLS 1.2 is used by all scripts, and consider the best option for future upgrades.


Insight like this doesn’t come with hard work and experience. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.

]]>
https://office365itpros.com/2021/11/12/microsoft-flags-need-upgrade-powershell-scripts-use-tls-12/feed/ 0 52336
How to Find When Azure AD User Accounts Receive Microsoft 365 Licenses https://office365itpros.com/2021/11/10/find-when-azure-ad-user-accounts-receive-microsoft-365-licenses/?utm_source=rss&utm_medium=rss&utm_campaign=find-when-azure-ad-user-accounts-receive-microsoft-365-licenses https://office365itpros.com/2021/11/10/find-when-azure-ad-user-accounts-receive-microsoft-365-licenses/#comments Wed, 10 Nov 2021 01:00:00 +0000 https://office365itpros.com/?p=52291

Licensing Report Has No Dates

I recently published a Practical365.com article explaining how to create a licensing report for an Office 365 tenant using cmdlets from the Microsoft Graph SDK for PowerShell.

A reader asked: “I am trying to determine when a specific license, in this case an E3 Security and Mobility license, was added for all users.”

No Dates for Licenses

It’s an interesting question. As written, my script generates a report based on the licenses and service plans assigned to user accounts. However, it doesn’t do anything to tell you when a license for a product like Enterprise Security and Mobility E3 (EMS E3) is assigned to a user. This is because Azure AD does not record assignment dates for the product license information held for user accounts. Instead, license information for an account is presented as a table of SKU identifiers with any disabled service plans in a SKU noted:

$User,AssignedLicenses

DisabledPlans                          SkuId
-------------                          -----
{bea4c11e-220a-4e6d-8eb8-8ea15d019f90} b05e124f-c7cc-45a0-a6aa-8cf78c946968
{}                                     8c4ce438-32a7-4ac5-91a6-e22ae08d9c8b
{}                                     4016f256-b063-4864-816e-d818aad600c9
{a23b959c-7ce8-4e57-9140-b90eb88a9e97} 6fd2c87f-b296-42f0-b197-1e91e994b900

However, date information is included in the service plan information for an account:

$User.AssignedPlans

AssignedDateTime    CapabilityStatus Service                       ServicePlanId
----------------    ---------------- -------                       -------------
09/11/2021 10:33:37 Enabled          AzureAdvancedThreatAnalytics  14ab5db5-e6c4-4b20-b4bc-13e36fd2227f
09/11/2021 10:33:37 Enabled          AADPremiumService             eec0eb4f-6444-4f95-aba0-50c24d67f998
09/11/2021 10:33:37 Enabled          RMSOnline                     5689bec4-755d-4753-8b61-40975025187c
09/11/2021 10:33:37 Enabled          SCO                           c1ec4a95-1f05-45b3-a911-aa3fa01094f5
09/11/2021 10:33:37 Enabled          AADPremiumService             41781fb2-bc02-4b7c-bd55-b576c07bb09d
09/11/2021 10:33:37 Enabled          MultiFactorService            8a256a2b-b617-496d-b51b-e76466e88db0
09/11/2021 10:33:37 Enabled          RMSOnline                     bea4c11e-220a-4e6d-8eb8-8ea15d019f90
09/11/2021 10:33:37 Enabled          RMSOnline                     6c57d4b6-3b23-47a5-9bc9-69f17b4947b3
09/11/2021 10:33:37 Enabled          Adallom                       2e2ddb96-6af9-4b1d-a3f0-d6ecfd22edb2

Checking Dates for Service Plan Assignments

Given that date information is available for service plans, it should therefore be possible to check against the service plan information for user accounts to find assignments of a service plan belonging to a product (SKU). Looking at the Product names and service plan identifiers for licensing page , we find the list of service plans included in EMS E3 (SKU identifier efccb6f7-5641-4e0e-bd10-b4976e1bf68e). The set of service plans are:

  • Azure Active Directory Premium P1: 41781fb2-bc02-4b7c-bd55-b576c07bb09d
  • Azure Information Protection Premium P1: 6c57d4b6-3b23-47a5-9bc9-69f17b4947b3
  • Cloud App Security Discovery: 932ad362-64a8-4783-9106-97849a1a30b9
  • Exchange Foundation: 113feb6c-3fe4-4440-bddc-54d774bf0318
  • Microsoft Azure Active Directory Rights: bea4c11e-220a-4e6d-8eb8-8ea15d019f90
  • Microsoft Azure Multi-Factor Authentication: 8a256a2b-b617-496d-b51b-e76466e88db0
  • Microsoft Intune: c1ec4a95-1f05-45b3-a911-aa3fa01094f5

The theory is that you should be able to check accounts assigned EMS E3 to retrieve information about one of the service plans in the SKU and retrieve and report the assigned date. I don’t have EMS E3 in my tenant, but I do have EMS E5. I therefore checked the theory by running this PowerShell code:

# Check the date when a service plan belonging to a product like EMS E3 is assigned to an account
$EMSE3 = "efccb6f7-5641-4e0e-bd10-b4976e1bf68e" # Product SKU identifier for Enterprise Mobility and Security E3
$EMSE5 = "b05e124f-c7cc-45a0-a6aa-8cf78c946968" # Product SKU identifier for Enterprise Mobility and Security E5
$TestSP = "41781fb2-bc02-4b7c-bd55-b576c07bb09d" # Azure Active Directory Premium P1
$Report = [System.Collections.Generic.List[Object]]::new()
# Find tenant accounts
Write-Host "Finding Azure AD accounts..."
[Array]$Users = Get-MgUser -Filter "UserType eq 'Member'" -All | Sort DisplayName
ForEach ($User in $Users) {
  ForEach ($SP in $User.AssignedPlans) {
   If (($User.AssignedLicenses.SkuId -contains $EMSE5) -and ($SP.ServicePlanId -eq $TestSP -and $SP.CapabilityStatus -eq "Enabled")) {
        $ReportLine = [PSCustomObject][Ordered]@{  
          User            = $User.DisplayName
          UPN             = $User.UserPrincipalName
          ServicePlan     = $SP.Service
          ServicePlanId   = $SP.ServicePlanId 
          Assigned        = Get-Date($SP.AssignedDateTime) -format g
         }
        $Report.Add($ReportLine)
    } #End if
  } #End ForEach Service plans
} #End ForEach Users

After defining some variables, the code calls the Get-MgUser cmdlet to find the Azure AD accounts in the tenant (I used the script described in this article as the basis; see this article for more information about the Microsoft Graph SDK for PowerShell). Make sure that you connect to the beta endpoint as license information is not available with the V1.0 endpoint (run Select-MgProfile beta after connecting to the Graph).

Next, the code checks the assigned plans and if the desired plan belongs to the right product and is enabled, we report it. Each line in the report is like this:

User          : Kim Akers
UPN           : Kim.Akers@office365itpros.com
ServicePlan   : AADPremiumService
ServicePlanId : 41781fb2-bc02-4b7c-bd55-b576c07bb09d
Assigned      : 11/11/2017 16:52

This is a quick and dirty answer to the problem of discovering when a product license is assigned to user accounts. It might serve to fill in while Microsoft improves matters.

As reported by Vasil Michev, Microsoft recently added a licenseAssignmentState resource to the Graph API. This isn’t yet available for PowerShell, but the date information can be retrieved using the Graph. In this snippet, we find user accounts and examine their assignment state for EMS E5 to discover when the license was assigned. The code assumes that you’ve already used a registered app to authenticate and fetch an access token to interact the Graph APIs. Remember that you might need to use pagination to fetch all the pages of user data available in the tenant. Anyway, here’s my quick and dirty code to prove the point:

# Use the Graph API to check license assignment states
Write-Host "Fetching user information from Azure AD..."
$Uri = "https://graph.microsoft.com/v1.0/users?&`$filter=userType eq 'Member'"
[Array]$Users = (Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Get -ContentType "application/json")
$Users = $Users.Value

Write-Host “Processing users…”
ForEach ($User in $Users) {
    $Uri = "https://graph.microsoft.com/beta/users/" + $User.UserPrincipalName + "?`$select=licenseAssignmentStates"
    [Array]$Assignments = Get-GraphData -Uri $Uri -AccessToken $Token
    ForEach ($License in $Assignments.LicenseAssignmentStates) {
        $LicenseUpdateDate = $Null
        If ($License.SkuId -eq $EMSE5 -and $License.State -eq "Active") {
           If ([string]::IsNullOrWhiteSpace(($License.lastUpdatedDateTime)) -eq $False ) {
              $LicenseUpdateDate = Get-Date($License.lastUpdatedDateTime) -format g }
           Else {
              $LicenseUpdateDate = "Not set" }
           Write-Host ("Last update for EMS for {0} on {1}" -f $User.DisplayName, $LicenseUpdateDate) }
    } # End ForEach License
} # End ForEach User

Last update for EMS for Tony Redmond on 15/07/2021 15:28
Last update for EMS for Andy Ruth (Director) on Not set
Last update for EMS for Kim Akers on 26/10/2021 16:58
Last update for EMS for Jack Hones on Not set
Last update for EMS for Oisin Johnston on 03/10/2020 13:18

The dates retrieved using this method differ to the values you get from service plans because Microsoft is populating these values using the last licensing change made to the account. However, in the future, the dates will be more accurate and usable because they will capture changes, hopefully when PowerShell access is possible.

No Audit Data

In passing, I note that the Office 365 audit log captures a “Change user license” audit record when an administrator updates the licenses for an account. However, the audit record doesn’t include details of what licenses were added, changed, or removed. The Azure AD team could do a better job of capturing audit information about license updates. I’m sure they’ll be happy to hear that.


Keep up with the changing world of the Microsoft 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. Monthly updates mean that our subscribers learn about new development as they happen.

]]>
https://office365itpros.com/2021/11/10/find-when-azure-ad-user-accounts-receive-microsoft-365-licenses/feed/ 3 52291
How to Use /Any Filters in Microsoft Graph API Queries with PowerShell https://office365itpros.com/2021/09/17/graph-lambda-operators-powershell/?utm_source=rss&utm_medium=rss&utm_campaign=graph-lambda-operators-powershell https://office365itpros.com/2021/09/17/graph-lambda-operators-powershell/#comments Fri, 17 Sep 2021 01:00:00 +0000 https://office365itpros.com/?p=51564

Why Lambda Operators are Sometimes Needed

A reader asked about the meaning of x:x in a Graph API query included in the article about upgrading Office 365 PowerShell scripts to use the Graph. You see this construct (a Lambda operator) in queries like those necessary to find the set of accounts assigned a certain license. For example, to search for accounts assigned Office 365 E3 (its SKU or product identifier is always 6fd2c87f-b296-42f0-b197-1e91e994b900):

https://graph.microsoft.com/beta/users?$filter=assignedLicenses/any(s:s/skuId eq 6fd2c87f-b296-42f0-b197-1e91e994b900)

Find the set of Microsoft 365 Groups in the tenant:

https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(a:a eq 'unified')

Find the set of Teams in the tenant:

https://graph.microsoft.com/beta/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')

As you might expect, because the cmdlets in the Microsoft Graph SDK for PowerShell essentially are wrappers around Graph API calls, these cmdlets use the same kind of filters. For example, here’s how to find accounts with the Office 365 licenses using the Get-MgUser cmdlet:

[array]$Users = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq 6fd2c87f-b296-42f0-b197-1e91e994b900)" -all

Lambda Operators and Advanced Graph Queries

All these queries use lambda operators to filter objects using values applied to multi-valued properties. For example, the query to find users based on an assigned license depends on the data held in the assignedLicenses property of Azure AD accounts, while discovering the set of Teams in a tenant relies on checking the resourceProvisioningOptions property for Microsoft 365 groups. These properties hold multiple values or multiple sets of values rather than simple strings or numbers. Because this is a query against a multivalue property for an Entra ID directory object, it’s called an advanced query.

Accessing license information is a good example to discuss because Microsoft is deprecating the Azure AD cmdlets for license management at the end of 2022, forcing tenants to upgrade scripts which include these cmdlets to replace them with cmdlets from the Microsoft Graph SDK for PowerShell or Graph API calls. This Practical365.com article explains an example of upgrading a script to use the SDK cmdlets.

If we look at the value of assignedLicenses property for an account, we might see something like this, showing that the account holds three licenses, one of which has a disabled service plan.

disabledPlans                          skuId
-------------                          -----
{33c4f319-9bdd-48d6-9c4d-410b750a4a5a} 6fd2c87f-b296-42f0-b197-1e91e994b900
{}                                     1f2f344a-700d-42c9-9427-5cea1d5d7ba6
{}                                     8c4ce438-32a7-4ac5-91a6-e22ae08d9c8b

It’s obvious that assignedLicenses is a more complex property than a single-value property like an account’s display name, which can be retrieved in several ways. For instance, here’s the query with a filter to find users whose display name starts with Tony.

https://graph.microsoft.com/v1.0/users?$filter=startswith(displayName,'Tony')

As we’re discussing PowerShell here, remember that you must escape the dollar character in filters. Taking the example above, here’s how it is passed in PowerShell:

$Uri = "https://graph.microsoft.com/v1.0/users?`$filter=startswith(displayName,'Tony')"
[array]$Users = Invoke-WebRequest -Method GET -Uri -ContentType "application/json" -Headers $Headers | ConvertFrom-Json

The data returned by the query is in the $Users array and can be processed like other PowerShell objects.

Using Any and All

Getting back to the lambda operators, while OData defines two (any and all), it seems like the all operator, which “applies a Boolean expression to each member of a collection and returns true if the expression is true for all members of the collection (otherwise it returns false)” is not used. At least, Microsoft’s documentation says it “is not supported by any property.”

As we’ve seen from the examples cited above, the any operator is used often. This operator “iteratively applies a Boolean expression to each member of a collection and returns true if the expression is true for any member of the collection, otherwise it returns false.”

If we look at the filter used to find accounts assigned a specific license:

filter=assignedLicenses/any(s:s/skuId eq 6fd2c87f-b296-42f0-b197-1e91e994b900)

My interpretation of the component parts (based on Microsoft documentation) of the filter is:

  • assignedLicenses is the parameter, or the property the filter is applied to. The property can contain a collection of values or a collection of entities. In this case, the assignedLicenses property for an account contains a collection of one or more license entities. Each license is composed of the SkuId and any disabled plans unavailable to the license holders.
  • s:sis a range variable that holds the current element of the collection during iteration.” The interesting thing is that you can give any name you like to the range variable. In this case, it could be license:license or even rubbish:debris. It’s just a name for a variable.
  • SkuId is the subparam, or value within the property being checked. When there’s only one value in a collection (as when you check for team-enabled groups), you don’t need to specify a subparam. In the case of assignedLicenses, it is needed because we want to match against the SkuId within the collection of entities in the assignedLicenses property.
  • 6fd2c87f-b296-42f0-b197-1e91e994b900 is the value to match against items.

I’m Not a Developer

All of this is second nature to professional developers but not so much to tenant administrators who want to develop some PowerShell scripts to automate operations. This then poses the question about how to discover when lambda qualifiers are needed. I don’t have a great answer except to look for examples in:

  • Microsoft’s Graph API documentation.
  • Code posted online as others describe their experiences working with the Graph APIs.

And when you find something which might seem like it could work, remember that the Graph Explorer is a great way to test queries against live data in your organization. Figure 1 shows the results of a query for license information.

Running a query with a lambda qualifier in the Graph Explorer
Figure 1: Running a query with a lambda qualifier in the Graph Explorer

Exploring the Mysteries of the Graph

One complaint often extended about Microsoft’s documentation for the Graph APIs is that it pays little attention to suitable PowerShell examples. The Graph SDK developers say that they understand this situation must change and they plan to improve their documentation for PowerShell over the next year. Although understandable that languages like Java and C# have been priorities up to now, Microsoft can’t expect the PowerShell community to embrace the Graph and learn its mysteries (like lambda qualifiers) without help. Let’s hope that the Graph SDK developers live up to their promise!


Learn how to exploit the Office 365 data available to tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.

]]>
https://office365itpros.com/2021/09/17/graph-lambda-operators-powershell/feed/ 4 51564
Microsoft Lays Out Future for Azure AD PowerShell Module https://office365itpros.com/2021/06/03/microsoft-graph-powershell-sdk/?utm_source=rss&utm_medium=rss&utm_campaign=microsoft-graph-powershell-sdk https://office365itpros.com/2021/06/03/microsoft-graph-powershell-sdk/#comments Thu, 03 Jun 2021 01:48:00 +0000 https://office365itpros.com/?p=50127

Microsoft Graph PowerShell SDK is the Future

For anyone who’s ever used the Azure AD or Microsoft Online Services (MSOL) PowerShell modules to write PowerShell code to automate some aspect of tenant administration, Microsoft’s June 2 announcement about their future direction for Azure AD PowerShell was big news. In a nutshell, Microsoft is focusing on the Graph APIs for identity management. As a consequence, any software which leverages the Azure AD Graph API, like the Azure AD module, is on the runway to deprecation.

Important Points for the Next Year

Last year, Microsoft announced that they would no longer support or provide security updates for the Azure AD Graph API after 30 June 2022. Now they are being more specific about what this end of support decision means for customers. The following points are important:

  • Microsoft’s future investments for identity management are focused on the Microsoft Graph SDK for PowerShell. This is a wrapper around the Graph APIs and is already in use for purposes like setting tenant privacy options for the Insights API.
  • The Azure AD and MSOL modules will not be supported for PowerShell 7.
  • New identity APIs will be available through the Microsoft Graph PowerShell SDK (Figure 1).
  • Microsoft’s investments will center on user, group, and application management, plus role-based access control (RBAC), which is important in terms of making sure that administrators don’t need all-powerful permissions to get work done. Microsoft 365 uses an increasing number of role groups to assign administrative work to different accounts.
  • Microsoft also says that they will invest in usability for the Microsoft Graph PowerShell SDK, which is a good thing because the SDK cmdlets aren’t quite as approachable as those in other modules. The documentation is not in good shape either. See my articles covering basic Azure AD user account management and group management for details.

 Connecting to the Microsoft Graph PowerShell SDK
Figure 1: Connecting to the Microsoft Graph PowerShell SDK

Microsoft says that their goal is that “every Azure AD feature has an API in Microsoft Graph so you can administer Azure AD through the Microsoft Graph API or Microsoft Graph SDK for PowerShell.” They don’t say that every feature will be accessible through the Microsoft Graph PowerShell SDK. In some cases, you’ll need to run pure Graph API calls, but that’s easily done using PowerShell (for an example, see this article on accessing Azure AD access reviews from PowerShell.

Update August 1, 2022: Microsoft has pushed out the previously announced retirement date for the license management cmdlets in the Azure AD and MSOL modules (August 26, 2022) to March 31, 2023. They have delayed the retirement of the Azure AD Graph API until the end of 2022 to give customers extra time to adjust.

It’s Different With the Graph

The net takeaway is that tenants need to review any PowerShell scripts which use the Azure AD or MSOL modules to prepare plans to upgrade scripts to use the Microsoft Graph PowerShell SDK or Graph API calls in the future. Given the number of Office 365 tenants, the pervasive use of PowerShell to automate operations, and the core position of Azure AD in those operations, it’s likely that millions of scripts will need upgrades. I know that I have a bunch of scripts to review and will discuss how the upgrade process proceeds in future articles. Already, I know it won’t be simply a case of replacing all occurrences of Azure AD cmdlets with equivalent Graph SDK calls, like replacing Get-AzureADUser with Get-MgUser. Parameters and output are likely to be different and code will need to be adjusted to cope.

While upgrading scripts is a big job, each script is a one-time activity. Interactive access is another issue. Today, it’s easy to run Connect-AzureAD to connect to Azure AD and then run whatever cmdlets you need to interrogate the directory. The equivalent actions with the SDK are:

First, you connect to the Graph and set the scope (permissions) needed to interact with Azure AD. Unless a suitable access token is available, this starts a device authentication sequence.

Connect-MgGraph -Scopes "User.Read.All","Group.ReadWrite.All"
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code D7DPGD3WL to authenticate.

Opening a web page and inputting the code causes another dialog to appear to confirm consent for the operation (Figure 2).

App consent required to use the Microsoft Graph PowerShell SDK
Figure 2: App consent required to use the Microsoft Graph PowerShell SDK

After consent is granted, you can then go ahead and issue commands. For example, here’s how to fetch a list of guest accounts:

$Guests = Get-MgUser -Filter "UserType eq 'Guest'"

Like most Graph commands, the amount of data returned is constrained to 100 items, so if you want more, you need to specify the All parameter.

The bottom line is that some more up-front thought is needed (to set permissions) before connecting to the Graph SDK and that the authentication flow is not as seamless as it is when running Connect-AzureAD. No doubt this is an area where Microsoft might look at to remove some rough edges.

Time to Prepare Upgrades

Losing support for the Azure AD and MSOL modules sometime in 2022 is a concern, but we’ve seen other instances when Microsoft has extended support to allow customers extra time to get work done, and anyway, losing support doesn’t mean that code will suddenly stop working. Scripts will continue to run. You just won’t be able to ask Microsoft to fix bugs.

One thing you can guarantee in the cloud is that change happens. This is just another example of how that change occurs.


The Office 365 for IT Pros team will document our learning with upgrading PowerShell scripts from the Azure AD module to use the Microsoft Graph PowerShell SDK in the months ago. It should be fun… Subscribe now to make sure that you stay abreast of developments.

]]>
https://office365itpros.com/2021/06/03/microsoft-graph-powershell-sdk/feed/ 12 50127