{"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 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 The output looks like this:<\/p>\n And now the destination group has these members:<\/p>\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 Yessir, it does!<\/p>\n There is a small problem though. Previously my code was doing:<\/p>\n But that’s not going to work coz there is no 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 Hmm. Here goes:<\/p>\n This works:<\/p>\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 Here’s the final version.<\/p>\n Here’s the two groups as of now:<\/p>\n They are identical. Now I remove someone from the destination group.<\/p>\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 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}]}}> $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
$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
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
> 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
> 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
while ($null -ne $results.'@odata.nextLink') { # process stuff }<\/pre>\n
@odata.nextLink<\/code> to begin with.<\/p>\n
@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
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
Group id: 83e46fb6-9557-416c-97c4-2b960f0be57a\r\n+++ Adding user: 4c97af8b-6ec4-41fe-bd03-8158590fe2e6 +++<\/pre>\n
@odata.nextLink<\/code> if that exists – which is possible if I have more than one page of results.<\/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
> 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
> 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
> 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