diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index c197e77fbf78fd51ee4dd01b3f08bfa9b1fd6cd8..dd23baa00ffadc98c666613dfda6397599166444 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -43,6 +43,8 @@ - 1 - - audit_events_audit_event_streaming - 1 +- - audit_events_user_impersonation_event_create + - 1 - - authorized_keys - 2 - - authorized_project_update diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index 02dc1489294ad45f0f250d936a1cab52eb44e199..d4902a18cac0150975ae00a965f7935ddd214d14 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -51,7 +51,10 @@ There are two kinds of events logged: When a user is being [impersonated](../user/admin_area/index.md#user-impersonation), their actions are logged as audit events as usual, with two additional details: 1. Usual audit events include information about the impersonating administrator. These are visible in their respective Audit Event pages depending on their type (Group/Project/User). -1. Extra audit events are recorded for the start and stop of the administrator's impersonation session. These are visible in the instance Audit Events. +1. Extra audit events are recorded for the start and stop of the administrator's impersonation session. These are visible in + the: + - Instance audit events. + - Group audit events for all groups the user belongs to (GitLab 14.8 and later). This is limited to 20 groups for performance reasons. ![audit events](img/impersonated_audit_events_v13_8.png) @@ -103,6 +106,7 @@ From there, you can see the following actions: - Group CI/CD variable added, removed, or protected status changed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30857) in GitLab 13.3. - Compliance framework created, updated, or deleted. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) in GitLab 14.5. - Event streaming destination created, updated, or deleted. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) in GitLab 14.6. +- Instance administrator started or stopped impersonation of a group member. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300961) in GitLab 14.8. Group events can also be accessed via the [Group Audit Events API](../api/audit_events.md#group-audit-events) diff --git a/ee/app/controllers/ee/admin/users_controller.rb b/ee/app/controllers/ee/admin/users_controller.rb index 8ed0f690ee1ad30cfe30e6e5badfa1e3092a3475..93430223e48a38e4620f2d02fc92e19dbae709aa 100644 --- a/ee/app/controllers/ee/admin/users_controller.rb +++ b/ee/app/controllers/ee/admin/users_controller.rb @@ -44,8 +44,7 @@ def log_impersonation_event end def log_audit_event - AuditEvents::ImpersonationAuditEventService.new(current_user, request.remote_ip, 'Started Impersonation') - .for_user(full_path: user.username, entity_id: user.id).security_event + ::AuditEvents::UserImpersonationEventCreateWorker.perform_async(current_user.id, user.id, request.remote_ip, 'started') end def allowed_user_params diff --git a/ee/app/controllers/ee/application_controller.rb b/ee/app/controllers/ee/application_controller.rb index f36c0626f9857fa7da8d9f24dab9cb31aa0a113a..8bbdea699999d54c1c26020fdccb5a67b9bb77b7 100644 --- a/ee/app/controllers/ee/application_controller.rb +++ b/ee/app/controllers/ee/application_controller.rb @@ -38,8 +38,7 @@ def log_impersonation_event end def log_audit_event - AuditEvents::ImpersonationAuditEventService.new(impersonator, request.remote_ip, 'Stopped Impersonation') - .for_user(full_path: current_user.username, entity_id: current_user.id).security_event + ::AuditEvents::UserImpersonationEventCreateWorker.perform_async(impersonator.id, current_user.id, request.remote_ip, 'stopped') end def set_current_ip_address(&block) diff --git a/ee/app/services/audit_events/user_impersonation_group_audit_event_service.rb b/ee/app/services/audit_events/user_impersonation_group_audit_event_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..6e496de010c25424aadc074a6eacd8b153126ab9 --- /dev/null +++ b/ee/app/services/audit_events/user_impersonation_group_audit_event_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Creates audit events at both the instance level +# and for all of a user's groups when the user is impersonated. +module AuditEvents + class UserImpersonationGroupAuditEventService + def initialize(impersonator:, user:, remote_ip:, action: :started) + @impersonator = impersonator + @user = user + @remote_ip = remote_ip + @action = action.to_s + end + + def execute + log_instance_audit_event + log_groups_audit_events + end + + def log_instance_audit_event + AuditEvents::ImpersonationAuditEventService.new(@impersonator, @remote_ip, "#{@action.capitalize} Impersonation") + .for_user(full_path: @user.username, entity_id: @user.id).security_event + end + + def log_groups_audit_events + # Limited to 20 groups because we can't batch insert audit events + # https://gitlab.com/gitlab-org/gitlab/-/issues/352483 + @user.groups.first(20).each do |group| + audit_context = { + name: "user_impersonation", + author: @impersonator, + scope: group, + target: @user, + message: "Instance administrator #{@action} impersonation of #{@user.username}" + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + end + end +end diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml index 3d428042928516b68d3a7e27bf6f980ce506a68f..e076009673ade033982869d970daaa9c3c4c89dc 100644 --- a/ee/app/workers/all_queues.yml +++ b/ee/app/workers/all_queues.yml @@ -903,6 +903,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: audit_events_user_impersonation_event_create + :worker_name: AuditEvents::UserImpersonationEventCreateWorker + :feature_category: :audit_events + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: + :tags: [] - :name: ci_batch_reset_minutes :worker_name: Ci::BatchResetMinutesWorker :feature_category: :continuous_integration diff --git a/ee/app/workers/audit_events/user_impersonation_event_create_worker.rb b/ee/app/workers/audit_events/user_impersonation_event_create_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..97b86e00485459d41486348b7086708f74e12293 --- /dev/null +++ b/ee/app/workers/audit_events/user_impersonation_event_create_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module AuditEvents + class UserImpersonationEventCreateWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + + data_consistency :sticky + feature_category :audit_events + + def perform(impersonator_id, user_id, remote_ip, action) + ::AuditEvents::UserImpersonationGroupAuditEventService.new(impersonator: User.find_by_id(impersonator_id), + user: User.find_by_id(user_id), + remote_ip: remote_ip, + action: action).execute + end + end +end diff --git a/ee/spec/controllers/admin/impersonations_controller_spec.rb b/ee/spec/controllers/admin/impersonations_controller_spec.rb index 6ef610a4639143c386e663c4e5c0d1b7442a3428..bcb6177b0f54da3235b19d79c73ce9b1551edcdb 100644 --- a/ee/spec/controllers/admin/impersonations_controller_spec.rb +++ b/ee/spec/controllers/admin/impersonations_controller_spec.rb @@ -18,8 +18,10 @@ stub_licensed_features(extended_audit_events: true) end - it 'creates an AuditEvent record' do - expect { delete :destroy }.to change { AuditEvent.count }.by(1) + it 'enqueues a new worker' do + expect(AuditEvents::UserImpersonationEventCreateWorker).to receive(:perform_async).with(impersonator.id, user.id, anything, 'stopped').once + + delete :destroy end end end diff --git a/ee/spec/controllers/admin/users_controller_spec.rb b/ee/spec/controllers/admin/users_controller_spec.rb index 724e9721ea683f73fc6fa61b8050114a5592b65d..94a597f4d81d78ad824aa26715f28d51284432ba 100644 --- a/ee/spec/controllers/admin/users_controller_spec.rb +++ b/ee/spec/controllers/admin/users_controller_spec.rb @@ -108,8 +108,10 @@ stub_licensed_features(extended_audit_events: true) end - it 'creates an AuditEvent record' do - expect { post :impersonate, params: { id: user.username } }.to change { AuditEvent.count }.by(1) + it 'enqueues a new worker' do + expect(AuditEvents::UserImpersonationEventCreateWorker).to receive(:perform_async).with(admin.id, user.id, anything, 'started').once + + post :impersonate, params: { id: user.username } end end end diff --git a/ee/spec/services/audit_events/user_impersonation_group_audit_event_service_spec.rb b/ee/spec/services/audit_events/user_impersonation_group_audit_event_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5b525216ebbd015ea606411673283c834991a3a5 --- /dev/null +++ b/ee/spec/services/audit_events/user_impersonation_group_audit_event_service_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AuditEvents::UserImpersonationGroupAuditEventService do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + + let(:service) { described_class.new(impersonator: admin, user: user, remote_ip: '111.112.11.2', action: :started) } + + before do + stub_licensed_features(audit_events: true) + stub_licensed_features(admin_audit_log: true) + stub_licensed_features(extended_audit_events: true) + end + + context 'when user belongs to a single group' do + before do + group.add_developer(user) + end + + it 'creates audit events for both the instance and group level' do + expect { service.execute }.to change { AuditEvent.count }.by(2) + + event = AuditEvent.first + expect(event.details[:custom_message]).to eq("Started Impersonation") + + group_audit_event = AuditEvent.last + expect(group_audit_event.details[:custom_message]).to eq("Instance administrator started impersonation of #{user.username}") + end + end + + context 'when user belongs to multiple groups' do + let!(:group2) { create(:group) } + let!(:group3) { create(:group) } + + before do + group.add_developer(user) + group2.add_developer(user) + group3.add_developer(user) + end + + it 'creates audit events for both the instance and group level' do + expect { service.execute }.to change { AuditEvent.count }.by(4) + + event = AuditEvent.first + expect(event.details[:custom_message]).to eq("Started Impersonation") + + group_audit_event = AuditEvent.last + expect(group_audit_event.details[:custom_message]).to eq("Instance administrator started impersonation of #{user.username}") + end + end + + context 'when user does not belong to any group' do + it 'creates audit events at the instance level' do + expect { service.execute }.to change { AuditEvent.count }.by(1) + + event = AuditEvent.last + expect(event.details[:custom_message]).to eq("Started Impersonation") + expect(event.author).to eq admin + expect(event.target_id).to eq user.id + end + end +end diff --git a/ee/spec/workers/audit_events/user_impersonation_event_create_worker_spec.rb b/ee/spec/workers/audit_events/user_impersonation_event_create_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..cfa0cc18720cc120a32f983361a90bb717fb9745 --- /dev/null +++ b/ee/spec/workers/audit_events/user_impersonation_event_create_worker_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AuditEvents::UserImpersonationEventCreateWorker do + describe "#perform" do + let_it_be(:impersonator) { create(:admin) } + let_it_be(:user) { create(:user) } + + let(:action) { :started } + + subject(:worker) { described_class.new } + + it 'invokes the UserImpersonationGroupAuditEventService' do + expect(::AuditEvents::UserImpersonationGroupAuditEventService).to receive(:new).with( + impersonator: impersonator, + user: user, + remote_ip: '111.112.11.2', + action: action + ).and_call_original + + subject.perform(impersonator.id, user.id, '111.112.11.2', action) + end + end +end