Implement PUT /Groups/:id SCIM endpoint for self-managed

This is part of Implement `PUT /Groups/:id` and `PATCH /Groups/... (#509428 - closed).

This depends on Add SyncScimGroupMembersWorker for SCIM group m... (!188566 - merged).

What does this MR do and why?

This MR implements the PUT /Groups/:id SCIM endpoint for self-managed instances, using the new async worker from !188566 (merged). This endpoint allows identity providers to update SCIM group membership by fully replacing the list of users in groups. This is part of the SCIM group sync functionality being developed in epic &15990 (closed).

The work here is behind the same feature flag as previous SCIM group endpoints (self_managed_scim_group_sync), which defaults to disabled.

Key facts:

  • Replaces the entire group membership with the provided list of users
  • Consistently handles multiple group links sharing the same SCIM group ID
  • Only affects SCIM-provisioned users, preserving manually added members
  • Adds users to the group that are in the request but not currently members
  • Removes SCIM-provisioned users from the group that aren't in the request
  • Follows the same architecture pattern as the PATCH endpoint for consistency

References

How to set up and validate locally

  1. Make sure you have SAML enabled on your GDK.
  2. Enter the Rails console:
gdk rails c
  1. Create test SAML group links and user identities:
# Create a group and SAML link with SCIM ID
group = Group.first # or create a specific test group
saml_group_link = SamlGroupLink.create!(
  group: group,
  saml_group_name: "engineering",
  access_level: Gitlab::Access::DEVELOPER,
  scim_group_uid: SecureRandom.uuid
)
puts saml_group_link.scim_group_uid # Copy this UUID for the curl command

# Create users with SCIM identities
user1 = User.first # or create a specific test user
identity1 = ScimIdentity.create!(
  user: user1,
  extern_uid: "user-scim-id-1",
  active: true
)
puts identity1.extern_uid # Copy this ID for the request payload

user2 = User.last # or create another specific test user
identity2 = ScimIdentity.create!(
  user: user2,
  extern_uid: "user-scim-id-2",
  active: true
)
puts identity2.extern_uid

# Add a user to the group directly (non-SCIM)
regular_user = User.where.not(id: [user1.id, user2.id]).first
group.add_member(regular_user, Gitlab::Access::DEVELOPER)
  1. Enable the required feature flag:
Feature.enable(:self_managed_scim_group_sync)
  1. Create a SCIM access token if needed:
token = ScimOauthAccessToken.create!
puts token.token  # Copy this token for the curl command
  1. Make the API request to replace group membership:
curl --location --request PUT 'http://localhost:3000/api/scim/v2/application/Groups/YOUR_GROUP_UUID' \
--header 'Accept: application/scim+json' \
--header 'Content-Type: application/scim+json' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--data '{
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
    "displayName": "Engineering",
    "members": [
        { "value": "user-scim-id-1" }
    ]
}'

Expected Results

  • API call should return 200 OK status code with the updated group when successful
  • Membership changes will be processed asynchronously via the worker
  • Only the specified user(s) should be members of the group via SCIM
  • Any previously SCIM-provisioned users not in the request should be removed
  • Non-SCIM users (added directly to the group) should remain
  • Multiple SAML group links with the same SCIM ID should all have their memberships updated
  • Non-existent group IDs should return 404
  • Invalid parameters should be gracefully handled (400 Bad Request)

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Paulo Barros

Merge request reports

Loading