Skip to content

Fix approval visible groups detection

What does this MR do and why?

Contributes to #255981 (closed)

Problem

  • User has access to Group A
  • Group B is a subgroup of the Group A
  • Group B is a approval group

User with inherited permissions from Group A cannot see approvers from approval Group B.

Solution

Apply subgroup permissions for approval rules check

Screenshots or screen recordings

Before

[
  {
    "id": 3,
    "name": "Approval",
    "rule_type": "regular",
    "eligible_approvers": [], # <- empty list
    "approvals_required": 1,
    "users": [],
    "groups": [],
    "contains_hidden_groups": true, # <- has hidden groups
    "protected_branches": []
  }
]

After

[
  {
    "id": 3,
    "name": "Approval",
    "rule_type": "regular",
    "eligible_approvers": [ # <- shows approvers
      {
        "id": 1,
        "username": "root",
        "name": "Root",
        "state": "active",
        "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
        "web_url": "http://127.0.0.1:3000/root"
      },
      {
        "id": 46,
        "username": "user2",
        "name": "Sidney Jones2",
        "state": "active",
        "avatar_url": "https://www.gravatar.com/avatar/2c75f98f3ea6cd6ee75cfbdfbb031388?s=80&d=identicon",
        "web_url": "http://127.0.0.1:3000/user2"
      }
    ],
    "approvals_required": 1,
    "users": [],
    "groups": [
      {
        "id": 213,
        "web_url": "http://127.0.0.1:3000/groups/group-a/approval",
        "name": "approval",
        "path": "approval",
        "description": "",
        "visibility": "private",
        "share_with_group_lock": false,
        "require_two_factor_authentication": false,
        "two_factor_grace_period": 48,
        "project_creation_level": "developer",
        "auto_devops_enabled": null,
        "subgroup_creation_level": "maintainer",
        "emails_disabled": null,
        "mentions_disabled": null,
        "lfs_enabled": true,
        "default_branch_protection": 2,
        "avatar_url": null,
        "request_access_enabled": true,
        "full_name": "Group A / approval",
        "full_path": "group-a/approval",
        "created_at": "2022-06-29T15:14:16.930Z",
        "parent_id": 130,
        "ldap_cn": null,
        "ldap_access": null,
        "marked_for_deletion_on": null
      }
    ],
    "contains_hidden_groups": false, # <- no hidden groups
    "protected_branches": []
  }
]

Database

Before

Click to expand
SELECT
    "namespaces".*
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" = 3
    AND ("namespaces"."visibility_level" IN (10, 20)
        OR EXISTS (
            SELECT
                1
            FROM (
                SELECT
                    "namespaces".*
                FROM (( WITH "direct_groups" AS MATERIALIZED (
                            SELECT
                                "namespaces".*
                            FROM ((
                                    SELECT
                                        "namespaces".*
                                    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" = 421631
                                        AND "members"."requested_at" IS NULL
                                        AND (
                                            access_level >= 10
)
)
                                UNION (
                                    SELECT
                                        "namespaces".*
                                    FROM
                                        "projects"
                                        INNER JOIN "project_authorizations" ON "projects"."id" = "project_authorizations"."project_id"
                                        INNER JOIN "namespaces" ON "namespaces"."id" = "projects"."namespace_id"
                                    WHERE
                                        "project_authorizations"."user_id" = 421631
)
) namespaces
                            WHERE
                                "namespaces"."type" = 'Group'
)
                            SELECT
                                "namespaces".*
                            FROM ((
                                    SELECT
                                        "namespaces".*
                                    FROM
                                        "direct_groups" "namespaces"
                                    WHERE
                                        "namespaces"."type" = 'Group')
                                UNION (
                                    SELECT
                                        "namespaces".*
                                    FROM
                                        "namespaces"
                                        INNER JOIN "group_group_links" ON "group_group_links"."shared_group_id" = "namespaces"."id"
                                    WHERE
                                        "namespaces"."type" = 'Group'
                                        AND "group_group_links"."shared_with_group_id" IN (
                                            SELECT
                                                "namespaces"."id"
                                            FROM
                                                "direct_groups" "namespaces"
                                            WHERE
                                                "namespaces"."type" = 'Group'))) namespaces
                                WHERE
                                    "namespaces"."type" = 'Group')
                            UNION (
                                SELECT
                                    "namespaces".*
                                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" = 421631
                                    AND "members"."access_level" = 5)) namespaces
                        WHERE
                            "namespaces"."type" = 'Group') authorized
                    WHERE
                        authorized. "id" = "namespaces"."id"))

https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/10891/commands/39060

After

I rely on group preload logic introduced here - !73121 (merged)

Click to expand
SELECT
    "namespaces".*
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

https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/10891/commands/39061

Click to expand
SELECT
    namespaces.*,
    root_query.id AS source_id
FROM
    "namespaces"
    INNER JOIN (
        SELECT
            id,
            traversal_ids[1] AS root_id
        FROM
            "namespaces"
        WHERE
            "namespaces"."type" = 'Group'
            AND "namespaces"."id" = 9970) AS root_query ON root_query.root_id = namespaces.id

https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/10891/commands/39063

Click to expand
SELECT
    MAX("members"."access_level") AS maximum_access_level,
    "hierarchy"."id" AS hierarchy_id
FROM
    "members"
    LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id"
INNER JOIN (
    SELECT
        id,
        unnest(traversal_ids) AS traversal_id
    FROM
        "namespaces"
    WHERE
        "namespaces"."id" = 9970) AS hierarchy ON members.source_id = hierarchy.traversal_id
WHERE
    "members"."type" = 'GroupMember'
    AND "members"."source_type" = 'Namespace'
    AND "users"."state" = 'active'
    AND "members"."state" = 0
    AND "members"."requested_at" IS NULL
    AND "members"."invite_token" IS NULL
    AND (members.access_level > 5)
    AND "members"."user_id" = 421631
GROUP BY
    "hierarchy"."id"

https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/10891/commands/39062

Click to expand
SELECT
    1 AS one
FROM ((
        SELECT
            "projects".*
        FROM
            "projects"
        WHERE
            "projects"."namespace_id" IN (
                SELECT
                    "namespaces"."id"
                FROM
                    "namespaces"
                WHERE
                    "namespaces"."type" = 'Group'
                    AND (traversal_ids @> ('{9970}'))))) projects
WHERE (EXISTS (
        SELECT
            1
        FROM
            "project_authorizations"
        WHERE
            "project_authorizations"."user_id" = 421631
            AND (project_authorizations.project_id = projects.id))
        OR projects.visibility_level IN (10, 20))
AND "projects"."hidden" = FALSE
LIMIT 1

https://console.postgres.ai/gitlab/gitlab-production-tunnel-pg12/sessions/10891/commands/39065

How to set up and validate locally

  1. Enable feature flag
    Feature.enable(:subgroups_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 an non-empty eligible_approvals array in the response

Or follow instructions from here

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