Class: Gitlab::Audit::Auditor

Inherits:
Object
  • Object
show all
Defined in:
lib/gitlab/audit/auditor.rb

Constant Summary collapse

PERMITTED_TARGET_CLASSES =
[
  ::Operations::FeatureFlag
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(context = {}) ⇒ Auditor

Returns a new instance of Auditor.

Raises:

  • (StandardError)


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/gitlab/audit/auditor.rb', line 62

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]

  return if @is_audit_event_yaml_defined

  raise StandardError, "Audit event type YML file is not defined for #{@name}. Please read " \
                       "https://docs.gitlab.com/ee/development/audit_event_guide/" \
                       "#how-to-instrument-new-audit-events for adding a new audit event"
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



6
7
8
# File 'lib/gitlab/audit/auditor.rb', line 6

def name
  @name
end

#scopeObject (readonly)

Returns the value of attribute scope.



6
7
8
# File 'lib/gitlab/audit/auditor.rb', line 6

def scope
  @scope
end

Class Method Details

.audit(context, &block) ⇒ Object

Record audit events

Examples:

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')

Using standard method call

i.e. single audit event

merge_approval_rule.save
Gitlab::Audit::Auditor.audit(audit_context)

Parameters:

  • context (Hash)

Options Hash (context):

  • :name (String)

    the operation name to be audited, used for error tracking

  • :author (User)

    the user who authors the change

  • :scope (User, Project, Group)

    the scope which audit event belongs to

  • :target (Object)

    the target object being audited

  • :message (String)

    the message describing the action

  • :additional_details (Hash)

    the additional details we want to merge into audit event details.

  • :created_at (Time)

    the time that the event occurred (defaults to the current time)

Returns:

  • result of block execution



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/gitlab/audit/auditor.rb', line 50

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

Instance Method Details

#audit_enabled?Boolean

Returns:

  • (Boolean)


117
118
119
# File 'lib/gitlab/audit/auditor.rb', line 117

def audit_enabled?
  authentication_event? || permitted_target?
end

#authentication_event?Boolean

Returns:

  • (Boolean)


125
126
127
# File 'lib/gitlab/audit/auditor.rb', line 125

def authentication_event?
  @authentication_event
end

#authentication_event_payloadObject



146
147
148
149
150
151
152
153
154
155
156
# File 'lib/gitlab/audit/auditor.rb', line 146

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.,
    result: AuthenticationEvent.results[:success],
    provider: @authentication_provider
  }
end

#author_if_userObject



158
159
160
# File 'lib/gitlab/audit/auditor.rb', line 158

def author_if_user
  @author if @author.is_a?(User)
end

#build_event(message) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/gitlab/audit/auditor.rb', line 166

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

#log_authentication_eventObject



137
138
139
140
141
142
143
144
# File 'lib/gitlab/audit/auditor.rb', line 137

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

#log_events_and_stream(events) ⇒ Object



101
102
103
104
105
106
107
108
109
110
# File 'lib/gitlab/audit/auditor.rb', line 101

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

#log_to_database(events) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
# File 'lib/gitlab/audit/auditor.rb', line 179

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

#log_to_file(events) ⇒ Object



191
192
193
194
195
# File 'lib/gitlab/audit/auditor.rb', line 191

def log_to_file(events)
  file_logger = ::Gitlab::AuditJsonLogger.build

  events.each { |event| file_logger.info(log_payload(event)) }
end

#log_to_file_and_stream(events) ⇒ Object



112
113
114
115
# File 'lib/gitlab/audit/auditor.rb', line 112

def log_to_file_and_stream(events)
  log_to_file(events)
  send_to_stream(events)
end

#multiple_auditObject



92
93
94
95
# File 'lib/gitlab/audit/auditor.rb', line 92

def multiple_audit
  # For now we dont have any need to implement multiple audit event functionality in CE
  # Defined in EE
end

#permitted_target?Boolean

Returns:

  • (Boolean)


121
122
123
# File 'lib/gitlab/audit/auditor.rb', line 121

def permitted_target?
  @target.class.in? PERMITTED_TARGET_CLASSES
end

#record(events) ⇒ Object



97
98
99
# File 'lib/gitlab/audit/auditor.rb', line 97

def record(events)
  @stream_only ? send_to_stream(events) : log_events_and_stream(events)
end

#send_to_stream(events) ⇒ Object



162
163
164
# File 'lib/gitlab/audit/auditor.rb', line 162

def send_to_stream(events)
  # Defined in EE
end

#single_auditObject



86
87
88
89
90
# File 'lib/gitlab/audit/auditor.rb', line 86

def single_audit
  events = [build_event(@message)]

  record(events)
end

#stream_only?Boolean

Returns:

  • (Boolean)


129
130
131
132
133
134
135
# File 'lib/gitlab/audit/auditor.rb', line 129

def stream_only?
  if @is_audit_event_yaml_defined
    Gitlab::Audit::Type::Definition.stream_only?(@name)
  else
    @context.fetch(:stream_only, false)
  end
end