From 6061ca7e589b1625ef455a3686643039b79f102e Mon Sep 17 00:00:00 2001
From: Harsimar Sandhu <hsandhu@gitlab.com>
Date: Mon, 4 Jul 2022 11:38:19 +0000
Subject: [PATCH] Use auditor when auditing using audit changes

EE: true
Changelog: changed
---
 ee/app/services/audit_events/build_service.rb |  9 +--
 ee/lib/audit/changes.rb                       | 39 ++++++++++++-
 ee/lib/gitlab/audit/auditor.rb                |  4 +-
 ee/spec/lib/audit/changes_spec.rb             | 58 ++++++++++---------
 ...protected_branches_changes_auditor_spec.rb |  6 +-
 ee/spec/lib/gitlab/audit/auditor_spec.rb      | 37 ++++++++++++
 .../services/groups/update_service_spec.rb    |  3 +-
 .../projects/transfer_service_spec.rb         |  3 +-
 .../services/projects/update_service_spec.rb  |  9 ++-
 9 files changed, 125 insertions(+), 43 deletions(-)

diff --git a/ee/app/services/audit_events/build_service.rb b/ee/app/services/audit_events/build_service.rb
index fc89c9d2b9f9b..c3ac3584cc208 100644
--- a/ee/app/services/audit_events/build_service.rb
+++ b/ee/app/services/audit_events/build_service.rb
@@ -10,7 +10,7 @@ class BuildService
     # @return [BuildService]
     def initialize(
       author:, scope:, target:, message:,
-      created_at: DateTime.current, additional_details: {}, ip_address: nil)
+      created_at: DateTime.current, additional_details: {}, ip_address: nil, target_details: nil)
       raise MissingAttributeError if missing_attribute?(author, scope, target, message)
 
       @author = build_author(author)
@@ -20,6 +20,7 @@ def initialize(
       @message = build_message(message)
       @created_at = created_at
       @additional_details = additional_details
+      @target_details = target_details
     end
 
     # Create an instance of AuditEvent
@@ -61,13 +62,13 @@ def base_payload
     end
 
     def base_details_payload
-      {
+      @additional_details.merge({
         author_name: @author.name,
         target_id: @target.id,
         target_type: @target.type,
-        target_details: @target.details,
+        target_details: @target_details || @target.details,
         custom_message: @message
-      }.merge(@additional_details)
+      })
     end
 
     def build_author(author)
diff --git a/ee/lib/audit/changes.rb b/ee/lib/audit/changes.rb
index fc6ff2937e9a0..1fcb44034b2a0 100644
--- a/ee/lib/audit/changes.rb
+++ b/ee/lib/audit/changes.rb
@@ -10,7 +10,7 @@ module Changes
     # @option options [User, Project, Group] :target_model scope the event belongs to
     # @option options [Object] :model object being audited
     # @option options [Boolean] :skip_changes whether to record from/to values
-    #
+    # @option options [String] :event_type adds event type in streaming audit event headers and payload
     # @return [AuditEvent, nil] the resulting object or nil if there is no
     #   change detected
     def audit_changes(column, options = {})
@@ -66,8 +66,41 @@ def parse_options(column, options)
     end
 
     def audit_event(options)
-      ::AuditEventService.new(@current_user, entity, options) # rubocop:disable Gitlab/ModuleWithInstanceVariables
-        .for_changes(model).security_event
+      return unless audit_enabled?
+
+      name = options.fetch(:event_type, 'audit_operation')
+      details = additional_details(options)
+      audit_context = {
+        name: name,
+        author: @current_user, # rubocop:disable Gitlab/ModuleWithInstanceVariables
+        scope: entity,
+        target: model,
+        message: build_message(details),
+        additional_details: details,
+        target_details: options[:target_details]
+      }
+
+      ::Gitlab::Audit::Auditor.audit(audit_context)
+    end
+
+    def additional_details(options)
+      { change: options[:as] || options[:column] }.merge(options.slice(:from, :to, :target_details))
+    end
+
+    def build_message(details)
+      message = ["Changed #{details[:change]}"]
+      message << "from #{details[:from]}" unless details[:from].blank?
+      message << "to #{details[:to]}" unless details[:to].blank?
+      message.join(' ')
+    end
+
+    # TODO: Remove this once we implement license feature checks in Auditor.
+    # issue link: https://gitlab.com/gitlab-org/gitlab/-/issues/365441
+    def audit_enabled?
+      return true if ::License.feature_available?(:admin_audit_log)
+      return true if ::License.feature_available?(:extended_audit_events)
+
+      entity.respond_to?(:feature_available?) && entity.licensed_feature_available?(:audit_events)
     end
   end
 end
diff --git a/ee/lib/gitlab/audit/auditor.rb b/ee/lib/gitlab/audit/auditor.rb
index b1ff59d34b633..16f43c7d6eb91 100644
--- a/ee/lib/gitlab/audit/auditor.rb
+++ b/ee/lib/gitlab/audit/auditor.rb
@@ -63,6 +63,7 @@ def initialize(context = {})
         @message = @context.fetch(:message, '')
         @additional_details = @context.fetch(:additional_details, {})
         @ip_address = @context[:ip_address]
+        @target_details = @context[:target_details]
       end
 
       def multiple_audit
@@ -103,7 +104,8 @@ def build_event(message)
           created_at: @created_at,
           message: message,
           additional_details: @additional_details,
-          ip_address: @ip_address
+          ip_address: @ip_address,
+          target_details: @target_details
         ).execute
       end
 
diff --git a/ee/spec/lib/audit/changes_spec.rb b/ee/spec/lib/audit/changes_spec.rb
index 1677ec57797df..1c679416dcb6e 100644
--- a/ee/spec/lib/audit/changes_spec.rb
+++ b/ee/spec/lib/audit/changes_spec.rb
@@ -39,42 +39,44 @@
     end
 
     describe 'audit changes' do
-      let(:audit_event_service) { instance_spy(AuditEventService) }
+      let(:options) { { model: user, event_type: 'audit_operation' } }
 
-      before do
-        allow(AuditEventService).to receive(:new).and_return(audit_event_service)
-      end
-
-      it 'calls the audit event service' do
+      it 'calls the auditor' do
         user.update!(name: 'Scrooge McDuck')
 
-        audit!
+        expect(::Gitlab::Audit::Auditor).to receive(:audit).with(
+          { additional_details: { change: :name,
+                                  from: "Donald Duck",
+                                  to: "Scrooge McDuck" },
+            name: 'audit_operation',
+            author: current_user,
+            scope: user,
+            target: user,
+            message: "Changed name from Donald Duck to Scrooge McDuck",
+            target_details: nil }
+        )
 
-        aggregate_failures 'audit event service interactions' do
-          expect(AuditEventService).to have_received(:new)
-            .with(
-              current_user, user,
-              model: user,
-              action: :update, column: :name,
-              from: 'Donald Duck', to: 'Scrooge McDuck'
-            )
-          expect(audit_event_service).to have_received(:for_changes)
-          expect(audit_event_service).to have_received(:security_event)
-        end
+        audit!
       end
 
-      context 'when entity is provided' do
-        let(:project) { Project.new }
-        let(:options) { { model: user, entity: project } }
-
-        it 'instantiates audit event service with the given entity' do
-          user.update!(name: 'Scrooge McDuck')
+      it 'creates audit event with correct attributes', :aggregate_failure do
+        user.update!(name: 'Scrooge McDuck')
 
-          audit!
+        audit!
 
-          expect(AuditEventService).to have_received(:new)
-            .with(anything, project, anything)
-        end
+        audit_event = AuditEvent.last
+
+        expect(audit_event.author_id).to eq(current_user.id)
+        expect(audit_event.entity_id).to eq(user.id)
+        expect(audit_event.entity_type).to eq(user.class.name)
+        expect(audit_event.details).to eq({ change: :name,
+                                            author_name: current_user.name,
+                                            from: "Donald Duck",
+                                            to: "Scrooge McDuck",
+                                            target_details: user.name,
+                                            target_id: user.id,
+                                            target_type: user.class.name,
+                                            custom_message: "Changed name from Donald Duck to Scrooge McDuck" })
       end
     end
   end
diff --git a/ee/spec/lib/ee/audit/protected_branches_changes_auditor_spec.rb b/ee/spec/lib/ee/audit/protected_branches_changes_auditor_spec.rb
index 1e9fba51684dc..8580a906ec59a 100644
--- a/ee/spec/lib/ee/audit/protected_branches_changes_auditor_spec.rb
+++ b/ee/spec/lib/ee/audit/protected_branches_changes_auditor_spec.rb
@@ -32,7 +32,8 @@
           expect { service.execute }.to change { AuditEvent.count }.by(1)
 
           event = AuditEvent.last
-          expect(event.details).to eq({ change: "#{setting}".humanize(capitalize: false),
+          change_text = setting.to_s.humanize(capitalize: false)
+          expect(event.details).to eq({ change: change_text,
                                         author_name: author.name,
                                         target_id: protected_branch.id,
                                         entity_path: entity.full_path,
@@ -40,7 +41,8 @@
                                         target_details: protected_branch.name,
                                         from: false,
                                         to: true,
-                                        ip_address: ip_address })
+                                        ip_address: ip_address,
+                                        custom_message: "Changed #{change_text} to true" })
 
           expect(event.author_id).to eq(author.id)
           expect(event.entity_id).to eq(entity.id)
diff --git a/ee/spec/lib/gitlab/audit/auditor_spec.rb b/ee/spec/lib/gitlab/audit/auditor_spec.rb
index 87b0996d54a82..9657b886bffa8 100644
--- a/ee/spec/lib/gitlab/audit/auditor_spec.rb
+++ b/ee/spec/lib/gitlab/audit/auditor_spec.rb
@@ -191,6 +191,43 @@
         end
       end
 
+      context 'when overriding the target_details' do
+        target_details = "this is my target details"
+        let(:context) do
+          { name: name,
+            author: author,
+            scope: scope,
+            target: target,
+            created_at: Time.zone.now,
+            target_details: target_details }
+        end
+
+        it 'logs audit events to database' do
+          freeze_time do
+            audit!
+
+            audit_event = AuditEvent.last
+            expect(audit_event.details).to include({ target_details: target_details })
+            expect(audit_event.target_details).to eq(target_details)
+          end
+        end
+
+        it 'logs audit events to file' do
+          freeze_time do
+            expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
+
+            audit!
+
+            expect(logger).to have_received(:info).exactly(2).times.with(
+              hash_including(
+                'details' => hash_including('target_details' => target_details),
+                'target_details' => target_details
+              )
+            )
+          end
+        end
+      end
+
       context 'when overriding the ip address' do
         ip_address = '192.168.8.8'
         let(:context) { { name: name, author: author, scope: scope, target: target, ip_address: ip_address } }
diff --git a/ee/spec/services/groups/update_service_spec.rb b/ee/spec/services/groups/update_service_spec.rb
index 232d3047cc010..ba7d2c97480be 100644
--- a/ee/spec/services/groups/update_service_spec.rb
+++ b/ee/spec/services/groups/update_service_spec.rb
@@ -40,7 +40,8 @@
             param[:details].merge!(
               change: 'visibility',
               from: 'Public',
-              to: 'Private'
+              to: 'Private',
+              custom_message: "Changed visibility from Public to Private"
             )
           end
         end
diff --git a/ee/spec/services/projects/transfer_service_spec.rb b/ee/spec/services/projects/transfer_service_spec.rb
index b1482933c292a..c481d83d9cce9 100644
--- a/ee/spec/services/projects/transfer_service_spec.rb
+++ b/ee/spec/services/projects/transfer_service_spec.rb
@@ -48,7 +48,8 @@
              author_name: user.name,
              target_id: project.id,
              target_type: 'Project',
-             target_details: project.full_path
+             target_details: project.full_path,
+             custom_message: "Changed namespace from #{project.old_path_with_namespace} to #{project.full_path}"
            }
          }
       end
diff --git a/ee/spec/services/projects/update_service_spec.rb b/ee/spec/services/projects/update_service_spec.rb
index af1eda5fa9c55..951951a596120 100644
--- a/ee/spec/services/projects/update_service_spec.rb
+++ b/ee/spec/services/projects/update_service_spec.rb
@@ -152,7 +152,8 @@
             param[:details].merge!(
               change: 'name',
               from: old_name,
-              to: project.full_name
+              to: project.full_name,
+              custom_message: "Changed name from #{old_name} to #{project.full_name}"
             )
           end
         end
@@ -171,7 +172,8 @@
             param[:details].merge!(
               change: 'path',
               from: project.old_path_with_namespace,
-              to: project.full_path
+              to: project.full_path,
+              custom_message: "Changed path from #{project.old_path_with_namespace} to #{project.full_path}"
             )
           end
         end
@@ -212,7 +214,8 @@
             param[:details].merge!(
               change: 'visibility',
               from: 'Private',
-              to: 'Internal'
+              to: 'Internal',
+              custom_message: "Changed visibility from Private to Internal"
             )
           end
         end
-- 
GitLab