journaled 5.1.0 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d0222aba969718f085949e0bf6b1fee163c70ed9512190c2b53abf75d0a120c
4
- data.tar.gz: d3d2ac0e99d142aeb608f66f7d1a1ae15831d56483a8975bb7a991d3447058ac
3
+ metadata.gz: 7fc4b493edc90534706cd22dbb1fae76cdacc693d95e44734f9ce92db2d0716c
4
+ data.tar.gz: 46edc5873055e2a4e7ceae9f8dc63c558b331a821826803cd36aa971b42258f5
5
5
  SHA512:
6
- metadata.gz: a6b4789d6314447dad04cc152b76cdf861f21d92cf27af5030f2ce500a8f2c6b791f7adfe86b975e614cba3f3af5b811022e6895feb32a831893851fabc5426d
7
- data.tar.gz: 735a2305bb0599d1b6db9b355e39c1523079df60cbd975627eb5b273c155228d3740a3215341983ad15a90ff53dd6772f81a154829d13237ea8dde0eed92d34b
6
+ metadata.gz: 6c9871cbc8b6439f9d29e29bbc24068d2a25667eb42003bed42ca016505b463586b8b3bc0b4c33d2f130a76d5dbc70bf7815d41b992def7a8dc4f71e697113e8
7
+ data.tar.gz: 6770fd7d6a3c92bce0122def374fa1a7e31f74c8a67d8ebeb9c2ccf5e1e02c616aae42274b5fd145e6cd59d6aa7e5b483dca134d8479822652e65c0bd7c1df84
data/README.md CHANGED
@@ -122,25 +122,19 @@ Both model-level directives accept additional options to be passed into ActiveJo
122
122
  # For change journaling:
123
123
  journal_changes_to :email, as: :identity_change, enqueue_with: { priority: 10 }
124
124
 
125
+ # For audit logging:
126
+ has_audit_log enqueue_with: { priority: 30 }
127
+
125
128
  # Or for custom journaling:
126
129
  journal_attributes :email, enqueue_with: { priority: 20, queue: 'journaled' }
127
130
  ```
128
131
 
129
- ### Change Journaling
130
-
131
- Out of the box, `Journaled` provides an event type and ActiveRecord
132
- mix-in for durably journaling changes to your model, implemented via
133
- ActiveRecord hooks. Use it like so:
134
-
135
- ```ruby
136
- class User < ApplicationRecord
137
- include Journaled::Changes
138
-
139
- journal_changes_to :email, :first_name, :last_name, as: :identity_change
140
- end
141
- ```
132
+ ### Attribution
142
133
 
143
- Add the following to your controller base class for attribution:
134
+ Before using `Journaled::Changes` or `Journaled::AuditLog`, you will want to
135
+ set up automatic "actor" attribution (i.e. tracking the current user session).
136
+ To enable this feature, add the following to your controller base class for
137
+ attribution:
144
138
 
145
139
  ```ruby
146
140
  class ApplicationController < ActionController::Base
@@ -153,6 +147,20 @@ end
153
147
  Your authenticated entity must respond to `#to_global_id`, which ActiveRecords do by default.
154
148
  This feature relies on `ActiveSupport::CurrentAttributes` under the hood.
155
149
 
150
+ ### Change Journaling with `Journaled::Changes`
151
+
152
+ Out of the box, `Journaled` provides an event type and ActiveRecord
153
+ mix-in for durably journaling changes to your model, implemented via
154
+ ActiveRecord hooks. Use it like so:
155
+
156
+ ```ruby
157
+ class User < ApplicationRecord
158
+ include Journaled::Changes
159
+
160
+ journal_changes_to :email, :first_name, :last_name, as: :identity_change
161
+ end
162
+ ```
163
+
156
164
  Every time any of the specified attributes is modified, or a `User`
157
165
  record is created or destroyed, an event will be sent to Kinesis with the following attributes:
158
166
 
@@ -179,6 +187,222 @@ journaling. Note that the less-frequently-used methods `toggle`,
179
187
  `increment*`, `decrement*`, and `update_counters` are not intercepted at
180
188
  this time.
181
189
 
190
+
191
+ ### Audit Logging with `Journaled::AuditLog`
192
+
193
+ Journaled includes a feature for producing audit logs of changes to your model.
194
+ Unlike `Journaled::Changes`, which will emit individual sets of changes as
195
+ "logical" events, `Journaled::AuditLog` will log all changes in their entirety,
196
+ unless otherwise told to ignore changes to specific columns.
197
+
198
+ This behavior is similar to
199
+ [papertrail](https://github.com/paper-trail-gem/paper_trail),
200
+ [audited](https://github.com/collectiveidea/audited), and
201
+ [logidze](https://github.com/palkan/logidze), except instead of storing
202
+ changes/versions locally (in your application's database), it emits them to
203
+ Kinesis (as Journaled events).
204
+
205
+ #### Audit Log Configuration
206
+
207
+ To enable audit logging for a given record, use the `has_audit_log` directive:
208
+
209
+ ```ruby
210
+ class MyModel < ApplicationRecord
211
+ has_audit_log
212
+
213
+ # This class will now be audited,
214
+ # but will ignore changes to `created_at` and `updated_at`.
215
+ end
216
+ ```
217
+
218
+ To ignore changes to additional columns, use the `ignore` option:
219
+
220
+ ```ruby
221
+ class MyModel < ApplicationRecord
222
+ has_audit_log ignore: :last_synced_at
223
+
224
+ # This class will be audited,
225
+ # and will ignore changes to `created_at`, `updated_at`, and `last_synced_at`.
226
+ end
227
+ ```
228
+
229
+ By default, changes to `updated_at` and `created_at` will be ignored (since
230
+ these generally change on every update), but this behavior can be reconfigured:
231
+
232
+ ```ruby
233
+ # change the defaults:
234
+ Journaled::AuditLog.default_ignored_columns = %i(createdAt updatedAt)
235
+
236
+ # or append new defaults:
237
+ Journaled::AuditLog.default_ignored_columns += %i(modified_at)
238
+
239
+ # or disable defaults entirely:
240
+ Journaled::AuditLog.default_ignored_columns = []
241
+ ```
242
+
243
+ Subclasses will inherit audit log configs:
244
+
245
+ ```ruby
246
+ class MyModel < ApplicationRecord
247
+ has_audit_log ignore: :last_synced_at
248
+ end
249
+
250
+ class MySubclass < MyModel
251
+ # this class will be audited,
252
+ # and will ignore `created_at`, `updated_at`, and `last_synced_at`.
253
+ end
254
+ ```
255
+
256
+ To disable audit logs on subclasses, use `skip_audit_log`:
257
+
258
+ ```ruby
259
+ class MySubclass < MyModel
260
+ skip_audit_log
261
+ end
262
+ ```
263
+
264
+ Subclasses may specify additional columns to ignore (which will be merged into
265
+ the inherited list):
266
+
267
+ ```ruby
268
+ class MySubclass < MyModel
269
+ has_audit_log ignore: :another_field
270
+
271
+ # this class will ignore `another_field`, IN ADDITION TO `created_at`, `updated_at`,
272
+ # and any other fields specified by the parent class.
273
+ end
274
+ ```
275
+
276
+ To temporarily disable audit logging globally, use the `without_audit_logging` directive:
277
+
278
+ ```ruby
279
+ Journaled::AuditLog.without_audit_logging do
280
+ # Any operation in here will skip audit logging
281
+ end
282
+ ```
283
+
284
+ #### Audit Log Events
285
+
286
+ Whenever an audited record is created, updated, or destroyed, a
287
+ `journaled_audit_log` event is emitted. For example, calling
288
+ `user.update!(name: 'Bart')` would result in an event that looks something like
289
+ this:
290
+
291
+ ```json
292
+ {
293
+ "id": "bc7cb6a6-88cf-4849-a4f0-a31b0b199c47",
294
+ "event_type": "journaled_audit_log",
295
+ "created_at": "2022-01-28T11:06:54.928-05:00",
296
+ "class_name": "User",
297
+ "table_name": "users",
298
+ "record_id": "123",
299
+ "database_operation": "update",
300
+ "changes": { "name": ["Homer", "Bart"] },
301
+ "snapshot": null,
302
+ "actor": "gid://app_name/AdminUser/456",
303
+ "tags": {}
304
+ }
305
+ ```
306
+
307
+ The field breakdown is as follows:
308
+
309
+ - `id`: a randomly-generated ID for the event itself
310
+ - `event_type`: the type of event (always `journaled_audit_log`)
311
+ - `created_at`: the time that the action occurred (should match `updated_at` on
312
+ the ActiveRecord)
313
+ - `class_name`: the name of the ActiveRecord class
314
+ - `table_name`: the underlying table that the class interfaces with
315
+ - `record_id`: the primary key of the ActiveRecord
316
+ - `database_operation`: the type of operation (`insert`, `update`, or `delete`)
317
+ - `changes`: the changes to the record, in the form of `"field_name":
318
+ ["from_value", "to_value"]`
319
+ - `snapshot`: an (optional) snapshot of all of the record's columns and their
320
+ values (see below).
321
+ - `actor`: the current `Journaled.actor`
322
+ - `tags`: the current `Journaled.tags`
323
+
324
+ #### Snapshots
325
+
326
+ When records are created, updated, and deleted, the `changes` field is populated
327
+ with only the columns that changed. While this keeps event payload size down, it
328
+ may make it harder to reconstruct the state of the record at a given point in
329
+ time.
330
+
331
+ This is where the `snapshot` field comes in! To produce a full snapshot of a
332
+ record as part of an update, set use the virtual `_log_snapshot` attribute, like
333
+ so:
334
+
335
+ ```ruby
336
+ my_user.update!(name: 'Bart', _log_snapshot: true)
337
+ ```
338
+
339
+ Or to produce snapshots for all records that change for a given operation,
340
+ wrap it a `with_snapshots` block, like so:
341
+
342
+ ```ruby
343
+ Journaled::AuditLog.with_snapshots do
344
+ ComplicatedOperation.run!
345
+ end
346
+ ```
347
+
348
+ Snapshots can also be enabled globally for all _deletion_ operations. Since
349
+ `changes` will be empty on deletion, you should consider using this if you care
350
+ about the contents of any records being deleted (and/or don't have a full audit
351
+ trail from their time of creation):
352
+
353
+ ```ruby
354
+ Journaled::AuditLog.snapshot_on_deletion = true
355
+ ```
356
+
357
+ Events with snapshots will continue to populate the `changes` field, but will
358
+ additionally contain a snapshot with the full state of the user:
359
+
360
+ ```json
361
+ {
362
+ "...": "...",
363
+ "changes": { "name": ["Homer", "Bart"] },
364
+ "snapshot": { "name": "Bart", "email": "simpson@example.com", "favorite_food": "pizza" },
365
+ "...": "..."
366
+ }
367
+ ```
368
+
369
+ #### Handling Sensitive Data
370
+
371
+ Both `changes` and `snapshot` will filter out sensitive fields, as defined by
372
+ your `Rails.application.config.filter_parameters` list:
373
+
374
+ ```json
375
+ {
376
+ "...": "...",
377
+ "changes": { "ssn": ["[FILTERED]", "[FILTERED]"] },
378
+ "snapshot": { "ssn": "[FILTERED]" },
379
+ "...": "..."
380
+ }
381
+ ```
382
+
383
+ They will also filter out any fields whose name ends in `_crypt` or `_hmac`, as
384
+ well as fields that rely on Active Record Encryption / `encrypts` ([introduced
385
+ in Rails 7](https://edgeguides.rubyonrails.org/active_record_encryption.html)).
386
+
387
+ This is done to avoid emitting values to locations where it is difficult or
388
+ impossible to rotate encryption keys (or otherwise scrub values after the
389
+ fact), and currently there is no built-in configuration to bypass this
390
+ behavior. If you need to track changes to sensitive/encrypted fields, it is
391
+ recommended that you store the values in a local history table (still
392
+ encrypted, of course!).
393
+
394
+ #### Caveats
395
+
396
+ Because Journaled events are not guaranteed to arrive in order, events emitted
397
+ by `Journaled::AuditLog` must be sorted by their `created_at` value, which
398
+ should correspond roughly to the time that the SQL statement was issued.
399
+ **There is currently no other means of globally ordering audit log events**,
400
+ making them susceptible to clock drift and race conditions.
401
+
402
+ These issues may be mitigated on a per-model basis via
403
+ `ActiveRecord::Locking::Optimistic` (and its auto-incrementing `lock_version`
404
+ column), and/or by careful use of other locking mechanisms.
405
+
182
406
  ### Custom Journaling
183
407
 
184
408
  For every custom implementation of journaling in your application, define the JSON schema for the attributes in your event.
@@ -338,7 +562,7 @@ Returns one of the following in order of preference:
338
562
  * a string of the form `gid://[app_name]` as a fallback
339
563
 
340
564
  In order for this to be most useful, you must configure your controller
341
- as described in [Change Journaling](#change-journaling) above.
565
+ as described in [Attribution](#attribution) above.
342
566
 
343
567
  ### Testing
344
568
 
@@ -3,7 +3,7 @@
3
3
  # make sense to move it to lib/.
4
4
  module Journaled
5
5
  module AuditLog
6
- Event = Struct.new(:record, :database_operation, :unfiltered_changes) do
6
+ Event = Struct.new(:record, :database_operation, :unfiltered_changes, :enqueue_opts) do
7
7
  include Journaled::Event
8
8
 
9
9
  journal_attributes :class_name, :table_name, :record_id,
@@ -13,6 +13,10 @@ module Journaled
13
13
  AuditLog.default_stream_name || super
14
14
  end
15
15
 
16
+ def journaled_enqueue_opts
17
+ record.class.audit_log_config.enqueue_opts
18
+ end
19
+
16
20
  def created_at
17
21
  case database_operation
18
22
  when 'insert'
@@ -54,7 +58,8 @@ module Journaled
54
58
  end
55
59
 
56
60
  def snapshot
57
- filtered_attributes if record._log_snapshot || AuditLog.snapshots_enabled
61
+ filtered_attributes if record._log_snapshot || AuditLog.snapshots_enabled ||
62
+ (database_operation == 'delete' && AuditLog.snapshot_on_deletion)
58
63
  end
59
64
 
60
65
  def actor
@@ -15,8 +15,10 @@ module Journaled
15
15
 
16
16
  mattr_accessor(:default_ignored_columns) { %i(created_at updated_at) }
17
17
  mattr_accessor(:default_stream_name) { Journaled.default_stream_name }
18
+ mattr_accessor(:default_enqueue_opts) { {} }
18
19
  mattr_accessor(:excluded_classes) { DEFAULT_EXCLUDED_CLASSES.dup }
19
20
  thread_mattr_accessor(:snapshots_enabled) { false }
21
+ thread_mattr_accessor(:snapshot_on_deletion) { false }
20
22
  thread_mattr_accessor(:_disabled) { false }
21
23
  thread_mattr_accessor(:_force) { false }
22
24
 
@@ -64,18 +66,37 @@ module Journaled
64
66
  end
65
67
  end
66
68
 
67
- Config = Struct.new(:enabled, :ignored_columns) do
68
- private :enabled
69
+ Config = Struct.new(:enabled, :ignored_columns, :enqueue_opts) do
70
+ def self.default
71
+ new(false, AuditLog.default_ignored_columns.dup, AuditLog.default_enqueue_opts.dup)
72
+ end
73
+
74
+ def initialize(*)
75
+ super
76
+ self.ignored_columns ||= []
77
+ self.enqueue_opts ||= {}
78
+ end
79
+
69
80
  def enabled?
70
81
  !AuditLog._disabled && self[:enabled].present?
71
82
  end
83
+
84
+ def dup
85
+ super.tap do |config|
86
+ config.ignored_columns = ignored_columns.dup
87
+ config.enqueue_opts = enqueue_opts.dup
88
+ end
89
+ end
90
+
91
+ private :enabled
72
92
  end
73
93
 
74
94
  included do
75
95
  prepend BlockedMethods
76
96
  singleton_class.prepend BlockedClassMethods
77
97
 
78
- class_attribute :audit_log_config, default: Config.new(false, AuditLog.default_ignored_columns)
98
+ class_attribute :audit_log_config, default: Config.default
99
+
79
100
  attr_accessor :_log_snapshot
80
101
 
81
102
  after_create { _emit_audit_log!('insert') }
@@ -84,19 +105,16 @@ module Journaled
84
105
  end
85
106
 
86
107
  class_methods do
87
- def has_audit_log(ignore: [])
88
- ignored_columns = _audit_log_inherited_ignored_columns + [ignore].flatten(1)
89
- self.audit_log_config = Config.new(true, ignored_columns.uniq)
108
+ def has_audit_log(ignore: [], enqueue_with: {})
109
+ self.audit_log_config = audit_log_config.dup
110
+ audit_log_config.enabled = true
111
+ audit_log_config.ignored_columns |= [ignore].flatten(1)
112
+ audit_log_config.enqueue_opts.merge!(enqueue_with)
90
113
  end
91
114
 
92
115
  def skip_audit_log
93
- self.audit_log_config = Config.new(false, _audit_log_inherited_ignored_columns.uniq)
94
- end
95
-
96
- private
97
-
98
- def _audit_log_inherited_ignored_columns
99
- (superclass.try(:audit_log_config)&.ignored_columns || []) + audit_log_config.ignored_columns
116
+ self.audit_log_config = audit_log_config.dup
117
+ audit_log_config.enabled = false
100
118
  end
101
119
  end
102
120
 
@@ -177,7 +195,7 @@ module Journaled
177
195
 
178
196
  def _emit_audit_log!(database_operation)
179
197
  if audit_log_config.enabled?
180
- event = Journaled::AuditLog::Event.new(self, database_operation, _audit_log_changes)
198
+ event = Journaled::AuditLog::Event.new(self, database_operation, _audit_log_changes, audit_log_config.enqueue_opts)
181
199
  ActiveSupport::Notifications.instrument('journaled.audit_log.journal', event: event) do
182
200
  event.journal!
183
201
  end
@@ -1,3 +1,3 @@
1
1
  module Journaled
2
- VERSION = "5.1.0".freeze
2
+ VERSION = "5.2.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: journaled
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.0
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Lipson
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2022-09-09 00:00:00.000000000 Z
14
+ date: 2022-10-13 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activejob