journaled 5.1.0 → 5.1.1

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: 4d27e8931e64963620ecd1fd44c4a46c244470a6715dc19c56a7a1a29aaa97b6
4
+ data.tar.gz: f95df32775abd3f65cb8d526c60fae948fc69425957a378e4007765fdade14c0
5
5
  SHA512:
6
- metadata.gz: a6b4789d6314447dad04cc152b76cdf861f21d92cf27af5030f2ce500a8f2c6b791f7adfe86b975e614cba3f3af5b811022e6895feb32a831893851fabc5426d
7
- data.tar.gz: 735a2305bb0599d1b6db9b355e39c1523079df60cbd975627eb5b273c155228d3740a3215341983ad15a90ff53dd6772f81a154829d13237ea8dde0eed92d34b
6
+ metadata.gz: 1cd4a144b2cc6dbb398b082dfe97b93829ab5d0e2ad7c22fb807ff63be9d9f5e1b24278db14eb947ce247c92f8fd09e135010ce9bc546e6e34589f98fdccaeb9
7
+ data.tar.gz: 10da7fde07cfe2e7d654978a99c0734976f7199efd1d1d5e6aa11684d30d537d625acf238e0f636f8dec75b298920a8bf3f64a8c41105ddab40f624b03f1e8d6
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,213 @@ 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
+ Events with snapshots will continue to populate the `changes` field, but will
349
+ additionally contain a snapshot with the full state of the user:
350
+
351
+ ```json
352
+ {
353
+ "...": "...",
354
+ "changes": { "name": ["Homer", "Bart"] },
355
+ "snapshot": { "name": "Bart", "email": "simpson@example.com", "favorite_food": "pizza" },
356
+ "...": "..."
357
+ }
358
+ ```
359
+
360
+ #### Handling Sensitive Data
361
+
362
+ Both `changes` and `snapshot` will filter out sensitive fields, as defined by
363
+ your `Rails.application.config.filter_parameters` list:
364
+
365
+ ```json
366
+ {
367
+ "...": "...",
368
+ "changes": { "ssn": ["[FILTERED]", "[FILTERED]"] },
369
+ "snapshot": { "ssn": "[FILTERED]" },
370
+ "...": "..."
371
+ }
372
+ ```
373
+
374
+ They will also filter out any fields whose name ends in `_crypt` or `_hmac`, as
375
+ well as fields that rely on Active Record Encryption / `encrypts` ([introduced
376
+ in Rails 7](https://edgeguides.rubyonrails.org/active_record_encryption.html)).
377
+
378
+ This is done to avoid emitting values to locations where it is difficult or
379
+ impossible to rotate encryption keys (or otherwise scrub values after the
380
+ fact), and currently there is no built-in configuration to bypass this
381
+ behavior. If you need to track changes to sensitive/encrypted fields, it is
382
+ recommended that you store the values in a local history table (still
383
+ encrypted, of course!).
384
+
385
+ #### Caveats
386
+
387
+ Because Journaled events are not guaranteed to arrive in order, events emitted
388
+ by `Journaled::AuditLog` must be sorted by their `created_at` value, which
389
+ should correspond roughly to the time that the SQL statement was issued.
390
+ **There is currently no other means of globally ordering audit log events**,
391
+ making them susceptible to clock drift and race conditions.
392
+
393
+ These issues may be mitigated on a per-model basis via
394
+ `ActiveRecord::Locking::Optimistic` (and its auto-incrementing `lock_version`
395
+ column), and/or by careful use of other locking mechanisms.
396
+
182
397
  ### Custom Journaling
183
398
 
184
399
  For every custom implementation of journaling in your application, define the JSON schema for the attributes in your event.
@@ -338,7 +553,7 @@ Returns one of the following in order of preference:
338
553
  * a string of the form `gid://[app_name]` as a fallback
339
554
 
340
555
  In order for this to be most useful, you must configure your controller
341
- as described in [Change Journaling](#change-journaling) above.
556
+ as described in [Attribution](#attribution) above.
342
557
 
343
558
  ### Testing
344
559
 
@@ -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'
@@ -15,6 +15,7 @@ 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 }
20
21
  thread_mattr_accessor(:_disabled) { false }
@@ -64,18 +65,37 @@ module Journaled
64
65
  end
65
66
  end
66
67
 
67
- Config = Struct.new(:enabled, :ignored_columns) do
68
- private :enabled
68
+ Config = Struct.new(:enabled, :ignored_columns, :enqueue_opts) do
69
+ def self.default
70
+ new(false, AuditLog.default_ignored_columns.dup, AuditLog.default_enqueue_opts.dup)
71
+ end
72
+
73
+ def initialize(*)
74
+ super
75
+ self.ignored_columns ||= []
76
+ self.enqueue_opts ||= {}
77
+ end
78
+
69
79
  def enabled?
70
80
  !AuditLog._disabled && self[:enabled].present?
71
81
  end
82
+
83
+ def dup
84
+ super.tap do |config|
85
+ config.ignored_columns = ignored_columns.dup
86
+ config.enqueue_opts = enqueue_opts.dup
87
+ end
88
+ end
89
+
90
+ private :enabled
72
91
  end
73
92
 
74
93
  included do
75
94
  prepend BlockedMethods
76
95
  singleton_class.prepend BlockedClassMethods
77
96
 
78
- class_attribute :audit_log_config, default: Config.new(false, AuditLog.default_ignored_columns)
97
+ class_attribute :audit_log_config, default: Config.default
98
+
79
99
  attr_accessor :_log_snapshot
80
100
 
81
101
  after_create { _emit_audit_log!('insert') }
@@ -84,19 +104,16 @@ module Journaled
84
104
  end
85
105
 
86
106
  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)
107
+ def has_audit_log(ignore: [], enqueue_with: {})
108
+ self.audit_log_config = audit_log_config.dup
109
+ audit_log_config.enabled = true
110
+ audit_log_config.ignored_columns |= [ignore].flatten(1)
111
+ audit_log_config.enqueue_opts.merge!(enqueue_with)
90
112
  end
91
113
 
92
114
  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
115
+ self.audit_log_config = audit_log_config.dup
116
+ audit_log_config.enabled = false
100
117
  end
101
118
  end
102
119
 
@@ -177,7 +194,7 @@ module Journaled
177
194
 
178
195
  def _emit_audit_log!(database_operation)
179
196
  if audit_log_config.enabled?
180
- event = Journaled::AuditLog::Event.new(self, database_operation, _audit_log_changes)
197
+ event = Journaled::AuditLog::Event.new(self, database_operation, _audit_log_changes, audit_log_config.enqueue_opts)
181
198
  ActiveSupport::Notifications.instrument('journaled.audit_log.journal', event: event) do
182
199
  event.journal!
183
200
  end
@@ -1,3 +1,3 @@
1
1
  module Journaled
2
- VERSION = "5.1.0".freeze
2
+ VERSION = "5.1.1".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.1.1
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-09-16 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activejob