sequel-auditer 0.1.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 +7 -0
- data/.gitignore +12 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +614 -0
- data/Rakefile +33 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/env.test.sample +7 -0
- data/lib/sequel/auditer.rb +37 -0
- data/lib/sequel/auditer/railtie.rb +26 -0
- data/lib/sequel/auditer/version.rb +5 -0
- data/lib/sequel/plugins/auditer.rb +325 -0
- data/lib/tasks/sequel-auditer/migrate.rake +18 -0
- data/lib/tasks/sequel-auditer/templates/audited_migration.rb +21 -0
- data/sequel-auditer.gemspec +39 -0
- metadata +200 -0
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
#
|
4
|
+
#
|
5
|
+
OSX = RUBY_PLATFORM.match(/darwin/)
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
Rake::TestTask.new(:spec) do |t|
|
10
|
+
t.libs << 'spec'
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :spec
|
16
|
+
|
17
|
+
desc 'Run specs with coverage'
|
18
|
+
task :coverage do
|
19
|
+
ENV['COVERAGE'] = '1'
|
20
|
+
Rake::Task['spec'].invoke
|
21
|
+
`open coverage/index.html` if OSX
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Run Rubocop report'
|
25
|
+
task :rubocop do
|
26
|
+
res = `which rubocop`
|
27
|
+
if res != ""
|
28
|
+
`rubocop -f html -o ./rubocop/report.html lib/`
|
29
|
+
`open rubocop/report.html` if OSX
|
30
|
+
else
|
31
|
+
puts "\nERROR: 'rubocop' gem is not installed or available. Please run 'gem install rubocop'."
|
32
|
+
end
|
33
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "sequel/auditer"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/env.test.sample
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "sequel/auditer/railtie"
|
2
|
+
require "sequel/auditer/version"
|
3
|
+
|
4
|
+
|
5
|
+
module Sequel
|
6
|
+
|
7
|
+
#
|
8
|
+
module Auditer
|
9
|
+
|
10
|
+
CREATE = 'create'
|
11
|
+
UPDATE = 'update'
|
12
|
+
DESTROY = 'destroy'
|
13
|
+
|
14
|
+
# set the name of the global method that provides the current user. Default: :current_user
|
15
|
+
@auditer_current_user_method = :current_user
|
16
|
+
# set any additional info such as :ip, :user_agent, ...
|
17
|
+
@auditer_additional_info_method = :additional_info
|
18
|
+
# enable swapping of the Audit model
|
19
|
+
@auditer_model_name = :AuditLog
|
20
|
+
# toggle for enabling / disabling auditing
|
21
|
+
@auditer_enabled = true
|
22
|
+
|
23
|
+
# by default ignore these columns
|
24
|
+
@auditer_default_ignored_columns = [
|
25
|
+
# :id, :ref, :password, :password_hash,
|
26
|
+
:lock_version,
|
27
|
+
:created_at, :updated_at, :created_on, :updated_on
|
28
|
+
]
|
29
|
+
|
30
|
+
class << self
|
31
|
+
attr_accessor :auditer_current_user_method, :auditer_additional_info_method,
|
32
|
+
:auditer_model_name, :auditer_enabled,
|
33
|
+
:auditer_default_ignored_columns
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Auditer
|
3
|
+
class Middleware
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
Sequel::Auditer::Railtie.env = env
|
10
|
+
@app.call(env)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Railtie < ::Rails::Engine
|
15
|
+
initializer "sequel-auditer_railtie.configure_rails_initialization" do |app|
|
16
|
+
app.middleware.use Sequel::Auditer::Middleware
|
17
|
+
end
|
18
|
+
attr_accessor :env
|
19
|
+
|
20
|
+
def self.user
|
21
|
+
return env['warden'].user if env && env.key?('warden')
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
require_relative '../auditer'
|
2
|
+
|
3
|
+
class AuditLog < Sequel::Model
|
4
|
+
# handle versioning of audited records
|
5
|
+
plugin :list, field: :version, scope: [:associated_type, :associated_id]
|
6
|
+
plugin :timestamps
|
7
|
+
plugin :polymorphic
|
8
|
+
|
9
|
+
# TODO: see if we should add these
|
10
|
+
many_to_one :associated, polymorphic: true
|
11
|
+
many_to_one :modifier, polymorphic: true
|
12
|
+
|
13
|
+
def before_validation
|
14
|
+
# grab the current user
|
15
|
+
if u = audit_user
|
16
|
+
self.modifier = u
|
17
|
+
end
|
18
|
+
|
19
|
+
# grab any additional info if any
|
20
|
+
if i = audit_additional_info
|
21
|
+
self.additional_info = i
|
22
|
+
end
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
# private
|
28
|
+
|
29
|
+
# Obtains the `current_user` based upon the `:auditer_current_user_method' value set in the
|
30
|
+
# audited model, either via defaults or via :user_method config options
|
31
|
+
#
|
32
|
+
# # NOTE! this allows overriding the default value on a per audited model
|
33
|
+
def audit_user
|
34
|
+
user = ::Sequel::Auditer::Railtie.user
|
35
|
+
return user if !user.nil?
|
36
|
+
|
37
|
+
begin
|
38
|
+
m = Kernel.const_get(associated_type)
|
39
|
+
m.send(m.auditer_current_user_method) || send(m.auditer_current_user_method)
|
40
|
+
rescue
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def audit_additional_info
|
46
|
+
begin
|
47
|
+
m = Kernel.const_get(associated_type)
|
48
|
+
m.send(m.auditer_additional_info_method) || send(m. auditer_additional_info_method)
|
49
|
+
rescue
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
module Sequel
|
57
|
+
module Plugins
|
58
|
+
|
59
|
+
# Given a Post model with these fields:
|
60
|
+
# [:id, :category_id, :title, :body, :author_id, :created_at, :updated_at]
|
61
|
+
#
|
62
|
+
#
|
63
|
+
# All fields
|
64
|
+
# plugin :auditer
|
65
|
+
# #=> [:category_id, :title, :body, :author_id] # NB! excluding @default_ignore_attrs
|
66
|
+
# #=> [:id, :created_at, :updated_at]
|
67
|
+
#
|
68
|
+
# Single field
|
69
|
+
# plugin :auditer, only: :title
|
70
|
+
# plugin :auditer, only: [:title]
|
71
|
+
# #=> [:title]
|
72
|
+
# #+> [:id, :category_id, :body, :author_id, :created_at, :updated_at] # ignored fields
|
73
|
+
#
|
74
|
+
# Multiple fields
|
75
|
+
# plugin :auditer, only: [:title, :body]
|
76
|
+
# #=> [:title, :body] # tracked fields
|
77
|
+
# #=> [:id, :category_id, :author_id, :created_at, :updated_at] # ignored fields
|
78
|
+
#
|
79
|
+
#
|
80
|
+
# All fields except certain fields
|
81
|
+
# plugin :auditer, except: :title
|
82
|
+
# plugin :auditer, except: [:title]
|
83
|
+
# #=> [:id, :category_id, :author_id, :created_at, :updated_at] # tracked fields
|
84
|
+
# #=> [:title] # ignored fields
|
85
|
+
#
|
86
|
+
#
|
87
|
+
#
|
88
|
+
module Auditer
|
89
|
+
|
90
|
+
# called when
|
91
|
+
def self.configure(model, opts = {})
|
92
|
+
model.instance_eval do
|
93
|
+
# add support for :dirty attributes tracking & JSON serializing of data
|
94
|
+
plugin(:dirty)
|
95
|
+
plugin(:json_serializer)
|
96
|
+
plugin(:polymorphic)
|
97
|
+
|
98
|
+
# set the default ignored columns or revert to defaults
|
99
|
+
set_default_ignored_columns(opts)
|
100
|
+
# sets the name of the current User method or revert to default: :current_user
|
101
|
+
# specifically for the audited model on a per model basis
|
102
|
+
set_user_method(opts)
|
103
|
+
set_additional_info_method(opts)
|
104
|
+
|
105
|
+
set_reference_method(opts)
|
106
|
+
|
107
|
+
only = opts.fetch(:only, [])
|
108
|
+
except = opts.fetch(:except, [])
|
109
|
+
|
110
|
+
unless only.empty?
|
111
|
+
# we should only track the provided column
|
112
|
+
included_columns = [only].flatten
|
113
|
+
# subtract the 'only' columns from all columns to get excluded_columns
|
114
|
+
excluded_columns = columns - included_columns
|
115
|
+
else # except:
|
116
|
+
# all columns minus any excepted columns and default ignored columns
|
117
|
+
included_columns = [
|
118
|
+
[columns - [except].flatten].flatten - @auditer_default_ignored_columns
|
119
|
+
].flatten.uniq
|
120
|
+
|
121
|
+
# except_columns = except.empty? ? [] : [except].flatten
|
122
|
+
excluded_columns = [columns - included_columns].flatten.uniq
|
123
|
+
# excluded_columns = [columns - [except_columns, included_columns].flatten].flatten.uniq
|
124
|
+
end
|
125
|
+
|
126
|
+
@auditer_included_columns = included_columns
|
127
|
+
@auditer_ignored_columns = excluded_columns
|
128
|
+
|
129
|
+
# each included model will have an associated versions
|
130
|
+
one_to_many(
|
131
|
+
:versions,
|
132
|
+
class: audit_model_name,
|
133
|
+
as: 'associated'
|
134
|
+
)
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
module ClassMethods
|
143
|
+
|
144
|
+
attr_accessor :auditer_default_ignored_columns, :auditer_current_user_method, :auditer_additional_info_method
|
145
|
+
# The holder of ignored columns
|
146
|
+
attr_reader :auditer_ignored_columns
|
147
|
+
# The holder of columns that should be audited
|
148
|
+
attr_reader :auditer_included_columns
|
149
|
+
|
150
|
+
attr_accessor :auditer_reference_method
|
151
|
+
|
152
|
+
|
153
|
+
Plugins.inherited_instance_variables(self,
|
154
|
+
:@auditer_default_ignored_columns => nil,
|
155
|
+
:@auditer_current_user_method => nil,
|
156
|
+
:@auditer_additional_info_method => nil,
|
157
|
+
:@auditer_included_columns => nil,
|
158
|
+
:@auditer_ignored_columns => nil,
|
159
|
+
:@auditer_reference_method => nil
|
160
|
+
)
|
161
|
+
|
162
|
+
def non_audited_columns
|
163
|
+
columns - auditer_columns
|
164
|
+
end
|
165
|
+
|
166
|
+
def auditer_columns
|
167
|
+
@auditer_columns ||= columns - @auditer_ignored_columns
|
168
|
+
end
|
169
|
+
|
170
|
+
# def default_ignored_attrs
|
171
|
+
# # TODO: how to reference the models primary_key value??
|
172
|
+
# arr = [pk.to_s]
|
173
|
+
# # handle STI (Class Table Inheritance) models with `plugin :single_table_inheritance`
|
174
|
+
# arr << 'sti_key' if self.respond_to?(:sti_key)
|
175
|
+
# arr
|
176
|
+
# end
|
177
|
+
|
178
|
+
#
|
179
|
+
# returns true / false if any audits have been made
|
180
|
+
#
|
181
|
+
# Post.auditer_versions? #=> true / false
|
182
|
+
#
|
183
|
+
def auditer_versions?
|
184
|
+
audit_model.where(associated_type: name.to_s).count >= 1
|
185
|
+
end
|
186
|
+
|
187
|
+
# grab all audits for a particular model based upon filters
|
188
|
+
#
|
189
|
+
# Posts.auditer_versions(:model_pk => 123)
|
190
|
+
# #=> filtered by primary_key value
|
191
|
+
#
|
192
|
+
# Posts.auditer_versions(:user_id => 88)
|
193
|
+
# #=> filtered by user name
|
194
|
+
#
|
195
|
+
# Posts.auditer_versions(:created_at < Date.today - 2)
|
196
|
+
# #=> filtered to last two (2) days only
|
197
|
+
#
|
198
|
+
# Posts.auditer_versions(:created_at > Date.today - 7)
|
199
|
+
# #=> filtered to older than last seven (7) days
|
200
|
+
#
|
201
|
+
def auditer_versions(opts = {})
|
202
|
+
audit_model.where(opts.merge(associated_type: name.to_s)).order(:version).all
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
|
209
|
+
def audit_model
|
210
|
+
const_get(audit_model_name)
|
211
|
+
end
|
212
|
+
|
213
|
+
def audit_model_name
|
214
|
+
::Sequel::Auditer.auditer_model_name
|
215
|
+
end
|
216
|
+
|
217
|
+
def set_default_ignored_columns(opts)
|
218
|
+
if opts[:default_ignored_columns]
|
219
|
+
@auditer_default_ignored_columns = opts[:default_ignored_columns]
|
220
|
+
else
|
221
|
+
@auditer_default_ignored_columns = ::Sequel::Auditer.auditer_default_ignored_columns
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def set_user_method(opts)
|
226
|
+
if opts[:user_method]
|
227
|
+
@auditer_current_user_method = opts[:user_method]
|
228
|
+
else
|
229
|
+
@auditer_current_user_method = ::Sequel::Auditer.auditer_current_user_method
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def set_additional_info_method(opts)
|
234
|
+
if opts[:additional_info]
|
235
|
+
@auditer_additional_info_method = opts[:additional_info]
|
236
|
+
else
|
237
|
+
@auditer_additional_info_method = ::Sequel::Auditer.auditer_additional_info_method
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def set_reference_method(opts)
|
242
|
+
if opts[:reference_method]
|
243
|
+
@auditer_reference_method = opts[:reference_method]
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
#
|
251
|
+
module InstanceMethods
|
252
|
+
|
253
|
+
# Returns who put the post into its current state.
|
254
|
+
#
|
255
|
+
# post.blame # => 'joeblogs'
|
256
|
+
#
|
257
|
+
# post.last_audited_by # => 'joeblogs'
|
258
|
+
#
|
259
|
+
# Note! returns 'not audited' if there's no audited version (new unsaved record)
|
260
|
+
#
|
261
|
+
def blame
|
262
|
+
v = versions.last unless versions.empty?
|
263
|
+
v ? v.modifier : 'not audited'
|
264
|
+
end
|
265
|
+
alias_method :last_audited_by, :blame
|
266
|
+
|
267
|
+
# Returns who put the post into its current state.
|
268
|
+
#
|
269
|
+
# post.last_audited_at # => '2015-12-19 @ 08:24:45'
|
270
|
+
#
|
271
|
+
# post.last_audited_on # => 'joeblogs'
|
272
|
+
#
|
273
|
+
# Note! returns 'not audited' if there's no audited version (new unsaved record)
|
274
|
+
#
|
275
|
+
def last_audited_at
|
276
|
+
v = versions.last unless versions.empty?
|
277
|
+
v ? v.created_at : 'not audited'
|
278
|
+
end
|
279
|
+
alias_method :last_audited_on, :last_audited_at
|
280
|
+
|
281
|
+
private
|
282
|
+
|
283
|
+
# extract audited values only
|
284
|
+
def auditer_values(event)
|
285
|
+
vals = case event
|
286
|
+
when Sequel::Auditer::CREATE
|
287
|
+
self.values
|
288
|
+
when Sequel::Auditer::UPDATE
|
289
|
+
(column_changes.empty? ? previous_changes : column_changes)
|
290
|
+
when Sequel::Auditer::DESTROY
|
291
|
+
self.values
|
292
|
+
end
|
293
|
+
vals.except(*model.auditer_default_ignored_columns)
|
294
|
+
end
|
295
|
+
|
296
|
+
def add_audited(event)
|
297
|
+
changed = auditer_values(event)
|
298
|
+
unless changed.blank?
|
299
|
+
add_version(
|
300
|
+
event: event,
|
301
|
+
changed: changed
|
302
|
+
)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
### CALLBACKS ###
|
307
|
+
|
308
|
+
def after_create
|
309
|
+
super
|
310
|
+
add_audited(Sequel::Auditer::CREATE)
|
311
|
+
end
|
312
|
+
|
313
|
+
def after_update
|
314
|
+
super
|
315
|
+
add_audited(Sequel::Auditer::UPDATE)
|
316
|
+
end
|
317
|
+
|
318
|
+
def after_destroy
|
319
|
+
super
|
320
|
+
add_audited(Sequel::Auditer::DESTROY)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|