diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md
index 7fea381fd091408b0484e15ba589065c08c075cc..d0ee6a53a958dcd279bf5b956aab1d7de8431cfd 100644
--- a/doc/administration/audit_event_streaming/audit_event_types.md
+++ b/doc/administration/audit_event_streaming/audit_event_types.md
@@ -155,6 +155,7 @@ audit events to external destinations.
| [`incident_reopened_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an incident is reopened using a project access token | **{check-circle}** Yes | **{check-circle}** Yes | `incident_management` | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) |
| [`instance_google_cloud_logging_configuration_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130663) | Triggered when Instance level Google Cloud Logging configuration is created | **{check-circle}** Yes | **{check-circle}** Yes | `audit_events` | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/423038) |
| [`instance_google_cloud_logging_configuration_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131752) | Triggered when instance level Google Cloud Logging configuration is deleted. | **{check-circle}** Yes | **{check-circle}** Yes | `audit_events` | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423040) |
+| [`instance_google_cloud_logging_configuration_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131790) | Triggered when instance level Google Cloud Logging configuration is updated. | **{check-circle}** Yes | **{check-circle}** Yes | `audit_events` | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423039) |
| [`ip_restrictions_changed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86037) | Event triggered on any changes in the IP AllowList | **{check-circle}** Yes | **{check-circle}** Yes | `system_access` | GitLab [15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/358986) |
| [`issue_closed_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an issue is closed using a project access token | **{check-circle}** Yes | **{check-circle}** Yes | `team_planning` | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) |
| [`issue_created_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an issue is created using a project access token | **{check-circle}** Yes | **{check-circle}** Yes | `team_planning` | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 510802a1d0279ad3b2d2bf8536a120539d4edc07..2f96e2f88e4e57d8d768bf9fbf69a11b6a799353 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -4193,6 +4193,30 @@ Input type: `InstanceGoogleCloudLoggingConfigurationDestroyInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.instanceGoogleCloudLoggingConfigurationUpdate`
+
+Input type: `InstanceGoogleCloudLoggingConfigurationUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientEmail` | [`String`](#string) | Email address associated with the service account that will be used to authenticate and interact with the Google Cloud Logging service. This is part of the IAM credentials. |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `googleProjectIdName` | [`String`](#string) | Unique identifier of the Google Cloud project to which the logging configuration belongs. |
+| `id` | [`AuditEventsInstanceGoogleCloudLoggingConfigurationID!`](#auditeventsinstancegooglecloudloggingconfigurationid) | ID of the instance google Cloud configuration to update. |
+| `logIdName` | [`String`](#string) | Unique identifier used to distinguish and manage different logs within the same Google Cloud project. |
+| `name` | [`String`](#string) | Destination name. |
+| `privateKey` | [`String`](#string) | Private Key associated with the service account. This key is used to authenticate the service account and authorize it to interact with the Google Cloud Logging service. |
+
+#### 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. |
+| `instanceGoogleCloudLoggingConfiguration` | [`InstanceGoogleCloudLoggingConfigurationType`](#instancegooglecloudloggingconfigurationtype) | configuration updated. |
+
### `Mutation.issuableResourceLinkCreate`
Input type: `IssuableResourceLinkCreateInput`
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index 74eda16c5f9e02a4fbc730466e76249e482883ed..f9c0ea716479c1b698f62775c819c722e44e5215 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -133,6 +133,7 @@ module MutationType
mount_mutation ::Mutations::AuditEvents::Streaming::InstanceEventTypeFilters::Destroy
mount_mutation ::Mutations::Security::CiConfiguration::ProjectSetContinuousVulnerabilityScanning
mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Destroy
+ mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update
mount_mutation ::Mutations::DependencyProxy::Packages::Settings::Update, alpha: { milestone: '16.5' }
prepend(Types::DeprecatedMutations)
diff --git a/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/common_update.rb b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/common_update.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7f04a572d11e159801177be01afbaef5dc8bf9b9
--- /dev/null
+++ b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/common_update.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AuditEvents
+ module GoogleCloudLoggingConfigurations
+ module CommonUpdate
+ extend ActiveSupport::Concern
+
+ AUDIT_EVENT_COLUMNS = [:google_project_id_name, :client_email, :private_key, :log_id_name, :name].freeze
+
+ included do
+ include ::Audit::Changes
+
+ argument :name, GraphQL::Types::String,
+ required: false,
+ description: 'Destination name.'
+
+ argument :google_project_id_name, GraphQL::Types::String,
+ required: false,
+ description: 'Unique identifier of the Google Cloud project ' \
+ 'to which the logging configuration belongs.'
+
+ argument :client_email, GraphQL::Types::String,
+ required: false,
+ description: 'Email address associated with the service account ' \
+ 'that will be used to authenticate and interact with the ' \
+ 'Google Cloud Logging service. This is part of the IAM credentials.'
+
+ argument :log_id_name, GraphQL::Types::String,
+ required: false,
+ description: 'Unique identifier used to distinguish and manage ' \
+ 'different logs within the same Google Cloud project.'
+
+ argument :private_key, GraphQL::Types::String,
+ required: false,
+ description: 'Private Key associated with the service account. This key ' \
+ 'is used to authenticate the service account and authorize it ' \
+ 'to interact with the Google Cloud Logging service.'
+ end
+
+ def update_config(
+ id:, google_project_id_name: nil, client_email: nil, private_key: nil, log_id_name: nil, name: nil
+ )
+ config = authorized_find!(id)
+ config_attributes = {
+ google_project_id_name: google_project_id_name,
+ client_email: client_email,
+ private_key: private_key,
+ log_id_name: log_id_name,
+ name: name
+ }.compact
+
+ if config.update(config_attributes)
+ audit_update(config)
+ [config, []]
+ else
+ [nil, Array(config.errors)]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/update.rb b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/update.rb
index abd0509538830355592981037d0a63d749fecde5..77d23a954335f4410e0dbd3663bd390119deeb18 100644
--- a/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/update.rb
+++ b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/update.rb
@@ -6,63 +6,25 @@ module GoogleCloudLoggingConfigurations
class Update < Base
graphql_name 'GoogleCloudLoggingConfigurationUpdate'
- include ::Audit::Changes
+ include Mutations::AuditEvents::GoogleCloudLoggingConfigurations::CommonUpdate
UPDATE_EVENT_NAME = 'google_cloud_logging_configuration_updated'
- AUDIT_EVENT_COLUMNS = [:google_project_id_name, :client_email, :private_key, :log_id_name, :name].freeze
-
authorize :admin_external_audit_events
argument :id, ::Types::GlobalIDType[::AuditEvents::GoogleCloudLoggingConfiguration],
required: true,
description: 'ID of the google Cloud configuration to update.'
- argument :name, GraphQL::Types::String,
- required: false,
- description: 'Destination name.'
-
- argument :google_project_id_name, GraphQL::Types::String,
- required: false,
- description: 'Unique identifier of the Google Cloud project ' \
- 'to which the logging configuration belongs.'
-
- argument :client_email, GraphQL::Types::String,
- required: false,
- description: 'Email address associated with the service account ' \
- 'that will be used to authenticate and interact with the ' \
- 'Google Cloud Logging service. This is part of the IAM credentials.'
-
- argument :log_id_name, GraphQL::Types::String,
- required: false,
- description: 'Unique identifier used to distinguish and manage ' \
- 'different logs within the same Google Cloud project.'
-
- argument :private_key, GraphQL::Types::String,
- required: false,
- description: 'Private Key associated with the service account. This key ' \
- 'is used to authenticate the service account and authorize it ' \
- 'to interact with the Google Cloud Logging service.'
-
field :google_cloud_logging_configuration, ::Types::AuditEvents::GoogleCloudLoggingConfigurationType,
null: true,
description: 'configuration updated.'
def resolve(id:, google_project_id_name: nil, client_email: nil, private_key: nil, log_id_name: nil, name: nil)
- config = authorized_find!(id)
- config_attributes = {
- google_project_id_name: google_project_id_name,
- client_email: client_email,
- private_key: private_key,
- log_id_name: log_id_name,
- name: name
- }.compact
+ config, errors = update_config(id: id, google_project_id_name: google_project_id_name,
+ client_email: client_email, private_key: private_key,
+ log_id_name: log_id_name, name: name)
- if config.update(config_attributes)
- audit_update(config)
- { google_cloud_logging_configuration: config, errors: [] }
- else
- { google_cloud_logging_configuration: nil, errors: Array(config.errors) }
- end
+ { google_cloud_logging_configuration: config, errors: errors }
end
private
diff --git a/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update.rb b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab2d0bf8adc90c4e98e4996e89fe608db9ec4a9d
--- /dev/null
+++ b/ee/app/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AuditEvents
+ module Instance
+ module GoogleCloudLoggingConfigurations
+ class Update < Base
+ graphql_name 'InstanceGoogleCloudLoggingConfigurationUpdate'
+
+ include Mutations::AuditEvents::GoogleCloudLoggingConfigurations::CommonUpdate
+
+ UPDATE_EVENT_NAME = 'instance_google_cloud_logging_configuration_updated'
+
+ argument :id, ::Types::GlobalIDType[::AuditEvents::Instance::GoogleCloudLoggingConfiguration],
+ required: true,
+ description: 'ID of the instance google Cloud configuration to update.'
+
+ field :instance_google_cloud_logging_configuration,
+ ::Types::AuditEvents::Instance::GoogleCloudLoggingConfigurationType,
+ null: true,
+ description: 'configuration updated.'
+
+ def resolve(
+ id:,
+ google_project_id_name: nil,
+ client_email: nil,
+ private_key: nil,
+ log_id_name: nil,
+ name: nil
+ )
+ config, errors = update_config(id: id, google_project_id_name: google_project_id_name,
+ client_email: client_email, private_key: private_key,
+ log_id_name: log_id_name, name: name)
+
+ { instance_google_cloud_logging_configuration: config, errors: errors }
+ end
+
+ private
+
+ def audit_update(config)
+ AUDIT_EVENT_COLUMNS.each do |column|
+ audit_changes(
+ column,
+ as: column.to_s,
+ entity: Gitlab::Audit::InstanceScope.new,
+ model: config,
+ event_type: UPDATE_EVENT_NAME,
+ target_details: config.name
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ee/config/audit_events/types/instance_google_cloud_logging_configuration_updated.yml b/ee/config/audit_events/types/instance_google_cloud_logging_configuration_updated.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f7fbc59dfc961db968b37db072c89191a1d1e075
--- /dev/null
+++ b/ee/config/audit_events/types/instance_google_cloud_logging_configuration_updated.yml
@@ -0,0 +1,9 @@
+---
+name: instance_google_cloud_logging_configuration_updated
+description: Triggered when instance level Google Cloud Logging configuration is updated.
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/423039
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131790
+milestone: '16.5'
+feature_category: audit_events
+saved_to_database: true
+streamed: true
diff --git a/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update_spec.rb b/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ceeb7774aa7e078a7cbf41d01ab53d5004ef0e9a
--- /dev/null
+++ b/ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/update_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Update Instance Google Cloud logging configuration', feature_category: :audit_events do
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:config) { create(:instance_google_cloud_logging_configuration) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:updated_google_project_id_name) { 'updated-project' }
+ let_it_be(:updated_client_email) { 'updated-email@example.com' }
+ let_it_be(:updated_private_key) { OpenSSL::PKey::RSA.new(4096).to_pem }
+ let_it_be(:updated_log_id_name) { 'updated_log_id_name' }
+ let_it_be(:updated_destination_name) { 'updated_destination_name' }
+ let_it_be(:config_gid) { global_id_of(config) }
+
+ let(:current_user) { admin }
+ let(:mutation) { graphql_mutation(:instance_google_cloud_logging_configuration_update, input) }
+ let(:mutation_response) { graphql_mutation_response(:instance_google_cloud_logging_configuration_update) }
+
+ let(:input) do
+ {
+ id: config_gid,
+ googleProjectIdName: updated_google_project_id_name,
+ clientEmail: updated_client_email,
+ privateKey: updated_private_key,
+ logIdName: updated_log_id_name,
+ name: updated_destination_name
+ }
+ end
+
+ subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ shared_examples 'a mutation that does not update the configuration' do
+ it 'does not update the configuration' do
+ expect { mutate }.not_to change { config.reload.attributes }
+ end
+
+ it 'does not create audit event' do
+ expect { mutate }.not_to change { AuditEvent.count }
+ end
+ end
+
+ context 'when feature is licensed' do
+ before do
+ stub_licensed_features(external_audit_events: true)
+ end
+
+ context 'when current user is instance admin' do
+ before do
+ allow(Gitlab::Audit::Auditor).to receive(:audit)
+ end
+
+ it 'updates the configuration' do
+ mutate
+
+ config.reload
+
+ expect(config.google_project_id_name).to eq(updated_google_project_id_name)
+ expect(config.client_email).to eq(updated_client_email)
+ expect(config.private_key).to eq(updated_private_key)
+ expect(config.log_id_name).to eq(updated_log_id_name)
+ expect(config.name).to eq(updated_destination_name)
+ end
+
+ it 'audits the update' do
+ Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update::AUDIT_EVENT_COLUMNS.each do |column|
+ message = if column == :private_key
+ "Changed #{column}"
+ else
+ "Changed #{column} from #{config[column]} to #{input[column.to_s.camelize(:lower).to_sym]}"
+ end
+
+ expected_hash = {
+ name: Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update::UPDATE_EVENT_NAME,
+ author: current_user,
+ scope: an_instance_of(Gitlab::Audit::InstanceScope),
+ target: config,
+ message: message,
+ target_details: updated_destination_name
+ }
+
+ expect(Gitlab::Audit::Auditor).to receive(:audit).once.ordered.with(hash_including(expected_hash))
+ end
+
+ subject
+ end
+
+ context 'when the fields are updated with existing values' do
+ let(:input) do
+ {
+ id: config_gid,
+ googleProjectIdName: config.google_project_id_name,
+ name: config.name
+ }
+ end
+
+ it 'does not audit the event' do
+ expect(Gitlab::Audit::Auditor).not_to receive(:audit)
+
+ subject
+ end
+ end
+
+ context 'when no fields are provided for update' do
+ let(:input) do
+ {
+ id: config_gid
+ }
+ end
+
+ it_behaves_like 'a mutation that does not update the configuration'
+ end
+
+ context 'when there is error while updating' do
+ before do
+ allow_next_instance_of(
+ Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update) do |mutation|
+ allow(mutation).to receive(:authorized_find!).with(config_gid).and_return(config)
+ end
+
+ allow(config).to receive(:update).and_return(false)
+
+ errors = ActiveModel::Errors.new(config).tap { |e| e.add(:base, 'error message') }
+ allow(config).to receive(:errors).and_return(errors)
+ end
+
+ it 'does not update the configuration and returns the error' do
+ mutate
+
+ expect(mutation_response).to include(
+ 'instanceGoogleCloudLoggingConfiguration' => nil,
+ 'errors' => ['error message']
+ )
+ end
+ end
+ end
+
+ context 'when current user is not instance admin' do
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ it_behaves_like 'a mutation that does not update the configuration'
+ end
+ end
+
+ context 'when feature is unlicensed' do
+ before do
+ stub_licensed_features(external_audit_events: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ it_behaves_like 'a mutation that does not update the configuration'
+ end
+end