audited 4.7.0 → 4.10.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of audited might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.gitignore +0 -1
- data/.rubocop.yml +25 -0
- data/.travis.yml +32 -27
- data/Appraisals +29 -12
- data/CHANGELOG.md +77 -1
- data/README.md +73 -17
- data/gemfiles/rails42.gemfile +3 -0
- data/gemfiles/rails50.gemfile +3 -0
- data/gemfiles/rails51.gemfile +3 -0
- data/gemfiles/rails52.gemfile +4 -2
- data/gemfiles/rails60.gemfile +10 -0
- data/gemfiles/rails61.gemfile +10 -0
- data/lib/audited/audit.rb +31 -25
- data/lib/audited/auditor.rb +102 -29
- data/lib/audited/version.rb +1 -1
- data/lib/audited.rb +2 -1
- data/lib/generators/audited/templates/add_version_to_auditable_index.rb +21 -0
- data/lib/generators/audited/templates/install.rb +1 -1
- data/lib/generators/audited/upgrade_generator.rb +4 -0
- data/spec/audited/audit_spec.rb +88 -21
- data/spec/audited/auditor_spec.rb +240 -54
- data/spec/audited/sweeper_spec.rb +15 -6
- data/spec/audited_spec_helpers.rb +3 -1
- data/spec/rails_app/app/assets/config/manifest.js +1 -0
- data/spec/rails_app/app/controllers/application_controller.rb +2 -0
- data/spec/rails_app/config/application.rb +5 -0
- data/spec/rails_app/config/database.yml +1 -0
- data/spec/spec_helper.rb +3 -1
- data/spec/support/active_record/models.rb +22 -0
- data/spec/support/active_record/schema.rb +4 -2
- data/test/db/version_6.rb +2 -0
- data/test/test_helper.rb +1 -2
- data/test/upgrade_generator_test.rb +10 -0
- metadata +59 -22
- data/gemfiles/rails40.gemfile +0 -9
- data/gemfiles/rails41.gemfile +0 -8
data/lib/audited/auditor.rb
CHANGED
@@ -34,6 +34,16 @@ module Audited
|
|
34
34
|
# * +require_comment+ - Ensures that audit_comment is supplied before
|
35
35
|
# any create, update or destroy operation.
|
36
36
|
# * +max_audits+ - Limits the number of stored audits.
|
37
|
+
|
38
|
+
# * +redacted+ - Changes to these fields will be logged, but the values
|
39
|
+
# will not. This is useful, for example, if you wish to audit when a
|
40
|
+
# password is changed, without saving the actual password in the log.
|
41
|
+
# To store values as something other than '[REDACTED]', pass an argument
|
42
|
+
# to the redaction_value option.
|
43
|
+
#
|
44
|
+
# class User < ActiveRecord::Base
|
45
|
+
# audited redacted: :password, redaction_value: SecureRandom.uuid
|
46
|
+
# end
|
37
47
|
#
|
38
48
|
# * +if+ - Only audit the model when the given function returns true
|
39
49
|
# * +unless+ - Only audit the model when the given function returns false
|
@@ -55,7 +65,7 @@ module Audited
|
|
55
65
|
|
56
66
|
class_attribute :audit_associated_with, instance_writer: false
|
57
67
|
class_attribute :audited_options, instance_writer: false
|
58
|
-
attr_accessor :
|
68
|
+
attr_accessor :audit_version, :audit_comment
|
59
69
|
|
60
70
|
self.audited_options = options
|
61
71
|
normalize_audited_options
|
@@ -90,6 +100,8 @@ module Audited
|
|
90
100
|
end
|
91
101
|
|
92
102
|
module AuditedInstanceMethods
|
103
|
+
REDACTED = '[REDACTED]'
|
104
|
+
|
93
105
|
# Temporarily turns off auditing while saving.
|
94
106
|
def save_without_auditing
|
95
107
|
without_auditing { save }
|
@@ -105,6 +117,21 @@ module Audited
|
|
105
117
|
self.class.without_auditing(&block)
|
106
118
|
end
|
107
119
|
|
120
|
+
# Temporarily turns on auditing while saving.
|
121
|
+
def save_with_auditing
|
122
|
+
with_auditing { save }
|
123
|
+
end
|
124
|
+
|
125
|
+
# Executes the block with the auditing callbacks enabled.
|
126
|
+
#
|
127
|
+
# @foo.with_auditing do
|
128
|
+
# @foo.save
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
def with_auditing(&block)
|
132
|
+
self.class.with_auditing(&block)
|
133
|
+
end
|
134
|
+
|
108
135
|
# Gets an array of the revisions available
|
109
136
|
#
|
110
137
|
# user.revisions.each do |revision|
|
@@ -142,7 +169,16 @@ module Audited
|
|
142
169
|
|
143
170
|
# List of attributes that are audited.
|
144
171
|
def audited_attributes
|
145
|
-
attributes.except(*non_audited_columns)
|
172
|
+
audited_attributes = attributes.except(*self.class.non_audited_columns)
|
173
|
+
normalize_enum_changes(audited_attributes)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns a list combined of record audits and associated audits.
|
177
|
+
def own_and_associated_audits
|
178
|
+
Audited.audit_class.unscoped
|
179
|
+
.where('(auditable_type = :type AND auditable_id = :id) OR (associated_type = :type AND associated_id = :id)',
|
180
|
+
type: self.class.name, id: id)
|
181
|
+
.order(created_at: :desc)
|
146
182
|
end
|
147
183
|
|
148
184
|
# Combine multiple audits into one.
|
@@ -159,18 +195,9 @@ module Audited
|
|
159
195
|
|
160
196
|
protected
|
161
197
|
|
162
|
-
def non_audited_columns
|
163
|
-
self.class.non_audited_columns
|
164
|
-
end
|
165
|
-
|
166
|
-
def audited_columns
|
167
|
-
self.class.audited_columns
|
168
|
-
end
|
169
|
-
|
170
198
|
def revision_with(attributes)
|
171
199
|
dup.tap do |revision|
|
172
200
|
revision.id = id
|
173
|
-
revision.send :instance_variable_set, '@attributes', self.attributes if rails_below?('4.2.0')
|
174
201
|
revision.send :instance_variable_set, '@new_record', destroyed?
|
175
202
|
revision.send :instance_variable_set, '@persisted', !destroyed?
|
176
203
|
revision.send :instance_variable_set, '@readonly', false
|
@@ -193,25 +220,62 @@ module Audited
|
|
193
220
|
end
|
194
221
|
end
|
195
222
|
|
196
|
-
def rails_below?(rails_version)
|
197
|
-
Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
|
198
|
-
end
|
199
|
-
|
200
223
|
private
|
201
224
|
|
202
225
|
def audited_changes
|
203
226
|
all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
227
|
+
filtered_changes = \
|
228
|
+
if audited_options[:only].present?
|
229
|
+
all_changes.slice(*self.class.audited_columns)
|
230
|
+
else
|
231
|
+
all_changes.except(*self.class.non_audited_columns)
|
232
|
+
end
|
233
|
+
|
234
|
+
filtered_changes = redact_values(filtered_changes)
|
235
|
+
filtered_changes = normalize_enum_changes(filtered_changes)
|
236
|
+
filtered_changes.to_hash
|
237
|
+
end
|
238
|
+
|
239
|
+
def normalize_enum_changes(changes)
|
240
|
+
self.class.defined_enums.each do |name, values|
|
241
|
+
if changes.has_key?(name)
|
242
|
+
changes[name] = \
|
243
|
+
if changes[name].is_a?(Array)
|
244
|
+
changes[name].map { |v| values[v] }
|
245
|
+
elsif rails_below?('5.0')
|
246
|
+
changes[name]
|
247
|
+
else
|
248
|
+
values[changes[name]]
|
249
|
+
end
|
250
|
+
end
|
208
251
|
end
|
252
|
+
changes
|
253
|
+
end
|
254
|
+
|
255
|
+
def redact_values(filtered_changes)
|
256
|
+
[audited_options[:redacted]].flatten.compact.each do |option|
|
257
|
+
changes = filtered_changes[option.to_s]
|
258
|
+
new_value = audited_options[:redaction_value] || REDACTED
|
259
|
+
if changes.is_a? Array
|
260
|
+
values = changes.map { new_value }
|
261
|
+
else
|
262
|
+
values = new_value
|
263
|
+
end
|
264
|
+
hash = Hash[option.to_s, values]
|
265
|
+
filtered_changes.merge!(hash)
|
266
|
+
end
|
267
|
+
|
268
|
+
filtered_changes
|
269
|
+
end
|
270
|
+
|
271
|
+
def rails_below?(rails_version)
|
272
|
+
Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
|
209
273
|
end
|
210
274
|
|
211
275
|
def audits_to(version = nil)
|
212
276
|
if version == :previous
|
213
|
-
version = if self.
|
214
|
-
self.
|
277
|
+
version = if self.audit_version
|
278
|
+
self.audit_version - 1
|
215
279
|
else
|
216
280
|
previous = audits.descending.offset(1).first
|
217
281
|
previous ? previous.version : 1
|
@@ -226,7 +290,7 @@ module Audited
|
|
226
290
|
end
|
227
291
|
|
228
292
|
def audit_update
|
229
|
-
unless (changes = audited_changes).empty? && audit_comment.blank?
|
293
|
+
unless (changes = audited_changes).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
|
230
294
|
write_audit(action: 'update', audited_changes: changes,
|
231
295
|
comment: audit_comment)
|
232
296
|
end
|
@@ -290,17 +354,12 @@ module Audited
|
|
290
354
|
|
291
355
|
def run_conditional_check(condition, matching: true)
|
292
356
|
return true if condition.blank?
|
293
|
-
|
294
357
|
return condition.call(self) == matching if condition.respond_to?(:call)
|
295
|
-
return send(condition) == matching if respond_to?(condition.to_sym)
|
358
|
+
return send(condition) == matching if respond_to?(condition.to_sym, true)
|
296
359
|
|
297
360
|
true
|
298
361
|
end
|
299
362
|
|
300
|
-
def auditing_enabled=(val)
|
301
|
-
self.class.auditing_enabled = val
|
302
|
-
end
|
303
|
-
|
304
363
|
def reconstruct_attributes(audits)
|
305
364
|
attributes = {}
|
306
365
|
audits.each { |audit| attributes.merge!(audit.new_attributes) }
|
@@ -338,6 +397,20 @@ module Audited
|
|
338
397
|
enable_auditing if auditing_was_enabled
|
339
398
|
end
|
340
399
|
|
400
|
+
# Executes the block with auditing enabled.
|
401
|
+
#
|
402
|
+
# Foo.with_auditing do
|
403
|
+
# @foo.save
|
404
|
+
# end
|
405
|
+
#
|
406
|
+
def with_auditing
|
407
|
+
auditing_was_enabled = auditing_enabled
|
408
|
+
enable_auditing
|
409
|
+
yield
|
410
|
+
ensure
|
411
|
+
disable_auditing unless auditing_was_enabled
|
412
|
+
end
|
413
|
+
|
341
414
|
def disable_auditing
|
342
415
|
self.auditing_enabled = false
|
343
416
|
end
|
@@ -355,7 +428,7 @@ module Audited
|
|
355
428
|
end
|
356
429
|
|
357
430
|
def auditing_enabled
|
358
|
-
Audited.store.fetch("#{table_name}_auditing_enabled", true)
|
431
|
+
Audited.store.fetch("#{table_name}_auditing_enabled", true) && Audited.auditing_enabled
|
359
432
|
end
|
360
433
|
|
361
434
|
def auditing_enabled=(val)
|
data/lib/audited/version.rb
CHANGED
data/lib/audited.rb
CHANGED
@@ -2,7 +2,7 @@ require 'active_record'
|
|
2
2
|
|
3
3
|
module Audited
|
4
4
|
class << self
|
5
|
-
attr_accessor :ignored_attributes, :current_user_method, :max_audits
|
5
|
+
attr_accessor :ignored_attributes, :current_user_method, :max_audits, :auditing_enabled
|
6
6
|
attr_writer :audit_class
|
7
7
|
|
8
8
|
def audit_class
|
@@ -21,6 +21,7 @@ module Audited
|
|
21
21
|
@ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
|
22
22
|
|
23
23
|
@current_user_method = :current_user
|
24
|
+
@auditing_enabled = true
|
24
25
|
end
|
25
26
|
|
26
27
|
require 'audited/auditor'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class <%= migration_class_name %> < <%= migration_parent %>
|
2
|
+
def self.up
|
3
|
+
if index_exists?(:audits, [:auditable_type, :auditable_id], name: index_name)
|
4
|
+
remove_index :audits, name: index_name
|
5
|
+
add_index :audits, [:auditable_type, :auditable_id, :version], name: index_name
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.down
|
10
|
+
if index_exists?(:audits, [:auditable_type, :auditable_id, :version], name: index_name)
|
11
|
+
remove_index :audits, name: index_name
|
12
|
+
add_index :audits, [:auditable_type, :auditable_id], name: index_name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def index_name
|
19
|
+
'auditable_index'
|
20
|
+
end
|
21
|
+
end
|
@@ -17,7 +17,7 @@ class <%= migration_class_name %> < <%= migration_parent %>
|
|
17
17
|
t.column :created_at, :datetime
|
18
18
|
end
|
19
19
|
|
20
|
-
add_index :audits, [:auditable_type, :auditable_id], :name => 'auditable_index'
|
20
|
+
add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index'
|
21
21
|
add_index :audits, [:associated_type, :associated_id], :name => 'associated_index'
|
22
22
|
add_index :audits, [:user_id, :user_type], :name => 'user_index'
|
23
23
|
add_index :audits, :request_uuid
|
@@ -58,6 +58,10 @@ module Audited
|
|
58
58
|
if indexes.any? { |i| i.columns == %w[associated_id associated_type] }
|
59
59
|
yield :revert_polymorphic_indexes_order
|
60
60
|
end
|
61
|
+
|
62
|
+
if indexes.any? { |i| i.columns == %w[auditable_type auditable_id] }
|
63
|
+
yield :add_version_to_auditable_index
|
64
|
+
end
|
61
65
|
end
|
62
66
|
end
|
63
67
|
end
|
data/spec/audited/audit_spec.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
|
+
SingleCov.covered! uncovered: 1 # Rails version check
|
4
|
+
|
3
5
|
describe Audited::Audit do
|
4
6
|
let(:user) { Models::ActiveRecord::User.new name: "Testing" }
|
5
7
|
|
@@ -38,43 +40,64 @@ describe Audited::Audit do
|
|
38
40
|
end
|
39
41
|
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
+
context "when a custom audit class is not configured" do
|
44
|
+
it "should default to #{described_class}" do
|
45
|
+
TempModel.audited
|
46
|
+
|
47
|
+
record = TempModel.create
|
48
|
+
|
49
|
+
audit = record.audits.first
|
50
|
+
expect(audit).to be_a Audited::Audit
|
51
|
+
expect(audit.respond_to?(:custom_method)).to be false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#audited_changes" do
|
57
|
+
let(:audit) { Audited.audit_class.new }
|
58
|
+
|
59
|
+
it "can unserialize yaml from text columns" do
|
60
|
+
audit.audited_changes = {foo: "bar"}
|
61
|
+
expect(audit.audited_changes).to eq foo: "bar"
|
62
|
+
end
|
63
|
+
|
64
|
+
it "does not unserialize from binary columns" do
|
65
|
+
allow(Audited::YAMLIfTextColumnType).to receive(:text_column?).and_return(false)
|
66
|
+
audit.audited_changes = {foo: "bar"}
|
67
|
+
expect(audit.audited_changes).to eq "{:foo=>\"bar\"}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#undo" do
|
72
|
+
let(:user) { Models::ActiveRecord::User.create(name: "John") }
|
73
|
+
|
74
|
+
it "undos changes" do
|
43
75
|
user.update_attribute(:name, 'Joe')
|
44
76
|
user.audits.last.undo
|
45
77
|
user.reload
|
46
|
-
|
47
78
|
expect(user.name).to eq("John")
|
48
79
|
end
|
49
80
|
|
50
|
-
it "
|
51
|
-
user = Models::ActiveRecord::User.create(name: "John")
|
81
|
+
it "undos destroy" do
|
52
82
|
user.destroy
|
53
83
|
user.audits.last.undo
|
54
84
|
user = Models::ActiveRecord::User.find_by(name: "John")
|
55
85
|
expect(user.name).to eq("John")
|
56
86
|
end
|
57
87
|
|
58
|
-
it "
|
59
|
-
user
|
88
|
+
it "undos creation" do
|
89
|
+
user # trigger create
|
60
90
|
expect {user.audits.last.undo}.to change(Models::ActiveRecord::User, :count).by(-1)
|
61
91
|
end
|
62
92
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
record = TempModel.create
|
68
|
-
|
69
|
-
audit = record.audits.first
|
70
|
-
expect(audit).to be_a Audited::Audit
|
71
|
-
expect(audit.respond_to?(:custom_method)).to be false
|
72
|
-
end
|
93
|
+
it "fails when trying to undo unknown" do
|
94
|
+
audit = user.audits.last
|
95
|
+
audit.action = 'oops'
|
96
|
+
expect { audit.undo }.to raise_error("invalid action given oops")
|
73
97
|
end
|
74
98
|
end
|
75
99
|
|
76
100
|
describe "user=" do
|
77
|
-
|
78
101
|
it "should be able to set the user to a model object" do
|
79
102
|
subject.user = user
|
80
103
|
expect(subject.user).to eq(user)
|
@@ -110,11 +133,9 @@ describe Audited::Audit do
|
|
110
133
|
subject.user = user
|
111
134
|
expect(subject.username).to be_nil
|
112
135
|
end
|
113
|
-
|
114
136
|
end
|
115
137
|
|
116
138
|
describe "revision" do
|
117
|
-
|
118
139
|
it "should recreate attributes" do
|
119
140
|
user = Models::ActiveRecord::User.create name: "1"
|
120
141
|
5.times {|i| user.update_attribute :name, (i + 2).to_s }
|
@@ -148,6 +169,34 @@ describe Audited::Audit do
|
|
148
169
|
end
|
149
170
|
end
|
150
171
|
|
172
|
+
describe ".collection_cache_key" do
|
173
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
174
|
+
it "uses created at" do
|
175
|
+
Audited::Audit.delete_all
|
176
|
+
audit = Models::ActiveRecord::User.create(name: "John").audits.last
|
177
|
+
audit.update_columns(created_at: Time.zone.parse('2018-01-01'))
|
178
|
+
expect(Audited::Audit.collection_cache_key).to match(/-20180101\d+$/)
|
179
|
+
end
|
180
|
+
else
|
181
|
+
it "is not defined" do
|
182
|
+
expect { Audited::Audit.collection_cache_key }.to raise_error(NoMethodError)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe ".assign_revision_attributes" do
|
188
|
+
it "dups when frozen" do
|
189
|
+
user.freeze
|
190
|
+
assigned = Audited::Audit.assign_revision_attributes(user, name: "Bar")
|
191
|
+
expect(assigned.name).to eq "Bar"
|
192
|
+
end
|
193
|
+
|
194
|
+
it "ignores unassignable attributes" do
|
195
|
+
assigned = Audited::Audit.assign_revision_attributes(user, oops: "Bar")
|
196
|
+
expect(assigned.name).to eq "Testing"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
151
200
|
it "should set the version number on create" do
|
152
201
|
user = Models::ActiveRecord::User.create! name: "Set Version Number"
|
153
202
|
expect(user.audits.first.version).to eq(1)
|
@@ -213,6 +262,25 @@ describe Audited::Audit do
|
|
213
262
|
end
|
214
263
|
end
|
215
264
|
|
265
|
+
it "should support nested as_user" do
|
266
|
+
Audited::Audit.as_user("sidekiq") do
|
267
|
+
company = Models::ActiveRecord::Company.create name: "The auditors"
|
268
|
+
company.name = "The Auditors, Inc"
|
269
|
+
company.save
|
270
|
+
expect(company.audits[-1].user).to eq("sidekiq")
|
271
|
+
|
272
|
+
Audited::Audit.as_user(user) do
|
273
|
+
company.name = "NEW Auditors, Inc"
|
274
|
+
company.save
|
275
|
+
expect(company.audits[-1].user).to eq(user)
|
276
|
+
end
|
277
|
+
|
278
|
+
company.name = "LAST Auditors, Inc"
|
279
|
+
company.save
|
280
|
+
expect(company.audits[-1].user).to eq("sidekiq")
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
216
284
|
it "should record usernames" do
|
217
285
|
Audited::Audit.as_user(user.name) do
|
218
286
|
company = Models::ActiveRecord::Company.create name: "The auditors"
|
@@ -263,6 +331,5 @@ describe Audited::Audit do
|
|
263
331
|
}.to raise_exception('expected')
|
264
332
|
expect(Audited.store[:audited_user]).to be_nil
|
265
333
|
end
|
266
|
-
|
267
334
|
end
|
268
335
|
end
|