A good example on when to use $expand when querying the Microsoft Graph

There are often situations where you may want to query the Microsoft Graph for certain stuff, such as any person that is a manager for someone. Let’s have a look on ways to that, and then how we can improve it.

Recently a script popped up in my LinkedIn feed, where the scripts indeed fetches all users that are managers for someone. Now, let it be said that as long as the script works for your use case – go for it! But let’s analyze the script a bit with runtime in mind.

The below is a very simplified version of the script:

# 1. Get all users from the Graph, but keep only the id 
$users = Get-MgUser -All | Select-Object Id

# 2. Find all users that are managers by checking whether they have direct reports
$managers = $users | Where-Object {
    Get-MgUserDirectReport -UserId $_.Id
}

# 3. Get the details of the managers
$managerDetails = $managers | ForEach-Object {
    Get-MgUser -UserId $_.Id | Select-Object Id, DisplayName, Mail, UserPrincipalName
}

This script will essentially:

  1. Get all users in the tenant from the Graph, including the id, displayname, mail and userprincipalname
    • But then we select to keep only the id
  2. For each user, send a request to get the direct reports of the user, to check whether they are a manager or not
  3. For each user that had direct reports, send a request to get all the required attributes

First of all, we could optimize this a bit by not selecting away the attributes under #1, simply by doings this:

# 1. Get all users from the Graph, but keep only the id 
$users = Get-MgUser -All -Property id, displayname, mail, userprincipalname

# 2. Find all users that are managers by checking whether they have direct reports
$managerDetails = $users | Where-Object {
    Get-MgUserDirectReport -UserId $_.Id
}

The result is the same, but we do not need to get the managers again. However, while we do reduce the number of requests sent to the Graph, we will need to get direct reports of each and every user in the tenant. This means that if you have 60 000 users, you need to send 60 000 of these requests. Assuming you can do 3 requests per second, this is almost 6 hours of run time.

So, what can we do to improve?

The Graph contains a lot of features that can help us out, such as the $expand option! Let’s see what we can do with that?

First of all, we can get a list of direct reports when getting users:

Or we can get the manager along with the user:

We can use the Graph SDK as well, and check the speed of both:

$c1 = Measure-Command {
    $usersWithManager = Get-MgUser -PageSize 999 -All -ExpandProperty manager
}

$c2 = Measure-Command {
    $usersWithDirectReports = Get-MgUser -PageSize 999 -All -ExpandProperty directReports
}

Write-Host "Manager to $($c1.TotalSeconds)"
Write-Host "DirectReports to $($c2.TotalSeconds)"

In my tenant with 67,680 users, this took 142 seconds for expanding manager, and 117 seconds for direct reports. That is actually the opposite of what I thought, as I was sure that expanding manager would be way faster, since direct reports are multi valued.

Now, from this we can either go through directReports:

$usersWithDirectReports = Get-MgUser -PageSize 999 -All -ExpandProperty directReports
$usersWithDirectReports | Where-Object directReports | Measure-Object

# 923 users

Or using the manager field:

$usersWithManager = Get-MgUser -PageSize 999 -All -ExpandProperty manager
$usersById = $usersWithManager | Group-Object id -AsHashTable

$usersWithManager | 
Where-Object Manager | 
ForEach-Object {$_.Manager.id} | 
Sort-Object -Unique | 
ForEach-Object {$usersById[$_]} | 
measure

# 923 users

So, the total runtime is way faster, between 2 and 3 minutes, and it is also way simplier than the original code that we started with, just because we utilized the expand feature.

So, hope that helps someone 🙂

Working around custom security attribute limitations in .NET Graph SDK

Microsoft Graph SDK uses Kiota for serialization of generic objects from Graph, and this currently causes major headaches. I wanted to get two custom security attributes (CSAs) for my users, namely “personalinformation” attribute set and “firstname” and “lastname”, but the generic AdditionalData dictionary for CustomSecurityAttributes on Microsoft.Graph.Models.User was litterally impossible to use, even though the values were present i non-public members (_Properties). The issue is documented on github .

The solution I came up with is not optimal but works:

internal class DefaultStudentDisplayNameHandler : IStudentDisplayNameHandler
    {
        public string GetDisplayName(User user)
        {
            // Check whether CSA is present and return that
            if(user.CustomSecurityAttributes != null) {
                if(user.CustomSecurityAttributes.AdditionalData.ContainsKey("personalinformation")) {
                    var personalInformation = (UntypedNode) user.CustomSecurityAttributes.AdditionalData["personalinformation"];

                    // This is a somewhat funky workaround for this issue: https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/2442
                    var tempJson = KiotaJsonSerializer.SerializeAsString(personalInformation);
                    var asDict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(tempJson);

                    if(asDict != null && asDict.TryGetValue("lastname", out var lastname) && asDict.TryGetValue("firstname", out var firstname)) {
                        return $"{firstname} {lastname}";
                    }
                }
            }
            
            return user.DisplayName ?? user.Id ?? "Unknown";
        }
    }

Essentially – serialize to json and deserialize back into a Dictionary<string,object>. Hope this solution helps someone to not spend 2 hours crawling the web with no luck.

Blazor WASM in Azure Static Web Apps 404 when authenticating with Entra ID

Just a quick post on how to solve an issue where an Azure Static Web App with Blazor WASM and Entra ID sign-in causes a 404 not found when redirected back to authentication/login.

Essentially, in your Blazor WASM project you will have a web.config that should fix this, but is not compatible with Azure Static Web Apps:

To workaround this, simply place a staticwebapp.config.json file in your wwwroot folder with the following contents:

{
    "navigationFallback": {
        "rewrite": "/index.html"
    }
}

This will cause all navigation to point to index.html and your Blazor WASM app.

Issue when configuring Entra Cloud Sync

Just reporting about a quick issue I found when configuring Entra Cloud Sync. The user that was trying to configure the agent had Global Administrator assigned through PIM for Groups, meaning he was eligible member of a Group, and the group was active Global Administrator.

This will cause an error message:

Please provide the Azure AD credentials of a global administrator or a hybrid administrator

To fix, add yourself as a Global Admin in other means than through being eligible for a group – such as directly!

Populating a SQL database directly from Entra ID using ECMA Connector Host

I am often asked to come up with solutions for populating different types of applications with user data. One fairly common thing is to require Entra ID users to be populated in a database of some sort, and while we could very easily PowerShell our way through that, reading from the Graph and send SQL queries, we want an out of the box solution. This exists, and is called ECMA Connector Host.

And what does ECMA stand for? ECMA stands for Extensible Connectivity Management Agent and stems from the world of Microsoft Identity Manager (MIM). Using the Connector Host, Entra ID has a feature to actually run MIM connectors, without requiring the full MIM installation.

Let’s dive into configuring this!

Continue reading “Populating a SQL database directly from Entra ID using ECMA Connector Host”

Testing out the Entra ID inbound provisioning API

With the public preview of the new API-driven inbound provisioning for Entra ID (Previously known as Azure Active Directory), Microsoft is enabling new methods for integrating HR systems or other sources of record for employees or users. These APIs can be used by HR vendors to directly integrate their HR systems to Entra ID, or by system integrators reading data from services like ERP and writing it to Entra ID. There has of course always been the option of creating users through Microsoft Graph, but this does not support on-premises Active Directory.

Just as with how the Workday and SuccessFactors integrations have been working up until now, hybrid configurations with plain old Active Directory is also supported through a provisioning agent, which we will configure in this blog post.

Continue reading “Testing out the Entra ID inbound provisioning API”

Testing out Azure AD protected actions for securing conditional access policy management

Azure AD protected actions, currently in public preview, is a feature where certain actions (currently a very limited set of actions revolving conditinal access) can require a specific authentication context before being allowed. Let’s have a look at what we can use this for. We are going to try out creating a policy to ensure that only users using a phishing resistant form of authentication can actually manage conditional access policies and named locations. The feature can also be used to, say, require the user to be in a trusted location to manage conditional access, or to require a compliant device.

Continue reading “Testing out Azure AD protected actions for securing conditional access policy management”

Signing into Azure joined virtual machine from any device

Up until recently, Azure AD login to virtual machines was severely limited, because your device had to be joined to the same Azure AD as the virtual machine. For many, this was fine, but with external consultants this soon becomes cumbersome.

However, this is no longer the case, and you can actually sign in with any user in the same tenant as the virtual machine (No B2B currently). Let me show you how!

Continue reading “Signing into Azure joined virtual machine from any device”