Commit fcc99832 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '3009610-user-group-impersonation-audit-events' into 'master'

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

See merge request gitlab-org/gitlab!79340
parents b54d43bc 82954c4b
......@@ -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