Graph and Office licensing

I have been fiddling with licensing and Graph API and spent the better part of today morning trying to pull some licensing info via Graph queries. I feel it is time to put that out as a post so I can refer to it next time around.

One of my colleagues wanted to know why a lot of O365 Multi Geo licenses were suddenly assigned over the weekend. Another colleague came up with a list via the following Azure AD PowerShell snippet:

The code gets a list of all Azure AD users, gets the AssignedPlans property for the user, expands it to select ones that are enabled and have a specific Service Plan Id (which you can find from this list; in this case he was searching for the Exchange Online service plan), filters those with a date of assignment timestamp in the past 7 days, and outputs the UPN. Neat and tidy!

I was of course curious if I can do the same in Graph. I had been working on licensing and Graph for something else the past few days so this was a topic of interest.

The answer is YES, but the journey wasn’t as straight forward as I had hoped (took me the whole morning after all) so here’s some notes on what I discovered along the way.

The Properties

First off, every user object in Graph has three license related properties (screenshot from the official docs).

Note that all of these are not returned by default and we have to specifically ask for them.

The assignedLicenses property has a bunch of SKU Ids corresponding to the license assigned to the user, along with any disabled plans. For example:

The SKU Id is a standard one. In this case it is Visio. You can search this up in the plan Id list I linked to earlier. Remember: SKU Ids correspond to licenses.

The assignedPlans property is more useful. It has a list of plans, but more importantly also the timestamp of when it was assigned. You won’t get the license name from here, only the plan Id and name (along with time stamp and whether it is enabled). Example:

And lastly we have licenseAssignmentStates. This one tells you the licenses (SKU Ids) assigned to the user, the plans that are disabled per license, and more importantly the way the license is assigned (via a a group or direct). Example output:

Note that I can see the group Ids. If the group Id is empty it means the license was assigned directly.

This latter was what I had been working on recently. We wanted to find users that were assigned licenses directly or via one of our non-standard groups.

Searching

It is possible to do a Get-MgUser against a user object and then search within any of the properties above. But it is also possible to get Graph to only return user objects matching specific criteria for the above properties. It is not too flexible (which is where I got stuck at today morning) but it is a good start to return a filtered list via the Graph API which you can then filter further locally as needed.

The first thing to use is the any lambda operator. Since each of these properties are collections (i.e. not a single valued answer) when searching we have to expand the collection and search within it. The any operator does that. A screenshot from the official docs:

Interestingly, the docs have an example of using any to search against the assignedLicenses property:

I learnt that it is possible to combine this with other properties for instance:

In my case since I want to focus on the assignment time I will have to go with assignedPlans. Before that though, rather than use hard coded SKU Ids like all the examples above, it would be good to find the Id via Graph itself. Turns out there’s a cmdlet for that: Get-MgSubscribedSku

If I want to find the SKU Id for the Multi Geo license for instance:

Or in this case since I want the plan Id for Exchange Online within Multi Geo:

Assume I have set the result of the above in a variable, say $exchangeId. I can now find all users with this plan assigned to this thus:

That didn’t work as expected, sadly. Got an error: Complex query on property assignedPlans is not supported. The reason and fix for that is in this StackOverflow post. This MS blog post referred to from StackOverflow is a good read too. The solution is to add a few additional switches as follows:

Even better, pull the assignedPlans and UPN too as part of the output for use later on.

This works, so at this point I thought maybe I can filter on the assignedDateTime property too. As per the docs it is a timestamp of the following format:

Not a problem, I can generate a time stamp of that format easily:

This gives me something like “2022-06-02T14:10:49Z” as of writing. So I tried to search based on this:

This fails with the following error: An unsupported property was specified.

I am not sure why. My first thought was that the gt operator is not supported (remember the docs said only eq and not are supported) so I tried changing it to eq and also a timestamp that had the seconds zeroed out (just in case):

Same error!

I know its just the AssignedDateTime that has an issue because others like capatabilityStatus works fine. After fiddling with this for a lot of time I decided to give up and do the filtering based on time locally instead. Thus I do the following via Graph:

Note how I can search both properties within the assignedPlans collection within the any operator.

The filter clause is getting to be a mouthful so better to keep it separate.

At this point the results of Get-MgUser are all the users with the Exchange Online Mutli-Geo plan assigned to them (and enabled). Now I need to filter these to capture ones where the assignment happened in the last 5 days.

This will output a list of UPNs.

I can now build upon this if needed to show where the plan comes from… via the licenseAssignment property. I don’t need that info currently, so didn’t explore further.