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 +4 -4
- data/app/models/concerns/journaled/changes.rb +1 -1
- data/app/models/journaled/audit_log/event.rb +87 -0
- data/journaled_schemas/journaled/audit_log/event.json +31 -0
- data/lib/journaled/audit_log.rb +194 -0
- data/lib/journaled/version.rb +1 -1
- data/lib/journaled.rb +2 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d0222aba969718f085949e0bf6b1fee163c70ed9512190c2b53abf75d0a120c
|
4
|
+
data.tar.gz: d3d2ac0e99d142aeb608f66f7d1a1ae15831d56483a8975bb7a991d3447058ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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: {})
|
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! }
|
data/lib/journaled/version.rb
CHANGED
data/lib/journaled.rb
CHANGED
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.
|
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-
|
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
|