From 970a30a0386243481f8a62dc9f01702dfcc642ea Mon Sep 17 00:00:00 2001 From: Per Jakobsen Date: Fri, 3 Jul 2026 10:16:32 +0200 Subject: [PATCH 1/2] Fix broken add-owner/add-member Graph calls in Groups utilities UpdateOwners/UpdateMembers in both GroupsUtility and UnifiedGroupsUtility passed accessToken positionally into MakePostRequest parameter 2, which is the request body (object content) - not the access token. This sent the token as the POST body with no auth header and no @odata.id reference payload, so adding owners/members was non-functional. The bug came from the Graph SDK -> HttpHelper migration (2435d060, cf619edd). Align all four add/remove paths with the working ObjectTeams pattern: - POST .../{owners|members}/$ref with an {"@odata.id": ".../users/{id}"} body, application/json content type, and accessToken in the correct slot - Correct the DELETE reference URLs from /ref to /$ref Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/PnP.Framework/Graph/GroupsUtility.cs | 14 ++++++++------ .../PnP.Framework/Graph/UnifiedGroupsUtility.cs | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/lib/PnP.Framework/Graph/GroupsUtility.cs b/src/lib/PnP.Framework/Graph/GroupsUtility.cs index 354f67f1f..9dc5d43ff 100644 --- a/src/lib/PnP.Framework/Graph/GroupsUtility.cs +++ b/src/lib/PnP.Framework/Graph/GroupsUtility.cs @@ -121,8 +121,9 @@ private static void UpdateMembers(string[] members, string groupId, bool removeO try { // And if any, add it to the collection of group's members - var memberUrl = $"{groupRequestUrl}/members/{user.Id}/ref"; - HttpHelper.MakePostRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + var memberUrl = $"{groupRequestUrl}/members/$ref"; + object content = new JObject { ["@odata.id"] = $"{userRequestUrl}/{user.Id}" }; + HttpHelper.MakePostRequest(memberUrl, content, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); } catch (Exception ex) when (ex.Message.Contains("Request_BadRequest") && ex.Message.Contains("added object references already exist")) @@ -150,7 +151,7 @@ private static void UpdateMembers(string[] members, string groupId, bool removeO try { // If it is not in the list of current members, just remove it - var memberUrl = $"{groupRequestUrl}/members/{member.Id}/ref"; + var memberUrl = $"{groupRequestUrl}/members/{member.Id}/$ref"; HttpHelper.MakeDeleteRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); } catch (HttpResponseException ex) when (ex.StatusCode == 400) @@ -187,8 +188,9 @@ private static void UpdateOwners(string[] owners, string groupId, bool removeOth try { // And if any, add it to the collection of group's owners - var memberUrl = $"{groupRequestUrl}/owners/{user.Id}/ref"; - HttpHelper.MakePostRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + var memberUrl = $"{groupRequestUrl}/owners/$ref"; + object content = new JObject { ["@odata.id"] = $"{userRequestUrl}/{user.Id}" }; + HttpHelper.MakePostRequest(memberUrl, content, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); } catch (Exception ex) when (ex.Message.Contains("Request_BadRequest") && ex.Message.Contains("added object references already exist")) @@ -216,7 +218,7 @@ private static void UpdateOwners(string[] owners, string groupId, bool removeOth try { // If it is not in the list of current owners, just remove it - var memberUrl = $"{groupRequestUrl}/owners/{owner.Id}/ref"; + var memberUrl = $"{groupRequestUrl}/owners/{owner.Id}/$ref"; HttpHelper.MakeDeleteRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); } catch (HttpResponseException ex) when (ex.StatusCode == 400) diff --git a/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs b/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs index ea3fb453e..69f366e70 100644 --- a/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs +++ b/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs @@ -324,8 +324,9 @@ private static void UpdateMembers(string[] members, string groupId, bool removeO try { // And if any, add it to the collection of group's members - var memberUrl = $"{groupRequestUrl}/members/{user.Id}/ref"; - HttpHelper.MakePostRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + var memberUrl = $"{groupRequestUrl}/members/$ref"; + object content = new JObject { ["@odata.id"] = $"{userRequestUrl}/{user.Id}" }; + HttpHelper.MakePostRequest(memberUrl, content, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); } catch (Exception ex) when (ex.Message.Contains("Request_BadRequest") && ex.Message.Contains("added object references already exist")) @@ -353,7 +354,7 @@ private static void UpdateMembers(string[] members, string groupId, bool removeO try { // If it is not in the list of current members, just remove it - var memberUrl = $"{groupRequestUrl}/members/{member.Id}/ref"; + var memberUrl = $"{groupRequestUrl}/members/{member.Id}/$ref"; HttpHelper.MakeDeleteRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); } catch (HttpResponseException ex) when (ex.StatusCode == 400) @@ -390,8 +391,9 @@ private static void UpdateOwners(string[] owners, string groupId, bool removeOth try { // And if any, add it to the collection of group's owners - var memberUrl = $"{groupRequestUrl}/owners/{user.Id}/ref"; - HttpHelper.MakePostRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + var memberUrl = $"{groupRequestUrl}/owners/$ref"; + object content = new JObject { ["@odata.id"] = $"{userRequestUrl}/{user.Id}" }; + HttpHelper.MakePostRequest(memberUrl, content, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); } catch (Exception ex) when (ex.Message.Contains("Request_BadRequest") && ex.Message.Contains("added object references already exist")) @@ -419,7 +421,7 @@ private static void UpdateOwners(string[] owners, string groupId, bool removeOth try { // If it is not in the list of current owners, just remove it - var memberUrl = $"{groupRequestUrl}/owners/{owner.Id}/ref"; + var memberUrl = $"{groupRequestUrl}/owners/{owner.Id}/$ref"; HttpHelper.MakeDeleteRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); } catch (HttpResponseException ex) when (ex.StatusCode == 400) From fbeb513a47d10ab6ede76a7a4be839b6a7616219 Mon Sep 17 00:00:00 2001 From: Per Jakobsen Date: Fri, 3 Jul 2026 10:26:56 +0200 Subject: [PATCH 2/2] Fix delete reference URLs in standalone Remove group methods RemoveGroupMembers/RemoveGroupOwners and RemoveUnifiedGroupMembers/ RemoveUnifiedGroupOwners build their DELETE reference URL with /ref instead of /$ref, so member/owner removal via these methods (and the Clear* methods that delegate to them) hit an invalid Graph endpoint. Correct the four sites to /$ref, matching the Update* methods and the ObjectTeams pattern. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/PnP.Framework/Graph/GroupsUtility.cs | 4 ++-- src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/PnP.Framework/Graph/GroupsUtility.cs b/src/lib/PnP.Framework/Graph/GroupsUtility.cs index 9dc5d43ff..a6588c865 100644 --- a/src/lib/PnP.Framework/Graph/GroupsUtility.cs +++ b/src/lib/PnP.Framework/Graph/GroupsUtility.cs @@ -643,7 +643,7 @@ public static void RemoveGroupMembers(string groupId, string[] members, string a try { // If it is not in the list of current members, just remove it - var deleteGroupMemberUrl = $"{groupRequestUrl}/members/{userId}/ref"; + var deleteGroupMemberUrl = $"{groupRequestUrl}/members/{userId}/$ref"; HttpHelper.MakeDeleteRequest(deleteGroupMemberUrl, accessToken, retryCount: retryCount, delay: delay); } catch (HttpResponseException ex) when (ex.StatusCode == 400) @@ -696,7 +696,7 @@ public static void RemoveGroupOwners(string groupId, string[] owners, string acc try { // If it is not in the list of current owners, just remove it - var deleteGroupMemberUrl = $"{groupRequestUrl}/owners/{userId}/ref"; + var deleteGroupMemberUrl = $"{groupRequestUrl}/owners/{userId}/$ref"; HttpHelper.MakeDeleteRequest(deleteGroupMemberUrl, accessToken, retryCount: retryCount, delay: delay); } catch (HttpResponseException ex) when (ex.StatusCode == 400) diff --git a/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs b/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs index 69f366e70..ad5353510 100644 --- a/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs +++ b/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs @@ -1162,7 +1162,7 @@ public static void RemoveUnifiedGroupMembers(string groupId, string[] members, s try { // If it is not in the list of current members, just remove it - var deleteGroupMemberUrl = $"{groupRequestUrl}/members/{userId}/ref"; + var deleteGroupMemberUrl = $"{groupRequestUrl}/members/{userId}/$ref"; HttpHelper.MakeDeleteRequest(deleteGroupMemberUrl, accessToken, retryCount: retryCount, delay: delay); } catch (HttpResponseException ex) when (ex.StatusCode == 400) @@ -1215,7 +1215,7 @@ public static void RemoveUnifiedGroupOwners(string groupId, string[] owners, str try { // If it is not in the list of current owners, just remove it - var deleteGroupMemberUrl = $"{groupRequestUrl}/owners/{userId}/ref"; + var deleteGroupMemberUrl = $"{groupRequestUrl}/owners/{userId}/$ref"; HttpHelper.MakeDeleteRequest(deleteGroupMemberUrl, accessToken, retryCount: retryCount, delay: delay); } catch (HttpResponseException ex) when (ex.StatusCode == 400)