journaled 5.0.0 → 5.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac63d8988a5cbbd63340c019b82642a039a9dfee81f46d83b15e5a71d83e9cb7
4
- data.tar.gz: ae809e4b05901d6eb73eb0196119999be2b0fd414c3af3c4bb30d280216098cf
3
+ metadata.gz: 5d0222aba969718f085949e0bf6b1fee163c70ed9512190c2b53abf75d0a120c
4
+ data.tar.gz: d3d2ac0e99d142aeb608f66f7d1a1ae15831d56483a8975bb7a991d3447058ac
5
5
  SHA512:
6
- metadata.gz: 72bcbe0ae43717280eb8a0b3383672cc87762e4c2b9b082778f1beac249004ef90a41e32e6fbabd693377a4d0af6cf6fc78df8a2b8ff44067276dd502cece63a
7
- data.tar.gz: b21401ba2cf6155a25f50f1c58c634e3036754ec69b8d7f287c86408f69819aed5cfac1a43d55636a2be461545096b1510318a6ce665bf47595f30c8b90bceb9
6
+ metadata.gz: a6b4789d6314447dad04cc152b76cdf861f21d92cf27af5030f2ce500a8f2c6b791f7adfe86b975e614cba3f3af5b811022e6895feb32a831893851fabc5426d
7
+ data.tar.gz: 735a2305bb0599d1b6db9b355e39c1523079df60cbd975627eb5b273c155228d3740a3215341983ad15a90ff53dd6772f81a154829d13237ea8dde0eed92d34b
@@ -56,7 +56,7 @@ module Journaled::Changes
56
56
  end
57
57
 
58
58
  class_methods do
59
- def journal_changes_to(*attribute_names, as:, enqueue_with: {}) # rubocop:disable Naming/MethodParameterName
59
+ def journal_changes_to(*attribute_names, as:, enqueue_with: {})
60
60
  if attribute_names.empty? || attribute_names.any? { |n| !n.is_a?(Symbol) }
61
61
  raise "one or more symbol attribute_name arguments is required"
62
62
  end
@@ -0,0 +1,87 @@
1
+ # FIXME: This cannot be included in lib/ because Journaled::Event is autoloaded via app/models
2
+ # Autoloading Journaled::Event isn't strictly necessary, and for compatibility it would
3
+ # make sense to move it to lib/.
4
+ module Journaled
5
+ module AuditLog
6
+ Event = Struct.new(:record, :database_operation, :unfiltered_changes) do
7
+ include Journaled::Event
8
+
9
+ journal_attributes :class_name, :table_name, :record_id,
10
+ :database_operation, :changes, :snapshot, :actor, tagged: true
11
+
12
+ def journaled_stream_name
13
+ AuditLog.default_stream_name || super
14
+ end
15
+
16
+ def created_at
17
+ case database_operation
18
+ when 'insert'
19
+ record_created_at
20
+ when 'update'
21
+ record_updated_at
22
+ when 'delete'
23
+ Time.zone.now
24
+ else
25
+ raise "Unhandled database operation type: #{database_operation}"
26
+ end
27
+ end
28
+
29
+ def record_created_at
30
+ record.try(:created_at) || Time.zone.now
31
+ end
32
+
33
+ def record_updated_at
34
+ record.try(:updated_at) || Time.zone.now
35
+ end
36
+
37
+ def class_name
38
+ record.class.name
39
+ end
40
+
41
+ def table_name
42
+ record.class.table_name
43
+ end
44
+
45
+ def record_id
46
+ record.id
47
+ end
48
+
49
+ def changes
50
+ filtered_changes = unfiltered_changes.deep_dup.deep_symbolize_keys
51
+ filtered_changes.each do |key, value|
52
+ filtered_changes[key] = value.map { |val| '[FILTERED]' if val } if filter_key?(key)
53
+ end
54
+ end
55
+
56
+ def snapshot
57
+ filtered_attributes if record._log_snapshot || AuditLog.snapshots_enabled
58
+ end
59
+
60
+ def actor
61
+ Journaled.actor_uri
62
+ end
63
+
64
+ private
65
+
66
+ def filter_key?(key)
67
+ filter_params.include?(key) || encrypted_column?(key)
68
+ end
69
+
70
+ def encrypted_column?(key)
71
+ key.to_s.end_with?('_crypt', '_hmac') ||
72
+ (Rails::VERSION::MAJOR >= 7 && record.encrypted_attribute?(key))
73
+ end
74
+
75
+ def filter_params
76
+ Rails.application.config.filter_parameters
77
+ end
78
+
79
+ def filtered_attributes
80
+ attrs = record.attributes.dup.symbolize_keys
81
+ attrs.each do |key, _value|
82
+ attrs[key] = '[FILTERED]' if filter_key?(key)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,31 @@
1
+ {
2
+ "type": "object",
3
+ "title": "audit_log_event",
4
+ "additionalProperties": false,
5
+ "required": [
6
+ "id",
7
+ "event_type",
8
+ "created_at",
9
+ "class_name",
10
+ "table_name",
11
+ "record_id",
12
+ "database_operation",
13
+ "changes",
14
+ "snapshot",
15
+ "actor",
16
+ "tags"
17
+ ],
18
+ "properties": {
19
+ "id": { "type": "string" },
20
+ "event_type": { "type": "string" },
21
+ "created_at": { "type": "string" },
22
+ "class_name": { "type": "string" },
23
+ "table_name": { "type": "string" },
24
+ "record_id": { "type": ["string", "integer"] },
25
+ "database_operation": { "type": "string" },
26
+ "changes": { "type": "object", "additionalProperties": true },
27
+ "snapshot": { "type": ["object", "null"], "additionalProperties": true },
28
+ "actor": { "type": "string" },
29
+ "tags": { "type": "object", "additionalProperties": true }
30
+ }
31
+ }
@@ -0,0 +1,194 @@
1
+ require 'active_support/core_ext/module/attribute_accessors_per_thread'
2
+
3
+ module Journaled
4
+ module AuditLog
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_EXCLUDED_CLASSES = %w(
8
+ Delayed::Job
9
+ PaperTrail::Version
10
+ ActiveStorage::Attachment
11
+ ActiveStorage::Blob
12
+ ActiveRecord::InternalMetadata
13
+ ActiveRecord::SchemaMigration
14
+ ).freeze
15
+
16
+ mattr_accessor(:default_ignored_columns) { %i(created_at updated_at) }
17
+ mattr_accessor(:default_stream_name) { Journaled.default_stream_name }
18
+ mattr_accessor(:excluded_classes) { DEFAULT_EXCLUDED_CLASSES.dup }
19
+ thread_mattr_accessor(:snapshots_enabled) { false }
20
+ thread_mattr_accessor(:_disabled) { false }
21
+ thread_mattr_accessor(:_force) { false }
22
+
23
+ class << self
24
+ def exclude_classes!
25
+ excluded_classes.each do |name|
26
+ if Rails::VERSION::MAJOR >= 6 && Rails.autoloaders.zeitwerk_enabled?
27
+ zeitwerk_exclude!(name)
28
+ else
29
+ classic_exclude!(name)
30
+ end
31
+ end
32
+ end
33
+
34
+ def with_snapshots
35
+ snapshots_enabled_was = snapshots_enabled
36
+ self.snapshots_enabled = true
37
+ yield
38
+ ensure
39
+ self.snapshots_enabled = snapshots_enabled_was
40
+ end
41
+
42
+ def without_audit_logging
43
+ disabled_was = _disabled
44
+ self._disabled = true
45
+ yield
46
+ ensure
47
+ self._disabled = disabled_was
48
+ end
49
+
50
+ private
51
+
52
+ def zeitwerk_exclude!(name)
53
+ if Object.const_defined?(name)
54
+ name.constantize.skip_audit_log
55
+ else
56
+ Rails.autoloaders.main.on_load(name) { |klass, _path| klass.skip_audit_log }
57
+ end
58
+ end
59
+
60
+ def classic_exclude!(name)
61
+ name.constantize.skip_audit_log
62
+ rescue NameError
63
+ nil
64
+ end
65
+ end
66
+
67
+ Config = Struct.new(:enabled, :ignored_columns) do
68
+ private :enabled
69
+ def enabled?
70
+ !AuditLog._disabled && self[:enabled].present?
71
+ end
72
+ end
73
+
74
+ included do
75
+ prepend BlockedMethods
76
+ singleton_class.prepend BlockedClassMethods
77
+
78
+ class_attribute :audit_log_config, default: Config.new(false, AuditLog.default_ignored_columns)
79
+ attr_accessor :_log_snapshot
80
+
81
+ after_create { _emit_audit_log!('insert') }
82
+ after_update { _emit_audit_log!('update') if _audit_log_changes.any? }
83
+ after_destroy { _emit_audit_log!('delete') }
84
+ end
85
+
86
+ 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)
90
+ end
91
+
92
+ 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
100
+ end
101
+ end
102
+
103
+ module BlockedMethods
104
+ BLOCKED_METHODS = {
105
+ delete: '#destroy',
106
+ update_column: '#update!',
107
+ update_columns: '#update!',
108
+ }.freeze
109
+
110
+ def delete(**kwargs)
111
+ _journaled_audit_log_check!(:delete, **kwargs) do
112
+ super()
113
+ end
114
+ end
115
+
116
+ def update_column(name, value, **kwargs)
117
+ _journaled_audit_log_check!(:update_column, **kwargs.merge(name => value)) do
118
+ super(name, value)
119
+ end
120
+ end
121
+
122
+ def update_columns(args = {}, **kwargs)
123
+ _journaled_audit_log_check!(:update_columns, **args.merge(kwargs)) do
124
+ super(args.merge(kwargs).except(:_force))
125
+ end
126
+ end
127
+
128
+ def _journaled_audit_log_check!(method, **kwargs) # rubocop:disable Metrics/AbcSize
129
+ force_was = AuditLog._force
130
+ AuditLog._force = kwargs.delete(:_force) if kwargs.key?(:_force)
131
+ audited_columns = kwargs.keys - audit_log_config.ignored_columns
132
+
133
+ if method == :delete || audited_columns.any?
134
+ column_message = <<~MSG if kwargs.any?
135
+ You are attempting to change the following audited columns:
136
+ #{audited_columns.inspect}
137
+
138
+ MSG
139
+ raise <<~MSG if audit_log_config.enabled? && !AuditLog._force
140
+ #{column_message}Using `#{method}` is blocked because it skips audit logging (and other Rails callbacks)!
141
+ Consider using `#{BLOCKED_METHODS[method]}` instead, or pass `_force: true` as an argument.
142
+ MSG
143
+ end
144
+
145
+ yield
146
+ ensure
147
+ AuditLog._force = force_was
148
+ end
149
+ end
150
+
151
+ module BlockedClassMethods
152
+ BLOCKED_METHODS = {
153
+ delete_all: '.destroy_all',
154
+ insert: '.create!',
155
+ insert_all: '.each { create!(...) }',
156
+ update_all: '.find_each { update!(...) }',
157
+ upsert: '.create_or_find_by!',
158
+ upsert_all: '.each { create_or_find_by!(...) }',
159
+ }.freeze
160
+
161
+ BLOCKED_METHODS.each do |method, alternative|
162
+ define_method(method) do |*args, **kwargs, &block|
163
+ force_was = AuditLog._force
164
+ AuditLog._force = kwargs.delete(:_force) if kwargs.key?(:_force)
165
+
166
+ raise <<~MSG if audit_log_config.enabled? && !AuditLog._force
167
+ `#{method}` is blocked because it skips callbacks and audit logs!
168
+ Consider using `#{alternative}` instead, or pass `_force: true` as an argument.
169
+ MSG
170
+
171
+ super(*args, **kwargs, &block)
172
+ ensure
173
+ AuditLog._force = force_was
174
+ end
175
+ end
176
+ end
177
+
178
+ def _emit_audit_log!(database_operation)
179
+ if audit_log_config.enabled?
180
+ event = Journaled::AuditLog::Event.new(self, database_operation, _audit_log_changes)
181
+ ActiveSupport::Notifications.instrument('journaled.audit_log.journal', event: event) do
182
+ event.journal!
183
+ end
184
+ end
185
+ end
186
+
187
+ def _audit_log_changes
188
+ previous_changes.except(*audit_log_config.ignored_columns)
189
+ end
190
+ end
191
+ end
192
+
193
+ ActiveSupport.on_load(:active_record) { include Journaled::AuditLog }
194
+ Journaled::Engine.config.after_initialize { Journaled::AuditLog.exclude_classes! }
@@ -1,3 +1,3 @@
1
1
  module Journaled
2
- VERSION = "5.0.0".freeze
2
+ VERSION = "5.1.0".freeze
3
3
  end
data/lib/journaled.rb CHANGED
@@ -69,3 +69,5 @@ module Journaled
69
69
  Current.tags = Current.tags.merge(tags)
70
70
  end
71
71
  end
72
+
73
+ require 'journaled/audit_log'
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.0.0
4
+ version: 5.1.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-08-23 00:00:00.000000000 Z
14
+ date: 2022-09-09 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activejob
@@ -41,6 +41,20 @@ dependencies:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
43
  version: '0'
44
+ - !ruby/object:Gem::Dependency
45
+ name: activesupport
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
44
58
  - !ruby/object:Gem::Dependency
45
59
  name: aws-sdk-kinesis
46
60
  requirement: !ruby/object:Gem::Requirement
@@ -242,6 +256,7 @@ files:
242
256
  - app/jobs/journaled/delivery_job.rb
243
257
  - app/models/concerns/journaled/changes.rb
244
258
  - app/models/journaled/actor_uri_provider.rb
259
+ - app/models/journaled/audit_log/event.rb
245
260
  - app/models/journaled/change.rb
246
261
  - app/models/journaled/change_definition.rb
247
262
  - app/models/journaled/change_writer.rb
@@ -252,9 +267,11 @@ files:
252
267
  - config/initializers/change_protection.rb
253
268
  - config/spring.rb
254
269
  - journaled_schemas/base_event.json
270
+ - journaled_schemas/journaled/audit_log/event.json
255
271
  - journaled_schemas/journaled/change.json
256
272
  - journaled_schemas/tagged_event.json
257
273
  - lib/journaled.rb
274
+ - lib/journaled/audit_log.rb
258
275
  - lib/journaled/connection.rb
259
276
  - lib/journaled/current.rb
260
277
  - lib/journaled/engine.rb