diff --git a/app/graphql/resolvers/groups_resolver.rb b/app/graphql/resolvers/groups_resolver.rb index 9ac030502538193852ebfbfb2568e0c485c02e55..bd1a79dc0844d8c434b9f3404eebcf16f18f7b6e 100644 --- a/app/graphql/resolvers/groups_resolver.rb +++ b/app/graphql/resolvers/groups_resolver.rb @@ -3,6 +3,7 @@ module Resolvers class GroupsResolver < BaseResolver include ResolvesGroups + include Gitlab::Graphql::Authorize::AuthorizeResource type Types::GroupType.connection_type, null: true @@ -32,6 +33,10 @@ class GroupsResolver < BaseResolver "for example: `id_desc` or `name_asc`", default_value: 'name_asc' + argument :parent_path, GraphQL::Types::ID, + required: false, + description: 'Full path of the parent group.' + argument :all_available, GraphQL::Types::Boolean, required: false, default_value: true, @@ -43,11 +48,23 @@ class GroupsResolver < BaseResolver private - def resolve_groups(**args) + def resolve_groups(parent_path: nil, **args) + args[:parent] = find_authorized_parent!(parent_path) if parent_path + GroupsFinder .new(context[:current_user], args) .execute end + + def find_authorized_parent!(path) + group = Group.find_by_full_path(path) + + unless Ability.allowed?(current_user, :read_group, group) + raise_resource_not_available_error! format(_('Could not find parent group with path %{path}'), path: path) + end + + group + end end end diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 11bdbbf73ba54a821471b591b67a19a3c26fe155..08b69b1d7df4f063e1992b3c27f7764df785fe86 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -735,6 +735,7 @@ four standard [pagination arguments](#pagination-arguments): | <a id="querygroupsids"></a>`ids` | [`[ID!]`](#id) | Filter groups by IDs. | | <a id="querygroupsmarkedfordeletionon"></a>`markedForDeletionOn` | [`Date`](#date) | Date when the group was marked for deletion. | | <a id="querygroupsownedonly"></a>`ownedOnly` | [`Boolean`](#boolean) | Only include groups where the current user has an owner role. | +| <a id="querygroupsparentpath"></a>`parentPath` | [`ID`](#id) | Full path of the parent group. | | <a id="querygroupssearch"></a>`search` | [`String`](#string) | Search query for group name or group full path. | | <a id="querygroupssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. | | <a id="querygroupstoplevelonly"></a>`topLevelOnly` | [`Boolean`](#boolean) | Only include top-level groups. | @@ -32384,6 +32385,7 @@ four standard [pagination arguments](#pagination-arguments): | <a id="organizationgroupsids"></a>`ids` | [`[ID!]`](#id) | Filter groups by IDs. | | <a id="organizationgroupsmarkedfordeletionon"></a>`markedForDeletionOn` | [`Date`](#date) | Date when the group was marked for deletion. | | <a id="organizationgroupsownedonly"></a>`ownedOnly` | [`Boolean`](#boolean) | Only include groups where the current user has an owner role. | +| <a id="organizationgroupsparentpath"></a>`parentPath` | [`ID`](#id) | Full path of the parent group. | | <a id="organizationgroupssearch"></a>`search` | [`String`](#string) | Search query for group name or group full path. | | <a id="organizationgroupssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. | | <a id="organizationgroupstoplevelonly"></a>`topLevelOnly` | [`Boolean`](#boolean) | Only include top-level groups. | diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3a52e09ce7a685d32e3ae1dfd742f09e9386d296..ada561c740ee7a84b7b9025fd600730ff9b2ff2d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -17049,6 +17049,9 @@ msgstr "" msgid "Could not find iteration" msgstr "" +msgid "Could not find parent group with path %{path}" +msgstr "" + msgid "Could not load the user chart. Please refresh the page to try again." msgstr "" diff --git a/rubocop/cop/graphql/id_type.rb b/rubocop/cop/graphql/id_type.rb index 2a1cbdca6c8cf9ca57ffb9ee8f5a3adba773eaa0..1fd0952de9fff4ed8092dd7e0af8c546f90fa2d8 100644 --- a/rubocop/cop/graphql/id_type.rb +++ b/rubocop/cop/graphql/id_type.rb @@ -9,7 +9,7 @@ class IDType < RuboCop::Cop::Base ALLOWLISTED_ARGUMENTS = %i[ full_path project_path group_path target_project_path target_group_path target_path namespace_path - context_namespace_path + context_namespace_path parent_path ].freeze def_node_matcher :iid_with_id?, <<~PATTERN diff --git a/spec/graphql/resolvers/groups_resolver_spec.rb b/spec/graphql/resolvers/groups_resolver_spec.rb index 05fbec710158777f88dd6d8a505467a084ec4777..359c6b8437250897abb127d69147e200f6a84228 100644 --- a/spec/graphql/resolvers/groups_resolver_spec.rb +++ b/spec/graphql/resolvers/groups_resolver_spec.rb @@ -10,7 +10,7 @@ describe '#resolve' do let_it_be(:user) { create(:user) } let_it_be(:public_group) { create(:group, name: 'public-group') } - let_it_be(:private_group) { create(:group, :private, name: 'private-group') } + let_it_be_with_reload(:private_group) { create(:group, :private, name: 'private-group') } let(:params) { {} } @@ -78,6 +78,41 @@ end end + context 'with `parent_path` argument' do + let_it_be(:parent_group) { private_group } + let_it_be(:child_group) { create(:group, :private, parent: parent_group) } + + let(:params) { { parent_path: parent_group.full_path } } + + context 'when user has access to parent group' do + it 'returns child group' do + parent_group.add_developer(user) + + is_expected.to contain_exactly(child_group) + end + end + + context 'when user has no access to parent group' do + it 'generates error' do + expect_graphql_error_to_be_created( + ::Gitlab::Graphql::Errors::ResourceNotAvailable, + format(_('Could not find parent group with path %{path}'), path: parent_group.full_path) + ) { subject } + end + end + + context 'when parent_path has no match' do + let(:params) { { parent_path: 'non-existent-group' } } + + it 'generates error' do + expect_graphql_error_to_be_created( + ::Gitlab::Graphql::Errors::ResourceNotAvailable, + format(_('Could not find parent group with path %{path}'), path: 'non-existent-group') + ) { subject } + end + end + end + context 'with `all_available` argument' do where :args, :expected_param do {} | { all_available: true }