Home Dynamic graph filter generation
Post
Cancel

Dynamic graph filter generation

Requesting and processing data can be costly when done carelessly. You could make a request to the API for all users but that may well take multiple paging requests before you get them all.
Instead of requesting all the users and filtering the result, it’s far better to make the request with a filter included. That way the API only has to give you what you want.
But, sometimes you don’t know ahead of time while writing code what your filter has to look like. You might know you want guest users, but you only want a certain subset of guest users and not know exactly which ones while writing the script.
That’s where this solution comes in.

Desktop View I asked the API to filter for the best jokes, but all it gave me was bad puns!

Filtering users to only give us guest users

So lets start off simple, we want to tell Graph to give us users, but we only want guest users. Try it!

GET https://graph.microsoft.com/v1.0/users?$filter=userType eq 'Guest'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",
    "value": [
        {
            "businessPhones": [],
            "displayName": "Mickey McTesterson",
            "givenName": null,
            "jobTitle": null,
            "mail": "mtesterson@domain.tld",
            "mobilePhone": null,
            "officeLocation": null,
            "preferredLanguage": null,
            "surname": null,
            "userPrincipalName": "mtesterson_domain.tld#EXT#@youronmicrosoftdomain.onmicrosoft.com",
            "id": "00000000-0000-0000-0000-000000000000"
        },
        {
            "businessPhones": [],
            "displayName": "Mickey McTestersonson",
            "etc": "cut for brevity",
        }
    ]
}

Great! But, as you’ll notice when you try this we get quite a lot of extra information back. Let’s see if we can clean it up a bit. Try it!

GET https://graph.microsoft.com/v1.0/users?$select=id,mail&$filter=userType eq 'Guest'
1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",
    "value": [
        {
            "mail": "mtesterson@domain.tld",
            "id": "00000000-0000-0000-0000-000000000000"
        },
        {
            "mail": "mtestersonson@otherdomain.tld",
            "id": "00000000-0000-0000-0000-000000000000"
        }
    ]
}

Excellent! That result is a lot cleaner. Now in the next section lets see if we can filter on that mail property!

Filtering on a property, multiple times

In the previous section we retrieved our guest users and made sure to only request the mail and id properties. Now we’re going to see how we can filter on the mail property.
As you could see our first guest users has the mail property “mtesterson@domain.tld”. If we want, we can actually filter on that property, and specifically on the domain part.

There’s a little bit of a catch here, because we’re going to filter on a part of the mail property we’ll be using the endswith filter. This makes it a complex filter. Be sure to include $count=true and a ConsistencyLevel:eventual header.

Lets try filtering for just guest users who’s mail property ends in domain.tld. Try it!

GET https://graph.microsoft.com/v1.0/users?$count=true&$select=id,mail&$filter=userType eq 'Guest' and endswith(mail, 'domain.tld')
1
2
3
4
5
6
7
8
9
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",
    "value": [
        {
            "mail": "mtesterson@domain.tld",
            "id": "00000000-0000-0000-0000-000000000000"
        }
    ]
}

We are getting just what we asked for! But what if, what if we also want to filter for guest users who’s mail property ends with otherdomain.tld too, do we have to Where-Object the result? No, we can just keep chaining endswith filters. Try it! and endswith(mail, ‘otherdomain.tld’))

GET https://graph.microsoft.com/v1.0/users?$count=true&$select=id,mail&$filter=userType eq 'Guest' and endswith(mail, 'domain.tld') and endswith(mail, 'otherdomain.tld')
1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",
    "value": [
        {
            "mail": "mtesterson@domain.tld",
            "id": "00000000-0000-0000-0000-000000000000"
        },
        {
            "mail": "mtestersonson@otherdomain.tld",
            "id": "00000000-0000-0000-0000-000000000000"
        }
    ]
}

Filtering on a property dynamically

Lets combine what we learned earlier with some powershell. Lets say we have multiple domains we want to filter for.

1
$domains = @('domain.tld','otherdomain.tld')

Nice, an array of domains! Now using a bit of foreach-object magic what we can do is take those domains and generate our graph filter dynamically based on the array.

1
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,mail&`$filter=userType eq 'Guest' and $(($domains | ForEach-Object { "endswith(mail, '$_')" }) -join " or ")&`$count=true" -headers $header)    

What this will result in is the exact same uri we ended the previous paragraph with but without hardcoding the endswith filters in. The filter is generated on the basis of our array.

GET https://graph.microsoft.com/v1.0/users?$count=true&$select=id,mail&$filter=userType eq 'Guest' and endswith(mail, 'domain.tld') and endswith(mail, 'otherdomain.tld')

My use case

The use case for this was the new tenant offboarding feature that you’ll see added to CIPP soon. It will include a feature to automatically remove all guests users from your customers tenant that originate from your tenant.
How will CIPP do that you may ask?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if ($request.body.RemoveCSPGuestUsers) {
    # Delete guest users whose domains match the CSP tenants
    try {
        try {
            # Request all the domains present in the CSP tenant
            $domains = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/domains?`$select=id" -tenantid $env:TenantId -NoAuthCheck:$true).id

            # Dynamically generate a filter based on the previously requested domains
            $CSPGuestUsers = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,mail&`$filter=userType eq 'Guest' and $(($domains | ForEach-Object { "endswith(mail, '$_')" }) -join " or ")&`$count=true" -tenantid $Tenantfilter -ComplexFilter)    
        } catch {
            $errors.Add("Failed to retrieve guest users: $($_.Exception.message)")
        }

        # If there are guest users present matching the filter, we'll build a batch graph request to delete them
        if ($CSPGuestUsers) {
            [System.Collections.Generic.List[PSCustomObject]]$BulkRequests = @($CSPGuestUsers | ForEach-Object {
                @{
                    id = $($_.id)
                    method = "DELETE"
                    url = "/users/$($_.id)"
                }
            })

            # Send the request to the customer tenant to delete the guests
            $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter

            $results.Add("Succesfully removed guest users")
        } else {
            $results.Add("No guest users found to remove")
        }
    } catch {
        $errors.Add("Something went wrong while deleting guest users: $($_.Exception.message)")
    }
}

Conclusion

And that’s it! Now you know you can dynamically build a filter instead of harcoding in an endless number of conditions. I’ve used this in in a personal script too where I used it to dynamically generate a filter to match only users with a specific license. Instead of endlessly adding conditions for certain skuIds I could just ForEach-Object through an array.

Have fun and script wisely!

This post is licensed under CC BY 4.0 by the author.