diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index f234ee92af9a3af712771606d33dbd33777cd19f..b5483ceaf877c18797d151c492c7851327fbc13c 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -386,6 +386,12 @@ Audit event types belong to the following product categories. | [`epic_created_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an epic is created by a group access token| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | Group | | [`epic_reopened_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an epic is reopened by a group access token| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | Group | +### Product analytics data management + +| Name | Description | Saved to database | Streamed | Introduced in | Scope | +|:------------|:------------|:------------------|:---------|:--------------|:--------------| +| [`product_analytics_settings_update`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154101) | Triggered when product analytics settings are changed| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.1](https://gitlab.com/gitlab-org/gitlab/-/issues/463318) | Project | + ### Project | Name | Description | Saved to database | Streamed | Introduced in | Scope | diff --git a/ee/config/audit_events/types/product_analytics_settings_update.yml b/ee/config/audit_events/types/product_analytics_settings_update.yml new file mode 100644 index 0000000000000000000000000000000000000000..4c5a17f351a884a962e6fd569ac9fb73930fc7cc --- /dev/null +++ b/ee/config/audit_events/types/product_analytics_settings_update.yml @@ -0,0 +1,10 @@ +--- +name: product_analytics_settings_update +description: Triggered when product analytics settings are changed +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/463318 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154101 +feature_category: product_analytics_data_management +milestone: '17.1' +saved_to_database: true +streamed: true +scope: [Project] diff --git a/ee/lib/audit/project_analytics_changes_auditor.rb b/ee/lib/audit/project_analytics_changes_auditor.rb new file mode 100644 index 0000000000000000000000000000000000000000..447bedbb53e643e45584d98e83dc0ac749f03bd5 --- /dev/null +++ b/ee/lib/audit/project_analytics_changes_auditor.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Audit # rubocop:disable Gitlab/BoundedContexts -- govern::compliance will need to refactor all instances of Audit + class ProjectAnalyticsChangesAuditor < BaseChangesAuditor + ATTRIBUTE_NAMES = [ + :encrypted_product_analytics_configurator_connection_string, + :product_analytics_data_collector_host, + :cube_api_base_url, + :encrypted_cube_api_key + ].freeze + + def initialize(current_user, project_setting, project) + @project = project + + super(current_user, project_setting) + end + + def execute + ATTRIBUTE_NAMES.each do |attr| + next unless model.previous_changes.key?(attr.to_s) + + audit_context = { + name: 'product_analytics_settings_update', + author: @current_user, + scope: @project, + target: @project, + message: "Changed #{attr}", + additional_details: details(attr) + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + end + + def details(column) + return { change: column } if + [:encrypted_product_analytics_configurator_connection_string, :encrypted_cube_api_key].include?(column) + + { + change: column, + from: @model.previous_changes[column].first, + to: @model.previous_changes[column].last + } + end + end +end diff --git a/ee/lib/audit/project_changes_auditor.rb b/ee/lib/audit/project_changes_auditor.rb index 6fd6f3ff3d520e836b0116b20edb5267a5df2e61..aca8427fa0d590a1d0e46afe8a07ece71811df6c 100644 --- a/ee/lib/audit/project_changes_auditor.rb +++ b/ee/lib/audit/project_changes_auditor.rb @@ -111,6 +111,7 @@ def execute audit_compliance_framework_changes audit_project_setting_changes audit_project_ci_cd_setting_changes + audit_analytics_setting_changes end private @@ -133,6 +134,10 @@ def audit_merge_method ::Gitlab::Audit::Auditor.audit(audit_context) end + def audit_analytics_setting_changes + Audit::ProjectAnalyticsChangesAuditor.new(@current_user, model.project_setting, model).execute + end + def audit_project_feature_changes Audit::ProjectFeatureChangesAuditor.new(@current_user, model.project_feature, model).execute end diff --git a/ee/spec/lib/audit/project_analytics_changes_auditor_spec.rb b/ee/spec/lib/audit/project_analytics_changes_auditor_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0907cc900780085104df345f3d3a7804d2406b33 --- /dev/null +++ b/ee/spec/lib/audit/project_analytics_changes_auditor_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Audit::ProjectAnalyticsChangesAuditor, feature_category: :product_analytics_data_management do + describe 'auditing project analytics changes' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + subject(:auditor) { described_class.new(user, project.project_setting, project) } + + before do + project.reload + stub_licensed_features(extended_audit_events: true, external_audit_events: true) + end + + context 'when the cube_api_key is set' do + before do + project.project_setting.update!(cube_api_key: "thisisasecretkey") + end + + it 'adds an audit event', :aggregate_failures do + expect { auditor.execute }.to change { AuditEvent.count }.by(1) + expect(AuditEvent.last.details) + .to include({ change: :encrypted_cube_api_key }) + + # 'from' and 'to' should be nil, as their value is encrypted + # and we should not expose it in the audit logs + expect(AuditEvent.last.details[:from]).to be_nil + expect(AuditEvent.last.details[:to]).to be_nil + end + end + + context 'when the snowplow configurator connection string is set' do + before do + project.project_setting.update!(product_analytics_configurator_connection_string: "http://example.com") + end + + it 'adds an audit event', :aggregate_failures do + expect { auditor.execute }.to change { AuditEvent.count }.by(1) + expect(AuditEvent.last.details) + .to include({ change: :encrypted_product_analytics_configurator_connection_string }) + + # 'from' and 'to' should be nil, as their value is encrypted + # and we should not expose it in the audit logs + expect(AuditEvent.last.details[:from]).to be_nil + expect(AuditEvent.last.details[:to]).to be_nil + end + end + + context 'when the product_analytics_data_collector_host is set' do + before do + project.project_setting.update!(product_analytics_data_collector_host: "http://example2.com") + end + + it 'adds an audit event' do + expect { auditor.execute }.to change { AuditEvent.count }.by(1) + expect(AuditEvent.last.details) + .to include({ change: :product_analytics_data_collector_host, from: nil, to: "http://example2.com" }) + end + end + + context 'when the cube_api_base_url is set' do + before do + project.project_setting.update!(cube_api_base_url: "http://example3.com") + end + + it 'adds an audit event' do + expect { auditor.execute }.to change { AuditEvent.count }.by(1) + expect(AuditEvent.last.details) + .to include({ change: :cube_api_base_url, from: nil, to: "http://example3.com" }) + end + end + end +end