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.

@@ -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 :version, :audit_comment
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
- if audited_options[:only].present?
205
- all_changes.slice(*audited_columns)
206
- else
207
- all_changes.except(*non_audited_columns)
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.version
214
- self.version - 1
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)
@@ -1,3 +1,3 @@
1
1
  module Audited
2
- VERSION = "4.7.0"
2
+ VERSION = "4.10.0"
3
3
  end
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
@@ -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
- it "should undo changes" do
42
- user = Models::ActiveRecord::User.create(name: "John")
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 "should undo destroyed model" do
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 "should undo created model" do
59
- user = Models::ActiveRecord::User.create(name: "John")
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
- context "when a custom audit class is not configured" do
64
- it "should default to #{described_class}" do
65
- TempModel.audited
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