audited 4.9.0 → 5.0.2

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.

Potentially problematic release.


This version of audited might be problematic. Click here for more details.

Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +115 -0
  3. data/.standard.yml +5 -0
  4. data/Appraisals +22 -16
  5. data/CHANGELOG.md +68 -1
  6. data/Gemfile +1 -1
  7. data/README.md +49 -11
  8. data/Rakefile +6 -6
  9. data/gemfiles/rails60.gemfile +1 -1
  10. data/gemfiles/rails61.gemfile +10 -0
  11. data/gemfiles/rails70.gemfile +10 -0
  12. data/lib/audited/audit.rb +36 -28
  13. data/lib/audited/auditor.rb +69 -43
  14. data/lib/audited/railtie.rb +16 -0
  15. data/lib/audited/rspec_matchers.rb +5 -3
  16. data/lib/audited/sweeper.rb +3 -10
  17. data/lib/audited/version.rb +3 -1
  18. data/lib/audited-rspec.rb +3 -1
  19. data/lib/audited.rb +25 -8
  20. data/lib/generators/audited/install_generator.rb +9 -7
  21. data/lib/generators/audited/migration.rb +2 -0
  22. data/lib/generators/audited/migration_helper.rb +3 -1
  23. data/lib/generators/audited/templates/add_association_to_audits.rb +2 -0
  24. data/lib/generators/audited/templates/add_comment_to_audits.rb +2 -0
  25. data/lib/generators/audited/templates/add_remote_address_to_audits.rb +2 -0
  26. data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +2 -0
  27. data/lib/generators/audited/templates/add_version_to_auditable_index.rb +2 -0
  28. data/lib/generators/audited/templates/install.rb +2 -0
  29. data/lib/generators/audited/templates/rename_association_to_associated.rb +2 -0
  30. data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +2 -0
  31. data/lib/generators/audited/templates/rename_parent_to_association.rb +2 -0
  32. data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +2 -0
  33. data/lib/generators/audited/upgrade_generator.rb +16 -14
  34. data/spec/audited/audit_spec.rb +69 -47
  35. data/spec/audited/auditor_spec.rb +310 -246
  36. data/spec/audited/sweeper_spec.rb +19 -18
  37. data/spec/audited_spec.rb +18 -0
  38. data/spec/audited_spec_helpers.rb +7 -7
  39. data/spec/rails_app/app/assets/config/manifest.js +2 -0
  40. data/spec/rails_app/config/application.rb +3 -3
  41. data/spec/rails_app/config/database.yml +3 -2
  42. data/spec/rails_app/config/environment.rb +1 -1
  43. data/spec/rails_app/config/environments/test.rb +5 -5
  44. data/spec/rails_app/config/initializers/secret_token.rb +2 -2
  45. data/spec/spec_helper.rb +14 -14
  46. data/spec/support/active_record/models.rb +31 -12
  47. data/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb +1 -2
  48. data/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb +1 -2
  49. data/spec/support/active_record/schema.rb +26 -19
  50. data/test/db/version_1.rb +2 -2
  51. data/test/db/version_2.rb +2 -2
  52. data/test/db/version_3.rb +2 -3
  53. data/test/db/version_4.rb +2 -3
  54. data/test/db/version_5.rb +0 -1
  55. data/test/db/version_6.rb +1 -1
  56. data/test/install_generator_test.rb +18 -19
  57. data/test/test_helper.rb +5 -5
  58. data/test/upgrade_generator_test.rb +13 -18
  59. metadata +31 -30
  60. data/.rubocop.yml +0 -25
  61. data/.travis.yml +0 -58
  62. data/gemfiles/rails42.gemfile +0 -11
  63. data/spec/rails_app/app/controllers/application_controller.rb +0 -2
  64. data/spec/rails_app/config/environments/development.rb +0 -21
  65. data/spec/rails_app/config/environments/production.rb +0 -35
data/lib/audited/audit.rb CHANGED
@@ -1,4 +1,6 @@
1
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
2
4
 
3
5
  module Audited
4
6
  # Audit saves the changes to ActiveRecord models. It has the following attributes:
@@ -16,7 +18,7 @@ module Audited
16
18
  class YAMLIfTextColumnType
17
19
  class << self
18
20
  def load(obj)
19
- if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
21
+ if text_column?
20
22
  ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
21
23
  else
22
24
  obj
@@ -24,18 +26,22 @@ module Audited
24
26
  end
25
27
 
26
28
  def dump(obj)
27
- if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
29
+ if text_column?
28
30
  ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
29
31
  else
30
32
  obj
31
33
  end
32
34
  end
35
+
36
+ def text_column?
37
+ Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
38
+ end
33
39
  end
34
40
  end
35
41
 
36
42
  class Audit < ::ActiveRecord::Base
37
- belongs_to :auditable, polymorphic: true
38
- belongs_to :user, polymorphic: true
43
+ belongs_to :auditable, polymorphic: true
44
+ belongs_to :user, polymorphic: true
39
45
  belongs_to :associated, polymorphic: true
40
46
 
41
47
  before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
@@ -45,16 +51,16 @@ module Audited
45
51
 
46
52
  serialize :audited_changes, YAMLIfTextColumnType
47
53
 
48
- scope :ascending, ->{ reorder(version: :asc) }
49
- scope :descending, ->{ reorder(version: :desc)}
50
- scope :creates, ->{ where(action: 'create')}
51
- scope :updates, ->{ where(action: 'update')}
52
- scope :destroys, ->{ where(action: 'destroy')}
54
+ scope :ascending, -> { reorder(version: :asc) }
55
+ scope :descending, -> { reorder(version: :desc) }
56
+ scope :creates, -> { where(action: "create") }
57
+ scope :updates, -> { where(action: "update") }
58
+ scope :destroys, -> { where(action: "destroy") }
53
59
 
54
- scope :up_until, ->(date_or_time){ where("created_at <= ?", date_or_time) }
55
- scope :from_version, ->(version){ where('version >= ?', version) }
56
- scope :to_version, ->(version){ where('version <= ?', version) }
57
- scope :auditable_finder, ->(auditable_id, auditable_type){ where(auditable_id: auditable_id, auditable_type: auditable_type)}
60
+ scope :up_until, ->(date_or_time) { where("created_at <= ?", date_or_time) }
61
+ scope :from_version, ->(version) { where("version >= ?", version) }
62
+ scope :to_version, ->(version) { where("version <= ?", version) }
63
+ scope :auditable_finder, ->(auditable_id, auditable_type) { where(auditable_id: auditable_id, auditable_type: auditable_type) }
58
64
  # Return all audits older than the current one.
59
65
  def ancestors
60
66
  self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
@@ -71,31 +77,28 @@ module Audited
71
77
 
72
78
  # Returns a hash of the changed attributes with the new values
73
79
  def new_attributes
74
- (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
75
- attrs[attr] = values.is_a?(Array) ? values.last : values
76
- attrs
80
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
81
+ attrs[attr] = (action == "update" ? values.last : values)
77
82
  end
78
83
  end
79
84
 
80
85
  # Returns a hash of the changed attributes with the old values
81
86
  def old_attributes
82
- (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
83
- attrs[attr] = Array(values).first
84
-
85
- attrs
87
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
88
+ attrs[attr] = (action == "update" ? values.first : values)
86
89
  end
87
90
  end
88
91
 
89
92
  # Allows user to undo changes
90
93
  def undo
91
94
  case action
92
- when 'create'
95
+ when "create"
93
96
  # destroys a newly created record
94
97
  auditable.destroy!
95
- when 'destroy'
98
+ when "destroy"
96
99
  # creates a new record with the destroyed record attributes
97
100
  auditable_type.constantize.create!(audited_changes)
98
- when 'update'
101
+ when "update"
99
102
  # changes back attributes
100
103
  auditable.update!(audited_changes.transform_values(&:first))
101
104
  else
@@ -131,7 +134,7 @@ module Audited
131
134
  # by +user+. This method is hopefully threadsafe, making it ideal
132
135
  # for background operations that require audit information.
133
136
  def self.as_user(user)
134
- last_audited_user = ::Audited.store[:audited_user]
137
+ last_audited_user = ::Audited.store[:audited_user]
135
138
  ::Audited.store[:audited_user] = user
136
139
  yield
137
140
  ensure
@@ -143,7 +146,7 @@ module Audited
143
146
  audits.each_with_object({}) do |audit, all|
144
147
  all.merge!(audit.new_attributes)
145
148
  all[:audit_version] = audit.version
146
- end
149
+ end
147
150
  end
148
151
 
149
152
  # @private
@@ -168,8 +171,13 @@ module Audited
168
171
  private
169
172
 
170
173
  def set_version_number
171
- max = self.class.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
172
- self.version = max + 1
174
+ if action == "create"
175
+ self.version = 1
176
+ else
177
+ collection = Rails::VERSION::MAJOR >= 6 ? self.class.unscoped : self.class
178
+ max = collection.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
179
+ self.version = max + 1
180
+ end
173
181
  end
174
182
 
175
183
  def set_audit_user
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Audited
2
4
  # Specify this act if you want changes to your model to be saved in an
3
5
  # audit table. This assumes there is an audits table ready.
@@ -34,6 +36,16 @@ module Audited
34
36
  # * +require_comment+ - Ensures that audit_comment is supplied before
35
37
  # any create, update or destroy operation.
36
38
  # * +max_audits+ - Limits the number of stored audits.
39
+
40
+ # * +redacted+ - Changes to these fields will be logged, but the values
41
+ # will not. This is useful, for example, if you wish to audit when a
42
+ # password is changed, without saving the actual password in the log.
43
+ # To store values as something other than '[REDACTED]', pass an argument
44
+ # to the redaction_value option.
45
+ #
46
+ # class User < ActiveRecord::Base
47
+ # audited redacted: :password, redaction_value: SecureRandom.uuid
48
+ # end
37
49
  #
38
50
  # * +if+ - Only audit the model when the given function returns true
39
51
  # * +unless+ - Only audit the model when the given function returns false
@@ -54,7 +66,7 @@ module Audited
54
66
  include Audited::Auditor::AuditedInstanceMethods
55
67
 
56
68
  class_attribute :audit_associated_with, instance_writer: false
57
- class_attribute :audited_options, instance_writer: false
69
+ class_attribute :audited_options, instance_writer: false
58
70
  attr_accessor :audit_version, :audit_comment
59
71
 
60
72
  self.audited_options = options
@@ -70,8 +82,8 @@ module Audited
70
82
  has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
71
83
  Audited.audit_class.audited_class_names << to_s
72
84
 
73
- after_create :audit_create if audited_options[:on].include?(:create)
74
- before_update :audit_update if audited_options[:on].include?(:update)
85
+ after_create :audit_create if audited_options[:on].include?(:create)
86
+ before_update :audit_update if audited_options[:on].include?(:update)
75
87
  before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
76
88
 
77
89
  # Define and set after_audit and around_audit callbacks. This might be useful if you want
@@ -90,15 +102,7 @@ module Audited
90
102
  end
91
103
 
92
104
  module AuditedInstanceMethods
93
- # Deprecate version attribute in favor of audit_version attribute – preparing for eventual removal.
94
- def method_missing(method_name, *args, &block)
95
- if method_name == :version
96
- ActiveSupport::Deprecation.warn("`version` attribute has been changed to `audit_version`. This attribute will be removed.")
97
- audit_version
98
- else
99
- super
100
- end
101
- end
105
+ REDACTED = "[REDACTED]"
102
106
 
103
107
  # Temporarily turns off auditing while saving.
104
108
  def save_without_auditing
@@ -140,7 +144,7 @@ module Audited
140
144
  def revisions(from_version = 1)
141
145
  return [] unless audits.from_version(from_version).exists?
142
146
 
143
- all_audits = audits.select([:audited_changes, :version]).to_a
147
+ all_audits = audits.select([:audited_changes, :version, :action]).to_a
144
148
  targeted_audits = all_audits.select { |audit| audit.version >= from_version }
145
149
 
146
150
  previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
@@ -154,7 +158,7 @@ module Audited
154
158
  # Get a specific revision specified by the version number, or +:previous+
155
159
  # Returns nil for versions greater than revisions count
156
160
  def revision(version)
157
- if version == :previous || self.audits.last.version >= version
161
+ if version == :previous || audits.last.version >= version
158
162
  revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
159
163
  end
160
164
  end
@@ -174,9 +178,9 @@ module Audited
174
178
  # Returns a list combined of record audits and associated audits.
175
179
  def own_and_associated_audits
176
180
  Audited.audit_class.unscoped
177
- .where('(auditable_type = :type AND auditable_id = :id) OR (associated_type = :type AND associated_id = :id)',
178
- type: self.class.name, id: id)
179
- .order(created_at: :desc)
181
+ .where("(auditable_type = :type AND auditable_id = :id) OR (associated_type = :type AND associated_id = :id)",
182
+ type: self.class.base_class.name, id: id)
183
+ .order(created_at: :desc)
180
184
  end
181
185
 
182
186
  # Combine multiple audits into one.
@@ -196,12 +200,12 @@ module Audited
196
200
  def revision_with(attributes)
197
201
  dup.tap do |revision|
198
202
  revision.id = id
199
- revision.send :instance_variable_set, '@new_record', destroyed?
200
- revision.send :instance_variable_set, '@persisted', !destroyed?
201
- revision.send :instance_variable_set, '@readonly', false
202
- revision.send :instance_variable_set, '@destroyed', false
203
- revision.send :instance_variable_set, '@_destroyed', false
204
- revision.send :instance_variable_set, '@marked_for_destruction', false
203
+ revision.send :instance_variable_set, "@new_record", destroyed?
204
+ revision.send :instance_variable_set, "@persisted", !destroyed?
205
+ revision.send :instance_variable_set, "@readonly", false
206
+ revision.send :instance_variable_set, "@destroyed", false
207
+ revision.send :instance_variable_set, "@_destroyed", false
208
+ revision.send :instance_variable_set, "@marked_for_destruction", false
205
209
  Audited.audit_class.assign_revision_attributes(revision, attributes)
206
210
 
207
211
  # Remove any association proxies so that they will be recreated
@@ -229,17 +233,20 @@ module Audited
229
233
  all_changes.except(*self.class.non_audited_columns)
230
234
  end
231
235
 
236
+ filtered_changes = redact_values(filtered_changes)
232
237
  filtered_changes = normalize_enum_changes(filtered_changes)
233
238
  filtered_changes.to_hash
234
239
  end
235
240
 
236
241
  def normalize_enum_changes(changes)
242
+ return changes if Audited.store_synthesized_enums
243
+
237
244
  self.class.defined_enums.each do |name, values|
238
245
  if changes.has_key?(name)
239
246
  changes[name] = \
240
247
  if changes[name].is_a?(Array)
241
248
  changes[name].map { |v| values[v] }
242
- elsif rails_below?('5.0')
249
+ elsif rails_below?("5.0")
243
250
  changes[name]
244
251
  else
245
252
  values[changes[name]]
@@ -249,47 +256,66 @@ module Audited
249
256
  changes
250
257
  end
251
258
 
259
+ def redact_values(filtered_changes)
260
+ [audited_options[:redacted]].flatten.compact.each do |option|
261
+ changes = filtered_changes[option.to_s]
262
+ new_value = audited_options[:redaction_value] || REDACTED
263
+ values = if changes.is_a? Array
264
+ changes.map { new_value }
265
+ else
266
+ new_value
267
+ end
268
+ hash = {option.to_s => values}
269
+ filtered_changes.merge!(hash)
270
+ end
271
+
272
+ filtered_changes
273
+ end
274
+
252
275
  def rails_below?(rails_version)
253
276
  Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
254
277
  end
255
278
 
256
279
  def audits_to(version = nil)
257
280
  if version == :previous
258
- version = if self.audit_version
259
- self.audit_version - 1
260
- else
261
- previous = audits.descending.offset(1).first
262
- previous ? previous.version : 1
263
- end
281
+ version = if audit_version
282
+ audit_version - 1
283
+ else
284
+ previous = audits.descending.offset(1).first
285
+ previous ? previous.version : 1
286
+ end
264
287
  end
265
288
  audits.to_version(version)
266
289
  end
267
290
 
268
291
  def audit_create
269
- write_audit(action: 'create', audited_changes: audited_attributes,
292
+ write_audit(action: "create", audited_changes: audited_attributes,
270
293
  comment: audit_comment)
271
294
  end
272
295
 
273
296
  def audit_update
274
297
  unless (changes = audited_changes).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
275
- write_audit(action: 'update', audited_changes: changes,
298
+ write_audit(action: "update", audited_changes: changes,
276
299
  comment: audit_comment)
277
300
  end
278
301
  end
279
302
 
280
303
  def audit_destroy
281
- write_audit(action: 'destroy', audited_changes: audited_attributes,
282
- comment: audit_comment) unless new_record?
304
+ unless new_record?
305
+ write_audit(action: "destroy", audited_changes: audited_attributes,
306
+ comment: audit_comment)
307
+ end
283
308
  end
284
309
 
285
310
  def write_audit(attrs)
286
- attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
287
311
  self.audit_comment = nil
288
312
 
289
313
  if auditing_enabled
314
+ attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
315
+
290
316
  run_callbacks(:audit) {
291
317
  audit = audits.create(attrs)
292
- combine_audits_if_needed if attrs[:action] != 'create'
318
+ combine_audits_if_needed if attrs[:action] != "create"
293
319
  audit
294
320
  }
295
321
  end
@@ -297,14 +323,15 @@ module Audited
297
323
 
298
324
  def presence_of_audit_comment
299
325
  if comment_required_state?
300
- errors.add(:audit_comment, "Comment can't be blank!") unless audit_comment.present?
326
+ errors.add(:audit_comment, :blank) unless audit_comment.present?
301
327
  end
302
328
  end
303
329
 
304
330
  def comment_required_state?
305
331
  auditing_enabled &&
306
- ((audited_options[:on].include?(:create) && self.new_record?) ||
307
- (audited_options[:on].include?(:update) && self.persisted? && self.changed?))
332
+ audited_changes.present? &&
333
+ ((audited_options[:on].include?(:create) && new_record?) ||
334
+ (audited_options[:on].include?(:update) && persisted? && changed?))
308
335
  end
309
336
 
310
337
  def combine_audits_if_needed
@@ -317,8 +344,7 @@ module Audited
317
344
 
318
345
  def require_comment
319
346
  if auditing_enabled && audit_comment.blank?
320
- errors.add(:audit_comment, "Comment can't be blank!")
321
- return false if Rails.version.start_with?('4.')
347
+ errors.add(:audit_comment, :blank)
322
348
  throw(:abort)
323
349
  end
324
350
  end
@@ -328,7 +354,7 @@ module Audited
328
354
  end
329
355
 
330
356
  def auditing_enabled
331
- return run_conditional_check(audited_options[:if]) &&
357
+ run_conditional_check(audited_options[:if]) &&
332
358
  run_conditional_check(audited_options[:unless], matching: false) &&
333
359
  self.class.auditing_enabled
334
360
  end
@@ -346,7 +372,7 @@ module Audited
346
372
  audits.each { |audit| attributes.merge!(audit.new_attributes) }
347
373
  attributes
348
374
  end
349
- end # InstanceMethods
375
+ end
350
376
 
351
377
  module AuditedClassMethods
352
378
  # Returns an array of columns that are audited. See non_audited_columns
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audited
4
+ class Railtie < Rails::Railtie
5
+ initializer "audited.sweeper" do
6
+ ActiveSupport.on_load(:action_controller) do
7
+ if defined?(ActionController::Base)
8
+ ActionController::Base.around_action Audited::Sweeper.new
9
+ end
10
+ if defined?(ActionController::API)
11
+ ActionController::API.around_action Audited::Sweeper.new
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Audited
2
4
  module RspecMatchers
3
5
  # Ensure that the model is audited.
@@ -78,9 +80,9 @@ module Audited
78
80
  def description
79
81
  description = "audited"
80
82
  description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
81
- description += " only => #{@options[:only].join ', '}" if @options.key?(:only)
82
- description += " except => #{@options[:except].join(', ')}" if @options.key?(:except)
83
- description += " requires audit_comment" if @options.key?(:comment_required)
83
+ description += " only => #{@options[:only].join ", "}" if @options.key?(:only)
84
+ description += " except => #{@options[:except].join(", ")}" if @options.key?(:except)
85
+ description += " requires audit_comment" if @options.key?(:comment_required)
84
86
 
85
87
  description
86
88
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Audited
2
4
  class Sweeper
3
5
  STORED_DATA = {
@@ -10,7 +12,7 @@ module Audited
10
12
 
11
13
  def around(controller)
12
14
  self.controller = controller
13
- STORED_DATA.each { |k,m| store[k] = send(m) }
15
+ STORED_DATA.each { |k, m| store[k] = send(m) }
14
16
  yield
15
17
  ensure
16
18
  self.controller = nil
@@ -38,12 +40,3 @@ module Audited
38
40
  end
39
41
  end
40
42
  end
41
-
42
- ActiveSupport.on_load(:action_controller) do
43
- if defined?(ActionController::Base)
44
- ActionController::Base.around_action Audited::Sweeper.new
45
- end
46
- if defined?(ActionController::API)
47
- ActionController::API.around_action Audited::Sweeper.new
48
- end
49
- end