diff --git a/ee/changelogs/unreleased/sh-add-project-settings-audit-logs.yml b/ee/changelogs/unreleased/sh-add-project-settings-audit-logs.yml new file mode 100644 index 0000000000000000000000000000000000000000..aaca4aeb3a6b0ed10da32e018888af1bdcc89266 --- /dev/null +++ b/ee/changelogs/unreleased/sh-add-project-settings-audit-logs.yml @@ -0,0 +1,5 @@ +--- +title: 'Audit log: Add logging for project feature changes' +merge_request: 7962 +author: +type: added diff --git a/ee/lib/ee/audit/changes.rb b/ee/lib/ee/audit/changes.rb index 41a71079c64fade4abd6361c00b2573455929ca6..5152f5be7b322db4ed1411d8fda1d68112aff1dd 100644 --- a/ee/lib/ee/audit/changes.rb +++ b/ee/lib/ee/audit/changes.rb @@ -3,7 +3,10 @@ module Audit module Changes def audit_changes(column, options = {}) column = options[:column] || column - @model = options[:model] # rubocop:disable Gitlab/ModuleWithInstanceVariables + # rubocop:disable Gitlab/ModuleWithInstanceVariables + @target_model = options[:target_model] + @model = options[:model] + # rubocop:enable Gitlab/ModuleWithInstanceVariables return unless changed?(column) @@ -12,6 +15,10 @@ def audit_changes(column, options = {}) protected + def target_model + @target_model || model # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + def model @model end @@ -39,7 +46,7 @@ def parse_options(column, options) end def audit_event(options) - ::AuditEventService.new(@current_user, model, options) # rubocop:disable Gitlab/ModuleWithInstanceVariables + ::AuditEventService.new(@current_user, target_model, options) # rubocop:disable Gitlab/ModuleWithInstanceVariables .for_changes.security_event end end diff --git a/ee/lib/ee/audit/project_changes_auditor.rb b/ee/lib/ee/audit/project_changes_auditor.rb index 35c48b58520177245765159b07547b783a70edc6..fc26b3a56d4b0495a5a4fe9a6de7d7d33db73126 100644 --- a/ee/lib/ee/audit/project_changes_auditor.rb +++ b/ee/lib/ee/audit/project_changes_auditor.rb @@ -6,6 +6,14 @@ def execute audit_changes(:path, as: 'path', model: model) audit_changes(:name, as: 'name', model: model) audit_changes(:namespace_id, as: 'namespace', model: model) + audit_changes(:repository_size_limit, as: 'repository_size_limit', model: model) + audit_changes(:packages_enabled, as: 'packages_enabled', model: model) + + audit_project_feature_changes + end + + def audit_project_feature_changes + ::EE::Audit::ProjectFeatureChangesAuditor.new(@current_user, model.project_feature, model).execute end def attributes_from_auditable_model(column) @@ -30,6 +38,11 @@ def attributes_from_auditable_model(column) from: model.old_path_with_namespace, to: model.full_path } + else + { + from: model.previous_changes[column].first, + to: model.previous_changes[column].last + } end.merge(target_details: model.full_path) end end diff --git a/ee/lib/ee/audit/project_feature_changes_auditor.rb b/ee/lib/ee/audit/project_feature_changes_auditor.rb new file mode 100644 index 0000000000000000000000000000000000000000..1e12c701f0ed74d4676710bb2e5db04e40bf0719 --- /dev/null +++ b/ee/lib/ee/audit/project_feature_changes_auditor.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module EE + module Audit + class ProjectFeatureChangesAuditor < BaseChangesAuditor + attr_accessor :project + + COLUMNS = [:merge_requests_access_level, + :issues_access_level, + :wiki_access_level, + :snippets_access_level, + :builds_access_level, + :repository_access_level, + :pages_access_level].freeze + + def initialize(current_user, model, project) + @project = project + + super(current_user, model) + end + + def execute + COLUMNS.each do |column| + audit_changes(column, as: column.to_s, target_model: @project, model: model) + end + end + + def attributes_from_auditable_model(column) + base_data = { target_details: @project.full_path } + + return base_data unless COLUMNS.include?(column) + + { + from: ::Gitlab::VisibilityLevel.level_name(model.previous_changes[column].first), + to: ::Gitlab::VisibilityLevel.level_name(model.previous_changes[column].last) + }.merge(base_data) + end + end + end +end diff --git a/ee/spec/lib/ee/audit/project_changes_auditor_spec.rb b/ee/spec/lib/ee/audit/project_changes_auditor_spec.rb index 13396df98ca98ac8ea44d885a6109c8ecd2b1cda..0b49d18af299cc4bd904b60e077d66b2f158e2c7 100644 --- a/ee/spec/lib/ee/audit/project_changes_auditor_spec.rb +++ b/ee/spec/lib/ee/audit/project_changes_auditor_spec.rb @@ -48,6 +48,20 @@ expect { foo_instance.execute }.to change { SecurityEvent.count }.by(1) expect(SecurityEvent.last.details[:change]).to eq 'namespace' end + + it 'creates an event when the repository size limit changes' do + project.update!(repository_size_limit: 100) + + expect { foo_instance.execute }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:change]).to eq 'repository_size_limit' + end + + it 'creates an event when the packages enabled setting changes' do + project.update!(packages_enabled: false) + + expect { foo_instance.execute }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:change]).to eq 'packages_enabled' + end end end end diff --git a/ee/spec/lib/ee/audit/project_feature_changes_auditor_spec.rb b/ee/spec/lib/ee/audit/project_feature_changes_auditor_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..dde4e8194f5ca0377dd375c820b2e3ef0fcd03f9 --- /dev/null +++ b/ee/spec/lib/ee/audit/project_feature_changes_auditor_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe EE::Audit::ProjectFeatureChangesAuditor do + describe '#execute' do + let!(:user) { create(:user) } + let!(:project) { create(:project, visibility_level: 0) } + let(:features) { project.project_feature } + let(:foo_instance) { described_class.new(user, features, project) } + + before do + stub_licensed_features(extended_audit_events: true) + end + + it 'creates an event when any project feature level changes' do + columns = project.project_feature.attributes.keys.select { |attr| attr.end_with?('level') } + + columns.each do |column| + features.update_attribute(column, 0) + expect { foo_instance.execute }.to change { SecurityEvent.count }.by(1) + + event = SecurityEvent.last + expect(event.details[:from]).to eq 'Public' + expect(event.details[:to]).to eq 'Private' + expect(event.details[:change]).to eq column + end + end + end +end