Populate members in instance SCIM Groups API
What does this MR do and why?
The instance-level SCIM API (/api/scim/v2/application/Groups) exposed a /Groups resource
whose members array was hardcoded to []. Adding a member via
PATCH /Groups/:id (op: add, path: members) works and grants the linked GitLab group
entitlement, but reading the group back always returned no members. IdP connectors such as
SailPoint could therefore never reconcile or verify the entitlement associations they
created.
This MR populates members on GET /Groups and GET /Groups/:id, resolved from the
scim_group_memberships table to each user's instance-level SCIM identity.
Each member is returned per RFC 7643 §4.2:
{ "value": "<user SCIM id>", "display": "<user display name>", "type": "User" }where value is the user's SCIM id (ScimIdentity#extern_uid), matching the id returned
by the /Users endpoints and round-tripping with the PATCH members operation. value is
independent of the SAML login NameID format.
Implementation notes
- Membership resolution lives in a model class method,
Authn::ScimGroupMembership.members_by_scim_group_uid, which runs a bounded number of queries regardless of the number of groups. - The API endpoint preloads members once per page and passes them to the entity via the
scim_membersoption, so the serializer runs no queries and there is no N+1 (covered by aQueryRecordertest). - Members are not fetched when
excludedAttributes=membersis requested.
Screenshots or screen recordings
N/A (internal API response change; see updated docs and specs.)
How to set up and validate locally
On an instance with the instance_level_scim license and instance SAML configured:
- Create a SAML group link, then
POST /Groupsto associate ascim_group_uid. POST /Usersto provision a user, and note the returnedid.PATCH /Groups/:idwithop: add, path: members, value: [{ "value": "<user id>" }].GET /Groups/:idandGET /Groupsand confirmmembersnow contains the user as{ "value": "<user id>", "display": "<name>", "type": "User" }.- Confirm
GET /Groups?excludedAttributes=membersomitsmembers.
MR acceptance checklist
- Tests added (entity, model, request specs including an N+1
QueryRecorderguard). - Documentation updated (internal API reference).
Closes #604758 (closed). Related to #452118 and #604760 (closed).
Request for Help: gitlab-com/request-for-help#5019 (Zendesk 730757).
Database review
This MR adds one new read query in Authn::ScimGroupMembership.members_by_scim_group_uid,
used to populate members on the SCIM GET /Groups and GET /Groups/:id endpoints. It is a
single index-backed JOIN (no secondary IN clause):
-- Members for a page of SCIM groups (one query, no secondary IN clause)
-- Uses index_scim_group_memberships_on_scim_group_uid and
-- index_scim_identities_on_user_id_and_group_id (group_id IS NULL)
SELECT scim_group_memberships.scim_group_uid, scim_identities.extern_uid, users.name
FROM scim_group_memberships
INNER JOIN scim_identities
ON scim_identities.user_id = scim_group_memberships.user_id
AND scim_identities.group_id IS NULL
INNER JOIN users ON users.id = scim_group_memberships.user_id
WHERE scim_group_memberships.scim_group_uid IN ($1, $2, ...);Query plan (validated locally on GDK PostgreSQL 17.8, seeded with 12,000 scim_group_memberships
across 300 groups + 69 instance scim_identities, then rolled back):
Hash Join (actual time=0.024..0.040 rows=80 loops=1) Buffers: shared hit=7
Hash Cond: (scim_identities.user_id = users.id)
-> Hash Join (actual time=0.014..0.024 rows=80 loops=1)
Hash Cond: (scim_group_memberships.user_id = scim_identities.user_id)
-> Index Scan using index_scim_group_memberships_on_scim_group_uid on scim_group_memberships (rows=80)
Index Cond: (scim_group_uid = ANY ('{...}'::uuid[]))
-> Hash -> Seq Scan on scim_identities (rows=69, Filter: group_id IS NULL)
-> Hash -> Seq Scan on users (rows=69)
Execution Time: 0.049 msThe selective scim_group_uid index scan drives the query; the seq scans are only on the tiny
(69-row) scim_identities/users tables where a hash join is optimal. With enable_seqscan=off
(production-scale shape) the plan uses index_scim_identities_on_user_id_and_group_id and
users_pkey via merge/nested-loop joins (Execution Time: 0.059 ms). Full output in the
database review thread.
Note: instance SCIM is self-managed only, so these tables are empty on the GitLab.com Database Lab clone (a raw run returns 0 rows); the plan above was produced against seeded data.
Scale / memory
Result size is bounded by SCIM group membership, which maps to SAML-linked group entitlements.
The query uses pluck and returns lightweight tuples (no ActiveRecord object instantiation),
and the number of queries is constant regardless of the number of groups on the page
(preloaded once, verified by a QueryRecorder spec). The SCIM Group resource inherently embeds
all of a group's members, so there is no per-member pagination; clients that manage very large
groups can pass excludedAttributes=members on GET /Groups to skip the member lookup
entirely.