provenance 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE +21 -0
- data/README.md +333 -0
- data/lib/provenance/configuration.rb +34 -0
- data/lib/provenance/context.rb +58 -0
- data/lib/provenance/journal.rb +123 -0
- data/lib/provenance/trackers/auditable.rb +125 -0
- data/lib/provenance/trackers/bulk_operations.rb +72 -0
- data/lib/provenance/trackers/error_reporting.rb +48 -0
- data/lib/provenance/trackers/providers.rb +163 -0
- data/lib/provenance/trackers/trackable.rb +341 -0
- data/lib/provenance/transaction_key.rb +19 -0
- data/lib/provenance/version.rb +5 -0
- data/lib/provenance.rb +56 -0
- metadata +122 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "active_record"
|
|
5
|
+
|
|
6
|
+
module Provenance
|
|
7
|
+
# Model-side concern. Captures create/update/destroy through ActiveRecord
|
|
8
|
+
# callbacks, groups changes by transaction key, discards them on rollback and
|
|
9
|
+
# flushes the audit log once every transaction has committed. Also tracks
|
|
10
|
+
# `has_and_belongs_to_many` join-table changes, which bypass model callbacks,
|
|
11
|
+
# via SQL notifications.
|
|
12
|
+
module Trackable
|
|
13
|
+
extend ActiveSupport::Concern
|
|
14
|
+
|
|
15
|
+
INSERT_DELETE_REGEX = /\b(INSERT|DELETE)\s+(INTO|FROM)\b/i
|
|
16
|
+
INSERT_INTO_REGEX = /\bINSERT\s+INTO\b/i
|
|
17
|
+
TABLE_NAME_REGEX = /(?:INSERT\s+INTO|DELETE\s+FROM)\s+["`]?(\w+)["`]?/i
|
|
18
|
+
INSERT_COLUMNS_REGEX = /INSERT\s+INTO\s+["`]?\w+["`]?\s*\(([^)]+)\)/i
|
|
19
|
+
WHERE_REGEX = /WHERE\s+(.+?)(?:\s+RETURNING|\s*$)/i
|
|
20
|
+
|
|
21
|
+
def self.included(base)
|
|
22
|
+
super
|
|
23
|
+
|
|
24
|
+
base.class_eval do
|
|
25
|
+
before_create :capture_transaction_id
|
|
26
|
+
before_update :capture_transaction_id
|
|
27
|
+
before_destroy :capture_transaction_id
|
|
28
|
+
|
|
29
|
+
after_update :capture_update
|
|
30
|
+
after_create :capture_create
|
|
31
|
+
after_destroy :capture_destroy
|
|
32
|
+
|
|
33
|
+
after_commit :try_send_audit_after_commit
|
|
34
|
+
after_rollback :clear_tracked_changes
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
alias_method :original_has_and_belongs_to_many, :has_and_belongs_to_many
|
|
38
|
+
|
|
39
|
+
def has_and_belongs_to_many(name, scope = nil, **options, &extension)
|
|
40
|
+
original_has_and_belongs_to_many(name, scope, **options, &extension).tap do
|
|
41
|
+
association = reflect_on_association(name)
|
|
42
|
+
next unless association
|
|
43
|
+
|
|
44
|
+
Provenance::Trackable.register_habtm_join_table_from_association(association, self.name, name)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
base.setup_habtm_tracking
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# has_and_belongs_to_many join-table writes bypass model callbacks, so we
|
|
53
|
+
# observe them through SQL notifications and fold them into the journal.
|
|
54
|
+
# The change is recorded synchronously on the request thread; delivery is
|
|
55
|
+
# handled by Auditable once every transaction has committed, so no extra
|
|
56
|
+
# post-commit scheduling is required here.
|
|
57
|
+
return if @habtm_sql_subscribed
|
|
58
|
+
|
|
59
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start, _finish, _id, payload|
|
|
60
|
+
Provenance::Trackable.track_habtm_sql_changes(payload)
|
|
61
|
+
end
|
|
62
|
+
@habtm_sql_subscribed = true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class_methods do
|
|
66
|
+
def sensitive_attributes(*attributes)
|
|
67
|
+
@sensitive_attributes = Array(attributes).flatten
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def sensitive_attributes_list
|
|
71
|
+
@sensitive_attributes || []
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def setup_habtm_tracking
|
|
75
|
+
reflect_on_all_associations(:has_and_belongs_to_many).each do |association|
|
|
76
|
+
Provenance::Trackable.register_habtm_join_table_from_association(association, name, association.name)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Registry of join tables we watch, keyed by table name. Populated when a
|
|
82
|
+
# model that includes Trackable declares a has_and_belongs_to_many.
|
|
83
|
+
def self.register_habtm_join_table_from_association(association, model_class_name, association_name)
|
|
84
|
+
join_table = association.join_table
|
|
85
|
+
(@habtm_join_tables ||= {})[join_table] ||= []
|
|
86
|
+
@habtm_join_tables[join_table] << {
|
|
87
|
+
model_class_name: model_class_name,
|
|
88
|
+
association_name: association_name,
|
|
89
|
+
foreign_key: association.foreign_key,
|
|
90
|
+
association_foreign_key: association.association_foreign_key
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.habtm_join_tables
|
|
95
|
+
@habtm_join_tables ||= {}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Inspects a SQL statement; if it touches a watched join table, reconstructs
|
|
99
|
+
# the affected ids and folds the change into the journal as an update.
|
|
100
|
+
def self.track_habtm_sql_changes(payload)
|
|
101
|
+
journal = Provenance::Context.journal
|
|
102
|
+
return unless journal
|
|
103
|
+
|
|
104
|
+
sql = payload[:sql].to_s
|
|
105
|
+
return unless sql.match?(INSERT_DELETE_REGEX)
|
|
106
|
+
|
|
107
|
+
table_name = sql.match(TABLE_NAME_REGEX)&.[](1)
|
|
108
|
+
return unless table_name && (info_list = habtm_join_tables[table_name])
|
|
109
|
+
|
|
110
|
+
action = sql.match?(INSERT_INTO_REGEX) ? :add : :remove
|
|
111
|
+
|
|
112
|
+
info_list.each do |info|
|
|
113
|
+
values_list = extract_habtm_values(sql, payload[:binds], action, info[:foreign_key], info[:association_foreign_key])
|
|
114
|
+
next unless values_list
|
|
115
|
+
|
|
116
|
+
model = info[:model_class_name].constantize.find_by(id: values_list[0])
|
|
117
|
+
next unless model&.persisted?
|
|
118
|
+
|
|
119
|
+
associated_ids = Array(values_list[1])
|
|
120
|
+
model.send(:track_habtm_changes, info[:association_name], action, associated_ids)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.extract_habtm_values(sql, binds, action, foreign_key, association_foreign_key)
|
|
125
|
+
return nil unless binds
|
|
126
|
+
|
|
127
|
+
if action == :add
|
|
128
|
+
extract_insert_values(sql, binds, foreign_key, association_foreign_key)
|
|
129
|
+
else
|
|
130
|
+
extract_delete_values(sql, binds, foreign_key, association_foreign_key)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.extract_insert_values(sql, binds, foreign_key, association_foreign_key)
|
|
135
|
+
columns = sql.match(INSERT_COLUMNS_REGEX)&.[](1)
|
|
136
|
+
return nil unless columns
|
|
137
|
+
|
|
138
|
+
columns = columns.split(",").map { |c| c.strip.delete('"`') }
|
|
139
|
+
fk_idx = columns.index(foreign_key)
|
|
140
|
+
afk_idx = columns.index(association_foreign_key)
|
|
141
|
+
return nil unless fk_idx && afk_idx && binds[fk_idx] && binds[afk_idx]
|
|
142
|
+
|
|
143
|
+
[extract_value(binds[fk_idx]), extract_value(binds[afk_idx])]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.extract_delete_values(sql, binds, foreign_key, association_foreign_key)
|
|
147
|
+
conditions = sql.match(WHERE_REGEX)&.[](1)
|
|
148
|
+
return nil unless conditions
|
|
149
|
+
|
|
150
|
+
fk_pattern = /(?:["`]\w+["`]\.)?["`]?#{Regexp.escape(foreign_key)}["`]?\s*[=<>]\s*\$(\d+)/i
|
|
151
|
+
fk_match = conditions.match(fk_pattern)
|
|
152
|
+
return nil unless fk_match
|
|
153
|
+
|
|
154
|
+
fk_idx = fk_match[1].to_i - 1
|
|
155
|
+
return nil unless binds[fk_idx]
|
|
156
|
+
|
|
157
|
+
afk_pattern = /(?:["`]\w+["`]\.)?["`]?#{Regexp.escape(association_foreign_key)}["`]?\s*[=<>]\s*\$(\d+)/i
|
|
158
|
+
afk_match = conditions.match(afk_pattern)
|
|
159
|
+
|
|
160
|
+
if afk_match
|
|
161
|
+
afk_idx = afk_match[1].to_i - 1
|
|
162
|
+
return nil unless binds[afk_idx]
|
|
163
|
+
|
|
164
|
+
[extract_value(binds[fk_idx]), extract_value(binds[afk_idx])]
|
|
165
|
+
else
|
|
166
|
+
in_params = conditions.match(/#{Regexp.escape(association_foreign_key)}["`]?\s+IN\s*\(([^)]+)\)/i)&.[](1)
|
|
167
|
+
return nil unless in_params
|
|
168
|
+
|
|
169
|
+
param_indices = in_params.scan(/\$(\d+)/).flatten.map(&:to_i).map { |i| i - 1 }
|
|
170
|
+
associated_ids = param_indices.filter_map { |idx| extract_value(binds[idx]) if binds[idx] }
|
|
171
|
+
return nil if associated_ids.empty?
|
|
172
|
+
|
|
173
|
+
[extract_value(binds[fk_idx]), associated_ids]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.extract_value(bind)
|
|
178
|
+
return bind.value if bind.respond_to?(:value)
|
|
179
|
+
return bind.value_for_database if bind.respond_to?(:value_for_database)
|
|
180
|
+
|
|
181
|
+
bind
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def capture_transaction_id
|
|
187
|
+
@_audit_transaction_id = current_transaction_id
|
|
188
|
+
journal = Provenance::Context.journal
|
|
189
|
+
return unless journal && self.class.connection.transaction_open?
|
|
190
|
+
|
|
191
|
+
journal.register_transaction(@_audit_transaction_id)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def capture_create
|
|
195
|
+
journal = Provenance::Context.journal
|
|
196
|
+
return unless journal
|
|
197
|
+
|
|
198
|
+
attributes_data = if saved_changes.any?
|
|
199
|
+
saved_changes.transform_values(&:last)
|
|
200
|
+
else
|
|
201
|
+
attributes.except("id", "created_at", "updated_at")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
journal.add_change(
|
|
205
|
+
self,
|
|
206
|
+
:create,
|
|
207
|
+
{
|
|
208
|
+
attributes: attributes_data,
|
|
209
|
+
transaction_id: captured_transaction_id
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def capture_update
|
|
215
|
+
journal = Provenance::Context.journal
|
|
216
|
+
return unless journal
|
|
217
|
+
|
|
218
|
+
changed_attributes = saved_changes.transform_values(&:last)
|
|
219
|
+
previous_changes = saved_changes.transform_values(&:first)
|
|
220
|
+
|
|
221
|
+
journal.add_change(
|
|
222
|
+
self,
|
|
223
|
+
:update,
|
|
224
|
+
{
|
|
225
|
+
changed_attributes: changed_attributes,
|
|
226
|
+
previous_changes: previous_changes,
|
|
227
|
+
transaction_id: captured_transaction_id
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def capture_destroy
|
|
233
|
+
journal = Provenance::Context.journal
|
|
234
|
+
return unless journal
|
|
235
|
+
|
|
236
|
+
journal.add_change(
|
|
237
|
+
self,
|
|
238
|
+
:destroy,
|
|
239
|
+
{
|
|
240
|
+
attributes: attributes.except("id", "created_at", "updated_at"),
|
|
241
|
+
transaction_id: captured_transaction_id
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def try_send_audit_after_commit
|
|
247
|
+
return unless Provenance::Context.pending_audit_log
|
|
248
|
+
|
|
249
|
+
journal = Provenance::Context.journal
|
|
250
|
+
return unless journal
|
|
251
|
+
|
|
252
|
+
journal.complete_transaction(@_audit_transaction_id) if @_audit_transaction_id
|
|
253
|
+
|
|
254
|
+
active_transactions = journal.instance_variable_get(:@active_transactions)
|
|
255
|
+
return if active_transactions.any? || Thread.current[:provenance_send_scheduled]
|
|
256
|
+
|
|
257
|
+
Thread.current[:provenance_send_scheduled] = true
|
|
258
|
+
begin
|
|
259
|
+
Provenance::Context.pending_audit_log.call
|
|
260
|
+
ensure
|
|
261
|
+
Thread.current[:provenance_send_scheduled] = nil
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def clear_tracked_changes
|
|
266
|
+
journal = Provenance::Context.journal
|
|
267
|
+
return unless journal && @_audit_transaction_id
|
|
268
|
+
|
|
269
|
+
journal.remove_changes_for_transaction(@_audit_transaction_id)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def captured_transaction_id
|
|
273
|
+
@_audit_transaction_id || Provenance::Context.request_id || "no-request-id"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def current_transaction_id
|
|
277
|
+
Provenance::TransactionKey.for_connection(self.class.connection)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def track_habtm_changes(association_name, action, associated_ids)
|
|
281
|
+
journal = Provenance::Context.journal
|
|
282
|
+
return unless journal && persisted?
|
|
283
|
+
|
|
284
|
+
association = self.class.reflect_on_association(association_name)
|
|
285
|
+
return unless association&.macro == :has_and_belongs_to_many
|
|
286
|
+
|
|
287
|
+
connection = self.class.connection
|
|
288
|
+
register_transaction_if_needed(journal, connection)
|
|
289
|
+
|
|
290
|
+
ids_key = "#{association_name}_ids"
|
|
291
|
+
transaction_id = captured_transaction_id
|
|
292
|
+
request_id_value = Provenance::Context.request_id.to_s
|
|
293
|
+
|
|
294
|
+
existing_change = find_existing_habtm_change(journal, ids_key, transaction_id, request_id_value)
|
|
295
|
+
current_ids = get_current_habtm_ids(connection, association)
|
|
296
|
+
associated_ids_str = associated_ids.map(&:to_s)
|
|
297
|
+
|
|
298
|
+
if existing_change
|
|
299
|
+
existing_change[:changes][:changed_attributes][ids_key] = current_ids
|
|
300
|
+
if action == :remove
|
|
301
|
+
existing_previous = existing_change[:changes][:previous_changes][ids_key] || []
|
|
302
|
+
existing_change[:changes][:previous_changes][ids_key] = (existing_previous + associated_ids_str).uniq
|
|
303
|
+
end
|
|
304
|
+
else
|
|
305
|
+
previous_ids = action == :add ? current_ids - associated_ids_str : current_ids + associated_ids_str
|
|
306
|
+
journal.add_change(self, :update, {
|
|
307
|
+
changed_attributes: { ids_key => current_ids },
|
|
308
|
+
previous_changes: { ids_key => previous_ids },
|
|
309
|
+
transaction_id: transaction_id
|
|
310
|
+
})
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
journal.complete_transaction(transaction_id)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def register_transaction_if_needed(journal, connection)
|
|
317
|
+
return unless connection.transaction_open?
|
|
318
|
+
|
|
319
|
+
@_audit_transaction_id ||= current_transaction_id
|
|
320
|
+
journal.register_transaction(@_audit_transaction_id)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def find_existing_habtm_change(journal, ids_key, transaction_id, request_id_value)
|
|
324
|
+
model_name = self.class.name
|
|
325
|
+
journal.changes.find do |change|
|
|
326
|
+
change[:model] == model_name &&
|
|
327
|
+
change[:model_id] == id &&
|
|
328
|
+
change[:action] == "update" &&
|
|
329
|
+
change[:changes][:changed_attributes]&.key?(ids_key) &&
|
|
330
|
+
(change[:transaction_id]&.start_with?(request_id_value) || change[:transaction_id] == transaction_id)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def get_current_habtm_ids(connection, association)
|
|
335
|
+
sql = "SELECT #{connection.quote_column_name(association.association_foreign_key)} " \
|
|
336
|
+
"FROM #{connection.quote_table_name(association.join_table)} " \
|
|
337
|
+
"WHERE #{connection.quote_column_name(association.foreign_key)} = #{connection.quote(id)}"
|
|
338
|
+
connection.select_values(sql).map(&:to_s)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Provenance
|
|
4
|
+
# Builds the transaction key used to group changes inside the journal.
|
|
5
|
+
# A single format is shared across the gem (Trackable and BulkOperations);
|
|
6
|
+
# otherwise rollback cleanup (`remove_changes_for_transaction`) would fail to
|
|
7
|
+
# match the paired records.
|
|
8
|
+
module TransactionKey
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def for_connection(connection)
|
|
12
|
+
request_id = Provenance::Context.request_id
|
|
13
|
+
return request_id unless connection.transaction_open?
|
|
14
|
+
|
|
15
|
+
current = connection.current_transaction
|
|
16
|
+
current ? "#{request_id}:#{current.object_id}" : "#{request_id}:#{connection.open_transactions}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/provenance.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
5
|
+
require "active_support/core_ext/hash/keys"
|
|
6
|
+
require "json"
|
|
7
|
+
require "logger"
|
|
8
|
+
require "rails"
|
|
9
|
+
|
|
10
|
+
require_relative "provenance/version"
|
|
11
|
+
require_relative "provenance/configuration"
|
|
12
|
+
require_relative "provenance/context"
|
|
13
|
+
require_relative "provenance/transaction_key"
|
|
14
|
+
require_relative "provenance/journal"
|
|
15
|
+
require_relative "provenance/trackers/providers"
|
|
16
|
+
require_relative "provenance/trackers/trackable"
|
|
17
|
+
require_relative "provenance/trackers/bulk_operations"
|
|
18
|
+
require_relative "provenance/trackers/auditable"
|
|
19
|
+
require_relative "provenance/trackers/error_reporting"
|
|
20
|
+
|
|
21
|
+
# Provenance is a self-contained audit trail for Rails applications. It records
|
|
22
|
+
# user actions and model changes, sanitizes sensitive data and ships the
|
|
23
|
+
# resulting events to any sink you configure through audit hooks.
|
|
24
|
+
module Provenance
|
|
25
|
+
class Error < StandardError; end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def configure
|
|
29
|
+
yield config
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def config
|
|
33
|
+
@config ||= Configuration.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def setup_username_provider(provider)
|
|
37
|
+
config.username_provider = provider
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def setup_roles_provider(provider)
|
|
41
|
+
config.roles_provider = provider
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def setup_remote_ip_provider(provider)
|
|
45
|
+
config.remote_ip_provider = provider
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def setup_origin_ip_provider(provider)
|
|
49
|
+
config.origin_ip_provider = provider
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def setup_session_id_provider(provider)
|
|
53
|
+
config.session_id_provider = provider
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: provenance
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ivan Nikolaev
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: actionpack
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activerecord
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '6.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '6.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: activesupport
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '6.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '6.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rails
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '6.0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '6.0'
|
|
69
|
+
description: |
|
|
70
|
+
Provenance records user actions and ActiveRecord model changes in Rails
|
|
71
|
+
applications. It groups changes per request and transaction, sanitizes
|
|
72
|
+
sensitive data, tracks bulk operations and has_and_belongs_to_many changes,
|
|
73
|
+
and ships structured audit events to any sink through configurable hooks.
|
|
74
|
+
email:
|
|
75
|
+
- ivan_n6_20@icloud.com
|
|
76
|
+
executables: []
|
|
77
|
+
extensions: []
|
|
78
|
+
extra_rdoc_files: []
|
|
79
|
+
files:
|
|
80
|
+
- CHANGELOG.md
|
|
81
|
+
- LICENSE
|
|
82
|
+
- README.md
|
|
83
|
+
- lib/provenance.rb
|
|
84
|
+
- lib/provenance/configuration.rb
|
|
85
|
+
- lib/provenance/context.rb
|
|
86
|
+
- lib/provenance/journal.rb
|
|
87
|
+
- lib/provenance/trackers/auditable.rb
|
|
88
|
+
- lib/provenance/trackers/bulk_operations.rb
|
|
89
|
+
- lib/provenance/trackers/error_reporting.rb
|
|
90
|
+
- lib/provenance/trackers/providers.rb
|
|
91
|
+
- lib/provenance/trackers/trackable.rb
|
|
92
|
+
- lib/provenance/transaction_key.rb
|
|
93
|
+
- lib/provenance/version.rb
|
|
94
|
+
homepage: https://github.com/inikalaev/provenance
|
|
95
|
+
licenses:
|
|
96
|
+
- MIT
|
|
97
|
+
metadata:
|
|
98
|
+
homepage_uri: https://github.com/inikalaev/provenance
|
|
99
|
+
source_code_uri: https://github.com/inikalaev/provenance
|
|
100
|
+
changelog_uri: https://github.com/inikalaev/provenance/blob/main/CHANGELOG.md
|
|
101
|
+
bug_tracker_uri: https://github.com/inikalaev/provenance/issues
|
|
102
|
+
rubygems_mfa_required: 'true'
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: 3.2.2
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.5.22
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: 'Audit trail for Rails: user actions and model changes'
|
|
122
|
+
test_files: []
|