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