journaled 5.0.0 → 5.1.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: 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