Skip to content

Only show membership groups in group invites

Abdul Wadood requested to merge 224661-add-group-member-404 into master

What does this MR do and why?

Fixes #224661 (closed) Only show the groups that the current user is a member of in the project members invite page. Therefore, only fetch the groups where the current user has minimum access level >= Guest by using the min_access_level parameter in the /api/:version/groups.json` API.

Database

Raw SQL
EXPLAIN WITH RECURSIVE "base_and_descendants" AS (
    (
        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"."description_html",
            "namespaces"."lfs_enabled",
            "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
            "namespaces"
        WHERE
            "namespaces"."type" = 'Group'
            AND "namespaces"."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" = 33100
                    AND "members"."requested_at" IS NULL
                    AND (access_level >= 10)))
        UNION (
            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"."description_html",
                "namespaces"."lfs_enabled",
                "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
                "namespaces",
                "base_and_descendants"
            WHERE
                "namespaces"."type" = 'Group'
                AND "namespaces"."parent_id" = "base_and_descendants"."id"))
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"."description_html",
    "namespaces"."lfs_enabled",
    "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
    "base_and_descendants" AS "namespaces"
Query Plan
CTE Scan on base_and_descendants namespaces  (cost=1552.00..1556.02 rows=201 width=2945) (actual time=242.117..2923.712 rows=1390 loops=1)
   Buffers: shared hit=5150 read=2098 dirtied=60
   I/O Timings: read=2826.460 write=0.000
   CTE base_and_descendants
     ->  Recursive Union  (cost=12.26..1552.00 rows=201 width=358) (actual time=242.112..2918.975 rows=1390 loops=1)
           Buffers: shared hit=5150 read=2098 dirtied=60
           I/O Timings: read=2826.460 write=0.000
           ->  Nested Loop  (cost=12.26..12.51 rows=1 width=358) (actual time=242.096..242.253 rows=18 loops=1)
                 Buffers: shared hit=100 read=66 dirtied=1
                 I/O Timings: read=240.337 write=0.000
                 ->  HashAggregate  (cost=11.83..11.84 rows=1 width=8) (actual time=242.059..242.070 rows=18 loops=1)
                       Group Key: namespaces_2.id
                       Buffers: shared hit=28 read=66 dirtied=1
                       I/O Timings: read=240.337 write=0.000
                       ->  Nested Loop  (cost=1.00..11.83 rows=1 width=8) (actual time=24.653..241.909 rows=18 loops=1)
                             Buffers: shared hit=28 read=66 dirtied=1
                             I/O Timings: read=240.337 write=0.000
                             ->  Index Scan using index_members_on_user_id_source_id_source_type on public.members  (cost=0.56..8.37 rows=1 width=4) (actual time=19.635..81.011 rows=18 loops=1)
                                   Index Cond: ((members.user_id = 33100) AND ((members.source_type)::text = 'Namespace'::text))
                                   Filter: ((members.requested_at IS NULL) AND (members.access_level >= 10) AND ((members.type)::text = 'GroupMember'::text))
                                   Rows Removed by Filter: 0
                                   Buffers: shared hit=1 read=21 dirtied=1
                                   I/O Timings: read=80.712 write=0.000
                             ->  Index Scan using namespaces_pkey on public.namespaces namespaces_2  (cost=0.43..3.46 rows=1 width=4) (actual time=8.931..8.931 rows=1 loops=18)
                                   Index Cond: (namespaces_2.id = members.source_id)
                                   Filter: ((namespaces_2.type)::text = 'Group'::text)
                                   Rows Removed by Filter: 0
                                   Buffers: shared hit=27 read=45
                                   I/O Timings: read=159.625 write=0.000
                 ->  Index Scan using namespaces_pkey on public.namespaces namespaces_1  (cost=0.43..0.67 rows=1 width=358) (actual time=0.009..0.009 rows=1 loops=18)
                       Index Cond: (namespaces_1.id = namespaces_2.id)
                       Filter: ((namespaces_1.type)::text = 'Group'::text)
                       Rows Removed by Filter: 0
                       Buffers: shared hit=72
                       I/O Timings: read=0.000 write=0.000
           ->  Nested Loop  (cost=0.56..153.55 rows=20 width=358) (actual time=4.559..295.429 rows=154 loops=9)
                 Buffers: shared hit=5050 read=2032 dirtied=59
                 I/O Timings: read=2586.123 write=0.000
                 ->  WorkTable Scan on base_and_descendants  (cost=0.00..0.20 rows=10 width=4) (actual time=0.001..0.160 rows=154 loops=9)
                       I/O Timings: read=0.000 write=0.000
                 ->  Index Scan using index_namespaces_on_parent_id_and_id on public.namespaces namespaces_3  (cost=0.56..15.31 rows=2 width=358) (actual time=0.915..1.908 rows=1 loops=1390)
                       Index Cond: (namespaces_3.parent_id = base_and_descendants.id)
                       Filter: ((namespaces_3.type)::text = 'Group'::text)
                       Rows Removed by Filter: 0
                       Buffers: shared hit=5050 read=2032 dirtied=59
                       I/O Timings: read=2586.123 write=0.000
Database Lab https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/8237/commands/29147

Screenshots or screen recordings

Similar to #224661 (closed), just GitLab Data is not in the drop-down

How to set up and validate locally

Here are the ordered steps to reproduce the issue & validate the fix #224661 (comment 806790185)

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 #224661 (closed)

Edited by Abdul Wadood

Merge request reports