Resolve "Show groups arising from group shares in the list of permissible locations a project can be transferred to"
What does this MR do and why?
This fixes the problem in #296817 (closed)
Premise
When attempting to transfer a project via the UI, we show a list of groups in the dropdown as the possible targets where the project can be transferred to.
The bug here is: In this list of groups shown in the dropdown, we do not currently include groups where the current user has (Owner/Maintainer) access to via any group shares. They have access to transfer projects to this group (it works via the API), but it simply does not show up in the dropdown.
Reason
The reason for this bug is the query used to populate this dropdown with groups. It only included the logic to obtain groups where the current user has (Owner/Maintainer) access via direct membership. It never considered groups where the current user has (Owner/Maintainer) access via group shares.
Fix
We have changed the query to include groups originating from group shares that the current user will have (Owner/Maintainer) access to.
How to set up and validate locally
- Login as admin, create groups
- Group A
- Group B
- Subgroup B
- Add another user,
A
as member in Group A withOwner
access. - Create a project
Project P
in GroupA
- Go to members page of
Group B
, and inviteGroup A
into this group withOwner
orMaintainer
access. This means that userA
now hasOwner
orMaintainer
access with Group B and its subgroups. So they should be able to transfer a project they own to Group B's hierarchy. - Now, login as user
A
and try to transferProject P
Previously:
- In the dropdown showing permissible locations to transfer to,
Group B
andSubgroup B
were not being shown, even thoughA
has permissions to transfer a project into these groups. (IfA
performed this action via the project transfer API, it would work)
Now:
- In the dropdown showing permissible locations to transfer to,
Group B
andSubgroup B
is being shown. And ifA
attempts a transfer, it goes thru successfully.
Screenshots or screen recordings
Before | After |
---|---|
SQL queries generated
I used gitlab-qa
user as the current_user
, so as to obtain the queries.
user = User.find_by_username 'gitlab-qa' # ID is 1614863
Groups::AcceptingProjectTransfersFinder.new(user).execute.to_sql
Query generated
EXPLAIN
SELECT "namespaces"."id",
"namespaces"."name",
"namespaces"."path",
"namespaces"."owner_id",
"namespaces"."created_at",
"namespaces"."updated_at",
"namespaces"."type",
"namespaces"."description",
"namespaces"."avatar",
"namespaces"."membership_lock",
"namespaces"."share_with_group_lock",
"namespaces"."visibility_level",
"namespaces"."request_access_enabled",
"namespaces"."ldap_sync_status",
"namespaces"."ldap_sync_error",
"namespaces"."ldap_sync_last_update_at",
"namespaces"."ldap_sync_last_successful_update_at",
"namespaces"."ldap_sync_last_sync_at",
"namespaces"."lfs_enabled",
"namespaces"."description_html",
"namespaces"."parent_id",
"namespaces"."shared_runners_minutes_limit",
"namespaces"."repository_size_limit",
"namespaces"."require_two_factor_authentication",
"namespaces"."two_factor_grace_period",
"namespaces"."cached_markdown_version",
"namespaces"."project_creation_level",
"namespaces"."runners_token",
"namespaces"."file_template_project_id",
"namespaces"."saml_discovery_token",
"namespaces"."runners_token_encrypted",
"namespaces"."custom_project_templates_group_id",
"namespaces"."auto_devops_enabled",
"namespaces"."extra_shared_runners_minutes_limit",
"namespaces"."last_ci_minutes_notification_at",
"namespaces"."last_ci_minutes_usage_notification_level",
"namespaces"."subgroup_creation_level",
"namespaces"."emails_disabled",
"namespaces"."max_pages_size",
"namespaces"."max_artifacts_size",
"namespaces"."mentions_disabled",
"namespaces"."default_branch_protection",
"namespaces"."unlock_membership_to_ldap",
"namespaces"."max_personal_access_token_lifetime",
"namespaces"."push_rule_id",
"namespaces"."shared_runners_enabled",
"namespaces"."allow_descendants_override_disabled_shared_runners",
"namespaces"."traversal_ids"
FROM (
(WITH "descendants_base_cte" AS MATERIALIZED
(SELECT "namespaces"."traversal_ids", "namespaces"."id"
FROM "namespaces"
INNER
JOIN "members" ON "namespaces"."id" = "members"."source_id"
WHERE "members"."type" = 'GroupMember'
AND "members"."source_type" = 'Namespace'
AND "namespaces"."type" = 'Group'
AND "members"."user_id" = 1614863
AND "members"."requested_at" IS NULL
AND (access_level >= 10)
AND "members"."access_level" IN (40, 50)),
"superset" AS
(SELECT d1.traversal_ids
FROM descendants_base_cte d1
WHERE NOT EXISTS
(SELECT 1
FROM descendants_base_cte d2
WHERE d2.id = ANY(d1.traversal_ids)
AND d2.id <> d1.id ) ) SELECT DISTINCT "namespaces"."id",
"namespaces"."name",
"namespaces"."path",
"namespaces"."owner_id",
"namespaces"."created_at",
"namespaces"."updated_at",
"namespaces"."type",
"namespaces"."description",
"namespaces"."avatar",
"namespaces"."membership_lock",
"namespaces"."share_with_group_lock",
"namespaces"."visibility_level",
"namespaces"."request_access_enabled",
"namespaces"."ldap_sync_status",
"namespaces"."ldap_sync_error",
"namespaces"."ldap_sync_last_update_at",
"namespaces"."ldap_sync_last_successful_update_at",
"namespaces"."ldap_sync_last_sync_at",
"namespaces"."lfs_enabled",
"namespaces"."description_html",
"namespaces"."parent_id",
"namespaces"."shared_runners_minutes_limit",
"namespaces"."repository_size_limit",
"namespaces"."require_two_factor_authentication",
"namespaces"."two_factor_grace_period",
"namespaces"."cached_markdown_version",
"namespaces"."project_creation_level",
"namespaces"."runners_token",
"namespaces"."file_template_project_id",
"namespaces"."saml_discovery_token",
"namespaces"."runners_token_encrypted",
"namespaces"."custom_project_templates_group_id",
"namespaces"."auto_devops_enabled",
"namespaces"."extra_shared_runners_minutes_limit",
"namespaces"."last_ci_minutes_notification_at",
"namespaces"."last_ci_minutes_usage_notification_level",
"namespaces"."subgroup_creation_level",
"namespaces"."emails_disabled",
"namespaces"."max_pages_size",
"namespaces"."max_artifacts_size",
"namespaces"."mentions_disabled",
"namespaces"."default_branch_protection",
"namespaces"."unlock_membership_to_ldap",
"namespaces"."max_personal_access_token_lifetime",
"namespaces"."push_rule_id",
"namespaces"."shared_runners_enabled",
"namespaces"."allow_descendants_override_disabled_shared_runners",
"namespaces"."traversal_ids"
FROM "superset",
"namespaces"
WHERE "namespaces"."type" = 'Group'
AND next_traversal_ids_sibling("superset"."traversal_ids") > "namespaces"."traversal_ids"
AND "superset"."traversal_ids" <= "namespaces"."traversal_ids")
UNION
(WITH "descendants_base_cte" AS MATERIALIZED
(SELECT "namespaces"."traversal_ids", "namespaces"."id"
FROM "namespaces"
WHERE "namespaces"."type" = 'Group'
AND "namespaces"."id" IN
(SELECT "group_group_links"."shared_group_id"
FROM "group_group_links"
WHERE "group_group_links"."group_access" IN (50, 40)
AND "group_group_links"."shared_with_group_id" IN
(SELECT "namespaces"."id"
FROM "namespaces"
INNER JOIN "members" ON "namespaces"."id" = "members"."source_id"
WHERE "members"."type" = 'GroupMember'
AND "members"."source_type" = 'Namespace'
AND "namespaces"."type" = 'Group'
AND "members"."user_id" = 1614863
AND "members"."requested_at" IS NULL
AND (access_level >= 10)
AND "members"."access_level" IN (40, 50)))),
"superset" AS
(SELECT d1.traversal_ids
FROM descendants_base_cte d1
WHERE NOT EXISTS
(SELECT 1
FROM descendants_base_cte d2
WHERE d2.id = ANY(d1.traversal_ids)
AND d2.id <> d1.id ) ) SELECT DISTINCT "namespaces"."id",
"namespaces"."name",
"namespaces"."path",
"namespaces"."owner_id",
"namespaces"."created_at",
"namespaces"."updated_at",
"namespaces"."type",
"namespaces"."description",
"namespaces"."avatar",
"namespaces"."membership_lock",
"namespaces"."share_with_group_lock",
"namespaces"."visibility_level",
"namespaces"."request_access_enabled",
"namespaces"."ldap_sync_status",
"namespaces"."ldap_sync_error",
"namespaces"."ldap_sync_last_update_at",
"namespaces"."ldap_sync_last_successful_update_at",
"namespaces"."ldap_sync_last_sync_at",
"namespaces"."lfs_enabled",
"namespaces"."description_html",
"namespaces"."parent_id",
"namespaces"."shared_runners_minutes_limit",
"namespaces"."repository_size_limit",
"namespaces"."require_two_factor_authentication",
"namespaces"."two_factor_grace_period",
"namespaces"."cached_markdown_version",
"namespaces"."project_creation_level",
"namespaces"."runners_token",
"namespaces"."file_template_project_id",
"namespaces"."saml_discovery_token",
"namespaces"."runners_token_encrypted",
"namespaces"."custom_project_templates_group_id",
"namespaces"."auto_devops_enabled",
"namespaces"."extra_shared_runners_minutes_limit",
"namespaces"."last_ci_minutes_notification_at",
"namespaces"."last_ci_minutes_usage_notification_level",
"namespaces"."subgroup_creation_level",
"namespaces"."emails_disabled",
"namespaces"."max_pages_size",
"namespaces"."max_artifacts_size",
"namespaces"."mentions_disabled",
"namespaces"."default_branch_protection",
"namespaces"."unlock_membership_to_ldap",
"namespaces"."max_personal_access_token_lifetime",
"namespaces"."push_rule_id",
"namespaces"."shared_runners_enabled",
"namespaces"."allow_descendants_override_disabled_shared_runners",
"namespaces"."traversal_ids"
FROM "superset",
"namespaces"
WHERE "namespaces"."type" = 'Group'
AND next_traversal_ids_sibling("superset"."traversal_ids") > "namespaces"."traversal_ids"
AND "superset"."traversal_ids" <= "namespaces"."traversal_ids")) namespaces
WHERE "namespaces"."type" = 'Group'
AND ("namespaces"."project_creation_level" IN (2,
1)
OR "namespaces"."project_creation_level" IS NULL)
Query plan: https://explain.depesz.com/s/NgJy
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.
Related to #296817 (closed)