From 02fff57ec775c4f06020949e02a4dc699c35c2f5 Mon Sep 17 00:00:00 2001
From: Smriti Garg <sgarg@gitlab.com>
Date: Fri, 28 Jul 2023 06:06:37 +0000
Subject: [PATCH] Review feedback integrated

Encrypted column fix
---
 .../audit_event_types.md                      |  1 +
 .../ee/application_settings/update_service.rb |  8 ++
 .../types/application_setting_updated.yml     |  9 +++
 .../application_setting_changes_auditor.rb    | 33 ++++++++
 ...pplication_setting_changes_auditor_spec.rb | 78 +++++++++++++++++++
 .../update_service_spec.rb                    | 31 +++++++-
 .../audit_event_logging_shared_examples.rb    |  2 +-
 .../gems/attr_encrypted/lib/attr_encrypted.rb |  2 +
 8 files changed, 162 insertions(+), 2 deletions(-)
 create mode 100644 ee/config/audit_events/types/application_setting_updated.yml
 create mode 100644 ee/lib/audit/application_setting_changes_auditor.rb
 create mode 100644 ee/spec/lib/audit/application_setting_changes_auditor_spec.rb

diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md
index a8fa06dbc647..b34fab8f1fa4 100644
--- a/doc/administration/audit_event_streaming/audit_event_types.md
+++ b/doc/administration/audit_event_streaming/audit_event_types.md
@@ -21,6 +21,7 @@ Audit event types are used to [filter streamed audit events](index.md#update-eve
 | [`allow_committer_approval_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102256) | Event triggered on updating prevent merge request approval from committers from group merge request setting | **{check-circle}** Yes | **{check-circle}** Yes | `compliance_management` | [15.6](https://gitlab.com/gitlab-org/gitlab/-/issues/373949) |
 | [`allow_merge_on_skipped_pipeline_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83922) | There is a project setting which toggles the ability to merge when a pipeline is skipped. This audit event tracks changes to that setting. This MR adds a setting to allow this (like previous GitLab versions). | **{check-circle}** Yes | **{check-circle}** Yes | `continuous_integration` | [14.10](https://gitlab.com/gitlab-org/gitlab/-/issues/301124) |
 | [`allow_overrides_to_approver_list_per_merge_request_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102256) | Event triggered on updating prevent users from modifying MR approval rules in merge requests from group merge request setting | **{check-circle}** Yes | **{check-circle}** Yes | `compliance_management` | [15.6](https://gitlab.com/gitlab-org/gitlab/-/issues/373949) |
+| [`application_setting_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124639) | Triggered when Application setting is updated | **{check-circle}** Yes | **{check-circle}** Yes | `system_access` | [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/282428) |
 | [`approval_rule_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89939) | Triggered when a merge request approval rule is created | **{check-circle}** Yes | **{check-circle}** Yes | `source_code_management` | [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363092) |
 | [`approval_rule_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82297) | Triggered on successful approval rule deletion | **{check-circle}** Yes | **{check-circle}** Yes | `source_code_management` | [14.9](https://gitlab.com/gitlab-org/gitlab/-/issues/329514) |
 | [`audit_events_streaming_headers_create`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92068) | Triggered when a streaming header for audit events is created | **{check-circle}** Yes | **{check-circle}** Yes | `audit_events` | [15.3](https://gitlab.com/gitlab-org/gitlab/-/issues/366350) |
diff --git a/ee/app/services/ee/application_settings/update_service.rb b/ee/app/services/ee/application_settings/update_service.rb
index df799d5222b0..810fccb79185 100644
--- a/ee/app/services/ee/application_settings/update_service.rb
+++ b/ee/app/services/ee/application_settings/update_service.rb
@@ -27,6 +27,10 @@ def execute
           update_elasticsearch_containers(ElasticsearchIndexedNamespace, elasticsearch_namespace_ids)
           update_elasticsearch_containers(ElasticsearchIndexedProject, elasticsearch_project_ids)
           update_elasticsearch_index_settings(number_of_replicas: elasticsearch_replicas, number_of_shards: elasticsearch_shards)
+
+          # There are cases when current user is passed as nil like in elastic.rake
+          # we should not log audit events in such cases
+          log_audit_events if current_user
         end
 
         result
@@ -79,6 +83,10 @@ def should_auto_approve_blocked_users?
         super || user_cap_increased?
       end
 
+      def log_audit_events
+        Audit::ApplicationSettingChangesAuditor.new(current_user, application_setting).execute
+      end
+
       def user_cap_increased?
         return false unless application_setting.previous_changes.key?(:new_user_signups_cap)
 
diff --git a/ee/config/audit_events/types/application_setting_updated.yml b/ee/config/audit_events/types/application_setting_updated.yml
new file mode 100644
index 000000000000..0ad85b4562e1
--- /dev/null
+++ b/ee/config/audit_events/types/application_setting_updated.yml
@@ -0,0 +1,9 @@
+---
+name: application_setting_updated
+description: Triggered when Application setting is updated
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/282428
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124639
+feature_category: system_access
+milestone: '16.3'
+saved_to_database: true
+streamed: true
diff --git a/ee/lib/audit/application_setting_changes_auditor.rb b/ee/lib/audit/application_setting_changes_auditor.rb
new file mode 100644
index 000000000000..151655192c85
--- /dev/null
+++ b/ee/lib/audit/application_setting_changes_auditor.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Audit
+  class ApplicationSettingChangesAuditor < BaseChangesAuditor
+    def execute
+      return if model.blank?
+
+      changed_columns = model.previous_changes.except!(:updated_at)
+
+      changed_columns.each_key do |column|
+        next if auditable_attribute?(column)
+
+        audit_changes(column, as: column.to_s, model: model,
+          entity: Gitlab::Audit::InstanceScope.new,
+          event_type: "application_setting_updated")
+      end
+    end
+
+    private
+
+    def auditable_attribute?(column)
+      !!(column =~ /_html\Z/) || !!(column =~ /^encrypted_/)
+    end
+
+    def attributes_from_auditable_model(column)
+      {
+        from: model.previous_changes[column].first,
+        to: model.previous_changes[column].last,
+        target_details: column.humanize
+      }
+    end
+  end
+end
diff --git a/ee/spec/lib/audit/application_setting_changes_auditor_spec.rb b/ee/spec/lib/audit/application_setting_changes_auditor_spec.rb
new file mode 100644
index 000000000000..fee14eee164f
--- /dev/null
+++ b/ee/spec/lib/audit/application_setting_changes_auditor_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Audit::ApplicationSettingChangesAuditor, feature_category: :audit_events do
+  include StubENV
+
+  describe '.audit_changes' do
+    let!(:user) { create(:user) }
+    let!(:application_setting) { ApplicationSetting.create_from_defaults }
+    let!(:application_setting_auditor_instance) { described_class.new(user, application_setting) }
+
+    before do
+      stub_licensed_features(extended_audit_events: true, admin_audit_log: true)
+    end
+
+    shared_examples 'application_setting_audit_events_from_to' do
+      it 'calls auditor' do
+        expect(Gitlab::Audit::Auditor).to receive(:audit).with(
+          {
+            name: "application_setting_updated",
+            author: user,
+            scope: be_an_instance_of(Gitlab::Audit::InstanceScope),
+            target: application_setting,
+            message: "Changed #{change_field} from #{change_from} to #{change_to}",
+            additional_details: {
+              change: change_field.to_s,
+              from: change_from,
+              target_details: change_field.humanize,
+              to: change_to
+            },
+            target_details: change_field.humanize
+          }
+        ).and_call_original
+
+        expect { application_setting_auditor_instance.execute }.to change { AuditEvent.count }.by(1)
+
+        event = AuditEvent.last
+        expect(event.details[:from]).to eq change_from
+        expect(event.details[:to]).to eq change_to
+        expect(event.details[:change]).to eq change_field
+      end
+    end
+
+    context 'when any model change is made' do
+      let(:change_from) { 0 }
+      let(:change_to) { 10 }
+      let(:change_field) { "default_project_visibility" }
+
+      before do
+        application_setting.update!(default_project_visibility: 10)
+      end
+
+      it_behaves_like 'application_setting_audit_events_from_to'
+    end
+
+    context 'when ignored column is updated' do
+      it 'does not create an event for _html columns' do
+        application_setting.update!(after_sign_up_text_html: "test_text")
+
+        expect(AuditEvents::AuditEventStreamingWorker).not_to receive(:perform_async)
+        expect { application_setting_auditor_instance.execute }.not_to change { AuditEvent.count }
+      end
+    end
+
+    context 'when encrypted column is updated' do
+      it 'creates an event for encrypted columns' do
+        application_setting.update!(anthropic_api_key: 'ANTHROPIC_API_KEY')
+
+        expect { application_setting_auditor_instance.execute }.to change { AuditEvent.count }.by(1)
+
+        event = AuditEvent.last
+        expect(event.details[:change]).to eq "anthropic_api_key"
+        expect(event.details[:to]).to eq nil # For encrypted column encrypted_ column contains the cipher
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/application_settings/update_service_spec.rb b/ee/spec/services/application_settings/update_service_spec.rb
index f6d01ad96d50..e5060260e64d 100644
--- a/ee/spec/services/application_settings/update_service_spec.rb
+++ b/ee/spec/services/application_settings/update_service_spec.rb
@@ -3,19 +3,48 @@
 require 'spec_helper'
 
 RSpec.describe ApplicationSettings::UpdateService do
-  let(:user)    { create(:user) }
+  let!(:user) { create(:user) }
   let(:setting) { ApplicationSetting.create_from_defaults }
   let(:service) { described_class.new(setting, user, opts) }
 
+  shared_examples 'application_setting_audit_events_from_to' do
+    it 'calls auditor' do
+      expect { service.execute }.to change { AuditEvent.count }.by(1)
+      service.execute
+
+      event = AuditEvent.last
+      expect(event.details[:from]).to eq change_from
+      expect(event.details[:to]).to eq change_to
+      expect(event.details[:change]).to eq change_field
+    end
+
+    context 'when user is nil' do
+      let(:user) { nil }
+
+      it "does not log an event" do
+        expect { service.execute }.to change { AuditEvent.count }.by(0)
+      end
+    end
+  end
+
   describe '#execute' do
     context 'common params' do
       let(:opts) { { home_page_url: 'http://foo.bar' } }
+      let(:change_field) { 'home_page_url' }
+      let(:change_to) { 'http://foo.bar' }
+      let(:change_from) { nil }
+
+      before do
+        stub_licensed_features(extended_audit_events: true, admin_audit_log: true, code_owner_approval_required: true)
+      end
 
       it 'properly updates settings with given params' do
         service.execute
 
         expect(setting.home_page_url).to eql(opts[:home_page_url])
       end
+
+      it_behaves_like 'application_setting_audit_events_from_to'
     end
 
     context 'with valid params' do
diff --git a/ee/spec/support/shared_examples/services/audit_event_logging_shared_examples.rb b/ee/spec/support/shared_examples/services/audit_event_logging_shared_examples.rb
index 6b29b6543376..ebd7a72fbc47 100644
--- a/ee/spec/support/shared_examples/services/audit_event_logging_shared_examples.rb
+++ b/ee/spec/support/shared_examples/services/audit_event_logging_shared_examples.rb
@@ -41,7 +41,7 @@
     it 'does not log audit event if operation fails' do
       fail_condition!
 
-      expect { operation }.not_to change(AuditEvent, :count)
+      expect { operation }.not_to change { AuditEvent.count }
     end
 
     it 'does not log audit event if operation results in no change' do
diff --git a/vendor/gems/attr_encrypted/lib/attr_encrypted.rb b/vendor/gems/attr_encrypted/lib/attr_encrypted.rb
index 82a14086adea..28508e6d2603 100644
--- a/vendor/gems/attr_encrypted/lib/attr_encrypted.rb
+++ b/vendor/gems/attr_encrypted/lib/attr_encrypted.rb
@@ -186,6 +186,8 @@ def attr_encrypted_options
     @attr_encrypted_options ||= superclass.attr_encrypted_options.dup
   end
 
+  # Encrypted attributes are ignored as part of ApplicationSetting Audit Event
+  # Log. If the prefix is updated then the corresponding Regex in ApplicationSettingChangesAuditor should also be updated.
   def attr_encrypted_default_options
     {
       prefix:            'encrypted_',
-- 
GitLab