diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 3e8d656370984aa3315d37cb06ab152258ba96c2..02ae30db1c322647cf491972064b3dc70da027e6 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -5,7 +5,10 @@ class ForkService < BaseService def execute(fork_to_project = nil) forked_project = fork_to_project ? link_existing_project(fork_to_project) : fork_new_project - refresh_forks_count if forked_project&.saved? + if forked_project&.saved? + refresh_forks_count + stream_audit_event(forked_project) + end forked_project end @@ -133,5 +136,11 @@ def target_visibility_level def target_mr_default_target_self @target_mr_default_target_self ||= params[:mr_default_target_self] end + + def stream_audit_event(forked_project) + # Defined in EE + end end end + +Projects::ForkService.prepend_mod diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md index 1e87b3c65bff0f63e20cc2671bc1c49648104189..0135a59cba9ce82c563f0f6091c3b6eff8e7566c 100644 --- a/doc/administration/audit_event_streaming.md +++ b/doc/administration/audit_event_streaming.md @@ -555,3 +555,52 @@ X-Gitlab-Event-Streaming-Token: "event_type": "merge_request_create" } ``` + +## Audit event streaming on project fork actions + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90916) in GitLab 15.2. + +Stream audit events that relate to project fork actions using the `/logs` endpoint. + +Send API requests that contain the `X-Gitlab-Audit-Event-Type` header with value `project_fork_operation`. GitLab responds with JSON payloads with an +`event_type` field set to `project_fork_operation`. + +### Headers + +Headers are formatted as follows: + +```plaintext +POST /logs HTTP/1.1 +Host: +Content-Type: application/x-www-form-urlencoded +X-Gitlab-Audit-Event-Type: project_fork_operation +X-Gitlab-Event-Streaming-Token: +``` + +### Example payload + +```json +{ + "id": 1, + "author_id": 1, + "entity_id": 24, + "entity_type": "Project", + "details": { + "author_name": "example_username", + "target_id": 24, + "target_type": "Project", + "target_details": "example-project", + "custom_message": "Forked project to another-group/example-project-forked", + "ip_address": "127.0.0.1", + "entity_path": "example-group/example-project" + }, + "ip_address": "127.0.0.1", + "author_name": "example_username", + "entity_path": "example-group/example-project", + "target_details": "example-project", + "created_at": "2022-06-30T03:43:35.384Z", + "target_type": "Project", + "target_id": 24, + "event_type": "project_fork_operation" +} +``` diff --git a/ee/app/services/ee/projects/fork_service.rb b/ee/app/services/ee/projects/fork_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..884546157ad4303aace6aac282236c083cbf0ebf --- /dev/null +++ b/ee/app/services/ee/projects/fork_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module EE + module Projects + module ForkService + extend ::Gitlab::Utils::Override + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + override :stream_audit_event + def stream_audit_event(forked_project) + audit_context = { + name: 'project_fork_operation', + stream_only: true, + author: current_user, + scope: @project, + target: @project, + message: "Forked project to #{forked_project.full_path}" + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + end + end +end diff --git a/ee/spec/services/projects/fork_service_spec.rb b/ee/spec/services/projects/fork_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..26d55e449cb8e01cb7dc412cba9a23cafaaf09d6 --- /dev/null +++ b/ee/spec/services/projects/fork_service_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ForkService do + include ProjectForksHelper + + describe 'fork by user' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, namespace: group) } + let_it_be(:event_type) { "project_fork_operation" } + + before do + project.add_member(user, :developer) + group.external_audit_event_destinations.create!(destination_url: 'http://example.com') + end + + subject(:execute) { described_class.new(project, user).execute } + + it 'call auditor with currect context' do + audit_context = { + name: event_type, + stream_only: true, + author: user, + scope: project, + target: project, + message: "Forked project to #{user.namespace.path}/#{project.path}" + } + expect(::Gitlab::Audit::Auditor).to receive(:audit).with(hash_including(audit_context)) + + subject + end + + context "with license feature external_audit_events" do + before do + stub_licensed_features(external_audit_events: true) + end + + it 'sends correct event type in audit event stream' do + expect(AuditEvents::AuditEventStreamingWorker).to receive(:perform_async).with(event_type, nil, anything) + + subject + end + end + + context "without license feature external_audit_events" do + before do + stub_licensed_features(external_audit_events: false) + end + + it 'not sends audit event stream' do + expect(AuditEvents::AuditEventStreamingWorker).not_to receive(:perform_async) + + subject + end + end + end +end