From 45d8bc02374aefcf852f1a61b8849a11db930b6f Mon Sep 17 00:00:00 2001
From: Harsimar Sandhu <hsandhu@gitlab.com>
Date: Mon, 1 Apr 2024 21:54:23 +0000
Subject: [PATCH] New Audit Event when approval rule is updated

New audit event when project approval rule name or
target branch is changed

EE: true
Changelog: added
---
 .../concerns/approval_rules/updater.rb        |  58 +++++++-
 .../approval_rules/update_service_spec.rb     | 134 ++++++++++++++++++
 2 files changed, 185 insertions(+), 7 deletions(-)

diff --git a/ee/app/services/concerns/approval_rules/updater.rb b/ee/app/services/concerns/approval_rules/updater.rb
index 6340124d92845..ab641a686209a 100644
--- a/ee/app/services/concerns/approval_rules/updater.rb
+++ b/ee/app/services/concerns/approval_rules/updater.rb
@@ -4,6 +4,9 @@ module ApprovalRules
   module Updater
     include ::Audit::Changes
 
+    APPROVAL_RULE_UPDATE_EVENT_NAME = 'update_approval_rules'
+    APPROVAL_RULE_CREATE_EVENT_NAME = 'approval_rule_created'
+
     def execute
       if group_rule? && Feature.disabled?(:approval_group_rules, rule.group)
         return ServiceResponse.error(message: "The feature approval_group_rules is not enabled.")
@@ -29,7 +32,11 @@ def action
       filter_eligible_groups!
       filter_eligible_protected_branches!
 
-      update_rule ? success : error
+      old_protected_branches_names = nil
+
+      old_protected_branches_names = rule.protected_branches.map(&:name) if rule.is_a? ApprovalProjectRule
+
+      update_rule ? success_with_audit_logging(old_protected_branches_names) : error
     end
 
     def filter_eligible_users!
@@ -85,7 +92,7 @@ def update_rule
       return rule.update(params) unless current_user
 
       audit_context = {
-        name: rule.new_record? ? 'approval_rule_created' : 'update_approval_rules',
+        name: rule.new_record? ? APPROVAL_RULE_CREATE_EVENT_NAME : APPROVAL_RULE_UPDATE_EVENT_NAME,
         author: current_user,
         scope: container,
         target: rule
@@ -94,21 +101,40 @@ def update_rule
       ::Gitlab::Audit::Auditor.audit(audit_context) { rule.update(params) }
     end
 
-    def success
-      audit_changes_to_approvals_required if current_user
+    def success_with_audit_logging(old_protected_branches_names)
+      log_audit_changes(old_protected_branches_names) if current_user
 
       rule.reset
 
-      super
+      success
     end
 
-    def audit_changes_to_approvals_required
+    def log_audit_changes(old_protected_branches_names)
       audit_changes(
         :approvals_required,
         as: 'number of required approvals',
         entity: container,
         model: rule,
-        event_type: 'update_approval_rules'
+        event_type: APPROVAL_RULE_UPDATE_EVENT_NAME
+      )
+      audit_changes(:name,
+        as: 'name',
+        entity: container,
+        model: rule,
+        event_type: APPROVAL_RULE_UPDATE_EVENT_NAME)
+
+      return unless rule.is_a? ApprovalProjectRule
+
+      audit_message = protected_branch_change_audit_message(rule, old_protected_branches_names)
+
+      return unless audit_message.present?
+
+      ::Gitlab::Audit::Auditor.audit(
+        author: current_user,
+        name: APPROVAL_RULE_UPDATE_EVENT_NAME,
+        scope: container,
+        target: rule,
+        message: audit_message
       )
     end
 
@@ -118,5 +144,23 @@ def can_create_rule_for_protected_branches?
 
       skip_authorization || can?(current_user, :admin_project, project)
     end
+
+    def protected_branch_change_audit_message(rule, old_protected_branches_names)
+      new_protected_branches_names = rule.protected_branches.map(&:name)
+      recently_added_branch = new_protected_branches_names - old_protected_branches_names
+      enabled_all_protected_branches = rule.previous_changes["applies_to_all_protected_branches"] == [false, true]
+      disabled_all_protected_branches = rule.previous_changes["applies_to_all_protected_branches"] == [true, false]
+      from_protected_branch_to_empty = (rule.protected_branches.empty? && old_protected_branches_names.present?)
+
+      if enabled_all_protected_branches
+        "Changed target branch to all protected branches"
+      elsif disabled_all_protected_branches && new_protected_branches_names.present?
+        "Changed target branch to #{new_protected_branches_names.first} branch"
+      elsif from_protected_branch_to_empty
+        "Changed target branch to all branches"
+      elsif recently_added_branch.present?
+        "Changed target branch to #{recently_added_branch.first} branch"
+      end
+    end
   end
 end
diff --git a/ee/spec/services/approval_rules/update_service_spec.rb b/ee/spec/services/approval_rules/update_service_spec.rb
index 862b7f4841311..a17fd2b3e885e 100644
--- a/ee/spec/services/approval_rules/update_service_spec.rb
+++ b/ee/spec/services/approval_rules/update_service_spec.rb
@@ -311,6 +311,140 @@
               "Removed User Batman from approval group on Gotham rule"
             )
           end
+
+          it 'audits the name change of a approval rule' do
+            described_class.new(approval_rule, user, name: 'Avenger').execute
+
+            expect(AuditEvent.last).to have_attributes(
+              author: user,
+              entity: project,
+              target_id: approval_rule.id,
+              target_type: approval_rule.class.name,
+              target_details: approval_rule.name,
+              details: include(custom_message: "Changed name from Gotham to Avenger")
+            )
+          end
+
+          context 'when changing target branch' do
+            before do
+              stub_licensed_features(multiple_approval_rules: true)
+            end
+
+            let(:protected_branch) { create(:protected_branch, project: target) }
+
+            context 'when new target branch is applied' do
+              subject(:execute) do
+                described_class.new(
+                  approval_rule,
+                  user,
+                  protected_branch_ids: [protected_branch.id]
+                ).execute
+              end
+
+              it 'calls auditor with correct args' do
+                expect(::Gitlab::Audit::Auditor).to receive(:audit).ordered.and_call_original
+
+                expect(::Gitlab::Audit::Auditor).to receive(:audit).with(
+                  hash_including(
+                    author: user,
+                    name: 'update_approval_rules',
+                    scope: project,
+                    target: approval_rule,
+                    message: "Changed target branch to #{protected_branch.name} branch"
+                  )
+                ).ordered
+
+                execute
+              end
+            end
+
+            context 'when target branch is changed to all branches' do
+              before do
+                approval_rule.update!(protected_branches: [protected_branch])
+              end
+
+              subject(:execute) do
+                described_class.new(
+                  approval_rule,
+                  user,
+                  protected_branch_ids: []
+                ).execute
+              end
+
+              it 'calls auditor with correct args' do
+                expect(::Gitlab::Audit::Auditor).to receive(:audit).ordered.and_call_original
+
+                expect(::Gitlab::Audit::Auditor).to receive(:audit).with(
+                  hash_including(
+                    author: user,
+                    name: 'update_approval_rules',
+                    scope: project,
+                    target: approval_rule,
+                    message: "Changed target branch to all branches"
+                  )
+                ).ordered
+
+                execute
+              end
+            end
+
+            context 'when target branch is changed to all protected branches' do
+              subject(:execute) do
+                described_class.new(
+                  approval_rule,
+                  user,
+                  applies_to_all_protected_branches: true
+                ).execute
+              end
+
+              it 'calls auditor with correct args' do
+                expect(::Gitlab::Audit::Auditor).to receive(:audit).ordered.and_call_original
+
+                expect(::Gitlab::Audit::Auditor).to receive(:audit).with(
+                  hash_including(
+                    author: user,
+                    name: 'update_approval_rules',
+                    scope: project,
+                    target: approval_rule,
+                    message: "Changed target branch to all protected branches"
+                  )
+                ).ordered
+
+                execute
+              end
+            end
+
+            context 'when target branch is changed from all protected branches to a protected branch' do
+              before do
+                approval_rule.update!(applies_to_all_protected_branches: true)
+              end
+
+              subject(:execute) do
+                described_class.new(
+                  approval_rule,
+                  user,
+                  protected_branch_ids: [protected_branch.id],
+                  applies_to_all_protected_branches: false
+                ).execute
+              end
+
+              it 'calls auditor with correct args' do
+                expect(::Gitlab::Audit::Auditor).to receive(:audit).ordered.and_call_original
+
+                expect(::Gitlab::Audit::Auditor).to receive(:audit).with(
+                  hash_including(
+                    author: user,
+                    name: 'update_approval_rules',
+                    scope: project,
+                    target: approval_rule,
+                    message: "Changed target branch to #{protected_branch.name} branch"
+                  )
+                ).ordered
+
+                execute
+              end
+            end
+          end
         end
 
         context 'when rule update operation fails', :request_store do
-- 
GitLab