Skip to content
Snippets Groups Projects
Commit 179fd549 authored by Harsimar Sandhu's avatar Harsimar Sandhu :three: Committed by Alex Kalderimis
Browse files

Auditor for FOSS edition

Now that we want to create authentication events
using auditor framework, we will have to implement auditor
in FOSS because authentication audit events
are created for both CE and EE licence.

EE: true
Changelog: changed
parent 1060c493
No related branches found
No related tags found
1 merge request!94032Auditor for FOSS edition
Showing
with 580 additions and 94 deletions
......@@ -86,6 +86,18 @@ def as_json(options = {})
end
end
def target_type
super || details[:target_type]
end
def target_id
details[:target_id]
end
def target_details
super || details[:target_details]
end
private
def sanitize_message
......
......@@ -37,18 +37,7 @@ def missing_attribute?(author, scope, target, message)
end
def payload
if License.feature_available?(:admin_audit_log)
base_payload.merge(
details: base_details_payload.merge(
ip_address: @ip_address,
entity_path: @scope.full_path,
custom_message: @message
),
ip_address: @ip_address
)
else
base_payload.merge(details: base_details_payload)
end
base_payload.merge(details: base_details_payload)
end
def base_payload
......@@ -63,20 +52,20 @@ def base_payload
def base_details_payload
@additional_details.merge({
author_name: @author.name,
author_class: @author.class.name,
target_id: @target.id,
target_type: @target.type,
target_details: @target_details || @target.details,
custom_message: @message
})
author_name: @author.name,
author_class: @author.class.name,
target_id: @target.id,
target_type: @target.type,
target_details: @target_details || @target.details,
custom_message: @message
})
end
def build_author(author)
author.id = -2 if author.instance_of? DeployToken
author.id = -3 if author.instance_of? DeployKey
author.impersonated? ? ::Gitlab::Audit::ImpersonatedAuthor.new(author) : author
author
end
def build_target(target)
......@@ -86,11 +75,7 @@ def build_target(target)
end
def build_message(message)
if License.feature_available?(:admin_audit_log) && @author.impersonated?
"#{message} (by #{@author.impersonated_by})"
else
message
end
message
end
def build_ip_address
......@@ -98,3 +83,5 @@ def build_ip_address
end
end
end
AuditEvents::BuildService.prepend_mod_with('AuditEvents::BuildService')
......@@ -179,7 +179,7 @@ def compliance_framework_params
end
def log_audit_event(message:)
AuditEvents::CustomAuditEventService.new(
::AuditEvents::CustomAuditEventService.new(
current_user,
project,
request.remote_ip,
......
......@@ -55,18 +55,6 @@ def present
AuditEventPresenter.new(self)
end
def target_type
super || details[:target_type]
end
def target_id
details[:target_id]
end
def target_details
super || details[:target_details]
end
def ip_address
super&.to_s || details[:ip_address]
end
......@@ -85,7 +73,7 @@ def stream_to_external_destinations(use_json: false, event_name: 'audit_operatio
return unless can_stream_to_external_destination?
perform_params = use_json ? [event_name, nil, streaming_json] : [event_name, id, nil]
AuditEvents::AuditEventStreamingWorker.perform_async(*perform_params)
::AuditEvents::AuditEventStreamingWorker.perform_async(*perform_params)
end
def entity_is_group_or_project?
......
# frozen_string_literal: true
# rubocop:disable Gitlab/ModuleWithInstanceVariables
module EE
module AuditEvents
module BuildService
extend ::Gitlab::Utils::Override
private
override :payload
def payload
if License.feature_available?(:admin_audit_log)
base_payload.merge(
details: base_details_payload.merge(
ip_address: @ip_address,
entity_path: @scope.full_path,
custom_message: @message
),
ip_address: @ip_address
)
else
super
end
end
override :build_message
def build_message(message)
if License.feature_available?(:admin_audit_log) && @author.impersonated?
"#{message} (by #{@author.impersonated_by})"
else
super
end
end
override :build_author
def build_author(author)
super
author.impersonated? ? ::Gitlab::Audit::ImpersonatedAuthor.new(author) : author
end
end
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
......@@ -116,7 +116,7 @@ def project_finder_params_ee
override :send_git_archive
def send_git_archive(repository, **kwargs)
AuditEvents::RepositoryDownloadStartedAuditEventService.new(
::AuditEvents::RepositoryDownloadStartedAuditEventService.new(
current_user,
repository.project,
ip_address
......
......@@ -35,7 +35,7 @@ module Releases
override :log_release_created_audit_event
def log_release_created_audit_event(release)
AuditEvents::ReleaseCreatedAuditEventService.new(
::AuditEvents::ReleaseCreatedAuditEventService.new(
current_user,
user_project,
request.ip,
......@@ -45,7 +45,7 @@ def log_release_created_audit_event(release)
override :log_release_updated_audit_event
def log_release_updated_audit_event
AuditEvents::ReleaseUpdatedAuditEventService.new(
::AuditEvents::ReleaseUpdatedAuditEventService.new(
current_user,
user_project,
request.ip,
......@@ -55,7 +55,7 @@ def log_release_updated_audit_event
override :log_release_milestones_updated_audit_event
def log_release_milestones_updated_audit_event
AuditEvents::ReleaseAssociateMilestoneAuditEventService.new(
::AuditEvents::ReleaseAssociateMilestoneAuditEventService.new(
current_user,
user_project,
request.ip,
......
# frozen_string_literal: true
module EE
module Gitlab
module Audit
module Auditor
extend ::Gitlab::Utils::Override
override :multiple_audit
def multiple_audit
::Gitlab::Audit::EventQueue.begin!
return_value = yield
::Gitlab::Audit::EventQueue.current
.map { |message| build_event(message) }
.then { |events| record(events) }
return_value
ensure
::Gitlab::Audit::EventQueue.end!
end
override :send_to_stream
def send_to_stream(events)
events.each { |e| e.stream_to_external_destinations(use_json: true, event_name: name) }
end
override :audit_enabled?
def audit_enabled?
return true if super
return true if ::License.feature_available?(:admin_audit_log)
return true if ::License.feature_available?(:extended_audit_events)
scope.respond_to?(:feature_available?) && scope.licensed_feature_available?(:audit_events)
end
end
end
end
end
......@@ -84,7 +84,7 @@
auditor.execute
end
expect(recorder.count).to be <= 20
expect(recorder.count).to be <= 21
end
end
end
......@@ -193,31 +193,5 @@
end
end
end
context 'when attributes are missing' do
context 'when author is missing' do
let(:author) { nil }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when scope is missing' do
let(:scope) { nil }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when target is missing' do
let(:target) { nil }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when message is missing' do
let(:message) { nil }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
end
end
end
......@@ -3,6 +3,8 @@
module Gitlab
module Audit
class Auditor
attr_reader :scope, :name
# Record audit events
#
# @param [Hash] context
......@@ -66,36 +68,67 @@ def initialize(context = {})
@additional_details = @context.fetch(:additional_details, {})
@ip_address = @context[:ip_address]
@target_details = @context[:target_details]
@authentication_event = @context.fetch(:authentication_event, false)
@authentication_provider = @context[:authentication_provider]
end
def single_audit
events = [build_event(@message)]
record(events)
end
def multiple_audit
::Gitlab::Audit::EventQueue.begin!
# For now we dont have any need to implement multiple audit event functionality in CE
# Defined in EE
end
return_value = yield
def record(events)
log_events(events) unless @stream_only
send_to_stream(events)
end
::Gitlab::Audit::EventQueue.current
.map { |message| build_event(message) }
.then { |events| record(events) }
def log_events(events)
log_authentication_event
log_to_database(events)
log_to_file(events)
end
return_value
ensure
::Gitlab::Audit::EventQueue.end!
def audit_enabled?
authentication_event?
end
def single_audit
events = [build_event(@message)]
def authentication_event?
@authentication_event
end
record(events)
def log_authentication_event
return unless Gitlab::Database.read_write? && authentication_event?
event = AuthenticationEvent.new(authentication_event_payload)
event.save!
rescue ActiveRecord::RecordInvalid => error
::Gitlab::ErrorTracking.track_exception(error, audit_operation: @name)
end
def record(events)
log_to_database(events) unless @stream_only
log_to_file(events) unless @stream_only
send_to_stream(events)
def authentication_event_payload
{
# @author can be a User or various Gitlab::Audit authors.
# Only capture real users for successful authentication events.
user: author_if_user,
user_name: @author.name,
ip_address: @ip_address,
result: AuthenticationEvent.results[:success],
provider: @authentication_provider
}
end
def author_if_user
@author if @author.is_a?(User)
end
def send_to_stream(events)
events.each { |e| e.stream_to_external_destinations(use_json: true, event_name: @name) }
# Defined in EE
end
def build_event(message)
......@@ -123,13 +156,6 @@ def log_to_file(events)
events.each { |event| file_logger.info(log_payload(event)) }
end
def audit_enabled?
return true if ::License.feature_available?(:admin_audit_log)
return true if ::License.feature_available?(:extended_audit_events)
@scope.respond_to?(:feature_available?) && @scope.licensed_feature_available?(:audit_events)
end
private
def log_payload(event)
......@@ -145,3 +171,5 @@ def formatted_details(details)
end
end
end
Gitlab::Audit::Auditor.prepend_mod_with("Gitlab::Audit::Auditor")
File moved
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Audit::Auditor do
let(:name) { 'audit_operation' }
let(:author) { create(:user) }
let(:group) { create(:group) }
let(:provider) { 'standard' }
let(:context) do
{ name: name,
author: author,
scope: group,
target: group,
authentication_event: true,
authentication_provider: provider,
message: "Signed in using standard authentication" }
end
let(:logger) { instance_spy(Gitlab::AuditJsonLogger) }
subject(:auditor) { described_class }
describe '.audit' do
context 'when authentication event' do
let(:audit!) { auditor.audit(context) }
it 'creates an authentication event' do
expect(AuthenticationEvent).to receive(:new).with(
{
user: author,
user_name: author.name,
ip_address: author.current_sign_in_ip,
result: AuthenticationEvent.results[:success],
provider: provider
}
).and_call_original
audit!
end
it 'logs audit events to database', :aggregate_failures do
freeze_time do
audit!
audit_event = AuditEvent.last
expect(audit_event.author_id).to eq(author.id)
expect(audit_event.entity_id).to eq(group.id)
expect(audit_event.entity_type).to eq(group.class.name)
expect(audit_event.created_at).to eq(Time.zone.now)
expect(audit_event.details[:target_id]).to eq(group.id)
expect(audit_event.details[:target_type]).to eq(group.class.name)
end
end
it 'logs audit events to file' do
expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
audit!
expect(logger).to have_received(:info).with(
hash_including(
'author_id' => author.id,
'author_name' => author.name,
'entity_id' => group.id,
'entity_type' => group.class.name,
'details' => kind_of(Hash)
)
)
end
context 'when overriding the create datetime' do
let(:context) do
{ name: name,
author: author,
scope: group,
target: group,
created_at: 3.weeks.ago,
authentication_event: true,
authentication_provider: provider,
message: "Signed in using standard authentication" }
end
it 'logs audit events to database', :aggregate_failures do
freeze_time do
audit!
audit_event = AuditEvent.last
expect(audit_event.author_id).to eq(author.id)
expect(audit_event.entity_id).to eq(group.id)
expect(audit_event.entity_type).to eq(group.class.name)
expect(audit_event.created_at).to eq(3.weeks.ago)
expect(audit_event.details[:target_id]).to eq(group.id)
expect(audit_event.details[:target_type]).to eq(group.class.name)
end
end
it 'logs audit events to file' do
freeze_time do
expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
audit!
expect(logger).to have_received(:info).with(
hash_including(
'author_id' => author.id,
'author_name' => author.name,
'entity_id' => group.id,
'entity_type' => group.class.name,
'details' => kind_of(Hash),
'created_at' => 3.weeks.ago.iso8601(3)
)
)
end
end
end
context 'when overriding the additional_details' do
additional_details = { action: :custom, from: false, to: true }
let(:context) do
{ name: name,
author: author,
scope: group,
target: group,
created_at: Time.zone.now,
additional_details: additional_details,
authentication_event: true,
authentication_provider: provider,
message: "Signed in using standard authentication" }
end
it 'logs audit events to database' do
freeze_time do
audit!
expect(AuditEvent.last.details).to include(additional_details)
end
end
it 'logs audit events to file' do
freeze_time do
expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
audit!
expect(logger).to have_received(:info).with(
hash_including(
'details' => hash_including('action' => 'custom', 'from' => 'false', 'to' => 'true'),
'action' => 'custom',
'from' => 'false',
'to' => 'true'
)
)
end
end
end
context 'when overriding the target_details' do
target_details = "this is my target details"
let(:context) do
{
name: name,
author: author,
scope: group,
target: group,
created_at: Time.zone.now,
target_details: target_details,
authentication_event: true,
authentication_provider: provider,
message: "Signed in using standard authentication"
}
end
it 'logs audit events to database' do
freeze_time do
audit!
audit_event = AuditEvent.last
expect(audit_event.details).to include({ target_details: target_details })
expect(audit_event.target_details).to eq(target_details)
end
end
it 'logs audit events to file' do
freeze_time do
expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger)
audit!
expect(logger).to have_received(:info).with(
hash_including(
'details' => hash_including('target_details' => target_details),
'target_details' => target_details
)
)
end
end
end
end
context 'when authentication event is false' do
let(:context) do
{ name: name, author: author, scope: group,
target: group, authentication_event: false, message: "sample message" }
end
it 'does not create an authentication event' do
expect { auditor.audit(context) }.not_to change(AuthenticationEvent, :count)
end
end
context 'when authentication event is invalid' do
let(:audit!) { auditor.audit(context) }
before do
allow(AuthenticationEvent).to receive(:new).and_raise(ActiveRecord::RecordInvalid)
allow(Gitlab::ErrorTracking).to receive(:track_exception)
end
it 'tracks error' do
audit!
expect(Gitlab::ErrorTracking).to have_received(:track_exception).with(
kind_of(ActiveRecord::RecordInvalid),
{ audit_operation: name }
)
end
it 'does not throw exception' do
expect { auditor.audit(context) }.not_to raise_exception
end
end
context 'when audit events are invalid' do
let(:audit!) { auditor.audit(context) }
before do
allow(AuditEvent).to receive(:bulk_insert!).and_raise(ActiveRecord::RecordInvalid)
allow(Gitlab::ErrorTracking).to receive(:track_exception)
end
it 'tracks error' do
audit!
expect(Gitlab::ErrorTracking).to have_received(:track_exception).with(
kind_of(ActiveRecord::RecordInvalid),
{ audit_operation: name }
)
end
it 'does not throw exception' do
expect { auditor.audit(context) }.not_to raise_exception
end
end
end
end
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Audit::Target do
let(:object) { double('object') }
let(:object) { double('object') } # rubocop:disable Rspec/VerifiedDoubles
subject { described_class.new(object) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AuditEvents::BuildService do
let(:author) { build_stubbed(:author, current_sign_in_ip: '127.0.0.1') }
let(:deploy_token) { build_stubbed(:deploy_token, user: author) }
let(:scope) { build_stubbed(:group) }
let(:target) { build_stubbed(:project) }
let(:ip_address) { '192.168.8.8' }
let(:message) { 'Added an interesting field from project Gotham' }
let(:additional_details) { { action: :custom } }
subject(:service) do
described_class.new(
author: author,
scope: scope,
target: target,
message: message,
additional_details: additional_details,
ip_address: ip_address
)
end
describe '#execute', :request_store do
subject(:event) { service.execute }
before do
allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(ip_address)
end
it 'sets correct attributes', :aggregate_failures do
freeze_time do
expect(event).to have_attributes(
author_id: author.id,
author_name: author.name,
entity_id: scope.id,
entity_type: scope.class.name)
expect(event.details).to eq(
author_name: author.name,
author_class: author.class.name,
target_id: target.id,
target_type: target.class.name,
target_details: target.name,
custom_message: message,
action: :custom)
expect(event.ip_address).to be_nil
expect(event.created_at).to eq(DateTime.current)
end
end
context 'when IP address is not provided' do
let(:ip_address) { nil }
it 'uses author current_sign_in_ip' do
expect(event.ip_address).to be_nil
end
end
context 'when overriding target details' do
subject(:service) do
described_class.new(
author: author,
scope: scope,
target: target,
message: message,
target_details: "This is my target details"
)
end
it 'uses correct target details' do
expect(event.target_details).to eq("This is my target details")
end
end
context 'when deploy token is passed as author' do
let(:service) do
described_class.new(
author: deploy_token,
scope: scope,
target: target,
message: message
)
end
it 'expect author to be user' do
expect(event.author_id).to eq(-2)
expect(event.author_name).to eq(deploy_token.name)
end
end
context 'when deploy key is passed as author' do
let(:deploy_key) { build_stubbed(:deploy_key, user: author) }
let(:service) do
described_class.new(
author: deploy_key,
scope: scope,
target: target,
message: message
)
end
it 'expect author to be deploy key' do
expect(event.author_id).to eq(-3)
expect(event.author_name).to eq(deploy_key.name)
end
end
context 'when author is passed as UnauthenticatedAuthor' do
let(:service) do
described_class.new(
author: ::Gitlab::Audit::UnauthenticatedAuthor.new,
scope: scope,
target: target,
message: message
)
end
it 'sets author as unauthenticated user' do
expect(event.author).to be_an_instance_of(::Gitlab::Audit::UnauthenticatedAuthor)
expect(event.author_name).to eq('An unauthenticated user')
end
end
context 'when attributes are missing' do
context 'when author is missing' do
let(:author) { nil }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when scope is missing' do
let(:scope) { nil }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when target is missing' do
let(:target) { nil }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when message is missing' do
let(:message) { nil }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment