diff --git a/app/graphql/mutations/groups/update.rb b/app/graphql/mutations/groups/update.rb new file mode 100644 index 0000000000000000000000000000000000000000..f4eb94f9f8422a3999a8c191eb2484d9a1af376b --- /dev/null +++ b/app/graphql/mutations/groups/update.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Mutations + module Groups + class Update < Mutations::BaseMutation + include Mutations::ResolvesGroup + + graphql_name 'GroupUpdate' + + authorize :admin_group + + field :group, Types::GroupType, + null: true, + description: 'The group after update.' + + argument :full_path, GraphQL::Types::ID, + required: true, + description: 'Full path of the group that will be updated.' + argument :shared_runners_setting, Types::Namespace::SharedRunnersSettingEnum, + required: true, + description: copy_field_description(Types::GroupType, :shared_runners_setting) + + def resolve(full_path:, **args) + group = authorized_find!(full_path: full_path) + + unless ::Groups::UpdateService.new(group, current_user, args).execute + return { group: nil, errors: group.errors.full_messages } + end + + { group: group, errors: [] } + end + + private + + def find_object(full_path:) + resolve_group(full_path: full_path) + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 812943e0a1e1d65a1ca14429b266a3314ba9684a..530298ba07a33747e663fb2e584bdbe08c712b56 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -105,6 +105,7 @@ class MutationType < BaseObject mount_mutation Mutations::Ci::Runner::Delete, feature_flag: :runner_graphql_query mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, feature_flag: :runner_graphql_query mount_mutation Mutations::Namespace::PackageSettings::Update + mount_mutation Mutations::Groups::Update mount_mutation Mutations::UserCallouts::Create mount_mutation Mutations::Packages::Destroy mount_mutation Mutations::Packages::DestroyFile diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 99b1b023524c25208cbdacaad39ebf78bab46921..b6b266ada81068e4bf07a84200b54d69e1a08465 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2360,6 +2360,26 @@ Input type: `GitlabSubscriptionActivateInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `license` | [`CurrentLicense`](#currentlicense) | The current license. | +### `Mutation.groupUpdate` + +Input type: `GroupUpdateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `fullPath` | [`ID!`](#id) | Full path of the group that will be updated. | +| `sharedRunnersSetting` | [`SharedRunnersSetting!`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `group` | [`Group`](#group) | The group after update. | + ### `Mutation.httpIntegrationCreate` Input type: `HttpIntegrationCreateInput` diff --git a/spec/graphql/mutations/groups/update_spec.rb b/spec/graphql/mutations/groups/update_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2118134e8e6c83e55c1518babd21dd25e0ae0c62 --- /dev/null +++ b/spec/graphql/mutations/groups/update_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Groups::Update do + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + let(:params) { { full_path: group.full_path } } + + specify { expect(described_class).to require_graphql_authorizations(:admin_group) } + + describe '#resolve' do + subject { described_class.new(object: group, context: { current_user: user }, field: nil).resolve(**params) } + + RSpec.shared_examples 'updating the group shared runners setting' do + it 'updates the group shared runners setting' do + expect { subject } + .to change { group.reload.shared_runners_setting }.from('enabled').to('disabled_and_unoverridable') + end + + it 'returns no errors' do + expect(subject).to eq(errors: [], group: group) + end + + context 'with invalid params' do + let_it_be(:params) { { full_path: group.full_path, shared_runners_setting: 'inexistent_setting' } } + + it 'doesn\'t update the shared_runners_setting' do + expect { subject } + .not_to change { group.reload.shared_runners_setting } + end + + it 'returns an error' do + expect(subject).to eq( + group: nil, + errors: ["Update shared runners state must be one of: #{::Namespace::SHARED_RUNNERS_SETTINGS.join(', ')}"] + ) + end + end + end + + RSpec.shared_examples 'denying access to group shared runners setting' do + it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'changing shared runners setting' do + let_it_be(:params) do + { full_path: group.full_path, + shared_runners_setting: 'disabled_and_unoverridable' } + end + + where(:user_role, :shared_examples_name) do + :owner | 'updating the group shared runners setting' + :developer | 'denying access to group shared runners setting' + :reporter | 'denying access to group shared runners setting' + :guest | 'denying access to group shared runners setting' + :anonymous | 'denying access to group shared runners setting' + end + + with_them do + before do + group.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/groups/update_spec.rb b/spec/requests/api/graphql/mutations/groups/update_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b9dfb8e37ab1e3c267183490c152718677c36f3f --- /dev/null +++ b/spec/requests/api/graphql/mutations/groups/update_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'GroupUpdate' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:group) { create(:group) } + + let(:variables) do + { + full_path: group.full_path, + shared_runners_setting: 'DISABLED_WITH_OVERRIDE' + } + end + + let(:mutation) { graphql_mutation(:group_update, variables) } + + context 'when unauthorized' do + shared_examples 'unauthorized' do + it 'returns an error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + end + end + + context 'when not a group member' do + it_behaves_like 'unauthorized' + end + + context 'when a non-admin group member' do + before do + group.add_developer(user) + end + + it_behaves_like 'unauthorized' + end + end + + context 'when authorized' do + before do + group.add_owner(user) + end + + it 'updates shared runners settings' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_errors).to be_nil + expect(group.reload.shared_runners_setting).to eq(variables[:shared_runners_setting].downcase) + end + + context 'when bad arguments are provided' do + let(:variables) { { full_path: '', shared_runners_setting: 'INVALID' } } + + it 'returns the errors' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + expect(group.reload.shared_runners_setting).to eq('enabled') + end + end + end +end