Commit 82954c4b authored by Max Woolf's avatar Max Woolf Committed by Bob Van Landuyt

Save audit events for start/stop user impersonation to group level

Adds additional audit events to all of the groups
that a user belongs to to inform group owners
that their users have been impersonated by
instance administrators.

EE: true
Changelog: added
parent 42ef30b8
......@@ -47,6 +47,8 @@
- 1
- - audit_events_audit_event_streaming
- 1
- - audit_events_user_impersonation_event_create
- 1
- - authorized_keys
- 2
- - authorized_project_update
......
......@@ -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)
......
......@@ -44,8 +44,7 @@ module EE
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
......
......@@ -38,8 +38,7 @@ module EE
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)
......
# 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
......@@ -921,6 +921,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
......
# 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
......@@ -18,8 +18,10 @@ RSpec.describe Admin::ImpersonationsController do
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
......
......@@ -108,8 +108,10 @@ RSpec.describe Admin::UsersController do
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
# 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
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment