From f579c7fcd8eb13002dd5bba358dce02c0e0d87a1 Mon Sep 17 00:00:00 2001 From: Abdul Wadood <awadood@gitlab.com> Date: Thu, 11 Apr 2024 12:12:08 +0000 Subject: [PATCH] Return all visible groups for the Organization.groups GraphQL query As we want to display all visible groups on the organization groups dashboard on /-/organizations/<organization path>/groups_and_projects. Changelog: other --- app/finders/organizations/groups_finder.rb | 28 +++++++++ .../organizations/groups_resolver.rb | 10 ++- .../resolve_all_organization_groups.yml | 9 +++ .../organizations/groups_finder_spec.rb | 63 +++++++++++++++++++ .../organizations/organization_query_spec.rb | 37 ++++++++++- 5 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 app/finders/organizations/groups_finder.rb create mode 100644 config/feature_flags/gitlab_com_derisk/resolve_all_organization_groups.yml create mode 100644 spec/finders/organizations/groups_finder_spec.rb diff --git a/app/finders/organizations/groups_finder.rb b/app/finders/organizations/groups_finder.rb new file mode 100644 index 0000000000000..a4925664b743a --- /dev/null +++ b/app/finders/organizations/groups_finder.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Organizations + class GroupsFinder < GroupsFinder + def execute + groups = find_union(filtered_groups, Group) + + unless default_organization? + cte = Gitlab::SQL::CTE.new(:filtered_groups_cte, groups, materialized: false) + groups = Group.with(cte.to_arel).from(cte.alias_to(Group.arel_table)) # rubocop: disable CodeReuse/ActiveRecord -- CTE use + end + + sort(groups).with_route + end + + private + + def default_organization? + params[:organization]&.default? == true + end + + def all_groups + return [membership_groups, Group.none].compact if default_organization? + + super + end + end +end diff --git a/app/graphql/resolvers/organizations/groups_resolver.rb b/app/graphql/resolvers/organizations/groups_resolver.rb index 10c0c9545b087..d6ec89a924ae2 100644 --- a/app/graphql/resolvers/organizations/groups_resolver.rb +++ b/app/graphql/resolvers/organizations/groups_resolver.rb @@ -13,8 +13,16 @@ class GroupsResolver < Resolvers::GroupsResolver alias_method :organization, :object + def resolve_groups(**args) + ::Organizations::GroupsFinder.new(context[:current_user], finder_params(args)).execute + end + def finder_params(args) - extra_args = { organization: organization, include_ancestors: false, all_available: false } + extra_args = if Feature.enabled?(:resolve_all_organization_groups, current_user) + { organization: organization } + else + { organization: organization, include_ancestors: false, all_available: false } + end super.merge(extra_args) end diff --git a/config/feature_flags/gitlab_com_derisk/resolve_all_organization_groups.yml b/config/feature_flags/gitlab_com_derisk/resolve_all_organization_groups.yml new file mode 100644 index 0000000000000..9189c029bf4ee --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/resolve_all_organization_groups.yml @@ -0,0 +1,9 @@ +--- +name: resolve_all_organization_groups +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/444218 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146600 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/444981 +milestone: '16.11' +group: group::tenant scale +type: gitlab_com_derisk +default_enabled: false diff --git a/spec/finders/organizations/groups_finder_spec.rb b/spec/finders/organizations/groups_finder_spec.rb new file mode 100644 index 0000000000000..2d2ee009f177b --- /dev/null +++ b/spec/finders/organizations/groups_finder_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Organizations::GroupsFinder, feature_category: :groups_and_projects do + describe '#execute' do + let_it_be(:organization_user) { create(:organization_user) } + let_it_be(:organization) { organization_user.organization } + let_it_be(:user) { organization_user.user } + let_it_be(:public_group) { create(:group, name: 'public-group', organization: organization) } + let_it_be(:outside_organization_group) { create(:group) } + let_it_be(:private_group) { create(:group, :private, name: 'private-group', organization: organization) } + let_it_be(:no_access_group_in_org) { create(:group, :private, name: 'no-access', organization: organization) } + + let(:current_user) { user } + let(:params) { { organization: organization } } + let(:finder) { described_class.new(current_user, params) } + + subject(:result) { finder.execute.to_a } + + before_all do + private_group.add_developer(user) + public_group.add_developer(user) + outside_organization_group.add_developer(user) + end + + context 'when user is only authorized to read the public group' do + let(:current_user) { create(:user) } + + it { is_expected.to contain_exactly(public_group) } + end + + it 'return all groups inside the organization' do + expect(result).to contain_exactly(public_group, private_group) + end + + it 'creates a CTE with filtered groups' do + allow(Gitlab::SQL::CTE).to receive(:new).and_call_original + expect(Gitlab::SQL::CTE).to receive(:new).with( + :filtered_groups_cte, kind_of(ActiveRecord::Relation), materialized: false + ) + + result + end + + context 'with default organization' do + before do + allow(organization).to receive(:default?).and_return(true) + end + + it { is_expected.to contain_exactly(public_group, private_group) } + + it 'does not build a query using the CTE' do + allow(Gitlab::SQL::CTE).to receive(:new).and_call_original + expect(Gitlab::SQL::CTE).not_to receive(:new).with( + :filtered_groups_cte, kind_of(ActiveRecord::Relation), materialized: false + ) + + result + end + end + end +end diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb index 5bd0b27586661..83d09ebf97123 100644 --- a/spec/requests/api/graphql/organizations/organization_query_spec.rb +++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb @@ -28,6 +28,8 @@ create(:group, name: 'other-group', organization: organization) { |g| g.add_developer(user) } end + let_it_be(:organization_group) { create(:group, organization: organization) } + subject(:request_organization) { post_graphql(query, current_user: current_user) } context 'when the user does not have access to the organization' do @@ -110,6 +112,7 @@ def run_query context 'when requesting groups' do let(:groups) { graphql_data_at(:organization, :groups, :nodes) } let_it_be(:parent_group) { create(:group, name: 'parent-group', organization: organization) } + let_it_be(:parent_group_global_id) { parent_group.to_global_id.to_s } let_it_be(:public_group) do create(:group, name: 'public-group', parent: parent_group, organization: organization) end @@ -125,10 +128,38 @@ def run_query create(:group) { |g| g.add_developer(user) } # outside organization end - it 'does not return ancestors of authorized groups' do + it 'returns ancestors of authorized groups' do + request_organization + + expect(groups.pluck('id')).to include(parent_group_global_id) + end + + it 'returns all visible groups' do request_organization - expect(groups.pluck('id')).not_to include(parent_group.to_global_id.to_s) + expected_groups = [parent_group, public_group, private_group, other_group, organization_group] + .map { |group| group.to_global_id.to_s } + expect(groups.pluck('id')).to match_array(expected_groups) + end + + context 'when resolve_all_organization_groups feature flag is disabled' do + before do + stub_feature_flags(resolve_all_organization_groups: false) + end + + it 'does not return ancestors of authorized groups' do + request_organization + + expect(groups.pluck('id')).not_to include(parent_group_global_id) + end + + it 'does not return all visible groups' do + request_organization + + visible_groups = [parent_group, organization_group] + .map { |group| group.to_global_id.to_s } + expect(groups.pluck('id')).not_to include(*visible_groups) + end end context 'with `search` argument' do @@ -154,7 +185,7 @@ def run_query end describe 'group sorting' do - let_it_be(:authorized_groups) { [public_group, private_group, other_group] } + let_it_be(:authorized_groups) { [parent_group, public_group, private_group, other_group, organization_group] } let_it_be(:first_param) { 2 } let_it_be(:data_path) { [:organization, :groups] } -- GitLab