tangledwires-audited 6.0.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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +37 -0
  3. data/CHANGELOG.md +539 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE +19 -0
  6. data/README.md +447 -0
  7. data/Rakefile +16 -0
  8. data/audited.gemspec +38 -0
  9. data/lib/audited/audit.rb +204 -0
  10. data/lib/audited/audit_associate.rb +8 -0
  11. data/lib/audited/auditor.rb +564 -0
  12. data/lib/audited/railtie.rb +16 -0
  13. data/lib/audited/rspec_matchers.rb +228 -0
  14. data/lib/audited/sweeper.rb +42 -0
  15. data/lib/audited/version.rb +5 -0
  16. data/lib/audited-rspec.rb +6 -0
  17. data/lib/audited.rb +60 -0
  18. data/lib/generators/audited/install_generator.rb +27 -0
  19. data/lib/generators/audited/migration.rb +25 -0
  20. data/lib/generators/audited/migration_helper.rb +11 -0
  21. data/lib/generators/audited/templates/add_association_to_audits.rb +13 -0
  22. data/lib/generators/audited/templates/add_comment_to_audits.rb +11 -0
  23. data/lib/generators/audited/templates/add_remote_address_to_audits.rb +12 -0
  24. data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +12 -0
  25. data/lib/generators/audited/templates/add_version_to_auditable_index.rb +23 -0
  26. data/lib/generators/audited/templates/create_audit_associates.rb +26 -0
  27. data/lib/generators/audited/templates/install.rb +39 -0
  28. data/lib/generators/audited/templates/rename_association_to_associated.rb +25 -0
  29. data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +11 -0
  30. data/lib/generators/audited/templates/rename_parent_to_association.rb +13 -0
  31. data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +22 -0
  32. data/lib/generators/audited/upgrade_generator.rb +74 -0
  33. data/shell.nix +8 -0
  34. metadata +241 -0
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audited
4
+ module RspecMatchers
5
+ # Ensure that the model is audited.
6
+ #
7
+ # Options:
8
+ # * <tt>associated_with</tt> - tests that the audit makes use of the associated_with option
9
+ # * <tt>only</tt> - tests that the audit makes use of the only option *Overrides <tt>except</tt> option*
10
+ # * <tt>except</tt> - tests that the audit makes use of the except option
11
+ # * <tt>requires_comment</tt> - if specified, then the audit must require comments through the <tt>audit_comment</tt> attribute
12
+ # * <tt>on</tt> - tests that the audit makes use of the on option with specified parameters
13
+ #
14
+ # Example:
15
+ # it { should be_audited }
16
+ # it { should be_audited.associated_with(:user) }
17
+ # it { should be_audited.only(:field_name) }
18
+ # it { should be_audited.except(:password) }
19
+ # it { should be_audited.requires_comment }
20
+ # it { should be_audited.on(:create).associated_with(:user).except(:password) }
21
+ #
22
+ def be_audited
23
+ AuditMatcher.new
24
+ end
25
+
26
+ # Ensure that the model has associated audits
27
+ #
28
+ # Example:
29
+ # it { should have_associated_audits }
30
+ #
31
+ def have_associated_audits
32
+ AssociatedAuditMatcher.new
33
+ end
34
+
35
+ class AuditMatcher # :nodoc:
36
+ def initialize
37
+ @options = {}
38
+ end
39
+
40
+ def associated_with(model)
41
+ @options[:associated_with] = model
42
+ self
43
+ end
44
+
45
+ def only(*fields)
46
+ @options[:only] = fields.flatten.map(&:to_s)
47
+ self
48
+ end
49
+
50
+ def except(*fields)
51
+ @options[:except] = fields.flatten.map(&:to_s)
52
+ self
53
+ end
54
+
55
+ def requires_comment
56
+ @options[:comment_required] = true
57
+ self
58
+ end
59
+
60
+ def on(*actions)
61
+ @options[:on] = actions.flatten.map(&:to_sym)
62
+ self
63
+ end
64
+
65
+ def matches?(subject)
66
+ @subject = subject
67
+ auditing_enabled? && required_checks_for_options_satisfied?
68
+ end
69
+
70
+ def failure_message
71
+ "Expected #{@expectation}"
72
+ end
73
+
74
+ def negative_failure_message
75
+ "Did not expect #{@expectation}"
76
+ end
77
+
78
+ alias_method :failure_message_when_negated, :negative_failure_message
79
+
80
+ def description
81
+ description = "audited"
82
+ description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
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)
86
+
87
+ description
88
+ end
89
+
90
+ protected
91
+
92
+ def expects(message)
93
+ @expectation = message
94
+ end
95
+
96
+ def auditing_enabled?
97
+ expects "#{model_class} to be audited"
98
+ model_class.respond_to?(:auditing_enabled) && model_class.auditing_enabled
99
+ end
100
+
101
+ def model_class
102
+ @subject.class
103
+ end
104
+
105
+ def associated_with_model?
106
+ expects "#{model_class} to record audits to associated model #{@options[:associated_with]}"
107
+ model_class.audit_associated_with == @options[:associated_with]
108
+ end
109
+
110
+ def records_changes_to_specified_fields?
111
+ ignored_fields = build_ignored_fields_from_options
112
+
113
+ expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{ignored_fields})"
114
+ model_class.non_audited_columns.to_set == ignored_fields.to_set
115
+ end
116
+
117
+ def comment_required_valid?
118
+ expects "to require audit_comment before #{model_class.audited_options[:on]} when comment required"
119
+ validate_callbacks_include_presence_of_comment? && destroy_callbacks_include_comment_required?
120
+ end
121
+
122
+ def only_audit_on_designated_callbacks?
123
+ {
124
+ create: [:after, :audit_create],
125
+ update: [:before, :audit_update],
126
+ destroy: [:before, :audit_destroy]
127
+ }.map do |(action, kind_callback)|
128
+ kind, callback = kind_callback
129
+ callbacks_for(action, kind: kind).include?(callback) if @options[:on].include?(action)
130
+ end.compact.all?
131
+ end
132
+
133
+ def validate_callbacks_include_presence_of_comment?
134
+ if @options[:comment_required] && audited_on_create_or_update?
135
+ callbacks_for(:validate).include?(:presence_of_audit_comment)
136
+ else
137
+ true
138
+ end
139
+ end
140
+
141
+ def audited_on_create_or_update?
142
+ model_class.audited_options[:on].include?(:create) || model_class.audited_options[:on].include?(:update)
143
+ end
144
+
145
+ def destroy_callbacks_include_comment_required?
146
+ if @options[:comment_required] && model_class.audited_options[:on].include?(:destroy)
147
+ callbacks_for(:destroy).include?(:require_comment)
148
+ else
149
+ true
150
+ end
151
+ end
152
+
153
+ def requires_comment_before_callbacks?
154
+ [:create, :update, :destroy].map do |action|
155
+ if @options[:comment_required] && model_class.audited_options[:on].include?(action)
156
+ callbacks_for(action).include?(:require_comment)
157
+ end
158
+ end.compact.all?
159
+ end
160
+
161
+ def callbacks_for(action, kind: :before)
162
+ model_class.send("_#{action}_callbacks").select { |cb| cb.kind == kind }.map(&:filter)
163
+ end
164
+
165
+ def build_ignored_fields_from_options
166
+ default_ignored_attributes = model_class.default_ignored_attributes
167
+
168
+ if @options[:only].present?
169
+ (default_ignored_attributes | model_class.column_names) - @options[:only]
170
+ elsif @options[:except].present?
171
+ default_ignored_attributes | @options[:except]
172
+ else
173
+ default_ignored_attributes
174
+ end
175
+ end
176
+
177
+ def required_checks_for_options_satisfied?
178
+ {
179
+ only: :records_changes_to_specified_fields?,
180
+ except: :records_changes_to_specified_fields?,
181
+ comment_required: :comment_required_valid?,
182
+ associated_with: :associated_with_model?,
183
+ on: :only_audit_on_designated_callbacks?
184
+ }.map do |(option, check)|
185
+ send(check) if @options[option].present?
186
+ end.compact.all?
187
+ end
188
+ end
189
+
190
+ class AssociatedAuditMatcher # :nodoc:
191
+ def matches?(subject)
192
+ @subject = subject
193
+
194
+ association_exists?
195
+ end
196
+
197
+ def failure_message
198
+ "Expected #{model_class} to have associated audits"
199
+ end
200
+
201
+ def negative_failure_message
202
+ "Expected #{model_class} to not have associated audits"
203
+ end
204
+
205
+ alias_method :failure_message_when_negated, :negative_failure_message
206
+
207
+ def description
208
+ "has associated audits"
209
+ end
210
+
211
+ protected
212
+
213
+ def model_class
214
+ @subject.class
215
+ end
216
+
217
+ def reflection
218
+ model_class.reflect_on_association(:associated_audits)
219
+ end
220
+
221
+ def association_exists?
222
+ !reflection.nil? &&
223
+ reflection.macro == :has_many &&
224
+ reflection.options[:class_name] == Audited.audit_class.name
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audited
4
+ class Sweeper
5
+ STORED_DATA = {
6
+ current_remote_address: :remote_ip,
7
+ current_request_uuid: :request_uuid,
8
+ current_user: :current_user
9
+ }
10
+
11
+ delegate :store, to: ::Audited
12
+
13
+ def around(controller)
14
+ self.controller = controller
15
+ STORED_DATA.each { |k, m| store[k] = send(m) }
16
+ yield
17
+ ensure
18
+ self.controller = nil
19
+ STORED_DATA.keys.each { |k| store.delete(k) }
20
+ end
21
+
22
+ def current_user
23
+ lambda { controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true) }
24
+ end
25
+
26
+ def remote_ip
27
+ controller.try(:request).try(:remote_ip)
28
+ end
29
+
30
+ def request_uuid
31
+ controller.try(:request).try(:uuid)
32
+ end
33
+
34
+ def controller
35
+ store[:current_controller]
36
+ end
37
+
38
+ def controller=(value)
39
+ store[:current_controller] = value
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audited
4
+ VERSION = "6.0.0"
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "audited/rspec_matchers"
4
+ module RSpec::Matchers
5
+ include Audited::RspecMatchers
6
+ end
data/lib/audited.rb ADDED
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Audited
6
+ # Wrapper around ActiveSupport::CurrentAttributes
7
+ class RequestStore < ActiveSupport::CurrentAttributes
8
+ attribute :audited_store
9
+ end
10
+
11
+ class << self
12
+ attr_accessor \
13
+ :auditing_enabled,
14
+ :current_user_method,
15
+ :ignored_attributes,
16
+ :ignored_default_callbacks,
17
+ :max_audits,
18
+ :store_synthesized_enums
19
+ attr_writer :audit_class
20
+
21
+ def audit_class
22
+ # The audit_class is set as String in the initializer. It can not be constantized during initialization and must
23
+ # be constantized at runtime. See https://github.com/collectiveidea/audited/issues/608
24
+ @audit_class = @audit_class.safe_constantize if @audit_class.is_a?(String)
25
+ @audit_class ||= Audited::Audit
26
+ end
27
+
28
+ # remove audit_model in next major version it was only shortly present in 5.1.0
29
+ alias_method :audit_model, :audit_class
30
+ deprecate audit_model: "use Audited.audit_class instead of Audited.audit_model. This method will be removed.",
31
+ deprecator: ActiveSupport::Deprecation.new('6.0.0', 'Audited')
32
+
33
+ def store
34
+ RequestStore.audited_store ||= {}
35
+ end
36
+
37
+ def config
38
+ yield(self)
39
+ end
40
+ end
41
+
42
+ @ignored_attributes = %w[lock_version created_at updated_at created_on updated_on]
43
+ @ignored_default_callbacks = []
44
+
45
+ @current_user_method = :current_user
46
+ @auditing_enabled = true
47
+ @store_synthesized_enums = false
48
+ end
49
+
50
+ require 'audited/auditor'
51
+ require 'audited/audit'
52
+ require 'audited/audit_associate'
53
+
54
+ ActiveSupport.on_load :active_record do
55
+ require "audited/audit"
56
+ include Audited::Auditor
57
+ end
58
+
59
+ require "audited/sweeper"
60
+ require "audited/railtie" if Audited.const_defined?(:Rails)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+ require "active_record"
6
+ require "rails/generators/active_record"
7
+ require "generators/audited/migration"
8
+ require "generators/audited/migration_helper"
9
+
10
+ module Audited
11
+ module Generators
12
+ class InstallGenerator < Rails::Generators::Base
13
+ include Rails::Generators::Migration
14
+ include Audited::Generators::MigrationHelper
15
+ extend Audited::Generators::Migration
16
+
17
+ class_option :audited_changes_column_type, type: :string, default: "text", required: false
18
+ class_option :audited_user_id_column_type, type: :string, default: "integer", required: false
19
+
20
+ source_root File.expand_path("../templates", __FILE__)
21
+
22
+ def copy_migration
23
+ migration_template "install.rb", "db/migrate/install_audited.rb"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audited
4
+ module Generators
5
+ module Migration
6
+ # Implement the required interface for Rails::Generators::Migration.
7
+ def next_migration_number(dirname) # :nodoc:
8
+ next_migration_number = current_migration_number(dirname) + 1
9
+ if timestamped_migrations?
10
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
11
+ else
12
+ "%.3d" % next_migration_number
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def timestamped_migrations?
19
+ (::ActiveRecord.version >= Gem::Version.new("7.0")) ?
20
+ ::ActiveRecord.timestamped_migrations :
21
+ ::ActiveRecord::Base.timestamped_migrations
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audited
4
+ module Generators
5
+ module MigrationHelper
6
+ def migration_parent
7
+ "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ add_column :audits, :association_id, :integer
6
+ add_column :audits, :association_type, :string
7
+ end
8
+
9
+ def self.down
10
+ remove_column :audits, :association_type
11
+ remove_column :audits, :association_id
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ add_column :audits, :comment, :string
6
+ end
7
+
8
+ def self.down
9
+ remove_column :audits, :comment
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ add_column :audits, :remote_address, :string
6
+ end
7
+
8
+ def self.down
9
+ remove_column :audits, :remote_address
10
+ end
11
+ end
12
+
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ add_column :audits, :request_uuid, :string
6
+ add_index :audits, :request_uuid
7
+ end
8
+
9
+ def self.down
10
+ remove_column :audits, :request_uuid
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ if index_exists?(:audits, [:auditable_type, :auditable_id], name: index_name)
6
+ remove_index :audits, name: index_name
7
+ add_index :audits, [:auditable_type, :auditable_id, :version], name: index_name
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ if index_exists?(:audits, [:auditable_type, :auditable_id, :version], name: index_name)
13
+ remove_index :audits, name: index_name
14
+ add_index :audits, [:auditable_type, :auditable_id], name: index_name
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def index_name
21
+ 'auditable_index'
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ class <%= migration_class_name %> < <%= migration_parent %>
2
+ def self.up
3
+ create_table :audited_audit_associates, :force => true do |t|
4
+ t.column :audit_id, :integer
5
+ t.column :associated_id, :integer
6
+ t.column :associated_type, :string
7
+ end
8
+
9
+ execute <<-SQL
10
+ INSERT INTO audited_audit_associates (audit_id, associated_id, associated_type)
11
+ SELECT id, associated_id, associated_type
12
+ FROM audits
13
+ SQL
14
+
15
+ add_index :audited_audit_associates, :audit_id, :name => 'index_audited_audit_associates_on_audit_id'
16
+ add_index :audited_audit_associates, [:associated_type, :associated_id], :name => 'index_audited_audit_associates_on_associated'
17
+
18
+ remove_index :audits, name: 'associated_index'
19
+ remove_column :audits, :associated_id
20
+ remove_column :audits, :associated_type
21
+ end
22
+
23
+ def self.down
24
+ raise ActiveRecord::IrreversibleMigration
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ create_table :audits, :force => true do |t|
6
+ t.column :auditable_id, :integer
7
+ t.column :auditable_type, :string
8
+ t.column :user_id, :<%= options[:audited_user_id_column_type] %>
9
+ t.column :user_type, :string
10
+ t.column :username, :string
11
+ t.column :action, :string
12
+ t.column :audited_changes, :<%= options[:audited_changes_column_type] %>
13
+ t.column :version, :integer, :default => 0
14
+ t.column :comment, :string
15
+ t.column :remote_address, :string
16
+ t.column :request_uuid, :string
17
+ t.column :created_at, :datetime
18
+ end
19
+
20
+ add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index'
21
+ add_index :audits, [:user_id, :user_type], :name => 'user_index'
22
+ add_index :audits, :request_uuid
23
+ add_index :audits, :created_at
24
+
25
+ create_table :audited_audit_associates, :force => true do |t|
26
+ t.column :audit_id, :integer
27
+ t.column :associated_id, :integer
28
+ t.column :associated_type, :string
29
+ end
30
+
31
+ add_index :audited_audit_associates, :audit_id, :name => 'index_audited_audit_associates_on_audit_id'
32
+ add_index :audited_audit_associates, [:associated_type, :associated_id], :name => 'index_audited_audit_associates_on_associated'
33
+ end
34
+
35
+ def self.down
36
+ drop_table :audited_audit_associates
37
+ drop_table :audits
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ if index_exists? :audits, [:association_id, :association_type], :name => 'association_index'
6
+ remove_index :audits, :name => 'association_index'
7
+ end
8
+
9
+ rename_column :audits, :association_id, :associated_id
10
+ rename_column :audits, :association_type, :associated_type
11
+
12
+ add_index :audits, [:associated_id, :associated_type], :name => 'associated_index'
13
+ end
14
+
15
+ def self.down
16
+ if index_exists? :audits, [:associated_id, :associated_type], :name => 'associated_index'
17
+ remove_index :audits, :name => 'associated_index'
18
+ end
19
+
20
+ rename_column :audits, :associated_type, :association_type
21
+ rename_column :audits, :associated_id, :association_id
22
+
23
+ add_index :audits, [:association_id, :association_type], :name => 'association_index'
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ rename_column :audits, :changes, :audited_changes
6
+ end
7
+
8
+ def self.down
9
+ rename_column :audits, :audited_changes, :changes
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ rename_column :audits, :auditable_parent_id, :association_id
6
+ rename_column :audits, :auditable_parent_type, :association_type
7
+ end
8
+
9
+ def self.down
10
+ rename_column :audits, :association_type, :auditable_parent_type
11
+ rename_column :audits, :association_id, :auditable_parent_id
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < <%= migration_parent %>
4
+ def self.up
5
+ fix_index_order_for [:associated_id, :associated_type], 'associated_index'
6
+ fix_index_order_for [:auditable_id, :auditable_type], 'auditable_index'
7
+ end
8
+
9
+ def self.down
10
+ fix_index_order_for [:associated_type, :associated_id], 'associated_index'
11
+ fix_index_order_for [:auditable_type, :auditable_id], 'auditable_index'
12
+ end
13
+
14
+ private
15
+
16
+ def fix_index_order_for(columns, index_name)
17
+ if index_exists? :audits, columns, name: index_name
18
+ remove_index :audits, name: index_name
19
+ add_index :audits, columns.reverse, name: index_name
20
+ end
21
+ end
22
+ end