diff --git a/config/audit_events/types/direct_transfer_relation_export_batch_downloaded.yml b/config/audit_events/types/direct_transfer_relation_export_batch_downloaded.yml new file mode 100644 index 0000000000000000000000000000000000000000..b09d739529902d6f3c24c0ca81f33bf33be390d7 --- /dev/null +++ b/config/audit_events/types/direct_transfer_relation_export_batch_downloaded.yml @@ -0,0 +1,10 @@ +--- +name: direct_transfer_relation_export_batch_downloaded +description: Direct Transfer relation export batch downloaded +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/441977 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/194872 +feature_category: importers +milestone: '18.2' +saved_to_database: true +scope: [Group, Project] +streamed: true diff --git a/config/audit_events/types/direct_transfer_relation_export_downloaded.yml b/config/audit_events/types/direct_transfer_relation_export_downloaded.yml new file mode 100644 index 0000000000000000000000000000000000000000..0318381190bd76aa44cf10d6861c2c206e1a6a15 --- /dev/null +++ b/config/audit_events/types/direct_transfer_relation_export_downloaded.yml @@ -0,0 +1,10 @@ +--- +name: direct_transfer_relation_export_downloaded +description: Direct Transfer relation export downloaded +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/441977 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/194872 +feature_category: importers +milestone: '18.2' +saved_to_database: true +scope: [Group, Project] +streamed: true diff --git a/config/audit_events/types/direct_transfer_relations_export_initiated.yml b/config/audit_events/types/direct_transfer_relations_export_initiated.yml new file mode 100644 index 0000000000000000000000000000000000000000..8093ef9c5cf3f35bf9e4114a490f5bfb69bb53cf --- /dev/null +++ b/config/audit_events/types/direct_transfer_relations_export_initiated.yml @@ -0,0 +1,10 @@ +--- +name: direct_transfer_relations_export_initiated +description: Direct Transfer relations export initiated +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/441977 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/194872 +feature_category: importers +milestone: '18.2' +saved_to_database: true +scope: [Group, Project] +streamed: true diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 5592d666c8e81ae4cd403a391b3b9fc65098d15c..08a11184143d58b1294fcf3f25d82fdd9e7532fd 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -397,6 +397,9 @@ Audit event types belong to the following product categories. | Type name | Event triggered when | Saved to database | Introduced in | Scope | |:----------|:---------------------|:------------------|:--------------|:------| +| [`direct_transfer_relation_export_batch_downloaded`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/194872) | Direct Transfer relation export batch downloaded | {{< icon name="check-circle" >}} Yes | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/issues/441977) | Group, Project | +| [`direct_transfer_relation_export_downloaded`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/194872) | Direct Transfer relation export downloaded | {{< icon name="check-circle" >}} Yes | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/issues/441977) | Group, Project | +| [`direct_transfer_relations_export_initiated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/194872) | Direct Transfer relations export initiated | {{< icon name="check-circle" >}} Yes | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/issues/441977) | Group, Project | | [`group_export_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/TODO) | A group file export is created | {{< icon name="check-circle" >}} Yes | GitLab [17.0](https://gitlab.com/gitlab-org/gitlab/-/issues/294168) | Group | | [`project_export_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/TODO) | A project file export is created | {{< icon name="check-circle" >}} Yes | GitLab [17.0](https://gitlab.com/gitlab-org/gitlab/-/issues/294168) | Project | diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb index 086d712cc18bacd9429e30353f9b0fe525195663..d3069bf3bd167aad0592407278a52d7d100caa64 100644 --- a/lib/api/group_export.rb +++ b/lib/api/group_export.rb @@ -8,6 +8,7 @@ class GroupExport < ::API::Base feature_category :importers urgency :low + helpers Helpers::BulkImports::AuditHelpers params do requires :id, type: String, desc: 'The ID of a group' @@ -94,6 +95,13 @@ class GroupExport < ::API::Base .execute if response.success? + log_direct_transfer_audit_event( + ::Import::BulkImports::Audit::Events::EXPORT_INITIATED, + 'Direct Transfer relations export initiated', + current_user, + user_group + ) + accepted! else render_api_error!(message: 'Group relations export could not be started.') @@ -132,6 +140,13 @@ class GroupExport < ::API::Base break render_api_error!('Batch not found', 404) unless batch break render_api_error!('Batch file not found', 404) unless batch_file + log_direct_transfer_audit_event( + ::Import::BulkImports::Audit::Events::EXPORT_BATCH_DOWNLOADED, + 'Direct Transfer relation export batch downloaded', + current_user, + user_group + ) + present_carrierwave_file!(batch_file) else file = export&.upload&.export_file @@ -139,6 +154,13 @@ class GroupExport < ::API::Base break render_api_error!('Export is batched', 400) if export.batched? break render_api_error!('Export file not found', 404) unless file + log_direct_transfer_audit_event( + ::Import::BulkImports::Audit::Events::EXPORT_DOWNLOADED, + 'Direct Transfer relation export downloaded', + current_user, + user_group + ) + present_carrierwave_file!(file) end end diff --git a/lib/api/helpers/bulk_imports/audit_helpers.rb b/lib/api/helpers/bulk_imports/audit_helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..6a53f7821edf7e2e6e7279ccbb6111745bd6146e --- /dev/null +++ b/lib/api/helpers/bulk_imports/audit_helpers.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module API + module Helpers + module BulkImports + module AuditHelpers + def log_direct_transfer_audit_event(event_name, event_message, current_user, scope) + ::Import::BulkImports::Audit::Auditor.new( + event_name: event_name, + event_message: event_message, + current_user: current_user, + scope: scope + ).execute + end + end + end + end +end diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index 2bc950cefa3291ed2a64e47799e9f6745e5f67e7..4979447a22f48dbb69d5db6f1675e2667dcf994d 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -4,6 +4,7 @@ module API class ProjectExport < ::API::Base feature_category :importers urgency :low + helpers Helpers::BulkImports::AuditHelpers params do requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' @@ -137,6 +138,13 @@ class ProjectExport < ::API::Base .execute if response.success? + log_direct_transfer_audit_event( + ::Import::BulkImports::Audit::Events::EXPORT_INITIATED, + 'Direct Transfer relations export initiated', + current_user, + user_project + ) + accepted! else render_api_error!('Project relations export could not be started.', 500) @@ -177,6 +185,13 @@ class ProjectExport < ::API::Base break render_api_error!('Batch not found', 404) unless batch break render_api_error!('Batch file not found', 404) unless batch_file + log_direct_transfer_audit_event( + ::Import::BulkImports::Audit::Events::EXPORT_BATCH_DOWNLOADED, + 'Direct Transfer relation export batch downloaded', + current_user, + user_project + ) + present_carrierwave_file!(batch_file) else file = export&.upload&.export_file @@ -184,6 +199,13 @@ class ProjectExport < ::API::Base break render_api_error!('Export is batched', 400) if export.batched? break render_api_error!('Export file not found', 404) unless file + log_direct_transfer_audit_event( + ::Import::BulkImports::Audit::Events::EXPORT_DOWNLOADED, + 'Direct Transfer relation export downloaded', + current_user, + user_project + ) + present_carrierwave_file!(file) end end diff --git a/lib/import/bulk_imports/audit/auditor.rb b/lib/import/bulk_imports/audit/auditor.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c2be895b191d4443e34248b745a87a019c50c50 --- /dev/null +++ b/lib/import/bulk_imports/audit/auditor.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Import + module BulkImports + module Audit + class Auditor + attr_reader :event_name, :event_message, :current_user, :scope + + def initialize(event_name:, event_message:, current_user:, scope:) + @event_name = event_name + @event_message = event_message + @current_user = current_user + @scope = scope + end + + def execute + return if silent_admin_export? + + ::Gitlab::Audit::Auditor.audit( + name: event_name, + author: current_user, + scope: scope, + target: scope, + message: event_message + ) + end + + private + + def silent_admin_export? + export_event? && + current_user.can_admin_all_resources? && + ::Gitlab::CurrentSettings.silent_admin_exports_enabled? + end + + def export_event? + event_name == Events::EXPORT_INITIATED || + event_name == Events::EXPORT_DOWNLOADED || + event_name == Events::EXPORT_BATCH_DOWNLOADED + end + end + end + end +end diff --git a/lib/import/bulk_imports/audit/events.rb b/lib/import/bulk_imports/audit/events.rb new file mode 100644 index 0000000000000000000000000000000000000000..a4abbb0b8e52838c1574846023ee5c9590dddcce --- /dev/null +++ b/lib/import/bulk_imports/audit/events.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Import + module BulkImports + module Audit + module Events + EXPORT_INITIATED = 'direct_transfer_relations_export_initiated' + EXPORT_BATCH_DOWNLOADED = 'direct_transfer_relation_export_batch_downloaded' + EXPORT_DOWNLOADED = 'direct_transfer_relation_export_downloaded' + end + end + end +end diff --git a/spec/lib/api/helpers/bulk_imports/audit_helpers_spec.rb b/spec/lib/api/helpers/bulk_imports/audit_helpers_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d227205ce0d6aec3f6717dbbcf36ad38abb032a --- /dev/null +++ b/spec/lib/api/helpers/bulk_imports/audit_helpers_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::API::Helpers::BulkImports::AuditHelpers, feature_category: :importers do + let(:klass) do + Struct.new(:params) do + include ::API::Helpers + include ::API::Helpers::BulkImports::AuditHelpers + end + end + + let(:object) { klass.new } + + describe '#log_direct_transfer_audit_event' do + it 'calls Import::BulkImports::Audit::Auditor' do + user = build_stubbed(:user) + project = build_stubbed(:project) + + expect_next_instance_of(Import::BulkImports::Audit::Auditor) do |auditor| + expect(auditor).to receive(:execute) + end + + object.log_direct_transfer_audit_event('foo', 'bar', user, project) + end + end +end diff --git a/spec/lib/import/bulk_imports/audit/auditor_spec.rb b/spec/lib/import/bulk_imports/audit/auditor_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2215fce209cda6d2b181be318238b58f641525e0 --- /dev/null +++ b/spec/lib/import/bulk_imports/audit/auditor_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::BulkImports::Audit::Auditor, feature_category: :importers do + let(:user) { build_stubbed(:user) } + let(:scope) { build_stubbed(:project) } + let(:event_name) { ::Import::BulkImports::Audit::Events::EXPORT_INITIATED } + let(:event_message) { 'message' } + + subject(:auditor) do + described_class.new( + event_name: event_name, + event_message: event_message, + current_user: user, + scope: scope + ) + end + + describe '#execute' do + shared_examples 'new audit event' do + it 'creates new audit event' do + expect(::Gitlab::Audit::Auditor) + .to receive(:audit) + .with( + name: event_name, + author: user, + scope: scope, + target: scope, + message: event_message + ) + + auditor.execute + end + end + + include_examples 'new audit event' + + context 'when target is group' do + let(:scope) { build_stubbed(:group) } + + include_examples 'new audit event' + end + + describe 'silent admin export' do + let(:scope) { build_stubbed(:group) } + + before do + stub_application_setting(silent_admin_exports_enabled: true) + allow(user).to receive(:can_admin_all_resources?).and_return(true) + end + + it 'does not create audit event' do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + + auditor.execute + end + + context 'when not export event' do + let(:event_name) { 'event' } + + include_examples 'new audit event' + end + + context 'when user is not admin' do + before do + allow(user).to receive(:can_admin_all_resources?).and_return(false) + end + + include_examples 'new audit event' + end + + context 'when silent admin exports application setting is disabled' do + before do + stub_application_setting(silent_admin_exports_enabled: false) + end + + include_examples 'new audit event' + end + end + end +end diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb index fd7c74c8193c399361eb50300b2c30fda0afc921..614e6305fa7d46ddfd34aca3edcf521bd675e48b 100644 --- a/spec/requests/api/group_export_spec.rb +++ b/spec/requests/api/group_export_spec.rb @@ -216,16 +216,37 @@ expect(response).to have_gitlab_http_status(:accepted) end + it 'creates new audit event' do + expect(::Import::BulkImports::Audit::Auditor) + .to receive(:new) + .with( + event_name: ::Import::BulkImports::Audit::Events::EXPORT_INITIATED, + event_message: 'Direct Transfer relations export initiated', + current_user: user, + scope: group + ) + + post api(path, user) + end + context 'when response is not success' do - it 'returns api error' do + before do allow_next_instance_of(BulkImports::ExportService) do |service| allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error', http_status: :error)) end + end + it 'returns api error' do post api(path, user) expect(response).to have_gitlab_http_status(:error) end + + it 'does not create audit event' do + expect(::Import::BulkImports::Audit::Auditor).not_to receive(:new) + + post api(path, user) + end end context 'when request is to export in batches' do @@ -255,6 +276,21 @@ expect(response).to have_gitlab_http_status(:ok) end + + it 'creates new audit event' do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) + + expect(::Import::BulkImports::Audit::Auditor) + .to receive(:new) + .with( + event_name: ::Import::BulkImports::Audit::Events::EXPORT_DOWNLOADED, + event_message: 'Direct Transfer relation export downloaded', + current_user: user, + scope: group + ) + + get api(download_path, user) + end end context 'when export_file.file does not exist' do @@ -299,6 +335,21 @@ .to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz") end + it 'creates new audit event' do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) + + expect(::Import::BulkImports::Audit::Auditor) + .to receive(:new) + .with( + event_name: ::Import::BulkImports::Audit::Events::EXPORT_BATCH_DOWNLOADED, + event_message: 'Direct Transfer relation export batch downloaded', + current_user: user, + scope: group + ) + + get api(download_path, user), params: { batched: true, batch_number: batch.batch_number } + end + context 'when request is to download not batched export' do it 'returns 400' do get api(download_path, user) diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 84fc499b60afff6c6edd4afc38630ca28fcf7178..ae516609a6c9b81ea008e6d21b616987154c42dc 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -534,16 +534,37 @@ expect(response).to have_gitlab_http_status(:accepted) end + it 'creates new audit event' do + expect(::Import::BulkImports::Audit::Auditor) + .to receive(:new) + .with( + event_name: ::Import::BulkImports::Audit::Events::EXPORT_INITIATED, + event_message: 'Direct Transfer relations export initiated', + current_user: user, + scope: project + ) + + post api(path, user) + end + context 'when response is not success' do - it 'returns api error' do + before do allow_next_instance_of(BulkImports::ExportService) do |service| allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error', http_status: :error)) end + end + it 'returns api error' do post api(path, user) expect(response).to have_gitlab_http_status(:error) end + + it 'does not create audit event' do + expect(::Import::BulkImports::Audit::Auditor).not_to receive(:new) + + post api(path, user) + end end context 'when request is to export in batches' do @@ -574,6 +595,21 @@ expect(response).to have_gitlab_http_status(:ok) expect(response.header['Content-Disposition']).to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz") end + + it 'creates new audit event' do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) + + expect(::Import::BulkImports::Audit::Auditor) + .to receive(:new) + .with( + event_name: ::Import::BulkImports::Audit::Events::EXPORT_DOWNLOADED, + event_message: 'Direct Transfer relation export downloaded', + current_user: user, + scope: project + ) + + get api(download_path, user) + end end context 'when relation is not portable' do @@ -624,6 +660,21 @@ expect(response.header['Content-Disposition']).to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz") end + it 'creates new audit event' do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) + + expect(::Import::BulkImports::Audit::Auditor) + .to receive(:new) + .with( + event_name: ::Import::BulkImports::Audit::Events::EXPORT_BATCH_DOWNLOADED, + event_message: 'Direct Transfer relation export batch downloaded', + current_user: user, + scope: project + ) + + get api(download_path, user), params: { batched: true, batch_number: batch.batch_number } + end + context 'when request is to download not batched export' do it 'returns 400' do get api(download_path, user)