Skip to content

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 with Owner access.
  • Create a project Project P in Group A
  • Go to members page of Group B, and invite Group A into this group with Owner or Maintainer access. This means that user A now has Owner or Maintainer 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 transfer Project P

Previously:

  • In the dropdown showing permissible locations to transfer to, Group B and Subgroup B were not being shown, even though A has permissions to transfer a project into these groups. (If A performed this action via the project transfer API, it would work)

Now:

  • In the dropdown showing permissible locations to transfer to, Group B and Subgroup B is being shown. And if A attempts a transfer, it goes thru successfully.

Screenshots or screen recordings

Before After
Screen_Shot_2022-06-16_at_12.41.48_PM Screen_Shot_2022-06-16_at_12.40.00_PM

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.

Related to #296817 (closed)

Edited by Manoj M J

Merge request reports

Loading