rails_audit_log 0.8.0 → 1.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.
- checksums.yaml +4 -4
- data/README.md +294 -55
- data/Rakefile +5 -0
- data/app/assets/stylesheets/rails_audit_log/_01_base.css +32 -0
- data/app/assets/stylesheets/rails_audit_log/_02_layout.css +34 -0
- data/app/assets/stylesheets/rails_audit_log/_03_table.css +39 -0
- data/app/assets/stylesheets/rails_audit_log/_04_badges.css +13 -0
- data/app/assets/stylesheets/rails_audit_log/_05_diff.css +94 -0
- data/app/assets/stylesheets/rails_audit_log/_06_timeline.css +21 -0
- data/app/assets/stylesheets/rails_audit_log/_07_detail.css +15 -0
- data/app/assets/stylesheets/rails_audit_log/_08_pagination.css +56 -0
- data/app/assets/stylesheets/rails_audit_log/_09_filters.css +54 -0
- data/app/assets/stylesheets/rails_audit_log/application.css +1 -15
- data/app/concerns/rails_audit_log/auditable.rb +57 -0
- data/app/concerns/rails_audit_log/controller.rb +29 -0
- data/app/controllers/rails_audit_log/application_controller.rb +13 -0
- data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +32 -0
- data/app/controllers/rails_audit_log/resources_controller.rb +14 -0
- data/app/helpers/rails_audit_log/application_helper.rb +14 -0
- data/app/javascript/rails_audit_log/application.js +8 -0
- data/app/javascript/rails_audit_log/diff_controller.js +20 -0
- data/app/javascript/rails_audit_log/search_controller.js +12 -0
- data/app/jobs/rails_audit_log/application_job.rb +1 -0
- data/app/models/rails_audit_log/application_record.rb +1 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +127 -25
- data/app/views/layouts/rails_audit_log/application.html.erb +16 -10
- data/app/views/rails_audit_log/audit_log_entries/_diff.html.erb +55 -0
- data/app/views/rails_audit_log/audit_log_entries/index.html.erb +76 -0
- data/app/views/rails_audit_log/audit_log_entries/show.html.erb +50 -0
- data/app/views/rails_audit_log/resources/show.html.erb +35 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +10 -0
- data/lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb +21 -0
- data/lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb +99 -0
- data/lib/rails_audit_log/engine.rb +29 -0
- data/lib/rails_audit_log/matchers.rb +56 -0
- data/lib/rails_audit_log/minitest_assertions.rb +36 -0
- data/lib/rails_audit_log/paper_trail_compat.rb +76 -0
- data/lib/rails_audit_log/test_helpers.rb +20 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +173 -5
- metadata +70 -5
|
@@ -1,24 +1,67 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# RSpec matchers for asserting audit log behaviour.
|
|
3
|
+
#
|
|
4
|
+
# == Setup
|
|
5
|
+
#
|
|
6
|
+
# # spec/rails_helper.rb
|
|
7
|
+
# require "rails_audit_log/matchers"
|
|
8
|
+
#
|
|
9
|
+
# RSpec.configure do |config|
|
|
10
|
+
# config.include RailsAuditLog::Matchers
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# == Usage
|
|
14
|
+
#
|
|
15
|
+
# # Assert a record already has an entry
|
|
16
|
+
# expect(post).to have_audit_log_entry(:update).touching(:title)
|
|
17
|
+
#
|
|
18
|
+
# # Assert a block creates a new entry
|
|
19
|
+
# expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update, touching: :title)
|
|
2
20
|
module Matchers
|
|
21
|
+
# Returns a matcher that asserts the record has at least one matching
|
|
22
|
+
# {AuditLogEntry}.
|
|
23
|
+
#
|
|
24
|
+
# @param event [Symbol, String, nil] optional event filter
|
|
25
|
+
# (<tt>:create</tt>, <tt>:update</tt>, or <tt>:destroy</tt>)
|
|
26
|
+
# @return [HaveAuditLogEntry]
|
|
27
|
+
# @example
|
|
28
|
+
# expect(post).to have_audit_log_entry
|
|
29
|
+
# expect(post).to have_audit_log_entry(:update)
|
|
30
|
+
# expect(post).to have_audit_log_entry(:update).touching(:title)
|
|
3
31
|
def have_audit_log_entry(event = nil)
|
|
4
32
|
HaveAuditLogEntry.new(event)
|
|
5
33
|
end
|
|
6
34
|
|
|
35
|
+
# Returns a matcher that asserts the block creates at least one new
|
|
36
|
+
# {AuditLogEntry} matching the given filters.
|
|
37
|
+
#
|
|
38
|
+
# @param event [Symbol, String, nil] optional event filter
|
|
39
|
+
# @param touching [Symbol, String, nil] optional attribute filter
|
|
40
|
+
# @return [CreateAuditLogEntry]
|
|
41
|
+
# @example
|
|
42
|
+
# expect { post.update!(title: "x") }.to create_audit_log_entry(event: :update)
|
|
43
|
+
# expect { post.update!(title: "x") }.to create_audit_log_entry(touching: :title)
|
|
7
44
|
def create_audit_log_entry(event: nil, touching: nil)
|
|
8
45
|
CreateAuditLogEntry.new(event: event, touching: touching)
|
|
9
46
|
end
|
|
10
47
|
|
|
48
|
+
# RSpec matcher — asserts that a record already has a matching entry.
|
|
11
49
|
class HaveAuditLogEntry
|
|
12
50
|
def initialize(event)
|
|
13
51
|
@event = event
|
|
14
52
|
@touching = nil
|
|
15
53
|
end
|
|
16
54
|
|
|
55
|
+
# Chains an attribute filter onto the matcher.
|
|
56
|
+
#
|
|
57
|
+
# @param attribute [Symbol, String]
|
|
58
|
+
# @return [self]
|
|
17
59
|
def touching(attribute)
|
|
18
60
|
@touching = attribute
|
|
19
61
|
self
|
|
20
62
|
end
|
|
21
63
|
|
|
64
|
+
# @api private
|
|
22
65
|
def matches?(record)
|
|
23
66
|
@record = record
|
|
24
67
|
scope = record.audit_log_entries
|
|
@@ -27,14 +70,17 @@ module RailsAuditLog
|
|
|
27
70
|
scope.exists?
|
|
28
71
|
end
|
|
29
72
|
|
|
73
|
+
# @api private
|
|
30
74
|
def failure_message
|
|
31
75
|
"expected #{@record.class}##{@record.id} to have an audit log entry#{qualifier}"
|
|
32
76
|
end
|
|
33
77
|
|
|
78
|
+
# @api private
|
|
34
79
|
def failure_message_when_negated
|
|
35
80
|
"expected #{@record.class}##{@record.id} not to have an audit log entry#{qualifier}"
|
|
36
81
|
end
|
|
37
82
|
|
|
83
|
+
# @api private
|
|
38
84
|
def description
|
|
39
85
|
"have an audit log entry#{qualifier}"
|
|
40
86
|
end
|
|
@@ -49,21 +95,28 @@ module RailsAuditLog
|
|
|
49
95
|
end
|
|
50
96
|
end
|
|
51
97
|
|
|
98
|
+
# RSpec matcher — asserts that a block creates a new matching entry.
|
|
52
99
|
class CreateAuditLogEntry
|
|
53
100
|
def initialize(event:, touching:)
|
|
54
101
|
@event = event
|
|
55
102
|
@touching = touching
|
|
56
103
|
end
|
|
57
104
|
|
|
105
|
+
# Chains an attribute filter onto the matcher.
|
|
106
|
+
#
|
|
107
|
+
# @param attribute [Symbol, String]
|
|
108
|
+
# @return [self]
|
|
58
109
|
def touching(attribute)
|
|
59
110
|
@touching = attribute
|
|
60
111
|
self
|
|
61
112
|
end
|
|
62
113
|
|
|
114
|
+
# @api private
|
|
63
115
|
def supports_block_expectations?
|
|
64
116
|
true
|
|
65
117
|
end
|
|
66
118
|
|
|
119
|
+
# @api private
|
|
67
120
|
def matches?(block)
|
|
68
121
|
@before = matching_scope.count
|
|
69
122
|
block.call
|
|
@@ -71,14 +124,17 @@ module RailsAuditLog
|
|
|
71
124
|
@after > @before
|
|
72
125
|
end
|
|
73
126
|
|
|
127
|
+
# @api private
|
|
74
128
|
def failure_message
|
|
75
129
|
"expected block to create an audit log entry#{qualifier}, but none was created"
|
|
76
130
|
end
|
|
77
131
|
|
|
132
|
+
# @api private
|
|
78
133
|
def failure_message_when_negated
|
|
79
134
|
"expected block not to create an audit log entry#{qualifier}, but one was created"
|
|
80
135
|
end
|
|
81
136
|
|
|
137
|
+
# @api private
|
|
82
138
|
def description
|
|
83
139
|
"create an audit log entry#{qualifier}"
|
|
84
140
|
end
|
|
@@ -1,11 +1,47 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# Opt-in Minitest assertions for audit log expectations.
|
|
3
|
+
#
|
|
4
|
+
# == Setup
|
|
5
|
+
#
|
|
6
|
+
# # test/test_helper.rb
|
|
7
|
+
# require "rails_audit_log/minitest_assertions"
|
|
8
|
+
#
|
|
9
|
+
# class ActiveSupport::TestCase
|
|
10
|
+
# include RailsAuditLog::MinitestAssertions
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# == Usage
|
|
14
|
+
#
|
|
15
|
+
# assert_audit_log_entry(post, event: :update, touching: :title)
|
|
16
|
+
# refute_audit_log_entry(post, event: :create)
|
|
2
17
|
module MinitestAssertions
|
|
18
|
+
# Asserts that +record+ has at least one {AuditLogEntry} matching the
|
|
19
|
+
# given filters. Fails with a descriptive message when no entry is found.
|
|
20
|
+
#
|
|
21
|
+
# @param record [ActiveRecord::Base] the audited record to check
|
|
22
|
+
# @param event [Symbol, String, nil] optional event filter
|
|
23
|
+
# (<tt>:create</tt>, <tt>:update</tt>, or <tt>:destroy</tt>)
|
|
24
|
+
# @param touching [Symbol, String, nil] optional attribute name filter
|
|
25
|
+
# @param message [String, nil] custom failure message; auto-generated when nil
|
|
26
|
+
# @return [void]
|
|
27
|
+
# @example
|
|
28
|
+
# assert_audit_log_entry(post, event: :update, touching: :title)
|
|
3
29
|
def assert_audit_log_entry(record, event: nil, touching: nil, message: nil)
|
|
4
30
|
scope = build_scope(record, event, touching)
|
|
5
31
|
msg = message || default_message("to have", record, event, touching)
|
|
6
32
|
assert scope.exists?, msg
|
|
7
33
|
end
|
|
8
34
|
|
|
35
|
+
# Asserts that +record+ has *no* {AuditLogEntry} matching the given filters.
|
|
36
|
+
# Fails with a descriptive message when a matching entry is found.
|
|
37
|
+
#
|
|
38
|
+
# @param record [ActiveRecord::Base] the audited record to check
|
|
39
|
+
# @param event [Symbol, String, nil] optional event filter
|
|
40
|
+
# @param touching [Symbol, String, nil] optional attribute name filter
|
|
41
|
+
# @param message [String, nil] custom failure message; auto-generated when nil
|
|
42
|
+
# @return [void]
|
|
43
|
+
# @example
|
|
44
|
+
# refute_audit_log_entry(post, event: :destroy)
|
|
9
45
|
def refute_audit_log_entry(record, event: nil, touching: nil, message: nil)
|
|
10
46
|
scope = build_scope(record, event, touching)
|
|
11
47
|
msg = message || default_message("not to have", record, event, touching)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
# Opt-in compatibility shim for gradual migration from PaperTrail.
|
|
3
|
+
# Include alongside RailsAuditLog::Auditable to keep PaperTrail's
|
|
4
|
+
# familiar API while your codebase migrates.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# class Article < ApplicationRecord
|
|
8
|
+
# include RailsAuditLog::Auditable
|
|
9
|
+
# include RailsAuditLog::PaperTrailCompat
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# article.versions # audit_log_entries, oldest-first
|
|
13
|
+
# article.paper_trail.version # most recent AuditLogEntry
|
|
14
|
+
# article.paper_trail.previous_version # reconstructed previous state
|
|
15
|
+
# article.paper_trail.originator # whodunnit_snapshot string
|
|
16
|
+
# article.paper_trail.version_at(1.week.ago) # time-travel reconstruction
|
|
17
|
+
module PaperTrailCompat
|
|
18
|
+
extend ActiveSupport::Concern
|
|
19
|
+
|
|
20
|
+
included do
|
|
21
|
+
has_many :versions,
|
|
22
|
+
-> { order(created_at: :asc, id: :asc) },
|
|
23
|
+
class_name: "RailsAuditLog::AuditLogEntry",
|
|
24
|
+
as: :item,
|
|
25
|
+
dependent: :destroy
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns a proxy object exposing PaperTrail's instance-level API.
|
|
29
|
+
#
|
|
30
|
+
# @return [Proxy]
|
|
31
|
+
def paper_trail
|
|
32
|
+
@paper_trail_proxy ||= Proxy.new(self)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Proxy providing the PaperTrail instance API surface backed by
|
|
36
|
+
# {AuditLogEntry} records.
|
|
37
|
+
class Proxy
|
|
38
|
+
# @param record [ActiveRecord::Base] the record this proxy wraps
|
|
39
|
+
def initialize(record)
|
|
40
|
+
@record = record
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The most recent {AuditLogEntry} for the record.
|
|
44
|
+
#
|
|
45
|
+
# @return [AuditLogEntry, nil]
|
|
46
|
+
def version
|
|
47
|
+
@record.audit_log_entries.order(id: :desc).first
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Reconstructs the record's state before the most recent change.
|
|
51
|
+
# Returns +nil+ for newly created records (no prior state exists).
|
|
52
|
+
#
|
|
53
|
+
# @return [ActiveRecord::Base, nil] an unpersisted instance; or +nil+
|
|
54
|
+
def previous_version
|
|
55
|
+
version&.reify
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Display name of the actor who made the most recent change, as stored
|
|
59
|
+
# in +whodunnit_snapshot+.
|
|
60
|
+
#
|
|
61
|
+
# @return [String, nil]
|
|
62
|
+
def originator
|
|
63
|
+
version&.whodunnit_snapshot
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Reconstructs the record's state as it was at +timestamp+.
|
|
67
|
+
# Delegates to {RailsAuditLog.version_at}.
|
|
68
|
+
#
|
|
69
|
+
# @param timestamp [Time]
|
|
70
|
+
# @return [ActiveRecord::Base, nil] an unpersisted instance; or +nil+
|
|
71
|
+
def version_at(timestamp)
|
|
72
|
+
RailsAuditLog.version_at(@record, timestamp)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# Opt-in test helper for suppressing audit writes in test setup code.
|
|
3
|
+
#
|
|
4
|
+
# == Setup
|
|
5
|
+
#
|
|
6
|
+
# # spec/rails_helper.rb
|
|
7
|
+
# require "rails_audit_log/test_helpers"
|
|
8
|
+
#
|
|
9
|
+
# RSpec.configure do |config|
|
|
10
|
+
# config.include RailsAuditLog::TestHelpers
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# == Usage
|
|
14
|
+
#
|
|
15
|
+
# let(:post) { without_audit_log { Post.create!(title: "fixture") } }
|
|
2
16
|
module TestHelpers
|
|
17
|
+
# Executes the block with audit logging disabled. A prefix-free wrapper
|
|
18
|
+
# around {RailsAuditLog.disable} intended for use in FactoryBot factories,
|
|
19
|
+
# +let+ blocks, and other test setup where audit noise is unwanted.
|
|
20
|
+
#
|
|
21
|
+
# @yield executes the block without recording any audit entries
|
|
22
|
+
# @return [Object] the return value of the block
|
|
3
23
|
def without_audit_log(&block)
|
|
4
24
|
RailsAuditLog.disable(&block)
|
|
5
25
|
end
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -1,39 +1,153 @@
|
|
|
1
1
|
require "rails_audit_log/version"
|
|
2
2
|
require "rails_audit_log/engine"
|
|
3
3
|
|
|
4
|
+
# RailsAuditLog is a Rails engine that tracks ActiveRecord +create+, +update+,
|
|
5
|
+
# and +destroy+ events as {AuditLogEntry} records with JSON-first storage and
|
|
6
|
+
# thread-local actor context.
|
|
7
|
+
#
|
|
8
|
+
# == Quick start
|
|
9
|
+
#
|
|
10
|
+
# # config/initializers/rails_audit_log.rb
|
|
11
|
+
# RailsAuditLog.configure do |config|
|
|
12
|
+
# config.ignored_attributes = %w[updated_at cached_at]
|
|
13
|
+
# config.store_snapshot = true
|
|
14
|
+
# config.async = false
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # app/models/article.rb
|
|
18
|
+
# class Article < ApplicationRecord
|
|
19
|
+
# include RailsAuditLog::Auditable
|
|
20
|
+
# audit_log only: %i[title body]
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # app/controllers/application_controller.rb
|
|
24
|
+
# class ApplicationController < ActionController::Base
|
|
25
|
+
# include RailsAuditLog::Controller
|
|
26
|
+
# audit_log_actor { current_user }
|
|
27
|
+
# end
|
|
4
28
|
module RailsAuditLog
|
|
5
|
-
#
|
|
6
|
-
#
|
|
29
|
+
# Columns ignored on every audited model unless overridden with +only:+ or
|
|
30
|
+
# +ignore:+ on {Auditable.audit_log}.
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<String>]
|
|
7
33
|
mattr_accessor :ignored_attributes, default: %w[updated_at]
|
|
8
34
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
35
|
+
# Whether to store a full snapshot of the record's attributes in the +object+
|
|
36
|
+
# column alongside +object_changes+. Disable to reduce storage at the cost of
|
|
37
|
+
# losing {AuditLogEntry#reify} fidelity for pre-snapshot entries.
|
|
38
|
+
#
|
|
39
|
+
# @return [Boolean]
|
|
12
40
|
mattr_accessor :store_snapshot, default: true
|
|
41
|
+
|
|
42
|
+
# When +true+, captures +remote_ip+ and +user_agent+ from the current request
|
|
43
|
+
# and merges them into every entry's +metadata+ column.
|
|
44
|
+
# Requires {Controller} to be included in your base controller.
|
|
45
|
+
#
|
|
46
|
+
# @return [Boolean]
|
|
13
47
|
mattr_accessor :capture_request_metadata, default: false
|
|
48
|
+
|
|
49
|
+
# Global cap on the number of {AuditLogEntry} records kept per tracked object.
|
|
50
|
+
# Oldest entries are pruned after each write once the limit is exceeded.
|
|
51
|
+
# Override per-model with <tt>audit_log version_limit: N</tt>.
|
|
52
|
+
#
|
|
53
|
+
# @return [Integer, nil]
|
|
14
54
|
mattr_accessor :version_limit, default: nil
|
|
55
|
+
|
|
56
|
+
# When +true+, all audit writes are dispatched via +WriteAuditLogJob+ instead
|
|
57
|
+
# of being written inline. Override per-model with <tt>audit_log async: true</tt>.
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean]
|
|
15
60
|
mattr_accessor :async, default: false
|
|
61
|
+
|
|
62
|
+
# Passes +connects_to+ options directly to {AuditLogEntry} so audit entries
|
|
63
|
+
# can be stored on a separate database.
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash, nil]
|
|
66
|
+
# @example
|
|
67
|
+
# RailsAuditLog.connects_to = { database: { writing: :audit_primary } }
|
|
16
68
|
mattr_accessor :connects_to, default: nil
|
|
69
|
+
|
|
70
|
+
# Number of entries per page in the web dashboard.
|
|
71
|
+
#
|
|
72
|
+
# @return [Integer]
|
|
73
|
+
mattr_accessor :page_size, default: 25
|
|
74
|
+
|
|
75
|
+
# Controls how an actor object is serialised into the +whodunnit_snapshot+
|
|
76
|
+
# string column. Defaults to +actor.name+ when available, otherwise +to_s+.
|
|
77
|
+
#
|
|
78
|
+
# @return [Proc]
|
|
79
|
+
# @example Store email instead of name
|
|
80
|
+
# RailsAuditLog.whodunnit_display = ->(actor) { actor.email }
|
|
17
81
|
mattr_accessor :whodunnit_display, default: ->(actor) {
|
|
18
82
|
actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
|
|
19
83
|
}
|
|
20
84
|
|
|
85
|
+
# Yields the module so every +mattr_accessor+ setter is reachable as
|
|
86
|
+
# <tt>config.setting = value</tt>.
|
|
87
|
+
#
|
|
88
|
+
# @yield [RailsAuditLog] the module itself
|
|
89
|
+
# @return [void]
|
|
90
|
+
# @example
|
|
91
|
+
# RailsAuditLog.configure do |config|
|
|
92
|
+
# config.ignored_attributes = %w[updated_at]
|
|
93
|
+
# config.async = true
|
|
94
|
+
# end
|
|
95
|
+
def self.configure
|
|
96
|
+
yield self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Sets or returns the authentication block used to gate the web dashboard.
|
|
100
|
+
# The block is evaluated in controller context, so controller helpers
|
|
101
|
+
# (e.g. +current_user+) are available directly.
|
|
102
|
+
# When the block returns falsy, the engine falls back to HTTP Basic auth.
|
|
103
|
+
#
|
|
104
|
+
# @yield block evaluated in controller context; return truthy to allow access
|
|
105
|
+
# @return [Proc, nil] the stored block, or +nil+ when not configured
|
|
106
|
+
# @example Require admin access
|
|
107
|
+
# RailsAuditLog.authenticate { current_user&.admin? }
|
|
108
|
+
def self.authenticate(&block)
|
|
109
|
+
@authenticate = block if block_given?
|
|
110
|
+
@authenticate
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns the request metadata hash attached to the current thread.
|
|
114
|
+
# Populated by {Controller} when {.capture_request_metadata} is +true+.
|
|
115
|
+
#
|
|
116
|
+
# @return [Hash, nil]
|
|
21
117
|
def self.request_metadata
|
|
22
118
|
Thread.current[:rails_audit_log_request_metadata]
|
|
23
119
|
end
|
|
24
120
|
|
|
121
|
+
# @param value [Hash, nil] metadata hash to store on the current thread
|
|
122
|
+
# @return [Hash, nil]
|
|
25
123
|
def self.request_metadata=(value)
|
|
26
124
|
Thread.current[:rails_audit_log_request_metadata] = value
|
|
27
125
|
end
|
|
28
126
|
|
|
127
|
+
# Returns the actor set on the current thread (e.g. the signed-in user).
|
|
128
|
+
#
|
|
129
|
+
# @return [Object, nil]
|
|
29
130
|
def self.actor
|
|
30
131
|
Thread.current[:rails_audit_log_actor]
|
|
31
132
|
end
|
|
32
133
|
|
|
134
|
+
# Sets the actor on the current thread. Prefer {.with_actor} for scoped
|
|
135
|
+
# assignment so the value is always restored.
|
|
136
|
+
#
|
|
137
|
+
# @param actor [Object, nil]
|
|
138
|
+
# @return [Object, nil]
|
|
33
139
|
def self.actor=(actor)
|
|
34
140
|
Thread.current[:rails_audit_log_actor] = actor
|
|
35
141
|
end
|
|
36
142
|
|
|
143
|
+
# Sets the actor for the duration of the block, then restores the previous
|
|
144
|
+
# value. Use this in background jobs and rake tasks.
|
|
145
|
+
#
|
|
146
|
+
# @param actor [Object] the actor to set (e.g. a +User+ record)
|
|
147
|
+
# @yield executes the block with +actor+ as the current actor
|
|
148
|
+
# @return [Object] the return value of the block
|
|
149
|
+
# @example
|
|
150
|
+
# RailsAuditLog.with_actor(robot_user) { DataImporter.new.run }
|
|
37
151
|
def self.with_actor(actor)
|
|
38
152
|
previous = self.actor
|
|
39
153
|
self.actor = actor
|
|
@@ -42,10 +156,20 @@ module RailsAuditLog
|
|
|
42
156
|
self.actor = previous
|
|
43
157
|
end
|
|
44
158
|
|
|
159
|
+
# Returns +true+ when audit logging is active on the current thread.
|
|
160
|
+
#
|
|
161
|
+
# @return [Boolean]
|
|
45
162
|
def self.enabled?
|
|
46
163
|
!Thread.current[:rails_audit_log_disabled]
|
|
47
164
|
end
|
|
48
165
|
|
|
166
|
+
# Suspends audit logging for the duration of the block on the current thread.
|
|
167
|
+
# Useful in seeds, factories, and test setup where audit noise is unwanted.
|
|
168
|
+
#
|
|
169
|
+
# @yield executes the block with audit logging disabled
|
|
170
|
+
# @return [Object] the return value of the block
|
|
171
|
+
# @example
|
|
172
|
+
# RailsAuditLog.disable { Post.create!(title: "seed post") }
|
|
49
173
|
def self.disable
|
|
50
174
|
previous = Thread.current[:rails_audit_log_disabled]
|
|
51
175
|
Thread.current[:rails_audit_log_disabled] = true
|
|
@@ -54,14 +178,27 @@ module RailsAuditLog
|
|
|
54
178
|
Thread.current[:rails_audit_log_disabled] = previous
|
|
55
179
|
end
|
|
56
180
|
|
|
181
|
+
# Returns the reason string set on the current thread.
|
|
182
|
+
#
|
|
183
|
+
# @return [String, nil]
|
|
57
184
|
def self.reason
|
|
58
185
|
Thread.current[:rails_audit_log_reason]
|
|
59
186
|
end
|
|
60
187
|
|
|
188
|
+
# @param value [String, nil]
|
|
189
|
+
# @return [String, nil]
|
|
61
190
|
def self.reason=(value)
|
|
62
191
|
Thread.current[:rails_audit_log_reason] = value
|
|
63
192
|
end
|
|
64
193
|
|
|
194
|
+
# Sets a human-readable reason for the changes made within the block.
|
|
195
|
+
# The reason is stored in each {AuditLogEntry#reason} and restored afterwards.
|
|
196
|
+
#
|
|
197
|
+
# @param value [String] reason to attach to every entry written in the block
|
|
198
|
+
# @yield executes the block with +value+ as the current reason
|
|
199
|
+
# @return [Object] the return value of the block
|
|
200
|
+
# @example
|
|
201
|
+
# RailsAuditLog.audit_log_reason("bulk import") { records.each(&:save!) }
|
|
65
202
|
def self.audit_log_reason(value)
|
|
66
203
|
previous = self.reason
|
|
67
204
|
self.reason = value
|
|
@@ -70,6 +207,18 @@ module RailsAuditLog
|
|
|
70
207
|
self.reason = previous
|
|
71
208
|
end
|
|
72
209
|
|
|
210
|
+
# Collects all {AuditLogEntry} records created within the block and inserts
|
|
211
|
+
# them with a single <tt>INSERT ... VALUES (…), (…)</tt> via +insert_all!+
|
|
212
|
+
# instead of one INSERT per record.
|
|
213
|
+
#
|
|
214
|
+
# Calls are idempotent: if a batch is already in progress on the current
|
|
215
|
+
# thread (i.e. a nested call), the inner block joins the outer batch.
|
|
216
|
+
#
|
|
217
|
+
# @yield executes the block; any audit entries created are buffered
|
|
218
|
+
# @return [Object] the return value of the block
|
|
219
|
+
# @raise [ActiveRecord::RecordInvalid] if any entry fails the +insert_all!+
|
|
220
|
+
# @example
|
|
221
|
+
# RailsAuditLog.batch_audit { 500.times { |i| Post.create!(title: "Post #{i}") } }
|
|
73
222
|
def self.batch_audit
|
|
74
223
|
return yield if Thread.current[:rails_audit_log_batch]
|
|
75
224
|
|
|
@@ -84,10 +233,29 @@ module RailsAuditLog
|
|
|
84
233
|
end
|
|
85
234
|
end
|
|
86
235
|
|
|
236
|
+
# Returns the in-progress batch buffer for the current thread, or +nil+ when
|
|
237
|
+
# no batch is active.
|
|
238
|
+
#
|
|
239
|
+
# @api private
|
|
240
|
+
# @return [Array<Hash>, nil]
|
|
87
241
|
def self.batch_audit_buffer
|
|
88
242
|
Thread.current[:rails_audit_log_batch]
|
|
89
243
|
end
|
|
90
244
|
|
|
245
|
+
# Reconstructs the state of +record+ as it was at +time+ by replaying audit
|
|
246
|
+
# entries up to that timestamp.
|
|
247
|
+
#
|
|
248
|
+
# Returns an unsaved, non-persisted instance of +record.class+ whose
|
|
249
|
+
# attributes match the record's state at +time+, or +nil+ when no audit
|
|
250
|
+
# entry exists before +time+ or the record was destroyed at or before +time+.
|
|
251
|
+
#
|
|
252
|
+
# @param record [ActiveRecord::Base] the record to reconstruct
|
|
253
|
+
# @param time [Time] the point in time to reconstruct at
|
|
254
|
+
# @return [ActiveRecord::Base, nil] a new, unpersisted instance; or +nil+
|
|
255
|
+
# @example
|
|
256
|
+
# post = Post.find(42)
|
|
257
|
+
# snapshot = RailsAuditLog.version_at(post, 1.week.ago)
|
|
258
|
+
# snapshot.title # => title as it was a week ago
|
|
91
259
|
def self.version_at(record, time)
|
|
92
260
|
entry = AuditLogEntry
|
|
93
261
|
.where(item_type: record.class.name, item_id: record.id)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_audit_log
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -23,9 +23,52 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.2'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: turbo-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: importmap-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.2'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: pagy
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '43.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '43.0'
|
|
68
|
+
description: Rails engine that tracks ActiveRecord create, update, and destroy events
|
|
69
|
+
as structured JSON records. Ships a mountable web dashboard, whodunnit actor context,
|
|
70
|
+
batch inserts via insert_all!, async writes via ActiveJob, time-travel reconstruction,
|
|
71
|
+
RSpec matchers, Minitest assertions, and a migration path from PaperTrail.
|
|
29
72
|
email:
|
|
30
73
|
- chuck@eclecticcoding.com
|
|
31
74
|
executables: []
|
|
@@ -35,25 +78,47 @@ files:
|
|
|
35
78
|
- MIT-LICENSE
|
|
36
79
|
- README.md
|
|
37
80
|
- Rakefile
|
|
81
|
+
- app/assets/stylesheets/rails_audit_log/_01_base.css
|
|
82
|
+
- app/assets/stylesheets/rails_audit_log/_02_layout.css
|
|
83
|
+
- app/assets/stylesheets/rails_audit_log/_03_table.css
|
|
84
|
+
- app/assets/stylesheets/rails_audit_log/_04_badges.css
|
|
85
|
+
- app/assets/stylesheets/rails_audit_log/_05_diff.css
|
|
86
|
+
- app/assets/stylesheets/rails_audit_log/_06_timeline.css
|
|
87
|
+
- app/assets/stylesheets/rails_audit_log/_07_detail.css
|
|
88
|
+
- app/assets/stylesheets/rails_audit_log/_08_pagination.css
|
|
89
|
+
- app/assets/stylesheets/rails_audit_log/_09_filters.css
|
|
38
90
|
- app/assets/stylesheets/rails_audit_log/application.css
|
|
39
91
|
- app/concerns/rails_audit_log/auditable.rb
|
|
40
92
|
- app/concerns/rails_audit_log/controller.rb
|
|
41
93
|
- app/controllers/rails_audit_log/application_controller.rb
|
|
94
|
+
- app/controllers/rails_audit_log/audit_log_entries_controller.rb
|
|
95
|
+
- app/controllers/rails_audit_log/resources_controller.rb
|
|
42
96
|
- app/helpers/rails_audit_log/application_helper.rb
|
|
97
|
+
- app/javascript/rails_audit_log/application.js
|
|
98
|
+
- app/javascript/rails_audit_log/diff_controller.js
|
|
99
|
+
- app/javascript/rails_audit_log/search_controller.js
|
|
43
100
|
- app/jobs/rails_audit_log/application_job.rb
|
|
44
101
|
- app/jobs/rails_audit_log/write_audit_log_job.rb
|
|
45
102
|
- app/models/rails_audit_log/application_record.rb
|
|
46
103
|
- app/models/rails_audit_log/audit_log_entry.rb
|
|
47
104
|
- app/views/layouts/rails_audit_log/application.html.erb
|
|
105
|
+
- app/views/rails_audit_log/audit_log_entries/_diff.html.erb
|
|
106
|
+
- app/views/rails_audit_log/audit_log_entries/index.html.erb
|
|
107
|
+
- app/views/rails_audit_log/audit_log_entries/show.html.erb
|
|
108
|
+
- app/views/rails_audit_log/resources/show.html.erb
|
|
109
|
+
- config/importmap.rb
|
|
48
110
|
- config/routes.rb
|
|
49
111
|
- lib/generators/rails_audit_log/initializer/initializer_generator.rb
|
|
50
112
|
- lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb
|
|
51
113
|
- lib/generators/rails_audit_log/install/install_generator.rb
|
|
52
114
|
- lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb
|
|
115
|
+
- lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb
|
|
116
|
+
- lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb
|
|
53
117
|
- lib/rails_audit_log.rb
|
|
54
118
|
- lib/rails_audit_log/engine.rb
|
|
55
119
|
- lib/rails_audit_log/matchers.rb
|
|
56
120
|
- lib/rails_audit_log/minitest_assertions.rb
|
|
121
|
+
- lib/rails_audit_log/paper_trail_compat.rb
|
|
57
122
|
- lib/rails_audit_log/test_helpers.rb
|
|
58
123
|
- lib/rails_audit_log/version.rb
|
|
59
124
|
- lib/tasks/rails_audit_log_tasks.rake
|
|
@@ -81,5 +146,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
81
146
|
requirements: []
|
|
82
147
|
rubygems_version: 3.6.9
|
|
83
148
|
specification_version: 4
|
|
84
|
-
summary:
|
|
149
|
+
summary: Audit logging for Rails with a web dashboard and JSON-first storage
|
|
85
150
|
test_files: []
|