Skip to content

Include memberships from groups share in Preloader

What does this MR do and why?

Why?

In UserMaxAccessLevelInGroupsPreloader, we are currently preloading and setting access levels in the cache only for groups that arise from the direct memberships of the @user.

However, a user can also have access to groups via group sharing, and such groups aren't being considered here, which causes problems down the call stack because, for such groups, the preloader would have already set NO_ACCESS as the access level.

In the GraphQL layer, this is problematic because this piece of code would then remove groups arising from group shares from the final result.

This is because the preloader has already set the access level of @user to all of the shared groups in the list of @groups to NO_ACCESS in the cache and thus @user.can?(?:read_group, group) would always return false for such shared groups, when the reality is different.

This is currently causing problems in multiple GraphQL endpoints, such that the groups arising from group shares do not show up in the final result when you make a GraphQL call, even though, internally the finder acutally returns the correct set of groups, including shared groups.

With this MR, we are trying to correct this problem such that the access level is being set correctly in the cache even for groups arising from group shares. This way, groups arising from group shares also show up in the final result.

Also see the full discussion at: #296817 (comment 1058277275)

Queries:

Generated for

Preloaders::UserMaxAccessLevelInGroupsPreloader.new(Group.where(id: [9970, 6543, 10545889, 5652796, 7636872, 7967807, 7636863, 5723700]), User.find(4093067)).execute

User ID of 4093067 belongs to @manojmj

Previously:

Only memberships from direct memberships were considered, so it was:

SELECT MAX("members"."access_level") AS maximum_access_level, "hierarchy"."id" AS hierarchy_id FROM "members" LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id" INNER JOIN (SELECT id, unnest(traversal_ids) as traversal_id FROM "namespaces" WHERE "namespaces"."id" IN (4270, 4271, 4273, 4274)) as hierarchy ON members.source_id = hierarchy.traversal_id WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' AND "users"."state" = 'active' AND "members"."state" = 0 AND "members"."requested_at" IS NULL AND "members"."invite_token" IS NULL AND (members.access_level > 5) AND "members"."user_id" = 48 GROUP BY "hierarchy"."id"

Query plan: https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/15394/commands/53466

Now:

SELECT MAX("members"."access_level") AS maximum_access_level, "hierarchy"."id" AS hierarchy_id FROM ((SELECT "members"."id", "members"."access_level", "members"."source_id", "members"."source_type", "members"."user_id", "members"."notification_level", "members"."type", "members"."created_at", "members"."updated_at", "members"."created_by_id", "members"."invite_email", "members"."invite_token", "members"."invite_accepted_at", "members"."requested_at", "members"."expires_at", "members"."ldap", "members"."override", "members"."state", "members"."invite_email_success", "members"."member_namespace_id", "members"."member_role_id" FROM "members" LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id" WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' AND "users"."state" = 'active' AND "members"."state" = 0 AND "members"."requested_at" IS NULL AND "members"."invite_token" IS NULL AND (members.access_level > 5) AND "members"."user_id" = 4093067)
UNION
(SELECT "members"."id", LEAST("group_group_links"."group_access", "members"."access_level") AS access_level, "group_group_links"."shared_group_id" AS source_id, "members"."source_type", "members"."user_id", "members"."notification_level", "members"."type", "members"."created_at", "members"."updated_at", "members"."created_by_id", "members"."invite_email", "members"."invite_token", "members"."invite_accepted_at", "members"."requested_at", "members"."expires_at", "members"."ldap", "members"."override", "members"."state", "members"."invite_email_success", "members"."member_namespace_id", "members"."member_role_id" FROM "members" LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id" JOIN group_group_links ON members.source_id = group_group_links.shared_with_group_id WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' AND "users"."state" = 'active' AND "members"."state" = 0 AND "members"."requested_at" IS NULL AND "members"."invite_token" IS NULL AND (members.access_level > 5) AND "members"."user_id" = 4093067)) members INNER JOIN (SELECT id, unnest(traversal_ids) as traversal_id FROM "namespaces" WHERE "namespaces"."id" IN (9970, 6543, 10545889, 5652796, 7636872, 7967807, 7636863, 5723700 )) as hierarchy ON members.source_id = hierarchy.traversal_id WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' GROUP BY "hierarchy"."id"

Query plan: https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/15394/commands/53467

Screenshots or screen recordings

Screenshots are required for UI changes, and strongly recommended for all other merge requests.

How to set up and validate locally

Numbered steps to set up and validate the change are strongly suggested.

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #390331 (closed)

Edited by Manoj M J

Merge request reports