I had to conclude the previous post abruptly. Thinking about it later, I realized I hadn’t shown any proper examples and such. So here goes.
I have a source group with 3 members. The group Id is “83e46fb6-9557-416c-97c4-2b960f0be57a”. I have a destination group that’s currently empty. Its id is “98f792dd-bb9f-4245-b927-7300993c8e8d”. I want to sync (one way) from the source to the destination.
1 2 3 4 5 6 7 8 9 10 11 12 |
> $sourceGroupId = "83e46fb6-9557-416c-97c4-2b960f0be57a" > $destinationGroupId = "98f792dd-bb9f-4245-b927-7300993c8e8d" > Get-MgGroupMember -GroupId $sourceGroupId Id DeletedDateTime -- --------------- 0e262fa3-9c95-4455-b6c0-b75c50e6c274 b269b48d-afa4-49ed-a26e-d684531b62c7 8b14e387-2321-4f44-b3c9-8b5837911d8f > Get-MgGroupMember -GroupId $destinationGroupId > |
The beauty of the delta API, like I alluded to earlier, is that the first request gives you a list of members as additions. Subsequent delta requests continue to give both additions and deletions. So one can use the same code to do both the initial sync and further delta syncs.
Here’s the previous script modified to do just that:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
$sourceGroupId = "83e46fb6-9557-416c-97c4-2b960f0be57a" $graphURL = 'https://graph.microsoft.com/v1.0/groups/delta/?$filter=id eq ''' + $sourceGroupId + '''&$select=members' $destinationGroupId = "98f792dd-bb9f-4245-b927-7300993c8e8d" # 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 "+++ Adding user: $($member.id) +++" try { New-MgGroupMember -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop } catch { if ($_.Exception.Message -notmatch "already exist") { Write-Host "Error adding $($member.id)" } } # Else it was removed } else { Write-Host "--- Removing user: $($member.id) ---" try { Remove-MgGroupMemberByRef -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop } catch { if ($_.Exception.Message -notmatch "not present") { Write-Host "Error removing $($member.id)" } } } } } } elseif ($result.'@removed') { # I know the group exists, so this bit doesn't really matter. But I'll leave it behind from before. 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' |
The output looks like this:
1 2 3 4 |
Group id: 83e46fb6-9557-416c-97c4-2b960f0be57a +++ Adding user: b269b48d-afa4-49ed-a26e-d684531b62c7 +++ +++ Adding user: 0e262fa3-9c95-4455-b6c0-b75c50e6c274 +++ +++ Adding user: 8b14e387-2321-4f44-b3c9-8b5837911d8f +++ |
And now the destination group has these members:
1 2 3 4 5 6 7 |
> Get-MgGroupMember -GroupId $destinationGroupId Id DeletedDateTime -- --------------- 0e262fa3-9c95-4455-b6c0-b75c50e6c274 b269b48d-afa4-49ed-a26e-d684531b62c7 8b14e387-2321-4f44-b3c9-8b5837911d8f |
Let me add someone to the source group now.
And let’s see if that shows up if I query the delta link:
1 2 3 4 5 6 7 |
> Invoke-MgGraphRequest -Method 'GET' -Uri $deltaLink Name Value ---- ----- @odata.deltaLink https://graph.microsoft.com/v1.0/groups/delta/?$deltatoken=M4aXDbhIMw-MLPzz_VXZ_nV4Rv4MCjA0v433FRA-W_p2YCOiHMy-F5ORNMBkzZ0hhi9gzmVQWtbC… value {83e46fb6-9557-416c-97c4-2b960f0be57a} @odata.context https://graph.microsoft.com/v1.0/$metadata#groups |
Yessir, it does!
There is a small problem though. Previously my code was doing:
1 |
while ($null -ne $results.'@odata.nextLink') { # process stuff } |
But that’s not going to work coz there is no @odata.nextLink
to begin with.
This is something I should be wary of with the first delta link call too, I suppose. Because in my testing I always got a @odata.nextLink
I just kept looping until that was none; but I wonder if that’s always the case. Instead, what I should really focus on is @odata.deltaLink
and also keep looping until @odata.deltaLink
is not null.
Hmm. Here goes:
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 37 38 39 40 41 42 43 44 45 |
do { # Notice I am using the delta link for my first invocation now $results = Invoke-MgGraphRequest -Method 'GET' -Uri $deltaLink # 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 "+++ Adding user: $($member.id) +++" try { New-MgGroupMember -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop } catch { if ($_.Exception.Message -notmatch "already exist") { Write-Host "Error adding $($member.id)" } } # Else it was removed } else { Write-Host "--- Removing user: $($member.id) ---" try { Remove-MgGroupMemberByRef -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop } catch { if ($_.Exception.Message -notmatch "not present") { Write-Host "Error removing $($member.id)" } } } } } } elseif ($result.'@removed') { # I know the group exists, so this bit doesn't really matter. But I'll leave it behind from before. Write-Host "The group was deleted" } else { Write-Host "No change" } } } until ($null -ne $results.'@odata.deltaLink') |
This works:
1 2 |
Group id: 83e46fb6-9557-416c-97c4-2b960f0be57a +++ Adding user: 4c97af8b-6ec4-41fe-bd03-8158590fe2e6 +++ |
I should rewrite the code a bit so it can handle both initial invocation as well as subsequent delta invocations. I can do this based on whether I have a delta URL stored or not. Also, there is a bug in the above in that I am not capturing @odata.nextLink
if that exists – which is possible if I have more than one page of results.
Here’s the final version.
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
$sourceGroupId = "83e46fb6-9557-416c-97c4-2b960f0be57a" $destinationGroupId = "98f792dd-bb9f-4245-b927-7300993c8e8d" if ($deltaLink.Length -ne 0) { $graphURL = $deltaLink } else { $graphURL = 'https://graph.microsoft.com/v1.0/groups/delta/?$filter=id eq ''' + $sourceGroupId + '''&$select=members' } do { # Notice I am using the delta link for my first invocation now $results = Invoke-MgGraphRequest -Method 'GET' -Uri $graphURL # 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 "+++ Adding user: $($member.id) +++" try { New-MgGroupMember -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop } catch { if ($_.Exception.Message -notmatch "already exist") { Write-Host "Error adding $($member.id)" } } # Else it was removed } else { Write-Host "--- Removing user: $($member.id) ---" try { Remove-MgGroupMemberByRef -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop } catch { if ($_.Exception.Message -notmatch "not present") { Write-Host "Error removing $($member.id)" } } } } } } elseif ($result.'@removed') { # I know the group exists, so this bit doesn't really matter. But I'll leave it behind from before. Write-Host "The group was deleted" } else { Write-Host "No change" } } # Capture the nextLink $graphURL = $results.'@odata.nextLink' # What if nextLink is empty? Wouldn't my Graph call in the next iteration fail? # No, because if nextLink is empty then deltaLink is not empty, so the loop exits } until ($null -ne $results.'@odata.deltaLink') $deltaLink = $results.'@odata.deltaLink' |
Destination changes
Here’s the two groups as of now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
> Get-MgGroupMember -GroupId $sourceGroupId Id DeletedDateTime -- --------------- 0e262fa3-9c95-4455-b6c0-b75c50e6c274 b269b48d-afa4-49ed-a26e-d684531b62c7 5c6ba31e-43c6-4ef2-9ad7-447e24eb620a 8b14e387-2321-4f44-b3c9-8b5837911d8f c16c06bc-4985-4610-847e-7e9a4057733f 72687d4f-57b7-49d1-9748-069dc85b8b4d 4c97af8b-6ec4-41fe-bd03-8158590fe2e6 > Get-MgGroupMember -GroupId $destinationGroupId Id DeletedDateTime -- --------------- 0e262fa3-9c95-4455-b6c0-b75c50e6c274 b269b48d-afa4-49ed-a26e-d684531b62c7 5c6ba31e-43c6-4ef2-9ad7-447e24eb620a 8b14e387-2321-4f44-b3c9-8b5837911d8f c16c06bc-4985-4610-847e-7e9a4057733f 72687d4f-57b7-49d1-9748-069dc85b8b4d 4c97af8b-6ec4-41fe-bd03-8158590fe2e6 |
They are identical. Now I remove someone from the destination group.
1 2 3 4 5 6 7 8 9 10 11 |
> Remove-MgGroupMemberByRef -groupId $destinationGroupId -DirectoryObjectId "0e262fa3-9c95-4455-b6c0-b75c50e6c274" > Get-MgGroupMember -GroupId $destinationGroupId Id DeletedDateTime -- --------------- b269b48d-afa4-49ed-a26e-d684531b62c7 5c6ba31e-43c6-4ef2-9ad7-447e24eb620a 8b14e387-2321-4f44-b3c9-8b5837911d8f c16c06bc-4985-4610-847e-7e9a4057733f 72687d4f-57b7-49d1-9748-069dc85b8b4d 4c97af8b-6ec4-41fe-bd03-8158590fe2e6 |
Will this person get added on the next run? An easy to check that is to run the delta Link to see if it shows any additions.
1 2 3 4 5 6 7 |
> Invoke-MgGraphRequest -Method 'GET' -Uri $deltaLink Name Value ---- ----- @odata.deltaLink https://graph.microsoft.com/v1.0/groups/delta/?$deltatoken=M4aXDbhIMw-MLPzz_VXZ_nV4Rv4MCjA0v433FRA-W_rZTJ-Fjc6UE2mYijaY1YvJ5yyoxufn0e3T… value {} @odata.context https://graph.microsoft.com/v1.0/$metadata#groups |
No surprises there, it does not. So this way we can add/ remove users to the destination and they won’t be affected by the sync. Of course, if I remove someone in the destination group but also remove and add them on the source side then they will get added back.