1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
|
# frozen_string_literal: true
module Gitlab
module Audit
class Auditor
attr_reader :scope, :name
PERMITTED_TARGET_CLASSES = [
::Operations::FeatureFlag
].freeze
# 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]
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
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? || permitted_target?
end
def permitted_target?
@target.class.in? PERMITTED_TARGET_CLASSES
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")
|