Skip to content

Make group name lookup limitable by root container

What does this MR do and why?

Scan result policies allow specifying groups as approvers by specifying their name. Groups are identified by substring matching. Currently, groups are queried by their name globally. This is a problem:

Groups named compliance are matched but from a lot of different namespaces:

  • anthonybaer/gitlab-evaluation/compliance
  • gaia-x/lab/compliance
  • gitlab-org/govern/compliance
  • pensando/tbd/compliance
  • gitlab-gold/briecarranza/tickets/compliance
  • blee-group/demos/security/compliance

This MR:

  • introduces a new application setting security_policy_global_group_approvers_enabled which is enabled by default.
    • when enabled, groups are queried globally
    • when disabled, groups are queried within a container's namespace hierarchy
    • it is enabled by default so that the current behaviour is preserved. We'll disable the flag for SaaS.
  • pushes the application setting and the root namespace path to the frontend, which will issue different GraphQL queries depending on the setting in the future.

How to set up and validate locally

  1. Disable the application setting, which is enabled by default:
Gitlab::CurrentSettings.update_attribute(:security_policy_global_group_approvers_enabled, false)
  1. Create a new group nameless-bird
  2. Add any member to the group and assign the "Developer" role
  3. Create another top-level group purple-frog
  4. Create a new subgroup purple-frog/nameless-bird
  5. Add any other member to the subgroup and assign the "Developer" role
  6. Create a new project purple-frog/nameless-bird/example
  7. Within the project, navigate to Security & Compliance > Policies, click "New policy" and create a new Scan Result Policy:
type: scan_result_policy
name: Dependency Scanning
description: ''
enabled: true
rules:
  - type: scan_finding
    branches: []
    scanners:
      - dependency_scanning
    vulnerabilities_allowed: 0
    severity_levels:
      - critical
      - high
      - medium
      - low
      - unknown
      - info
    vulnerability_states:
      - newly_detected
      - detected
      - confirmed
      - dismissed
      - resolved
actions:
  - type: require_approval
    approvals_required: 1
    group_approvers: ["nameless-bird"]
  1. Within the project, navigate to Security & Compliance > Policies, click on the "Dependency Scanning" policy and verify that the sidebar lists only purple-frog/nameless-bird as approvers
  2. Create a new MR that adds Gemfile.lock with the following contents:
GEM
  remote: https://rubygems.org/
  specs:
    rack (0.1.0)

PLATFORMS
  x86_64-darwin-20

DEPENDENCIES
  rack (= 0.1.0)

BUNDLED WITH
   2.3.22
  1. Verify that the merge request approval rules lists members from purple-frog/nameless-bird, not the top-level nameless-bird

Database queries

postgres.ai

Toggle SQL query
SELECT 
  "namespaces".* 
FROM 
  (
    (
      SELECT 
        "namespaces".* 
      FROM 
        "namespaces" 
      WHERE 
        "namespaces"."type" = 'Group' 
        AND (
          traversal_ids @> ('{9878214}')
        ) 
        AND 1 = 0
    ) 
    UNION 
      (
        SELECT 
          "namespaces".* 
        FROM 
          "namespaces" 
        WHERE 
          "namespaces"."type" = 'Group' 
          AND (
            traversal_ids @> ('{9878214}')
          ) 
          AND "namespaces"."id" IN (
            SELECT 
              "routes"."namespace_id" 
            FROM 
              "routes" 
            WHERE 
              "routes"."source_type" = 'Namespace' 
              AND (
                LOWER(routes.path) IN ('dependencies', 'analyzers')
              )
          )
      ) 
    UNION 
      (
        SELECT 
          "namespaces".* 
        FROM 
          "namespaces" 
        WHERE 
          "namespaces"."type" = 'Group' 
          AND (
            traversal_ids @> ('{9878214}')
          ) 
          AND (
            LOWER(path) IN ('dependencies', 'analyzers')
          )
      )
  ) namespaces 
WHERE 
  "namespaces"."type" = 'Group' 
  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"."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 
                          "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" = 9878214 
                          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"."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 
                            "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" = 9878214
                        )
                    ) 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'
              )
            ) namespaces 
          WHERE 
            "namespaces"."type" = 'Group'
        ) authorized 
      WHERE 
        authorized."id" = "namespaces"."id"
    )
  );

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

Edited by Dominic Bauer

Merge request reports