Skip to content

Calculate correct access level of members invited through group

Abdul Wadood requested to merge 219230-fix-shared-group-member-access into master

What does this MR do and why?

While calculating the access level of invited group members we were missing a condition due to which the access level of any shared_group_id for the given shared_with_group_id from group_group_link was being picked.

Here we have fixed the issue by passing the shared_group_id to the method calculating the access level of the invited group.

Query plans

Before

Raw query
EXPLAIN
SELECT "members".*
FROM (SELECT DISTINCT ON (user_id, invite_email) *
      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",
                    "members"."expiry_notified_at"
             FROM "members"
             WHERE "members"."type" = 'GroupMember'
               AND "members"."source_type" = 'Namespace'
               AND "members"."requested_at" IS NULL
               AND "members"."source_id" IN (SELECT "namespaces"."id"
                                             FROM ((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"."max_pages_size",
                                                           "namespaces"."max_artifacts_size",
                                                           "namespaces"."mentions_disabled",
                                                           "namespaces"."default_branch_protection",
                                                           "namespaces"."max_personal_access_token_lifetime",
                                                           "namespaces"."push_rule_id",
                                                           "namespaces"."shared_runners_enabled",
                                                           "namespaces"."allow_descendants_override_disabled_shared_runners",
                                                           "namespaces"."traversal_ids",
                                                           "namespaces"."organization_id"
                                                    FROM "namespaces"
                                                    WHERE "namespaces"."type" = 'Group'
                                                      AND "namespaces"."id" = 60717473)) namespaces
                                             WHERE "namespaces"."type" = 'Group'))
            UNION
            (SELECT "members"."id",
                    LEAST("group_group_links"."group_access", "members"."access_level") AS 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",
                    "members"."expiry_notified_at"
             FROM "members"
                      LEFT OUTER 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 "members"."requested_at" IS NULL
               AND "members"."source_id" IN (SELECT "namespaces"."id"
                                             FROM "namespaces"
                                                      INNER JOIN "group_group_links"
                                                                 ON "group_group_links"."shared_with_group_id" = "namespaces"."id"
                                             WHERE "namespaces"."type" = 'Group'
                                               AND "group_group_links"."shared_group_id" IN (SELECT "namespaces"."id"
                                                                                             FROM "namespaces"
                                                                                             WHERE "namespaces"."type" = 'Group'
                                                                                               AND "namespaces"."id" = 60717473)))) members
      WHERE "members"."type" = 'GroupMember'
        AND "members"."source_type" = 'Namespace'
      ORDER BY user_id, invite_email,
               CASE
                   WHEN source_id = 60717473 AND source_type = 'Namespace'
                       THEN access_level + 1
                   ELSE access_level END DESC,
               expires_at DESC, created_at ASC) members
WHERE "members"."type" = 'GroupMember'
  AND "members"."invite_token" IS NULL
  AND "members"."requested_at" IS NULL
  AND "members"."user_id" = 116
ORDER BY "members"."id" ASC
LIMIT 20 OFFSET 0;

https://console.postgres.ai/gitlab/gitlab-production-main/sessions/28018/commands/87233

After

Raw query

EXPLAIN
SELECT "members".*
FROM (SELECT DISTINCT ON (user_id, invite_email) *
      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",
                    "members"."expiry_notified_at"
             FROM "members"
             WHERE "members"."type" = 'GroupMember'
               AND "members"."source_type" = 'Namespace'
               AND "members"."requested_at" IS NULL
               AND "members"."source_id" IN (SELECT "namespaces"."id"
                                             FROM ((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"."max_pages_size",
                                                           "namespaces"."max_artifacts_size",
                                                           "namespaces"."mentions_disabled",
                                                           "namespaces"."default_branch_protection",
                                                           "namespaces"."max_personal_access_token_lifetime",
                                                           "namespaces"."push_rule_id",
                                                           "namespaces"."shared_runners_enabled",
                                                           "namespaces"."allow_descendants_override_disabled_shared_runners",
                                                           "namespaces"."traversal_ids",
                                                           "namespaces"."organization_id"
                                                    FROM "namespaces"
                                                    WHERE "namespaces"."type" = 'Group'
                                                      AND "namespaces"."id" = 60717473)) namespaces
                                             WHERE "namespaces"."type" = 'Group'))
            UNION
            (SELECT "members"."id",
                    LEAST("group_group_links"."group_access", "members"."access_level") AS 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",
                    "members"."expiry_notified_at"
             FROM "members"
                      LEFT OUTER 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 "members"."requested_at" IS NULL
               AND "members"."source_id" IN (SELECT "namespaces"."id"
                                             FROM "namespaces"
                                                      INNER JOIN "group_group_links"
                                                                 ON "group_group_links"."shared_with_group_id" = "namespaces"."id"
                                             WHERE "namespaces"."type" = 'Group'
                                               AND "group_group_links"."shared_group_id" IN (SELECT "namespaces"."id"
                                                                                             FROM "namespaces"
                                                                                             WHERE "namespaces"."type" = 'Group'
                                                                                               AND "namespaces"."id" = 60717473))
               AND "group_group_links"."shared_group_id" IN (SELECT "namespaces"."id"
                                                             FROM "namespaces"
                                                             WHERE "namespaces"."type" = 'Group'
                                                               AND "namespaces"."id" = 60717473))) members
      WHERE "members"."type" = 'GroupMember'
        AND "members"."source_type" = 'Namespace'
      ORDER BY user_id, invite_email,
               CASE
                   WHEN source_id = 60717473 AND source_type = 'Namespace'
                       THEN access_level + 1
                   ELSE access_level END DESC,
               expires_at DESC, created_at ASC) members
WHERE "members"."type" = 'GroupMember'
  AND "members"."invite_token" IS NULL
  AND "members"."requested_at" IS NULL
  AND "members"."user_id" = 116
ORDER BY "members"."id" ASC
LIMIT 20 OFFSET 0;

https://console.postgres.ai/gitlab/gitlab-production-main/sessions/28018/commands/87244

https://console.postgres.ai/gitlab/gitlab-production-main/sessions/28018/commands/87236

MR acceptance checklist

Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Screenshots or screen recordings

Before After
image image

How to set up and validate locally

  1. Create 3 top-level groups: Invited group, Shared group 1, Shared group 2.
  2. Go to the membership page of the above groups https://gdk.test:3000/groups/<group-path>/-/group_members
  3. Invite User 1 to Invited group with Owner access.
  4. Invite Invited group to Shared group 1 with Developer access and invite Invited group to Shared group 2 with Owner access.
  5. Enable the feature flag in the rails console: Feature.enable(:webui_members_inherited_users)
  6. Check the access level of User 1 on the Shared group 1 membership page on the master branch. It will be Owner.
  7. On this branch, the access level of User 1 will be Developer since Invited group was invited with Developer access

Related to #219230 (closed)

Edited by Abdul Wadood

Merge request reports

Loading