Long time no posts!
The other day at work I had to setup an automation that synced group membership changes in one group to another.
Typically, I would have read the membership of the source group, membership of the destination group, and done a quick compare of them to add/ remove elements. I script in PowerShell and thanks to a StackOverflow post I came across in the past I use the following code to do this:
1 2 3 4 5 |
# Compare with new list. This makes use of System.Linq of .NET. It calls the Enumerable.Except method. # This method returns those elements in first that don't appear in second. It doesn't return those elements in second that don't appear in first. $ignoreCaseComparer = [System.StringComparer]::OrdinalIgnoreCase # this is to make linq ignore case; thanks https://stackoverflow.com/a/66662395 $addMembers = [string[]][Linq.Enumerable]::Except([string[]]$newMembers,[string[]]$existingMembers,$ignoreCaseComparer) $removeMembers = [string[]][Linq.Enumerable]::Except([string[]]$existingMembers,[string[]]$newMembers,$ignoreCaseComparer) |
This is slow though because I have some 100+ groups to do with this and while the actual comparision is fast I still have to make all those Graph API calls to get membership and such, which eventually leads to throttling and disconnects etc. It’s not bad, and I don’t plan on replacing a working thing with something else, but with the new task I had to do I wanted to try something different.
For one, while previously I wanted to do a full sync – i.e. remove anything in the destination group that’s not present in the source group – this time I only wanted a one way sync. Any additions/ deletions in the source group must be made in the destination, but I don’t care if anyone adds/ removes entries in the destination group itself. That is to say, if my source group contains users A, B, and C; and the destination group too contains the same. Then I add D and E, I want these to sync over to the destination; but now if someone removes E from the destination I am OK with that. The source group will thus contain A, B, C, D, E, while the destination contains A, B, C, D. If someone adds F to the destination group, I am fine with that stayin on… even though the source group doesn’t have it.
The best approach for this seems to be the group: delta Graph API call. It’s a bit of a weird call coz you don’t actually run this against a particular group. You could filter against a particular group, but by default it just gives everything. For example:
1 2 3 4 5 6 7 |
> Invoke-MgGraphRequest -Method 'GET' -Uri 'https://graph.microsoft.com/v1.0/groups/delta' Name Value ---- ----- value {a883ddc6-91ce-4559-b93a-05e112d7450b, 9c9c577c-77e0-4d47-b758-94d7925a0efb, 780dfcbb-9c04-445c-9f74-380631e98e64, 4938ba57-5e96-4a3e-b… @odata.context https://graph.microsoft.com/v1.0/$metadata#groups @odata.nextLink https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=eKgfrPQSvBw5VPEZGw9FfwaG4hGdROSHnSBQ-SjtnfiMLS-StzG-D5D2tgCYt_jY0rgHwzTVfhrAjD… |
The output contains value
contains everything. Here’s an example entry from the results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Name Value ---- ----- securityIdentifier S-1-12-1-2827214278-1163497934-3775216313-189126418 displayName All Company mailEnabled True visibility Public creationOptions {YammerProvisioning} id a883ddc6-91ce-4559-b93a-05e112d7450b groupTypes {Unified} proxyAddresses {SMTP:allcompany@rak.onmicrosoft.com} renewedDateTime 04/07/2021 13:52:38 createdDateTime 04/07/2021 13:52:38 securityEnabled False members@delta {c38e8340-f903-4e63-9624-3dee3241d1f3} mailNickname allcompany mail allcompany@rak.onmicrosoft.com resourceBehaviorOptions {CalendarMemberReadOnly} description This is the default group for everyone in the network |
This is a group that currently exists in my tenant. Notice the members@delta
entry? Here’s what that contains:
1 2 3 4 |
Name Value ---- ----- @odata.type #microsoft.graph.user id c38e8340-f903-4e63-9624-3dee3241d1f3 |
If I look at the group, it has just one user and its id matches the output of members@delta
.
This gives us an idea of the output of this API. It shows every group addition and deletion in the tenant, along with any group membership additions and deletions as well as any change in other information. One can “replay” this in a way to get to the same state as things are currently.
Here’s what another entry looks like:
1 2 3 4 |
Name Value ---- ----- @removed {[reason, deleted]} id 615396b9-7bb9-4729-882e-5e151307ffce |
I couldn’t find that id anywhere in my tenant. Until I went through the audit logs and realized this is a group I deleted a few days ago (oddly it didn’t appear in the “Deleted Groups” section either). This is what I was talking about – the output includes even groups that don’t exist any more.The @removed
property tells me the reason too – the group was deleted.
The output seems to be limited to additions/ deletions only, not other changes. For instance, I renamed a group but there was no property capturing this fact.
Going back to the original output:
1 2 3 4 5 |
Name Value ---- ----- value {a883ddc6-91ce-4559-b93a-05e112d7450b, 9c9c577c-77e0-4d47-b758-94d7925a0efb, 780dfcbb-9c04-445c-9f74-380631e98e64, 4938ba57-5e96-4a3e-b… @odata.nextLink https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=eKgfrPQSvBw5VPEZGw9FfwaG4hGdROSHnSBQ-SjtnfhHBeVcfzhAA6TDWKzjjAgvV3SAck9TQ6juBn… @odata.context https://graph.microsoft.com/v1.0/$metadata#groups |
There’s also an @odata.nextLink
property. This is a link that can be called to get the next set of results. If I do that, I see the following (I am doing the first step again so I capture @odata.nextLink)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
> $results = Invoke-MgGraphRequest -Method 'GET' -Uri 'https://graph.microsoft.com/v1.0/groups/delta' > $results Name Value ---- ----- value {a883ddc6-91ce-4559-b93a-05e112d7450b, 9c9c577c-77e0-4d47-b758-94d7925a0efb, a6131882-b110-4dc6-b1ea-e8c3c5cfccdb, 780dfcbb-9c04-445c-9… @odata.nextLink https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=eKgfrPQSvBw5VPEZGw9FfwaG4hGdROSHnSBQ-SjtnfiTHbOzloTkSNn4jstzTvRmMEIDvlyo2u8u8P… @odata.context https://graph.microsoft.com/v1.0/$metadata#groups > Invoke-MgGraphRequest -Method 'GET' -Uri $results.'@odata.nextLink' Name Value ---- ----- @odata.deltaLink https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=eKgfrPQSvBw5VPEZGw9FfwaG4hGdROSHnSBQ-SjtnfieYyXuWJtQh6zgTE8gNR82IVgOAiUGfs5np… value {} @odata.context https://graph.microsoft.com/v1.0/$metadata#groups |
In the second run, I don’t get a @odata.nextLink
any more. Instead I get @odata.deltaLink
. What does this mean?
The way the delta API works is that the output includes these tokens – a skip token ($skiptoken
) and a delta token ($deltatoken
). The skip token is an indicator of how much output was returned; so calling the API again with the skip token gives you the next set of output with a new skip token. The idea is you keep calling the API with skip tokens until you get all the output and skip token is empty.
To make things easy, the API gives you a new URL each time with the skip token added. That’s what @odata.nextLink
is, which is why the URL looks like https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=eKgfrPQSvBw5VPEZGw9FfwaG4...
– the API is being helpful.
Similarly, the delta token is a token that captures the point in time or state of the output. You only get the delta token when you get the final set of results – i.e. when there is no more skip token. So essentially, you keep polling the API with skip tokens until you get no skip token; at which point you should have a delta token and so you capture that somewhere. The next time you call the API – which can be immediately or after how many every hours or days – if you do so with the delta token, it will give you all the changes since the previous invocation. Nice, huh? And at the end of that you will get a new delta token which can be used in subsequent invocations to get all the changes since then.
This is what @odata.deltaLink contains – which is why the URL looks like https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=eKgfrPQSvBw5VPEZGw9Ff..
.
All this and more can be found in the delta API docs by the way.
Here’s me invoking the URL with the delta token.
1 2 3 4 5 6 7 8 9 |
> $results2 = Invoke-MgGraphRequest -Method 'GET' -Uri $results.'@odata.nextLink' > Invoke-MgGraphRequest -Method 'GET' -Uri $results2.'@odata.deltaLink' Name Value ---- ----- @odata.deltaLink https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=eKgfrPQSvBw5VPEZGw9FfwaG4hGdROSHnSBQ-Sjtnfjgpof7dJCb2aqzjcTwppBUAQChgCTiB18yZ… value {} @odata.context https://graph.microsoft.com/v1.0/$metadata#groups |
No changes, hence value
is empty.
Let me go and rename a group now and run the same URL.
1 2 3 4 5 6 7 |
> Invoke-MgGraphRequest -Method 'GET' -Uri $results2.'@odata.deltaLink' Name Value ---- ----- @odata.deltaLink https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=eKgfrPQSvBw5VPEZGw9FfwaG4hGdROSHnSBQ-SjtnfgromWFp-OJzQCD997FZeF1KK9Z2rjh0NcDI… value {a6131882-b110-4dc6-b1ea-e8c3c5cfccdb} @odata.context https://graph.microsoft.com/v1.0/$metadata#groups |
Nice! There’s the group Id. Let’s look at the value
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
> (Invoke-MgGraphRequest -Method 'GET' -Uri $results2.'@odata.deltaLink').value Name Value ---- ----- visibility securityEnabled True mailNickname 00000000-0000-0000-0000-000000000000 securityIdentifier S-1-12-1-2786269314-1304867088-3286821553-3687632837 description Members of this group are allowed to run Office Scripts in Power Platform createdDateTime 14/06/2022 10:04:00 id a6131882-b110-4dc6-b1ea-e8c3c5cfccdb renewedDateTime 14/06/2022 10:04:00 mailEnabled False displayName Power Platform - Allow Office Scripts |
Hmm, not super helpful. I know what I changed in this case – the displayName – but there’s no way to glean that from the output. For comparison, here’s the output from the original delta query with none of the tokens. Since I stored that in a variable I can refer to it here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$results.Value[2] Name Value ---- ----- visibility securityEnabled True mailNickname 00000000-0000-0000-0000-000000000000 securityIdentifier S-1-12-1-2786269314-1304867088-3286821553-3687632837 description Members of this group are allowed to run Office Scripts in Power Platform createdDateTime 14/06/2022 10:04:00 id a6131882-b110-4dc6-b1ea-e8c3c5cfccdb renewedDateTime 14/06/2022 10:04:00 mailEnabled False displayName Power Platform - Allow Office Scripts 2 |
Turns out there’s a way to get just the difference.
1 2 3 4 5 6 7 8 |
> (Invoke-MgGraphRequest -Method 'GET' -Uri $results2.'@odata.deltaLink' -Headers @{"Prefer" = "return=minimal"}).value Name Value ---- ----- visibility displayName Power Platform - Allow Office Scripts id a6131882-b110-4dc6-b1ea-e8c3c5cfccdb securityIdentifier S-1-12-1-2786269314-1304867088-3286821553-3687632837 |
I was hoping it might tell me what the previous value is, but it doesn’t. Instead all this one does is show the changed properties, along with standard output like the id.
Here’s the same after I changed the description too:
1 2 3 4 5 6 7 8 9 |
> (Invoke-MgGraphRequest -Method 'GET' -Uri $results2.'@odata.deltaLink' -Headers @{"Prefer" = "return=minimal"}).value Name Value ---- ----- description Members of this group are allowed to run Office Scripts in Power Platform - TEST visibility displayName Power Platform - Allow Office Scripts id a6131882-b110-4dc6-b1ea-e8c3c5cfccdb securityIdentifier S-1-12-1-2786269314-1304867088-3286821553-3687632837 |
Now let me add someone and try with the same URL:
1 2 3 4 5 6 7 8 9 10 |
> (Invoke-MgGraphRequest -Method 'GET' -Uri $results2.'@odata.deltaLink' -Headers @{"Prefer" = "return=minimal"}).value Name Value ---- ----- displayName Power Platform - Allow Office Scripts description Members of this group are allowed to run Office Scripts in Power Platform - TEST visibility members@delta {b269b48d-afa4-49ed-a26e-d684531b62c7} id a6131882-b110-4dc6-b1ea-e8c3c5cfccdb securityIdentifier S-1-12-1-2786269314-1304867088-3286821553-3687632837 |
Kind of obvious, but like with other Graph API calls one can focus on the properties to be tracked. Say I am not interested in any other changes except the display name. In this case my initial query will have to mention that:
1 |
https://graph.microsoft.com/v1.0/groups/delta?$select=displayName |
And subequent delta queries will only show changes to that.
Ditto for just group membership changes, which is what I am interested in. $select=members
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 |
> $results = Invoke-MgGraphRequest -Method 'GET' -Uri 'https://graph.microsoft.com/v1.0/groups/delta?$select=members' > $results Name Value ---- ----- value {a883ddc6-91ce-4559-b93a-05e112d7450b, 9c9c577c-77e0-4d47-b758-94d7925a0efb, 780dfcbb-9c04-445c-9f74-380631e98e64, 4938ba57-5e96-4a3e-b… @odata.nextLink https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=47I-s40Sdr-3gWyfmEvd9O1xwM5sgBeb0tvvFd6567qEtR3qX7jYDWZn9bYhQ9HpBrE2NFEsgz5BV4… @odata.context https://graph.microsoft.com/v1.0/$metadata#groups(members) > $results2 = Invoke-MgGraphRequest -Method 'GET' -Uri $results.'@odata.nextLink' > $results2 Name Value ---- ----- value {} @odata.nextLink https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=eKgfrPQSvBw5VPEZGw9FfwaG4hGdROSHnSBQ-SjtnfgYzRcmJIOdjBJG3bXHoTmh1SDzwU_MPDIB8c… @odata.context https://graph.microsoft.com/v1.0/$metadata#groups > $results3 = Invoke-MgGraphRequest -Method 'GET' -Uri $results2.'@odata.nextLink' > $results3 Name Value ---- ----- @odata.deltaLink https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=47I-s40Sdr-3gWyfmEvd9O1xwM5sgBeb0tvvFd6567oHQlCb5AO_yN7ySxFZukNpHQQWE95UwSyvV… value {} @odata.context https://graph.microsoft.com/v1.0/$metadata#groups |
What is the actual output in the value
field?
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 |
$results.Value Name Value ---- ----- members@delta {c38e8340-f903-4e63-9624-3dee3241d1f3} id a883ddc6-91ce-4559-b93a-05e112d7450b members@delta {c38e8340-f903-4e63-9624-3dee3241d1f3} id 9c9c577c-77e0-4d47-b758-94d7925a0efb members@delta {b269b48d-afa4-49ed-a26e-d684531b62c7, 0e262fa3-9c95-4455-b6c0-b75c50e6c274} id 780dfcbb-9c04-445c-9f74-380631e98e64 id 4938ba57-5e96-4a3e-b069-a066dd194a55 id fac2e82c-01eb-4b71-aecb-ce7c4edebe33 members@delta {b269b48d-afa4-49ed-a26e-d684531b62c7, c38e8340-f903-4e63-9624-3dee3241d1f3} id 49cf8d72-1227-481b-9458-c212f5837760 members@delta {c38e8340-f903-4e63-9624-3dee3241d1f3} id 648cce96-5e79-440d-b6b3-4352c4bf182f members@delta {0e262fa3-9c95-4455-b6c0-b75c50e6c274, c38e8340-f903-4e63-9624-3dee3241d1f3, 208becde-72bc-4d66-b57a-b31254e7edc1, d75c5832-4a63-4ffb-8… id 62c9d890-046a-49d7-a8fe-045e82948880 id 4dcac9d8-c86c-4a27-b900-7e873d496c99 id e464452d-c63e-4669-881a-d7613aaf11fa id 98f792dd-bb9f-4245-b927-7300993c8e8d id 9bfd969d-30eb-4ee9-9ad6-1c9cbfc62d77 members@delta {b269b48d-afa4-49ed-a26e-d684531b62c7, 0e262fa3-9c95-4455-b6c0-b75c50e6c274, 8b14e387-2321-4f44-b3c9-8b5837911d8f} id 83e46fb6-9557-416c-97c4-2b960f0be57a members@delta {b269b48d-afa4-49ed-a26e-d684531b62c7} id a6131882-b110-4dc6-b1ea-e8c3c5cfccdb @removed {[reason, deleted]} id 615396b9-7bb9-4729-882e-5e151307ffce |
How can I do this better? Need a loop! 😊
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 35 36 |
# Initial call $results = Invoke-MgGraphRequest -Method 'GET' -Uri 'https://graph.microsoft.com/v1.0/groups/delta?$select=members' while ($null -ne $results.'@odata.nextLink') { # Process each result from the API call foreach ($result in $results.value) { Write-Host "Group id: $($result.id)" # Are there any membership changes? if ($result.'members@delta') { # If so process each change foreach ($member in $result.'members@delta') { # Only process users though, not groups or other membership changes if ($member.'@odata.type' -eq '#microsoft.graph.user') { # If it doesn't have a '@removed' property then the member was added if ($null -eq $member.'@removed') { Write-Host "+++ Add user: $($member.id) +++" # Else it was removed } else { Write-Host "--- Remove user: $($member.id) ---" } } } } elseif ($result.'@removed') { Write-Host "The group was deleted" } else { Write-Host "No change" } } # Make the next API call $results = Invoke-MgGraphRequest -Method 'GET' $results.'@odata.nextLink' } $deltaLink = $results.'@odata.deltaLink' Write-Host "Next time call $deltaLink" |
Here’s a sample output:
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 |
Group id: a883ddc6-91ce-4559-b93a-05e112d7450b +++ Add user: c38e8340-f903-4e63-9624-3dee3241d1f3 +++ Group id: 9c9c577c-77e0-4d47-b758-94d7925a0efb +++ Add user: c38e8340-f903-4e63-9624-3dee3241d1f3 +++ Group id: 780dfcbb-9c04-445c-9f74-380631e98e64 +++ Add user: b269b48d-afa4-49ed-a26e-d684531b62c7 +++ +++ Add user: 0e262fa3-9c95-4455-b6c0-b75c50e6c274 +++ Group id: 4938ba57-5e96-4a3e-b069-a066dd194a55 No change Group id: fac2e82c-01eb-4b71-aecb-ce7c4edebe33 No change Group id: 49cf8d72-1227-481b-9458-c212f5837760 +++ Add user: b269b48d-afa4-49ed-a26e-d684531b62c7 +++ +++ Add user: c38e8340-f903-4e63-9624-3dee3241d1f3 +++ Group id: 648cce96-5e79-440d-b6b3-4352c4bf182f +++ Add user: c38e8340-f903-4e63-9624-3dee3241d1f3 +++ Group id: 62c9d890-046a-49d7-a8fe-045e82948880 +++ Add user: 0e262fa3-9c95-4455-b6c0-b75c50e6c274 +++ +++ Add user: c38e8340-f903-4e63-9624-3dee3241d1f3 +++ <snip> Group id: 9bfd969d-30eb-4ee9-9ad6-1c9cbfc62d77 No change Group id: 83e46fb6-9557-416c-97c4-2b960f0be57a +++ Add user: b269b48d-afa4-49ed-a26e-d684531b62c7 +++ +++ Add user: 0e262fa3-9c95-4455-b6c0-b75c50e6c274 +++ +++ Add user: 8b14e387-2321-4f44-b3c9-8b5837911d8f +++ Group id: a6131882-b110-4dc6-b1ea-e8c3c5cfccdb +++ Add user: b269b48d-afa4-49ed-a26e-d684531b62c7 +++ Group id: 615396b9-7bb9-4729-882e-5e151307ffce The group was deleted |
So far I have been dealing with multiple groups. Let’s focus on one now. To do that you filter by the group id.
1 2 3 4 5 6 7 |
> Invoke-MgGraphRequest -Method 'GET' -Uri 'https://graph.microsoft.com/v1.0/groups/delta?$select=members&$filter= id eq ''a6131882-b110-4dc6-b1ea-e8c3c5cfccdb''' Name Value ---- ----- value {a6131882-b110-4dc6-b1ea-e8c3c5cfccdb} @odata.nextLink https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=M4aXDbhIMw-MLPzz_VXZ_nV4Rv4MCjA0v433FRA-W_pRssMgNCzjL1a22LsXOKV9qZNbhF-02u2iAj… @odata.context https://graph.microsoft.com/v1.0/$metadata#groups(members) |
I have to do $filter= id eq 'a6131882-b110-4dc6-b1ea-e8c3c5cfccdb'
but since I am already within single quotes I got to escape these. Thus I put $filter= id eq ''a6131882-b110-4dc6-b1ea-e8c3c5cfccdb''
(to escape the single quote put it twice).
A brief digression on the Get-MgGroupDelta
cmdlet.
It looks to be useful but is pretty useless actually. Here’s how its default output looks like:
1 2 3 4 5 6 7 8 9 10 |
Id DisplayName Description GroupTypes -- ----------- ----------- ---------- a883ddc6-91ce-4559-b93a-05e112d7450b All Company This is the default group for everyone in the network {Unified} 9c9c577c-77e0-4d47-b758-94d7925a0efb Give Planning Team Give Planning Team {Unified} a6131882-b110-4dc6-b1ea-e8c3c5cfccdb Power Platform - Allow Office Scripts Members of this group are allowed to run Office Scripts in Power Platform - TEST 780dfcbb-9c04-445c-9f74-380631e98e64 ExchangeAccessPolicy 4938ba57-5e96-4a3e-b069-a066dd194a55 MailSec Group fac2e82c-01eb-4b71-aecb-ce7c4edebe33 Password Reset Self 49cf8d72-1227-481b-9458-c212f5837760 Forms Site Forms Site {Unified} 648cce96-5e79-440d-b6b3-4352c4bf182f Test Team {Unified} |
Nice. The equivalent to get a single group would be:
1 2 3 4 5 |
> Get-MgGroupDelta -Filter "id eq 'a6131882-b110-4dc6-b1ea-e8c3c5cfccdb'" Id DisplayName Description GroupTypes -- ----------- ----------- ---------- a6131882-b110-4dc6-b1ea-e8c3c5cfccdb Power Platform - Allow Office Scripts Members of this group are allowed to run Office Scripts in Power Platform - TEST |
So far so good. The beauty of using this cmdlet is that one doesn’t have to bother with @odata.nextLink
any more. The cmdlet will loop over that and return all results. Any deltas are under AdditionalProperties
.
1 2 3 4 5 6 7 |
> $results = Get-MgGroupDelta -Filter "id eq 'a6131882-b110-4dc6-b1ea-e8c3c5cfccdb'" > $results.AdditionalProperties.'members@delta' Key Value --- ----- @odata.type #microsoft.graph.user id b269b48d-afa4-49ed-a26e-d684531b62c7 |
But where do I get the delta link for the next invocation? No idea. Or how do I invoke this cmdlet in a delta run? No idea. The help page is useless, and I couldn’t find anything in Google or GitHub. Eventually I gave up.
Back to what we were doing. Here’s the equivalent code for a single group id.
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 35 36 |
$sourceGroupId = "a6131882-b110-4dc6-b1ea-e8c3c5cfccdb" $graphURL = 'https://graph.microsoft.com/v1.0/groups/delta/?$filter=id eq ''' + $sourceGroupId + '''&$select=members' # Initial call $results = Invoke-MgGraphRequest -Method 'GET' -Uri $graphURL while ($null -ne $results.'@odata.nextLink') { # Process each result from the API call foreach ($result in $results.value) { Write-Host "Group id: $($result.id)" # Are there any membership changes? if ($result.'members@delta') { # If so process each change foreach ($member in $result.'members@delta') { # Only process users though, not groups or other membership changes if ($member.'@odata.type' -eq '#microsoft.graph.user') { # If it doesn't have a '@removed' property then the member was added if ($null -eq $member.'@removed') { Write-Host "+++ Add user: $($member.id) +++" # Else it was removed } else { Write-Host "--- Remove user: $($member.id) ---" } } } } elseif ($result.'@removed') { Write-Host "The group was deleted" } else { Write-Host "No change" } } # Make the next API call $results = Invoke-MgGraphRequest -Method 'GET' $results.'@odata.nextLink' } |
In my case I had to do this for about 40+ groups. Not a problem, I can loop over the ids. But what do I do with the delta links? How do I store it for the next time around?
Typically I would use SharePoint for such a task. In fact, I would read the group names from there and store the link in one of the columns. But this was an Azure Function I was expecting to run quite often and for some reason I didn’t want to depend on SharePoint. I felt it could be an additional point of failure – Graph or PnP.PowerShell might make a call and fail and I don’t want the group syncs to fail because of that. I wanted something local to the Function itself where I store the delta links temporarily.
I suppose I could have gone with storing them as files in the storage account associated with the Function. But then I thought let me use Azure Table storage. This too is present in the storage account after all.
I am not going to go into the details of that here. I’ll probably blog it later. As of now I spent most of my weekend morning writing the current post. 😱
Update: Continued…