velocity_audited 5.1.3
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 +7 -0
- data/.github/workflows/ci.yml +115 -0
- data/.gitignore +17 -0
- data/.standard.yml +5 -0
- data/.yardopts +3 -0
- data/Appraisals +44 -0
- data/CHANGELOG.md +419 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +433 -0
- data/Rakefile +18 -0
- data/gemfiles/rails50.gemfile +10 -0
- data/gemfiles/rails51.gemfile +10 -0
- data/gemfiles/rails52.gemfile +10 -0
- data/gemfiles/rails60.gemfile +10 -0
- data/gemfiles/rails61.gemfile +10 -0
- data/gemfiles/rails70.gemfile +10 -0
- data/lib/audited/audit.rb +198 -0
- data/lib/audited/auditor.rb +476 -0
- data/lib/audited/railtie.rb +16 -0
- data/lib/audited/rspec_matchers.rb +228 -0
- data/lib/audited/sweeper.rb +67 -0
- data/lib/audited/version.rb +5 -0
- data/lib/audited-rspec.rb +6 -0
- data/lib/audited.rb +49 -0
- data/lib/generators/audited/install_generator.rb +27 -0
- data/lib/generators/audited/migration.rb +17 -0
- data/lib/generators/audited/migration_helper.rb +11 -0
- data/lib/generators/audited/templates/add_association_to_audits.rb +13 -0
- data/lib/generators/audited/templates/add_comment_to_audits.rb +11 -0
- data/lib/generators/audited/templates/add_remote_address_to_audits.rb +12 -0
- data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +12 -0
- data/lib/generators/audited/templates/add_version_to_auditable_index.rb +23 -0
- data/lib/generators/audited/templates/install.rb +32 -0
- data/lib/generators/audited/templates/rename_association_to_associated.rb +25 -0
- data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +11 -0
- data/lib/generators/audited/templates/rename_parent_to_association.rb +13 -0
- data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +22 -0
- data/lib/generators/audited/upgrade_generator.rb +70 -0
- data/lib/velocity_audited.rb +5 -0
- data/spec/audited/audit_spec.rb +357 -0
- data/spec/audited/auditor_spec.rb +1097 -0
- data/spec/audited/rspec_matchers_spec.rb +69 -0
- data/spec/audited/sweeper_spec.rb +133 -0
- data/spec/audited_spec.rb +18 -0
- data/spec/audited_spec_helpers.rb +32 -0
- data/spec/rails_app/app/assets/config/manifest.js +2 -0
- data/spec/rails_app/config/application.rb +13 -0
- data/spec/rails_app/config/database.yml +26 -0
- data/spec/rails_app/config/environment.rb +5 -0
- data/spec/rails_app/config/environments/test.rb +47 -0
- data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails_app/config/initializers/inflections.rb +2 -0
- data/spec/rails_app/config/initializers/secret_token.rb +3 -0
- data/spec/rails_app/config/routes.rb +3 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/active_record/models.rb +151 -0
- data/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb +11 -0
- data/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb +11 -0
- data/spec/support/active_record/schema.rb +90 -0
- data/test/db/version_1.rb +17 -0
- data/test/db/version_2.rb +18 -0
- data/test/db/version_3.rb +18 -0
- data/test/db/version_4.rb +19 -0
- data/test/db/version_5.rb +17 -0
- data/test/db/version_6.rb +19 -0
- data/test/install_generator_test.rb +62 -0
- data/test/test_helper.rb +18 -0
- data/test/upgrade_generator_test.rb +97 -0
- metadata +260 -0
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Audited
|
6
|
+
# Audit saves the changes to ActiveRecord models. It has the following attributes:
|
7
|
+
#
|
8
|
+
# * <tt>auditable</tt>: the ActiveRecord model that was changed
|
9
|
+
# * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
|
10
|
+
# * <tt>action</tt>: one of create, update, or delete
|
11
|
+
# * <tt>audited_changes</tt>: a hash of all the changes
|
12
|
+
# * <tt>comment</tt>: a comment set with the audit
|
13
|
+
# * <tt>version</tt>: the version of the model
|
14
|
+
# * <tt>request_uuid</tt>: a uuid based that allows audits from the same controller request
|
15
|
+
# * <tt>created_at</tt>: Time that the change was performed
|
16
|
+
#
|
17
|
+
|
18
|
+
class YAMLIfTextColumnType
|
19
|
+
class << self
|
20
|
+
def load(obj)
|
21
|
+
if text_column?
|
22
|
+
ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
|
23
|
+
else
|
24
|
+
obj
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def dump(obj)
|
29
|
+
if text_column?
|
30
|
+
ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
|
31
|
+
else
|
32
|
+
obj
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def text_column?
|
37
|
+
Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Audit < ::ActiveRecord::Base
|
43
|
+
belongs_to :auditable, polymorphic: true
|
44
|
+
belongs_to :user, polymorphic: true
|
45
|
+
belongs_to :associated, polymorphic: true
|
46
|
+
|
47
|
+
before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
|
48
|
+
|
49
|
+
cattr_accessor :audited_class_names
|
50
|
+
self.audited_class_names = Set.new
|
51
|
+
|
52
|
+
serialize :audited_changes, YAMLIfTextColumnType
|
53
|
+
|
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") }
|
59
|
+
|
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) }
|
64
|
+
# Return all audits older than the current one.
|
65
|
+
def ancestors
|
66
|
+
self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return an instance of what the object looked like at this revision. If
|
70
|
+
# the object has been destroyed, this will be a new record.
|
71
|
+
def revision
|
72
|
+
clazz = auditable_type.constantize
|
73
|
+
(clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
|
74
|
+
self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(audit_version: version))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns a hash of the changed attributes with the new values
|
79
|
+
def new_attributes
|
80
|
+
(audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
|
81
|
+
attrs[attr] = (action == "update" ? values.last : values)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a hash of the changed attributes with the old values
|
86
|
+
def old_attributes
|
87
|
+
(audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
|
88
|
+
attrs[attr] = (action == "update" ? values.first : values)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Allows user to undo changes
|
93
|
+
def undo
|
94
|
+
case action
|
95
|
+
when "create"
|
96
|
+
# destroys a newly created record
|
97
|
+
auditable.destroy!
|
98
|
+
when "destroy"
|
99
|
+
# creates a new record with the destroyed record attributes
|
100
|
+
auditable_type.constantize.create!(audited_changes)
|
101
|
+
when "update"
|
102
|
+
# changes back attributes
|
103
|
+
auditable.update!(audited_changes.transform_values(&:first))
|
104
|
+
else
|
105
|
+
raise StandardError, "invalid action given #{action}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Allows user to be set to either a string or an ActiveRecord object
|
110
|
+
# @private
|
111
|
+
def user_as_string=(user)
|
112
|
+
# reset both either way
|
113
|
+
self.user_as_model = self.username = nil
|
114
|
+
user.is_a?(::ActiveRecord::Base) ?
|
115
|
+
self.user_as_model = user :
|
116
|
+
self.username = user
|
117
|
+
end
|
118
|
+
alias_method :user_as_model=, :user=
|
119
|
+
alias_method :user=, :user_as_string=
|
120
|
+
|
121
|
+
# @private
|
122
|
+
def user_as_string
|
123
|
+
user_as_model || username
|
124
|
+
end
|
125
|
+
alias_method :user_as_model, :user
|
126
|
+
alias_method :user, :user_as_string
|
127
|
+
|
128
|
+
# Returns the list of classes that are being audited
|
129
|
+
def self.audited_classes
|
130
|
+
audited_class_names.map(&:constantize)
|
131
|
+
end
|
132
|
+
|
133
|
+
# All audits made during the block called will be recorded as made
|
134
|
+
# by +user+. This method is hopefully threadsafe, making it ideal
|
135
|
+
# for background operations that require audit information.
|
136
|
+
def self.as_user(user)
|
137
|
+
last_audited_user = ::Audited.store[:audited_user]
|
138
|
+
::Audited.store[:audited_user] = user
|
139
|
+
yield
|
140
|
+
ensure
|
141
|
+
::Audited.store[:audited_user] = last_audited_user
|
142
|
+
end
|
143
|
+
|
144
|
+
# @private
|
145
|
+
def self.reconstruct_attributes(audits)
|
146
|
+
audits.each_with_object({}) do |audit, all|
|
147
|
+
all.merge!(audit.new_attributes)
|
148
|
+
all[:audit_version] = audit.version
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# @private
|
153
|
+
def self.assign_revision_attributes(record, attributes)
|
154
|
+
attributes.each do |attr, val|
|
155
|
+
record = record.dup if record.frozen?
|
156
|
+
|
157
|
+
if record.respond_to?("#{attr}=")
|
158
|
+
record.attributes.key?(attr.to_s) ?
|
159
|
+
record[attr] = val :
|
160
|
+
record.send("#{attr}=", val)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
record
|
164
|
+
end
|
165
|
+
|
166
|
+
# use created_at as timestamp cache key
|
167
|
+
def self.collection_cache_key(collection = all, *)
|
168
|
+
super(collection, :created_at)
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def set_version_number
|
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
|
181
|
+
end
|
182
|
+
|
183
|
+
def set_audit_user
|
184
|
+
self.user ||= ::Audited.store[:audited_user] # from .as_user
|
185
|
+
self.user ||= ::Audited.store[:current_user].try!(:call) # from Sweeper
|
186
|
+
nil # prevent stopping callback chains
|
187
|
+
end
|
188
|
+
|
189
|
+
def set_request_uuid
|
190
|
+
self.request_uuid ||= ::Audited.store[:current_request_uuid]
|
191
|
+
self.request_uuid ||= SecureRandom.uuid
|
192
|
+
end
|
193
|
+
|
194
|
+
def set_remote_address
|
195
|
+
self.remote_address ||= ::Audited.store[:current_remote_address]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,476 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Audited
|
4
|
+
# Specify this act if you want changes to your model to be saved in an
|
5
|
+
# audit table. This assumes there is an audits table ready.
|
6
|
+
#
|
7
|
+
# class User < ActiveRecord::Base
|
8
|
+
# audited
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# To store an audit comment set model.audit_comment to your comment before
|
12
|
+
# a create, update or destroy operation.
|
13
|
+
#
|
14
|
+
# See <tt>Audited::Auditor::ClassMethods#audited</tt>
|
15
|
+
# for configuration options
|
16
|
+
module Auditor #:nodoc:
|
17
|
+
extend ActiveSupport::Concern
|
18
|
+
|
19
|
+
CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
# == Configuration options
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# * +only+ - Only audit the given attributes
|
26
|
+
# * +except+ - Excludes fields from being saved in the audit log.
|
27
|
+
# By default, Audited will audit all but these fields:
|
28
|
+
#
|
29
|
+
# [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
|
30
|
+
# You can add to those by passing one or an array of fields to skip.
|
31
|
+
#
|
32
|
+
# class User < ActiveRecord::Base
|
33
|
+
# audited except: :password
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# * +require_comment+ - Ensures that audit_comment is supplied before
|
37
|
+
# any create, update or destroy operation.
|
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
|
49
|
+
#
|
50
|
+
# * +if+ - Only audit the model when the given function returns true
|
51
|
+
# * +unless+ - Only audit the model when the given function returns false
|
52
|
+
#
|
53
|
+
# class User < ActiveRecord::Base
|
54
|
+
# audited :if => :active?
|
55
|
+
#
|
56
|
+
# def active?
|
57
|
+
# self.status == 'active'
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
def audited(options = {})
|
62
|
+
# don't allow multiple calls
|
63
|
+
return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
|
64
|
+
|
65
|
+
extend Audited::Auditor::AuditedClassMethods
|
66
|
+
include Audited::Auditor::AuditedInstanceMethods
|
67
|
+
|
68
|
+
class_attribute :audit_associated_with, instance_writer: false
|
69
|
+
class_attribute :audited_options, instance_writer: false
|
70
|
+
attr_accessor :audit_version, :audit_comment
|
71
|
+
|
72
|
+
self.audited_options = options
|
73
|
+
normalize_audited_options
|
74
|
+
|
75
|
+
self.audit_associated_with = audited_options[:associated_with]
|
76
|
+
|
77
|
+
if audited_options[:comment_required]
|
78
|
+
validate :presence_of_audit_comment
|
79
|
+
before_destroy :require_comment if audited_options[:on].include?(:destroy)
|
80
|
+
end
|
81
|
+
|
82
|
+
has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
|
83
|
+
Audited.audit_class.audited_class_names << to_s
|
84
|
+
|
85
|
+
after_create :audit_create if audited_options[:on].include?(:create)
|
86
|
+
before_update :audit_update if audited_options[:on].include?(:update)
|
87
|
+
before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
|
88
|
+
|
89
|
+
# Define and set after_audit and around_audit callbacks. This might be useful if you want
|
90
|
+
# to notify a party after the audit has been created or if you want to access the newly-created
|
91
|
+
# audit.
|
92
|
+
define_callbacks :audit
|
93
|
+
set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
|
94
|
+
set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
|
95
|
+
|
96
|
+
enable_auditing
|
97
|
+
end
|
98
|
+
|
99
|
+
def has_associated_audits
|
100
|
+
has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
module AuditedInstanceMethods
|
105
|
+
REDACTED = "[REDACTED]"
|
106
|
+
|
107
|
+
# Temporarily turns off auditing while saving.
|
108
|
+
def save_without_auditing
|
109
|
+
without_auditing { save }
|
110
|
+
end
|
111
|
+
|
112
|
+
# Executes the block with the auditing callbacks disabled.
|
113
|
+
#
|
114
|
+
# @foo.without_auditing do
|
115
|
+
# @foo.save
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
def without_auditing(&block)
|
119
|
+
self.class.without_auditing(&block)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Temporarily turns on auditing while saving.
|
123
|
+
def save_with_auditing
|
124
|
+
with_auditing { save }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Executes the block with the auditing callbacks enabled.
|
128
|
+
#
|
129
|
+
# @foo.with_auditing do
|
130
|
+
# @foo.save
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
def with_auditing(&block)
|
134
|
+
self.class.with_auditing(&block)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Gets an array of the revisions available
|
138
|
+
#
|
139
|
+
# user.revisions.each do |revision|
|
140
|
+
# user.name
|
141
|
+
# user.version
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
def revisions(from_version = 1)
|
145
|
+
return [] unless audits.from_version(from_version).exists?
|
146
|
+
|
147
|
+
all_audits = audits.select([:audited_changes, :version, :action]).to_a
|
148
|
+
targeted_audits = all_audits.select { |audit| audit.version >= from_version }
|
149
|
+
|
150
|
+
previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
|
151
|
+
|
152
|
+
targeted_audits.map do |audit|
|
153
|
+
previous_attributes.merge!(audit.new_attributes)
|
154
|
+
revision_with(previous_attributes.merge!(version: audit.version))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Get a specific revision specified by the version number, or +:previous+
|
159
|
+
# Returns nil for versions greater than revisions count
|
160
|
+
def revision(version)
|
161
|
+
if version == :previous || audits.last.version >= version
|
162
|
+
revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Find the oldest revision recorded prior to the date/time provided.
|
167
|
+
def revision_at(date_or_time)
|
168
|
+
audits = self.audits.up_until(date_or_time)
|
169
|
+
revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
|
170
|
+
end
|
171
|
+
|
172
|
+
# List of attributes that are audited.
|
173
|
+
def audited_attributes
|
174
|
+
audited_attributes = attributes.except(*self.class.non_audited_columns)
|
175
|
+
normalize_enum_changes(audited_attributes)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns a list combined of record audits and associated audits.
|
179
|
+
def own_and_associated_audits
|
180
|
+
Audited.audit_class.unscoped
|
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)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Combine multiple audits into one.
|
187
|
+
def combine_audits(audits_to_combine)
|
188
|
+
combine_target = audits_to_combine.last
|
189
|
+
combine_target.audited_changes = audits_to_combine.pluck(:audited_changes).reduce(&:merge)
|
190
|
+
combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."
|
191
|
+
|
192
|
+
transaction do
|
193
|
+
combine_target.save!
|
194
|
+
audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
protected
|
199
|
+
|
200
|
+
def revision_with(attributes)
|
201
|
+
dup.tap do |revision|
|
202
|
+
revision.id = id
|
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
|
209
|
+
Audited.audit_class.assign_revision_attributes(revision, attributes)
|
210
|
+
|
211
|
+
# Remove any association proxies so that they will be recreated
|
212
|
+
# and reference the correct object for this revision. The only way
|
213
|
+
# to determine if an instance variable is a proxy object is to
|
214
|
+
# see if it responds to certain methods, as it forwards almost
|
215
|
+
# everything to its target.
|
216
|
+
revision.instance_variables.each do |ivar|
|
217
|
+
proxy = revision.instance_variable_get ivar
|
218
|
+
if !proxy.nil? && proxy.respond_to?(:proxy_respond_to?)
|
219
|
+
revision.instance_variable_set ivar, nil
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
def audited_changes
|
228
|
+
all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
|
229
|
+
filtered_changes = \
|
230
|
+
if audited_options[:only].present?
|
231
|
+
all_changes.slice(*self.class.audited_columns)
|
232
|
+
else
|
233
|
+
all_changes.except(*self.class.non_audited_columns)
|
234
|
+
end
|
235
|
+
|
236
|
+
filtered_changes = redact_values(filtered_changes)
|
237
|
+
filtered_changes = normalize_enum_changes(filtered_changes)
|
238
|
+
filtered_changes.to_hash
|
239
|
+
end
|
240
|
+
|
241
|
+
def normalize_enum_changes(changes)
|
242
|
+
return changes if Audited.store_synthesized_enums
|
243
|
+
|
244
|
+
self.class.defined_enums.each do |name, values|
|
245
|
+
if changes.has_key?(name)
|
246
|
+
changes[name] = \
|
247
|
+
if changes[name].is_a?(Array)
|
248
|
+
changes[name].map { |v| values[v] }
|
249
|
+
elsif rails_below?("5.0")
|
250
|
+
changes[name]
|
251
|
+
else
|
252
|
+
values[changes[name]]
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
changes
|
257
|
+
end
|
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
|
+
|
275
|
+
def rails_below?(rails_version)
|
276
|
+
Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
|
277
|
+
end
|
278
|
+
|
279
|
+
def audits_to(version = nil)
|
280
|
+
if version == :previous
|
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
|
287
|
+
end
|
288
|
+
audits.to_version(version)
|
289
|
+
end
|
290
|
+
|
291
|
+
def audit_create
|
292
|
+
write_audit(action: "create", audited_changes: audited_attributes,
|
293
|
+
comment: audit_comment)
|
294
|
+
end
|
295
|
+
|
296
|
+
def audit_update
|
297
|
+
unless (changes = audited_changes).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
|
298
|
+
write_audit(action: "update", audited_changes: changes,
|
299
|
+
comment: audit_comment)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def audit_destroy
|
304
|
+
unless new_record?
|
305
|
+
write_audit(action: "destroy", audited_changes: audited_attributes,
|
306
|
+
comment: audit_comment)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def write_audit(attrs)
|
311
|
+
self.audit_comment = nil
|
312
|
+
attrs[:db_audit] = true
|
313
|
+
attrs = add_metadata(attrs)
|
314
|
+
if auditing_enabled
|
315
|
+
attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
|
316
|
+
logger.audit(attrs.to_json)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def add_metadata(attrs)
|
321
|
+
attrs[:client] = ::Audited.store[:client]
|
322
|
+
attrs[:origin] = ::Audited.store[:origin]
|
323
|
+
attrs[:user_agent] = ::Audited.store[:user_agent]
|
324
|
+
attrs[:ip] = ::Audited.store[:ip]
|
325
|
+
attrs[:uid] = ::Audited.store[:uid]
|
326
|
+
attrs
|
327
|
+
end
|
328
|
+
|
329
|
+
def presence_of_audit_comment
|
330
|
+
if comment_required_state?
|
331
|
+
errors.add(:audit_comment, :blank) unless audit_comment.present?
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def comment_required_state?
|
336
|
+
auditing_enabled &&
|
337
|
+
audited_changes.present? &&
|
338
|
+
((audited_options[:on].include?(:create) && new_record?) ||
|
339
|
+
(audited_options[:on].include?(:update) && persisted? && changed?))
|
340
|
+
end
|
341
|
+
|
342
|
+
def combine_audits_if_needed
|
343
|
+
max_audits = audited_options[:max_audits]
|
344
|
+
if max_audits && (extra_count = audits.count - max_audits) > 0
|
345
|
+
audits_to_combine = audits.limit(extra_count + 1)
|
346
|
+
combine_audits(audits_to_combine)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def require_comment
|
351
|
+
if auditing_enabled && audit_comment.blank?
|
352
|
+
errors.add(:audit_comment, :blank)
|
353
|
+
throw(:abort)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
CALLBACKS.each do |attr_name|
|
358
|
+
alias_method "#{attr_name}_callback".to_sym, attr_name
|
359
|
+
end
|
360
|
+
|
361
|
+
def auditing_enabled
|
362
|
+
run_conditional_check(audited_options[:if]) &&
|
363
|
+
run_conditional_check(audited_options[:unless], matching: false) &&
|
364
|
+
self.class.auditing_enabled
|
365
|
+
end
|
366
|
+
|
367
|
+
def run_conditional_check(condition, matching: true)
|
368
|
+
return true if condition.blank?
|
369
|
+
return condition.call(self) == matching if condition.respond_to?(:call)
|
370
|
+
return send(condition) == matching if respond_to?(condition.to_sym, true)
|
371
|
+
|
372
|
+
true
|
373
|
+
end
|
374
|
+
|
375
|
+
def reconstruct_attributes(audits)
|
376
|
+
attributes = {}
|
377
|
+
audits.each { |audit| attributes.merge!(audit.new_attributes) }
|
378
|
+
attributes
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
module AuditedClassMethods
|
383
|
+
# Returns an array of columns that are audited. See non_audited_columns
|
384
|
+
def audited_columns
|
385
|
+
@audited_columns ||= column_names - non_audited_columns
|
386
|
+
end
|
387
|
+
|
388
|
+
# We have to calculate this here since column_names may not be available when `audited` is called
|
389
|
+
def non_audited_columns
|
390
|
+
@non_audited_columns ||= calculate_non_audited_columns
|
391
|
+
end
|
392
|
+
|
393
|
+
def non_audited_columns=(columns)
|
394
|
+
@audited_columns = nil # reset cached audited columns on assignment
|
395
|
+
@non_audited_columns = columns.map(&:to_s)
|
396
|
+
end
|
397
|
+
|
398
|
+
# Executes the block with auditing disabled.
|
399
|
+
#
|
400
|
+
# Foo.without_auditing do
|
401
|
+
# @foo.save
|
402
|
+
# end
|
403
|
+
#
|
404
|
+
def without_auditing
|
405
|
+
auditing_was_enabled = auditing_enabled
|
406
|
+
disable_auditing
|
407
|
+
yield
|
408
|
+
ensure
|
409
|
+
enable_auditing if auditing_was_enabled
|
410
|
+
end
|
411
|
+
|
412
|
+
# Executes the block with auditing enabled.
|
413
|
+
#
|
414
|
+
# Foo.with_auditing do
|
415
|
+
# @foo.save
|
416
|
+
# end
|
417
|
+
#
|
418
|
+
def with_auditing
|
419
|
+
auditing_was_enabled = auditing_enabled
|
420
|
+
enable_auditing
|
421
|
+
yield
|
422
|
+
ensure
|
423
|
+
disable_auditing unless auditing_was_enabled
|
424
|
+
end
|
425
|
+
|
426
|
+
def disable_auditing
|
427
|
+
self.auditing_enabled = false
|
428
|
+
end
|
429
|
+
|
430
|
+
def enable_auditing
|
431
|
+
self.auditing_enabled = true
|
432
|
+
end
|
433
|
+
|
434
|
+
# All audit operations during the block are recorded as being
|
435
|
+
# made by +user+. This is not model specific, the method is a
|
436
|
+
# convenience wrapper around
|
437
|
+
# @see Audit#as_user.
|
438
|
+
def audit_as(user, &block)
|
439
|
+
Audited.audit_class.as_user(user, &block)
|
440
|
+
end
|
441
|
+
|
442
|
+
def auditing_enabled
|
443
|
+
Audited.store.fetch("#{table_name}_auditing_enabled", true) && Audited.auditing_enabled
|
444
|
+
end
|
445
|
+
|
446
|
+
def auditing_enabled=(val)
|
447
|
+
Audited.store["#{table_name}_auditing_enabled"] = val
|
448
|
+
end
|
449
|
+
|
450
|
+
def default_ignored_attributes
|
451
|
+
[primary_key, inheritance_column] | Audited.ignored_attributes
|
452
|
+
end
|
453
|
+
|
454
|
+
protected
|
455
|
+
|
456
|
+
def normalize_audited_options
|
457
|
+
audited_options[:on] = Array.wrap(audited_options[:on])
|
458
|
+
audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
|
459
|
+
audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
|
460
|
+
audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
|
461
|
+
max_audits = audited_options[:max_audits] || Audited.max_audits
|
462
|
+
audited_options[:max_audits] = Integer(max_audits).abs if max_audits
|
463
|
+
end
|
464
|
+
|
465
|
+
def calculate_non_audited_columns
|
466
|
+
if audited_options[:only].present?
|
467
|
+
(column_names | default_ignored_attributes) - audited_options[:only]
|
468
|
+
elsif audited_options[:except].present?
|
469
|
+
default_ignored_attributes | audited_options[:except]
|
470
|
+
else
|
471
|
+
default_ignored_attributes
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
@@ -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
|