debian-mirror-gitlab/lib/gitlab/audit/auditor.rb
2023-03-05 14:24:40 +05:30

207 lines
6.7 KiB
Ruby

# frozen_string_literal: true
module Gitlab
module Audit
class Auditor
attr_reader :scope, :name
# Record audit events
#
# @param [Hash] context
# @option context [String] :name the operation name to be audited, used for error tracking
# @option context [User] :author the user who authors the change
# @option context [User, Project, Group] :scope the scope which audit event belongs to
# @option context [Object] :target the target object being audited
# @option context [String] :message the message describing the action
# @option context [Hash] :additional_details the additional details we want to merge into audit event details.
# @option context [Time] :created_at the time that the event occurred (defaults to the current time)
#
# @example Using block (useful when events are emitted deep in the call stack)
# i.e. multiple audit events
#
# audit_context = {
# name: 'merge_approval_rule_updated',
# author: current_user,
# scope: project_alpha,
# target: merge_approval_rule,
# message: 'a user has attempted to update an approval rule'
# }
#
# # in the initiating service
# Gitlab::Audit::Auditor.audit(audit_context) do
# service.execute
# 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
def self.audit(context, &block)
auditor = new(context)
return unless auditor.audit_enabled?
if block
auditor.multiple_audit(&block)
else
auditor.single_audit
end
end
def initialize(context = {})
@context = context
@name = @context.fetch(:name, 'audit_operation')
@is_audit_event_yaml_defined = Gitlab::Audit::Type::Definition.defined?(@name)
@stream_only = stream_only?
@author = @context.fetch(:author)
@scope = @context.fetch(:scope)
@target = @context.fetch(:target)
@created_at = @context.fetch(:created_at, DateTime.current)
@message = @context.fetch(:message, '')
@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]
# TODO: Remove this code once we close https://gitlab.com/gitlab-org/gitlab/-/issues/367870
return unless @is_audit_event_yaml_defined
# rubocop:disable Gitlab/RailsLogger
Rails.logger.warn('WARNING: Logging audit events without an event type definition will be deprecated soon.')
Rails.logger.warn('See https://docs.gitlab.com/ee/development/audit_event_guide/#event-type-definitions')
# rubocop:enable Gitlab/RailsLogger
end
def single_audit
events = [build_event(@message)]
record(events)
end
def multiple_audit
# For now we dont have any need to implement multiple audit event functionality in CE
# Defined in EE
end
def record(events)
@stream_only ? send_to_stream(events) : log_events_and_stream(events)
end
def log_events_and_stream(events)
log_authentication_event
saved_events = log_to_database(events)
# we only want to override events with saved_events when it successfully saves into database.
# we are doing so to ensure events in memory reflects events saved in database and have id column.
events = saved_events if saved_events.present?
log_to_file_and_stream(events)
end
def log_to_file_and_stream(events)
log_to_file(events)
send_to_stream(events)
end
def audit_enabled?
authentication_event?
end
def authentication_event?
@authentication_event
end
def stream_only?
if @is_audit_event_yaml_defined
Gitlab::Audit::Type::Definition.stream_only?(@name)
else
@context.fetch(:stream_only, false)
end
end
def log_authentication_event
return unless Gitlab::Database.read_write? && authentication_event?
event = AuthenticationEvent.new(authentication_event_payload)
event.save!
rescue ActiveRecord::RecordInvalid => e
::Gitlab::ErrorTracking.track_exception(e, audit_operation: @name)
end
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: Gitlab::RequestContext.instance.client_ip || @author.current_sign_in_ip,
result: AuthenticationEvent.results[:success],
provider: @authentication_provider
}
end
def author_if_user
@author if @author.is_a?(User)
end
def send_to_stream(events)
# Defined in EE
end
def build_event(message)
AuditEvents::BuildService.new(
author: @author,
scope: @scope,
target: @target,
created_at: @created_at,
message: message,
additional_details: @additional_details,
ip_address: @ip_address,
target_details: @target_details
).execute
end
def log_to_database(events)
if events.one?
events.first.save!
events
else
event_ids = AuditEvent.bulk_insert!(events, returns: :ids)
AuditEvent.id_in(event_ids)
end
rescue ActiveRecord::RecordInvalid => e
::Gitlab::ErrorTracking.track_exception(e, audit_operation: @name)
end
def log_to_file(events)
file_logger = ::Gitlab::AuditJsonLogger.build
events.each { |event| file_logger.info(log_payload(event)) }
end
private
def log_payload(event)
payload = event.as_json
details = formatted_details(event.details)
payload["details"] = details
payload.merge!(details).as_json
end
def formatted_details(details)
details.merge(details.slice(:from, :to).transform_values(&:to_s))
end
end
end
end
Gitlab::Audit::Auditor.prepend_mod_with("Gitlab::Audit::Auditor")