diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index 0af5533613fec45a7bfa192e17a81a08d221bf58..e11edbda6dca7476a99135e5f02670f6b3077630 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -19,5 +19,9 @@ class GroupVariable < Ci::ApplicationRecord scope :unprotected, -> { where(protected: false) } scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) } scope :for_groups, ->(group_ids) { where(group_id: group_ids) } + + def audit_details + key + end end end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 1e91f248fc43c274273fb4dc500e90e835b5fccf..c80c2ebe69ac5f23b0e0a838f38a056c0c1271c4 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -18,5 +18,9 @@ class Variable < Ci::ApplicationRecord scope :unprotected, -> { where(protected: false) } scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) } + + def audit_details + key + end end end diff --git a/ee/app/services/ci/audit_variable_change_service.rb b/ee/app/services/ci/audit_variable_change_service.rb index c5e89045a2e6151d49e1d00e70db0b65cf38c43d..3d72b170806b579230a4ef729ca60d1b06b614b4 100644 --- a/ee/app/services/ci/audit_variable_change_service.rb +++ b/ee/app/services/ci/audit_variable_change_service.rb @@ -4,8 +4,11 @@ module Ci class AuditVariableChangeService < ::BaseContainerService include ::Audit::Changes + AUDITABLE_VARIABLE_CLASSES = [::Ci::Variable, ::Ci::GroupVariable].freeze + def execute return unless container.feature_available?(:audit_events) + return unless AUDITABLE_VARIABLE_CLASSES.include? params[:variable].class case params[:action] when :create, :destroy @@ -14,7 +17,8 @@ def execute audit_changes( :protected, as: 'variable protection', entity: container, - model: params[:variable], target_details: params[:variable].key + model: params[:variable], target_details: params[:variable].key, + event_type: event_type_name(params[:variable], params[:action]) ) end end @@ -22,20 +26,52 @@ def execute private def log_audit_event(action, variable) - case variable.class.to_s - when ::Ci::Variable.to_s - ::AuditEventService.new( - current_user, - container, - action: action - ).for_project_variable(variable.key).security_event - when ::Ci::GroupVariable.to_s - ::AuditEventService.new( - current_user, - container, - action: action - ).for_group_variable(variable.key).security_event + audit_context = { + name: event_type_name(variable, action), + author: current_user || ::Gitlab::Audit::UnauthenticatedAuthor.new, + scope: container, + target: variable, + message: message(variable, action), + additional_details: build_additional_details(variable, action) + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + + def event_type_name(variable, action) + name = ci_variable_name(variable) + case action + when :create + "#{name}_created" + when :destroy + "#{name}_deleted" + when :update + "#{name}_updated" end end + + def message(variable, action) + name = ci_variable_name(variable).humanize(capitalize: false) + case action + when :create + "Added #{name}" + when :destroy + "Removed #{name}" + end + end + + def build_additional_details(variable, action) + name = ci_variable_name(variable) + case action + when :create + { add: name } + when :destroy + { remove: name } + end + end + + def ci_variable_name(variable) + variable.class.to_s.parameterize(preserve_case: true).underscore + end end end diff --git a/ee/lib/audit/changes.rb b/ee/lib/audit/changes.rb index 1fcb44034b2a00172581f3c921bcde21b36d295f..c510cc522317f3ad47b0107d0814894647d979bd 100644 --- a/ee/lib/audit/changes.rb +++ b/ee/lib/audit/changes.rb @@ -89,8 +89,8 @@ def additional_details(options) 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 << "from #{details[:from]}" if details[:from].to_s.present? + message << "to #{details[:to]}" if details[:to].to_s.present? message.join(' ') 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 8580a906ec59a7687f3fac918ea3c598ab724c13..cf6a571c4cabe1b2d3bd5fa2f8d7ae1a1e6fea02 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 @@ -42,7 +42,7 @@ from: false, to: true, ip_address: ip_address, - custom_message: "Changed #{change_text} to true" }) + custom_message: "Changed #{change_text} from false to true" }) expect(event.author_id).to eq(author.id) expect(event.entity_id).to eq(entity.id) diff --git a/ee/spec/services/ci/audit_variable_change_service_spec.rb b/ee/spec/services/ci/audit_variable_change_service_spec.rb index 03dbf5f83d8a933bad3ad1706ec0396bac08a981..9b568c78658039f72b8ccb4b2dce246ba63505df 100644 --- a/ee/spec/services/ci/audit_variable_change_service_spec.rb +++ b/ee/spec/services/ci/audit_variable_change_service_spec.rb @@ -8,7 +8,10 @@ let_it_be(:user) { create(:user) } let(:group) { create(:group) } - let(:variable) { create(:ci_group_variable) } + let(:group_variable) { create(:ci_group_variable, group: group) } + let(:destination) { create(:external_audit_event_destination, group: group) } + let(:project) { create(:project, group: group) } + let(:project_variable) { create(:ci_variable, project: project) } let(:service) do described_class.new( @@ -17,29 +20,58 @@ ) end - before do - group.variables << variable + shared_examples 'audit creation' do + let(:action) { :create } + + it 'logs audit event' do + expect { execute }.to change(AuditEvent, :count).from(0).to(1) + end + + it 'logs variable group creation' do + execute + + audit_event = AuditEvent.last.present + + expect(audit_event.action).to eq(message) + expect(audit_event.target).to eq(variable.key) + end + + it_behaves_like 'sends correct event type in audit event stream' do + let_it_be(:event_type) { event_type } + end end - context 'when audits are available' do + shared_examples 'audit when updating variable protection' do + let(:action) { :update } + before do - stub_licensed_features(audit_events: true) + variable.update!(protected: true) end - context 'when creating variable' do - let(:action) { :create } + it 'logs audit event' do + expect { execute }.to change(AuditEvent, :count).from(0).to(1) + end - it 'logs audit event' do - expect { execute }.to change(AuditEvent, :count).from(0).to(1) - end + it 'logs variable protection update' do + execute - it 'logs variable creation' do - execute + audit_event = AuditEvent.last.present + + expect(audit_event.action).to eq('Changed variable protection from false to true') + expect(audit_event.target).to eq(variable.key) + end + + it_behaves_like 'sends correct event type in audit event stream' do + let_it_be(:event_type) { event_type } + end + end - audit_event = AuditEvent.last.present + shared_examples 'no audit events are created' do + context 'when creating variable' do + let(:action) { :create } - expect(audit_event.action).to eq('Added ci group variable') - expect(audit_event.target).to eq(variable.key) + it 'does not log an audit event' do + expect { execute }.not_to change(AuditEvent, :count).from(0) end end @@ -50,68 +82,115 @@ variable.update!(protected: true) end - it 'logs audit event' do - expect { execute }.to change(AuditEvent, :count).from(0).to(1) - end - - it 'logs variable protection update' do - execute - - audit_event = AuditEvent.last.present - - expect(audit_event.action).to eq('Changed variable protection from false to true') - expect(audit_event.target).to eq(variable.key) + it 'does not log an audit event' do + expect { execute }.not_to change(AuditEvent, :count).from(0) end end context 'when destroying variable' do let(:action) { :destroy } - it 'logs audit event' do - expect { execute }.to change(AuditEvent, :count).from(0).to(1) + it 'does not log an audit event' do + expect { execute }.not_to change(AuditEvent, :count).from(0) end + end + end + + shared_examples 'when destroying variable' do + let(:action) { :destroy } - it 'logs variable destruction' do - execute + it 'logs audit event' do + expect { execute }.to change(AuditEvent, :count).from(0).to(1) + end - audit_event = AuditEvent.last.present + it 'logs variable destruction' do + execute - expect(audit_event.action).to eq('Removed ci group variable') - expect(audit_event.target).to eq(variable.key) - end + audit_event = AuditEvent.last.present + + expect(audit_event.action).to eq(message) + expect(audit_event.target).to eq(variable.key) + end + + it_behaves_like 'sends correct event type in audit event stream' do + let_it_be(:event_type) { "ci_group_variable_deleted" } end end - context 'when audits are not available' do + context 'when audits are available' do before do - stub_licensed_features(audit_events: false) + stub_licensed_features(audit_events: true) + stub_licensed_features(external_audit_events: true) + group.external_audit_event_destinations.create!(destination_url: 'http://example.com') end - context 'when creating variable' do - let(:action) { :create } + context 'when creating group variable' do + it_behaves_like 'audit creation' do + let(:variable) { group_variable } - it 'does not log an audit event' do - expect { execute }.not_to change(AuditEvent, :count).from(0) + let_it_be(:message) { 'Added ci group variable' } + let_it_be(:event_type) { "ci_group_variable_created" } end end - context 'when updating variable protection' do - let(:action) { :update } + context 'when updating group variable protection' do + it_behaves_like 'audit when updating variable protection' do + let(:variable) { group_variable } - before do - variable.update!(protected: true) + let_it_be(:event_type) { "ci_group_variable_updated" } end + end - it 'does not log an audit event' do - expect { execute }.not_to change(AuditEvent, :count).from(0) + context 'when deleting group variable' do + it_behaves_like 'audit when updating variable protection' do + let(:variable) { group_variable } + + let_it_be(:message) { 'Removed ci group variable' } + let_it_be(:event_type) { "ci_group_variable_updated" } end end - context 'when destroying variable' do - let(:action) { :destroy } + context 'when creating project variable' do + it_behaves_like 'audit creation' do + let(:variable) { project_variable } - it 'does not log an audit event' do - expect { execute }.not_to change(AuditEvent, :count).from(0) + let_it_be(:message) { 'Added ci variable' } + let_it_be(:event_type) { "ci_variable_created" } + end + end + + context 'when updating project variable protection' do + it_behaves_like 'audit when updating variable protection' do + let(:variable) { project_variable } + + let_it_be(:event_type) { "ci_variable_updated" } + end + end + + context 'when deleting project variable' do + it_behaves_like 'audit when updating variable protection' do + let(:variable) { project_variable } + + let_it_be(:message) { 'Removed ci variable' } + let_it_be(:event_type) { "ci_variable_updated" } + end + end + end + + context 'when audits are not available' do + before do + stub_licensed_features(audit_events: false) + end + + context 'for group variable' do + it_behaves_like 'no audit events are created' do + let(:variable) { group_variable } + end + end + + context 'for project variable' do + it_behaves_like 'no audit events are created' do + let(:variable) { project_variable } end end end diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb index 3a4b836e4532c9015a5b8a9ccc79e5e08818a441..fc5a9c879f6f81a3b3c1741dfed69a46b92547ee 100644 --- a/spec/models/ci/group_variable_spec.rb +++ b/spec/models/ci/group_variable_spec.rb @@ -56,4 +56,10 @@ let!(:parent) { model.group } end + + describe '#audit_details' do + it "equals to the group variable's key" do + expect(subject.audit_details).to eq(subject.key) + end + end end diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 29ca088ee04b744f5a538bebcb990399ff4e7ffb..f0af229ff2cb4b9d2efe627224b06e77ee486ea7 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -51,4 +51,10 @@ let!(:model) { create(:ci_variable, project: parent) } end end + + describe '#audit_details' do + it "equals to the variable's key" do + expect(subject.audit_details).to eq(subject.key) + end + end end