Commit 34099496 authored by Robert Speicher's avatar Robert Speicher

Merge branch '329268-add-support-for-standard-method-call-interface-on-auditor' into 'master'

Add support for standard method call interface on Auditor

See merge request gitlab-org/gitlab!60591
parents ca3745d8 b0bd70d5
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module AuditEvents module AuditEvents
class BuildService class BuildService
MissingAttributeError = Class.new(StandardError)
def initialize(author:, scope:, target:, ip_address:, message:) def initialize(author:, scope:, target:, ip_address:, message:)
@author = author @author = author
@scope = scope @scope = scope
...@@ -10,17 +12,28 @@ module AuditEvents ...@@ -10,17 +12,28 @@ module AuditEvents
@message = message @message = message
end end
# Create an instance of AuditEvent
#
# @raise [MissingAttributeError] when required attributes are blank
#
# @return [AuditEvent]
def execute def execute
raise MissingAttributeError if missing_attribute?
AuditEvent.new(payload) AuditEvent.new(payload)
end end
private private
def missing_attribute?
@author.blank? || @scope.blank? || @target.blank? || @message.blank?
end
def payload def payload
if License.feature_available?(:admin_audit_log) if License.feature_available?(:admin_audit_log)
base_payload.merge( base_payload.merge(
details: base_details_payload.merge( details: base_details_payload.merge(
ip_address: @ip_address, ip_address: ip_address,
entity_path: @scope.full_path entity_path: @scope.full_path
), ),
ip_address: ip_address ip_address: ip_address
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
module Audit module Audit
class Auditor class Auditor
# Record audit events for block # Record audit events
# #
# @param [Hash] context # @param [Hash] context
# @option context [String] :name the operation name to be audited, used for error tracking # @option context [String] :name the operation name to be audited, used for error tracking
...@@ -11,21 +11,47 @@ module Gitlab ...@@ -11,21 +11,47 @@ module Gitlab
# @option context [User, Project, Group] :scope the scope which audit event belongs to # @option context [User, Project, Group] :scope the scope which audit event belongs to
# @option context [Object] :target the target object being audited # @option context [Object] :target the target object being audited
# @option context [Object] :ip_address the request IP address # @option context [Object] :ip_address the request IP address
# @option context [String] :message the message describing the action
# #
# @example Wrap operation to be audit logged # @example Using block (useful when events are emitted deep in the call stack)
# i.e. multiple audit events
# #
# Gitlab::Audit::Auditor.audit(context) do # audit_context = {
# name: 'merge_approval_rule_updated',
# author: current_user,
# scope: project_alpha,
# target: merge_approval_rule,
# ip_address: request.remote_ip
# message: 'a user has attempted to update an approval rule'
# }
#
# # in the initiating service
# Gitlab::Audit::Auditor.audit(audit_context) do
# service.execute # service.execute
# end # end
# #
# # in the model
# Auditable.push_audit_event('an approver has been added')
# Auditable.push_audit_event('an approval group has been removed')
#
# @example Using standard method call
# i.e. single audit event
#
# merge_approval_rule.save
# Gitlab::Audit::Auditor.audit(audit_context)
#
# @return result of block execution # @return result of block execution
def self.audit(context) def self.audit(context, &block)
auditor = new(context) auditor = new(context)
auditor.audit { yield } if block
auditor.multiple_audit(&block)
else
auditor.single_audit
end
end end
def initialize(context) def initialize(context = {})
@context = context @context = context
@name = @context.fetch(:name, 'audit_operation') @name = @context.fetch(:name, 'audit_operation')
...@@ -33,25 +59,29 @@ module Gitlab ...@@ -33,25 +59,29 @@ module Gitlab
@scope = @context.fetch(:scope) @scope = @context.fetch(:scope)
@target = @context.fetch(:target) @target = @context.fetch(:target)
@ip_address = @context.fetch(:ip_address, nil) @ip_address = @context.fetch(:ip_address, nil)
@message = @context.fetch(:message, '')
end end
def audit def multiple_audit
::Gitlab::Audit::EventQueue.begin! ::Gitlab::Audit::EventQueue.begin!
return_value = yield return_value = yield
record ::Gitlab::Audit::EventQueue.current
.map { |message| build_event(message) }
.then { |events| record(events) }
return_value return_value
ensure ensure
::Gitlab::Audit::EventQueue.end! ::Gitlab::Audit::EventQueue.end!
end end
private def single_audit
events = [build_event(@message)]
def record record(events)
events = ::Gitlab::Audit::EventQueue.current.map { |message| build_event(message) } end
def record(events)
log_to_database(events) log_to_database(events)
log_to_file(events) log_to_file(events)
end end
......
...@@ -22,59 +22,102 @@ RSpec.describe Gitlab::Audit::Auditor do ...@@ -22,59 +22,102 @@ RSpec.describe Gitlab::Audit::Auditor do
subject(:auditor) { described_class } subject(:auditor) { described_class }
describe '.audit', :request_store do describe '.audit' do
it 'interacts with the event queue in correct order', :aggregate_failures do context 'when recording multiple events', :request_store do
allow(Gitlab::Audit::EventQueue).to receive(:begin!).and_call_original let(:audit!) { auditor.audit(context, &operation) }
allow(Gitlab::Audit::EventQueue).to receive(:end!).and_call_original
auditor.audit(context, &operation) it 'interacts with the event queue in correct order', :aggregate_failures do
allow(Gitlab::Audit::EventQueue).to receive(:begin!).and_call_original
allow(Gitlab::Audit::EventQueue).to receive(:end!).and_call_original
expect(Gitlab::Audit::EventQueue).to have_received(:begin!).ordered audit!
expect(Gitlab::Audit::EventQueue).to have_received(:end!).ordered
end
it 'records audit events in correct order', :aggregate_failures do expect(Gitlab::Audit::EventQueue).to have_received(:begin!).ordered
expect { auditor.audit(context, &operation) }.to change(AuditEvent, :count).by(2) expect(Gitlab::Audit::EventQueue).to have_received(:end!).ordered
end
event_messages = AuditEvent.all.map { |event| event.details[:custom_message] } it 'bulk-inserts audit events to database' do
allow(AuditEvent).to receive(:bulk_insert!)
expect(event_messages).to eq([add_message, remove_message]) audit!
end
it 'bulk-inserts audit events to database' do expect(AuditEvent).to have_received(:bulk_insert!)
allow(AuditEvent).to receive(:bulk_insert!) end
auditor.audit(context, &operation) it 'records audit events in correct order', :aggregate_failures do
expect { audit! }.to change(AuditEvent, :count).by(2)
expect(AuditEvent).to have_received(:bulk_insert!) event_messages = AuditEvent.all.map { |event| event.details[:custom_message] }
end
expect(event_messages).to eq([add_message, remove_message])
end
it 'logs audit events to database', :aggregate_failures do
audit!
audit_event = AuditEvent.last
expect(audit_event.author_id).to eq(author.id)
expect(audit_event.entity_id).to eq(scope.id)
expect(audit_event.entity_type).to eq(scope.class.name)
expect(audit_event.details[:target_id]).to eq(target.id)
expect(audit_event.details[:target_type]).to eq(target.class.name)
end
it 'logs audit events to database', :aggregate_failures do it 'logs audit events to file' do
auditor.audit(context, &operation) expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
audit_event = AuditEvent.last audit!
expect(audit_event.author_id).to eq(author.id) expect(logger).to have_received(:info).exactly(2).times.with(
expect(audit_event.entity_id).to eq(scope.id) hash_including(
expect(audit_event.entity_type).to eq(scope.class.name) 'author_id' => author.id,
expect(audit_event.details[:target_id]).to eq(target.id) 'author_name' => author.name,
expect(audit_event.details[:target_type]).to eq(target.class.name) 'entity_id' => scope.id,
'entity_type' => scope.class.name,
'details' => kind_of(Hash)
)
)
end
end end
it 'logs audit events to file' do context 'when recording single event' do
expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger) let(:audit!) { auditor.audit(context) }
let(:context) do
{
name: name, author: author, scope: scope, target: target, ip_address: ip_address,
message: 'Project has been deleted'
}
end
it 'logs audit event to database', :aggregate_failures do
expect { audit! }.to change(AuditEvent, :count).by(1)
auditor.audit(context, &operation) audit_event = AuditEvent.last
expect(logger).to have_received(:info).exactly(2).times.with( expect(audit_event.author_id).to eq(author.id)
hash_including( expect(audit_event.entity_id).to eq(scope.id)
'author_id' => author.id, expect(audit_event.entity_type).to eq(scope.class.name)
'author_name' => author.name, expect(audit_event.details[:target_id]).to eq(target.id)
'entity_id' => scope.id, expect(audit_event.details[:target_type]).to eq(target.class.name)
'entity_type' => scope.class.name, expect(audit_event.details[:custom_message]).to eq('Project has been deleted')
'details' => kind_of(Hash) end
it 'logs audit events to file' do
expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
audit!
expect(logger).to have_received(:info).once.with(
hash_including(
'author_id' => author.id,
'author_name' => author.name,
'entity_id' => scope.id,
'entity_type' => scope.class.name,
'details' => kind_of(Hash)
)
) )
) end
end end
context 'when audit events are invalid' do context 'when audit events are invalid' do
......
...@@ -87,5 +87,31 @@ RSpec.describe AuditEvents::BuildService do ...@@ -87,5 +87,31 @@ RSpec.describe AuditEvents::BuildService do
end end
end end
end end
context 'when attributes are missing' do
context 'when author is missing' do
let(:author) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) }
end
context 'when scope is missing' do
let(:scope) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) }
end
context 'when target is missing' do
let(:target) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) }
end
context 'when message is missing' do
let(:message) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) }
end
end
end 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