Skip to content

Return inherited members of approval groups as approvers

What does this MR do and why?

Problem

If the subgroup is an approval group, then indirect members of this group (direct members of parent group) don't show up as approvers.

Solution

  • Return group members not only from approval groups but also from their ancestors.
  • Add filter for group members to return only users with developer or higher permissions

Database

Click to expand
SELECT
    "users".*
FROM ((
        SELECT
            "users".*
        FROM
            "users"
            INNER JOIN "approval_project_rules_users" ON "users"."id" = "approval_project_rules_users"."user_id"
        WHERE
            "approval_project_rules_users"."approval_project_rule_id" = 72703)
    UNION (
        SELECT
            "users".*
        FROM
            "users"
        WHERE
            "users"."id" IN (
                SELECT
                    "members"."user_id"
                FROM
                    "members"
                LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id"
        WHERE
            "members"."type" = 'GroupMember'
            AND "members"."source_type" = 'Namespace'
            AND (("members"."user_id" IS NULL
                    AND "members"."invite_token" IS NOT NULL)
                OR "users"."state" = 'active')
            AND "members"."requested_at" IS NULL
            AND (members.access_level > 5)
            AND "members"."access_level" IN (50, 40, 30)
            AND "members"."source_id" IN ( WITH RECURSIVE "base_and_ancestors" 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"
                            INNER JOIN "approval_project_rules_groups" ON "namespaces"."id" = "approval_project_rules_groups"."group_id"
                        WHERE
                            "namespaces"."type" = 'Group'
                            AND "approval_project_rules_groups"."approval_project_rule_id" = 72703)
                    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_ancestors"
                        WHERE
                            "namespaces"."type" = 'Group'
                            AND "namespaces"."id" = "base_and_ancestors"."parent_id"))
                    SELECT
                        "id"
                    FROM
                        "base_and_ancestors" AS "namespaces")))) users

https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/11066/commands/39646

Screenshots or screen recordings

Before After
Screenshot_2022-07-18_at_14.09.07 Screenshot_2022-07-18_at_14.08.53

How to set up and validate locally

  1. Enable feature flags

    Feature.enable(:subgroups_approval_rules)
    Feature.enable(:use_inherited_permissions_for_approval_rules)
  2. Login as an admin

  3. Create "Group A"

  4. Create "Group B" as a subgroup of "Group A" ( Group A / Group B )

  5. Create "Project" as a project of "Group A" ( Group A / Project )

  6. Add "User 1" with Developer permissions to "Group A" (http://127.0.0.1:3000/groups/group-a/-/group_members)

  7. Add "Group B" with Developer permissions as a member group of "Project" ( http://127.0.0.1:3000/group-a/project/-/project_members?tab=groups )

  8. Create approval rule and select "Group B" as an approval (http://127.0.0.1:3000/group-a/project/edit)

  9. Login as a "User 1"

  10. You should have access to both groups and the project

  11. Visit http://127.0.0.1:3000/api/v4/projects/group-a%2Fproject/approval_rules

  12. You should see User 1 in eligible approvals list in the response

MR acceptance checklist

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

Edited by Vasilii Iakliushin

Merge request reports