{"id":7144,"date":"2023-06-24T21:42:50","date_gmt":"2023-06-24T20:42:50","guid":{"rendered":"https:\/\/rakhesh.com\/?p=7144"},"modified":"2023-07-26T23:31:49","modified_gmt":"2023-07-26T22:31:49","slug":"graph-api-group-delta-changes-contd","status":"publish","type":"post","link":"https:\/\/rakhesh.com\/azure\/graph-api-group-delta-changes-contd\/","title":{"rendered":"Graph API group delta changes (contd.)"},"content":{"rendered":"

I had to conclude the previous post<\/a> abruptly. Thinking about it later, I realized I hadn’t shown any proper examples and such. So here goes.<\/p>\n

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.<\/p>\n

> $sourceGroupId = \"83e46fb6-9557-416c-97c4-2b960f0be57a\"\r\n> $destinationGroupId = \"98f792dd-bb9f-4245-b927-7300993c8e8d\"\r\n> Get-MgGroupMember -GroupId $sourceGroupId\r\n\r\nId                                   DeletedDateTime\r\n--                                   ---------------\r\n0e262fa3-9c95-4455-b6c0-b75c50e6c274\r\nb269b48d-afa4-49ed-a26e-d684531b62c7\r\n8b14e387-2321-4f44-b3c9-8b5837911d8f\r\n\r\n> Get-MgGroupMember -GroupId $destinationGroupId\r\n><\/pre>\n

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.<\/p>\n

Here’s the previous script modified to do just that:<\/p>\n

$sourceGroupId = \"83e46fb6-9557-416c-97c4-2b960f0be57a\"\r\n$graphURL = 'https:\/\/graph.microsoft.com\/v1.0\/groups\/delta\/?$filter=id eq ''' + $sourceGroupId + '''&$select=members'\r\n$destinationGroupId = \"98f792dd-bb9f-4245-b927-7300993c8e8d\"\r\n\r\n# Initial call\r\n$results = Invoke-MgGraphRequest -Method 'GET' -Uri $graphURL\r\n\r\nwhile ($null -ne $results.'@odata.nextLink') {\r\n    # Process each result from the API call\r\n    foreach ($result in $results.value) {\r\n        Write-Host \"Group id: $($result.id)\"\r\n\r\n        # Are there any membership changes?\r\n        if ($result.'members@delta') {\r\n            # If so process each change\r\n            foreach ($member in $result.'members@delta') {\r\n                # Only process users though, not groups or other membership changes\r\n                if ($member.'@odata.type' -eq '#microsoft.graph.user') {\r\n                    # If it doesn't have a '@removed' property then the member was added\r\n                    if ($null -eq $member.'@removed') {\r\n                        Write-Host \"+++ Adding user: $($member.id) +++\"\r\n                        try {\r\n                            New-MgGroupMember -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop\r\n                        } catch {\r\n                            if ($_.Exception.Message -notmatch \"already exist\") {\r\n                                Write-Host \"Error adding $($member.id)\"\r\n                            }\r\n                        }\r\n                    # Else it was removed\r\n                    } else {\r\n                        Write-Host \"--- Removing user: $($member.id) ---\"\r\n                        try {\r\n                            Remove-MgGroupMemberByRef -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop\r\n                        } catch {\r\n                            if ($_.Exception.Message -notmatch \"not present\") {\r\n                                Write-Host \"Error removing $($member.id)\"\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        } elseif ($result.'@removed') {\r\n            # I know the group exists, so this bit doesn't really matter. But I'll leave it behind from before.\r\n            Write-Host \"The group was deleted\"\r\n        } else {\r\n            Write-Host \"No change\"\r\n        }\r\n    }\r\n\r\n    # Make the next API call\r\n    $results = Invoke-MgGraphRequest -Method 'GET' $results.'@odata.nextLink'\r\n}\r\n\r\n$deltaLink = $results.'@odata.deltaLink'<\/pre>\n

The output looks like this:<\/p>\n

Group id: 83e46fb6-9557-416c-97c4-2b960f0be57a\r\n+++ Adding user: b269b48d-afa4-49ed-a26e-d684531b62c7 +++\r\n+++ Adding user: 0e262fa3-9c95-4455-b6c0-b75c50e6c274 +++\r\n+++ Adding user: 8b14e387-2321-4f44-b3c9-8b5837911d8f +++<\/pre>\n

And now the destination group has these members:<\/p>\n

> Get-MgGroupMember -GroupId $destinationGroupId\r\n\r\nId                                   DeletedDateTime\r\n--                                   ---------------\r\n0e262fa3-9c95-4455-b6c0-b75c50e6c274\r\nb269b48d-afa4-49ed-a26e-d684531b62c7\r\n8b14e387-2321-4f44-b3c9-8b5837911d8f<\/pre>\n

Let me add someone to the source group now.<\/p>\n

And let’s see if that shows up if I query the delta link:<\/p>\n

> Invoke-MgGraphRequest -Method 'GET' -Uri $deltaLink\r\n\r\nName                           Value\r\n----                           -----\r\n@odata.deltaLink               https:\/\/graph.microsoft.com\/v1.0\/groups\/delta\/?$deltatoken=M4aXDbhIMw-MLPzz_VXZ_nV4Rv4MCjA0v433FRA-W_p2YCOiHMy-F5ORNMBkzZ0hhi9gzmVQWtbC\u2026\r\nvalue                          {83e46fb6-9557-416c-97c4-2b960f0be57a}\r\n@odata.context                 https:\/\/graph.microsoft.com\/v1.0\/$metadata#groups<\/pre>\n

Yessir, it does!<\/p>\n

There is a small problem though. Previously my code was doing:<\/p>\n

while ($null -ne $results.'@odata.nextLink') { # process stuff }<\/pre>\n

But that’s not going to work coz there is no @odata.nextLink<\/code> to begin with.<\/p>\n

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<\/code> 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<\/code>and also keep looping until<\/em> @odata.deltaLink<\/code> is not null.<\/p>\n

Hmm. Here goes:<\/p>\n

do {\r\n    # Notice I am using the delta link for my first invocation now\r\n    $results = Invoke-MgGraphRequest -Method 'GET' -Uri $deltaLink\r\n\r\n    # Process each result from the API call\r\n    foreach ($result in $results.value) {\r\n        Write-Host \"Group id: $($result.id)\"\r\n\r\n        # Are there any membership changes?\r\n        if ($result.'members@delta') {\r\n            # If so process each change\r\n            foreach ($member in $result.'members@delta') {\r\n                # Only process users though, not groups or other membership changes\r\n                if ($member.'@odata.type' -eq '#microsoft.graph.user') {\r\n                    # If it doesn't have a '@removed' property then the member was added\r\n                    if ($null -eq $member.'@removed') {\r\n                        Write-Host \"+++ Adding user: $($member.id) +++\"\r\n                        try {\r\n                            New-MgGroupMember -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop\r\n                        } catch {\r\n                            if ($_.Exception.Message -notmatch \"already exist\") {\r\n                                Write-Host \"Error adding $($member.id)\"\r\n                            }\r\n                        }\r\n                    # Else it was removed\r\n                    } else {\r\n                        Write-Host \"--- Removing user: $($member.id) ---\"\r\n                        try {\r\n                            Remove-MgGroupMemberByRef -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop\r\n                        } catch {\r\n                            if ($_.Exception.Message -notmatch \"not present\") {\r\n                                Write-Host \"Error removing $($member.id)\"\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        } elseif ($result.'@removed') {\r\n            # I know the group exists, so this bit doesn't really matter. But I'll leave it behind from before.\r\n            Write-Host \"The group was deleted\"\r\n        } else {\r\n            Write-Host \"No change\"\r\n        }\r\n    }\r\n} until ($null -ne $results.'@odata.deltaLink')<\/pre>\n

This works:<\/p>\n

Group id: 83e46fb6-9557-416c-97c4-2b960f0be57a\r\n+++ Adding user: 4c97af8b-6ec4-41fe-bd03-8158590fe2e6 +++<\/pre>\n

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<\/code> if that exists – which is possible if I have more than one page of results.<\/p>\n

Here’s the final version.<\/p>\n

$sourceGroupId = \"83e46fb6-9557-416c-97c4-2b960f0be57a\"\r\n$destinationGroupId = \"98f792dd-bb9f-4245-b927-7300993c8e8d\"\r\n\r\nif ($deltaLink.Length -ne 0) {\r\n    $graphURL = $deltaLink\r\n} else {\r\n    $graphURL = 'https:\/\/graph.microsoft.com\/v1.0\/groups\/delta\/?$filter=id eq ''' + $sourceGroupId + '''&$select=members'\r\n}\r\n\r\ndo {\r\n    # Notice I am using the delta link for my first invocation now\r\n    $results = Invoke-MgGraphRequest -Method 'GET' -Uri $graphURL\r\n\r\n    # Process each result from the API call\r\n    foreach ($result in $results.value) {\r\n        Write-Host \"Group id: $($result.id)\"\r\n\r\n        # Are there any membership changes?\r\n        if ($result.'members@delta') {\r\n            # If so process each change\r\n            foreach ($member in $result.'members@delta') {\r\n                # Only process users though, not groups or other membership changes\r\n                if ($member.'@odata.type' -eq '#microsoft.graph.user') {\r\n                    # If it doesn't have a '@removed' property then the member was added\r\n                    if ($null -eq $member.'@removed') {\r\n                        Write-Host \"+++ Adding user: $($member.id) +++\"\r\n                        try {\r\n                            New-MgGroupMember -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop\r\n                        } catch {\r\n                            if ($_.Exception.Message -notmatch \"already exist\") {\r\n                                Write-Host \"Error adding $($member.id)\"\r\n                            }\r\n                        }\r\n                    # Else it was removed\r\n                    } else {\r\n                        Write-Host \"--- Removing user: $($member.id) ---\"\r\n                        try {\r\n                            Remove-MgGroupMemberByRef -GroupId $destinationGroupId -DirectoryObjectId $member.id -ErrorAction Stop\r\n                        } catch {\r\n                            if ($_.Exception.Message -notmatch \"not present\") {\r\n                                Write-Host \"Error removing $($member.id)\"\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        } elseif ($result.'@removed') {\r\n            # I know the group exists, so this bit doesn't really matter. But I'll leave it behind from before.\r\n            Write-Host \"The group was deleted\"\r\n        } else {\r\n            Write-Host \"No change\"\r\n        }\r\n    }\r\n\r\n    # Capture the nextLink\r\n    $graphURL = $results.'@odata.nextLink'\r\n\r\n    # What if nextLink is empty? Wouldn't my Graph call in the next iteration fail?\r\n    # No, because if nextLink is empty then deltaLink is not empty, so the loop exits\r\n} until ($null -ne $results.'@odata.deltaLink')\r\n\r\n$deltaLink = $results.'@odata.deltaLink'<\/pre>\n

Destination changes<\/h3>\n

Here’s the two groups as of now:<\/p>\n

> Get-MgGroupMember -GroupId $sourceGroupId\r\n\r\nId                                   DeletedDateTime\r\n--                                   ---------------\r\n0e262fa3-9c95-4455-b6c0-b75c50e6c274\r\nb269b48d-afa4-49ed-a26e-d684531b62c7\r\n5c6ba31e-43c6-4ef2-9ad7-447e24eb620a\r\n8b14e387-2321-4f44-b3c9-8b5837911d8f\r\nc16c06bc-4985-4610-847e-7e9a4057733f\r\n72687d4f-57b7-49d1-9748-069dc85b8b4d\r\n4c97af8b-6ec4-41fe-bd03-8158590fe2e6\r\n\r\n> Get-MgGroupMember -GroupId $destinationGroupId\r\n\r\nId                                   DeletedDateTime\r\n--                                   ---------------\r\n0e262fa3-9c95-4455-b6c0-b75c50e6c274\r\nb269b48d-afa4-49ed-a26e-d684531b62c7\r\n5c6ba31e-43c6-4ef2-9ad7-447e24eb620a\r\n8b14e387-2321-4f44-b3c9-8b5837911d8f\r\nc16c06bc-4985-4610-847e-7e9a4057733f\r\n72687d4f-57b7-49d1-9748-069dc85b8b4d\r\n4c97af8b-6ec4-41fe-bd03-8158590fe2e6<\/pre>\n

They are identical. Now I remove someone from the destination group.<\/p>\n

> Remove-MgGroupMemberByRef -groupId $destinationGroupId -DirectoryObjectId \"0e262fa3-9c95-4455-b6c0-b75c50e6c274\"\r\n> Get-MgGroupMember -GroupId $destinationGroupId\r\n\r\nId                                   DeletedDateTime\r\n--                                   ---------------\r\nb269b48d-afa4-49ed-a26e-d684531b62c7\r\n5c6ba31e-43c6-4ef2-9ad7-447e24eb620a\r\n8b14e387-2321-4f44-b3c9-8b5837911d8f\r\nc16c06bc-4985-4610-847e-7e9a4057733f\r\n72687d4f-57b7-49d1-9748-069dc85b8b4d\r\n4c97af8b-6ec4-41fe-bd03-8158590fe2e6<\/pre>\n

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.<\/p>\n

> Invoke-MgGraphRequest -Method 'GET' -Uri $deltaLink\r\n\r\nName                           Value\r\n----                           -----\r\n@odata.deltaLink               https:\/\/graph.microsoft.com\/v1.0\/groups\/delta\/?$deltatoken=M4aXDbhIMw-MLPzz_VXZ_nV4Rv4MCjA0v433FRA-W_rZTJ-Fjc6UE2mYijaY1YvJ5yyoxufn0e3T\u2026\r\nvalue                          {}\r\n@odata.context                 https:\/\/graph.microsoft.com\/v1.0\/$metadata#groups<\/pre>\n

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<\/em> them on the source side then they will get added back.<\/p>\n","protected":false},"excerpt":{"rendered":"

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 … Continue reading Graph API group delta changes (contd.)<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false}}},"categories":[887],"tags":[762,1016,1015],"jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/posts\/7144"}],"collection":[{"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/comments?post=7144"}],"version-history":[{"count":4,"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/posts\/7144\/revisions"}],"predecessor-version":[{"id":7287,"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/posts\/7144\/revisions\/7287"}],"wp:attachment":[{"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/media?parent=7144"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/categories?post=7144"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rakhesh.com\/wp-json\/wp\/v2\/tags?post=7144"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}