auditor 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/README.rdoc +58 -11
- data/Rakefile +33 -0
- data/auditor.gemspec +23 -0
- data/init.rb +1 -0
- data/lib/auditor.rb +16 -4
- data/lib/auditor/audit.rb +49 -2
- data/lib/auditor/auditable.rb +44 -0
- data/lib/auditor/config.rb +38 -0
- data/lib/auditor/recorder.rb +37 -36
- data/lib/auditor/spec_helpers.rb +6 -6
- data/lib/auditor/status.rb +60 -0
- data/lib/auditor/user.rb +5 -6
- data/lib/auditor/version.rb +2 -2
- data/lib/generators/auditor/migration/migration_generator.rb +4 -4
- data/lib/generators/auditor/migration/templates/migration.rb +8 -4
- data/spec/audit_spec.rb +63 -0
- data/spec/auditable_spec.rb +111 -0
- data/spec/config_spec.rb +49 -0
- data/spec/recorder_spec.rb +73 -107
- data/spec/spec_helper.rb +2 -26
- data/spec/status_spec.rb +18 -0
- data/spec/support/db_setup.rb +50 -0
- data/spec/support/transactional_specs.rb +17 -0
- data/spec/user_spec.rb +5 -5
- metadata +70 -34
- data/lib/auditor/config_parser.rb +0 -36
- data/lib/auditor/integration.rb +0 -49
- data/lib/auditor/model_audit.rb +0 -47
- data/lib/auditor/thread_local.rb +0 -18
- data/lib/auditor/thread_status.rb +0 -34
- data/spec/config_parser_spec.rb +0 -53
- data/spec/model_audit_spec.rb +0 -83
- data/spec/support/auditor_helpers.rb +0 -29
- data/spec/thread_local_spec.rb +0 -14
- data/spec/thread_status_spec.rb +0 -16
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.rdoc
CHANGED
@@ -37,28 +37,24 @@ Auditor needs to know who the current user is, but with no standard for doing so
|
|
37
37
|
|
38
38
|
Auditor works very similarly to Joshua Clayton's acts_as_auditable plugin. There are two audit calls in the example below. The first declares that create and update actions should be audited for the EditablePage model and the string returned by the passed block should be included as a custom message. The second audit call simply changes the custom message when auditing destroy (aka delete) actions.
|
39
39
|
|
40
|
-
class
|
41
|
-
|
42
|
-
|
43
|
-
has_many :tags
|
44
|
-
|
45
|
-
audit(:create, :update) { |model, user| "Editable page modified by #{user.display_name}" }
|
46
|
-
audit(:destroy) { |model, user| "#{user.display_name} deleted editable page #{model.id}" }
|
40
|
+
class Page < ActiveRecord::Base
|
41
|
+
audit(:create, :update) { |model, user| "Page modified by #{user.display_name}" }
|
42
|
+
audit(:destroy) { |model, user| "#{user.display_name} deleted page #{model.id}" }
|
47
43
|
end
|
48
44
|
|
49
45
|
All audit data is stored in a table named Audits, which is automatically created for you when you run the migration included with the plugin. However, there's a lot more recorded than just the custom message, including:
|
50
46
|
|
51
47
|
* auditable_id - the primary key of the table belonging to the audited model object
|
52
48
|
* auditable_type - the class type of the audited model object
|
53
|
-
* auditable_version - the version number of the audited model object (if versioning is tracked)
|
54
49
|
* user_id - the primary key of the table belonging to the user being audited
|
55
50
|
* user_type - the class type of the model object representing users in your application
|
56
51
|
* action - a string indicating the action that was audited (create, update, destroy, or find)
|
57
|
-
*
|
58
|
-
*
|
52
|
+
* audited_changes - a YAML string containing the before and after state of any model attributes that changed
|
53
|
+
* comment - the custom message returned by any block passed to the audit call
|
54
|
+
* version - an auditor-internal revision number for the audited model
|
59
55
|
* created_at - the date and time the audit record was recorded
|
60
56
|
|
61
|
-
The
|
57
|
+
The audited_changes column automatically serializes the changes of any model attributes modified during the action. If there are only a few attributes you want to audit or a couple that you want to prevent from being audited, you can specify that in the audit call. For example
|
62
58
|
|
63
59
|
# Prevent SSN and passwords from being saved in the audit table
|
64
60
|
audit(:create, :destroy, :except => [:ssn, :password])
|
@@ -66,4 +62,55 @@ The edits column automatically serializes the before and after state of any mode
|
|
66
62
|
# Only audit edits to the title column when destroying/deleting
|
67
63
|
audit(:destroy, :only => :title)
|
68
64
|
|
65
|
+
= Make Auditing Important
|
66
|
+
|
67
|
+
There's an alternate form of specifying your audit requirements that will cause the create, find, update, or destroy to fail if for some reason the audit record cannot be saved to the database. Instead of calling audit, call audit! instead.
|
68
|
+
|
69
|
+
class Page < ActiveRecord::Base
|
70
|
+
audit!(:create, :update) { |model, user| "Page modified by #{user.display_name}" }
|
71
|
+
audit!(:destroy) { |model, user| "#{user.display_name} deleted page #{model.id}" }
|
72
|
+
end
|
73
|
+
|
74
|
+
= Auditable Versioning
|
75
|
+
|
76
|
+
Since auditor will keep a "diff" of all the changes applied to a model object, you can retrieve the state of any audited model object's attributes at any point in time. For this to work, you have to specify auditing for all actions that modify the table, which is create, update, and destroy. Assuming those attributes have been declared with a call to audit or audit!, the following shows you how to use the revisions.
|
77
|
+
|
78
|
+
p = Page.create(:title => "Revision 1")
|
79
|
+
p.audits.last.attribute_snapshot
|
80
|
+
> {:title => "Revision 1"}
|
81
|
+
time = Time.now
|
82
|
+
p.author = "Jeff"
|
83
|
+
p.save
|
84
|
+
p.audits.last.attribute_snapshot
|
85
|
+
> {:title => "Revision 1", :author => "Jeff"}
|
86
|
+
p.attributes_at(time)
|
87
|
+
> {:title => "Revision 1"}
|
88
|
+
|
89
|
+
= Integration
|
90
|
+
There may be some instances where you need to perform an action on your model object without Auditor recording the action. In those cases you can include the Auditor::Status module for help.
|
91
|
+
|
92
|
+
class PagesController < ApplicationController
|
93
|
+
include Auditor::Status
|
94
|
+
|
95
|
+
def update
|
96
|
+
page = Page.find(params[:id])
|
97
|
+
without_auditing { page.update_attributes(params[:page]) } # Auditor is disabled for the entire block
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
You can also force Auditor to audit any actions within a block as a specified user.
|
103
|
+
|
104
|
+
class PagesController < ApplicationController
|
105
|
+
include Auditor::Status
|
106
|
+
|
107
|
+
def update
|
108
|
+
page = Page.find(params[:id])
|
109
|
+
# Auditor will attribute update to 'another user'
|
110
|
+
audit_as(another_user) { page.update_attributes(params[:page]) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
69
116
|
Copyright (c) 2011 Near Infinity Corporation, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
$:.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
require 'bundler'
|
7
|
+
|
8
|
+
Bundler::GemHelper.install_tasks
|
9
|
+
|
10
|
+
desc 'Default: run specs'
|
11
|
+
task :default => :spec
|
12
|
+
|
13
|
+
desc "Run specs"
|
14
|
+
RSpec::Core::RakeTask.new do |t|
|
15
|
+
t.rspec_opts = %w(-fs --color)
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Run specs with RCov"
|
19
|
+
RSpec::Core::RakeTask.new(:rcov) do |t|
|
20
|
+
t.rspec_opts = %w(-fs --color)
|
21
|
+
t.rcov = true
|
22
|
+
t.rcov_opts = %w(--exclude "spec/*,gems/*")
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'Generate documentation for the gem.'
|
26
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
27
|
+
rdoc.rdoc_dir = 'rdoc'
|
28
|
+
rdoc.title = 'Auditor'
|
29
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
30
|
+
rdoc.rdoc_files.include('README.rdoc')
|
31
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
32
|
+
end
|
33
|
+
|
data/auditor.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "auditor/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "auditor"
|
7
|
+
s.version = Auditor::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Jeff Kunkle"]
|
10
|
+
s.homepage = "http://github.com/nearinfinity/auditor"
|
11
|
+
s.summary = %q{Rails 3 plugin for auditing access to your ActiveRecord model objects}
|
12
|
+
s.description = %q{Auditor allows you to declaratively specify what CRUD operations should be audited and save the audit data to the database.}
|
13
|
+
s.license = "MIT"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_development_dependency('rspec', '2.5.0')
|
21
|
+
s.add_development_dependency('sqlite3-ruby', '1.3.3')
|
22
|
+
s.add_development_dependency('activerecord', '> 3.0.0')
|
23
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'auditor'
|
data/lib/auditor.rb
CHANGED
@@ -1,9 +1,21 @@
|
|
1
1
|
require 'auditor/audit'
|
2
|
-
require 'auditor/
|
3
|
-
require 'auditor/model_audit'
|
4
|
-
require 'auditor/user'
|
5
|
-
require 'auditor/version'
|
2
|
+
require 'auditor/auditable'
|
6
3
|
|
7
4
|
module Auditor
|
8
5
|
class Error < StandardError; end
|
9
6
|
end
|
7
|
+
|
8
|
+
ActiveRecord::Base.send :include, Auditor::Auditable
|
9
|
+
|
10
|
+
if defined?(ActionController) and defined?(ActionController::Base)
|
11
|
+
|
12
|
+
require 'auditor/user'
|
13
|
+
|
14
|
+
ActionController::Base.class_eval do
|
15
|
+
before_filter do
|
16
|
+
Auditor::User.current_user = self.current_user if self.respond_to?(:current_user)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
data/lib/auditor/audit.rb
CHANGED
@@ -1,6 +1,53 @@
|
|
1
1
|
require 'active_record'
|
2
|
+
require 'auditor/config'
|
2
3
|
|
3
4
|
class Audit < ActiveRecord::Base
|
4
|
-
|
5
|
-
|
5
|
+
belongs_to :auditable, :polymorphic => true
|
6
|
+
belongs_to :user, :polymorphic => true
|
7
|
+
|
8
|
+
before_create :set_version_number
|
9
|
+
before_create :set_user
|
10
|
+
|
11
|
+
serialize :audited_changes
|
12
|
+
|
13
|
+
default_scope order(:version, :created_at)
|
14
|
+
scope :modifying, lambda { where('action in (?)', Auditor::Config.modifying_actions) }
|
15
|
+
scope :trail, lambda { |audit|
|
16
|
+
where('auditable_id = ? and auditable_type = ? and version <= ?',
|
17
|
+
audit.auditable_id, audit.auditable_type, audit.version)
|
18
|
+
}
|
19
|
+
|
20
|
+
def attribute_snapshot
|
21
|
+
attributes = {}.with_indifferent_access
|
22
|
+
self.class.modifying.trail(self).each do |predecessor|
|
23
|
+
attributes.merge!(predecessor.new_attributes)
|
24
|
+
end
|
25
|
+
attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
# Returns a hash of the changed attributes with the new values
|
31
|
+
def new_attributes
|
32
|
+
(audited_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
|
33
|
+
attrs[attr] = values.is_a?(Array) ? values.last : values
|
34
|
+
attrs
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def set_version_number
|
41
|
+
max = self.class.where(
|
42
|
+
:auditable_id => auditable_id,
|
43
|
+
:auditable_type => auditable_type
|
44
|
+
).maximum(:version) || 0
|
45
|
+
|
46
|
+
self.version = Auditor::Config.modifying_actions.include?(self.action.to_sym) ? max + 1 : max
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_user
|
50
|
+
self.user = Auditor::User.current_user if self.user_id.nil?
|
51
|
+
end
|
52
|
+
|
6
53
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'auditor/status'
|
2
|
+
require 'auditor/config'
|
3
|
+
require 'auditor/recorder'
|
4
|
+
|
5
|
+
module Auditor
|
6
|
+
module Auditable
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def audit(*args, &blk)
|
14
|
+
unless self.included_modules.include?(Auditor::Auditable::InstanceMethods)
|
15
|
+
include InstanceMethods
|
16
|
+
include Auditor::Status unless self.included_modules.include?(Auditor::Status)
|
17
|
+
has_many :audits, :as => :auditable
|
18
|
+
end
|
19
|
+
|
20
|
+
config = Auditor::Config.new(*args)
|
21
|
+
config.actions.each do |action|
|
22
|
+
send "after_#{action}", Auditor::Recorder.new(config.options, &blk)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def audit!(*args, &blk)
|
27
|
+
if args.last.kind_of?(Hash)
|
28
|
+
args.last[:fail_on_error] = true
|
29
|
+
else
|
30
|
+
args << { :fail_on_error => true }
|
31
|
+
end
|
32
|
+
|
33
|
+
audit(*args, &blk)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module InstanceMethods
|
38
|
+
def attributes_at(date_or_time)
|
39
|
+
audits.where('created_at <= ?', date_or_time).last.attribute_snapshot
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Auditor
|
2
|
+
class Config
|
3
|
+
attr_reader :actions
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def self.valid_actions
|
7
|
+
@valid_actions ||= [:create, :find, :update, :destroy]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.modifying_actions
|
11
|
+
@modifying_actions ||= [:create, :update, :destroy]
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
@options = (args.pop if args.last.kind_of?(Hash)) || {}
|
16
|
+
normalize_options(@options)
|
17
|
+
|
18
|
+
@actions = args.map(&:to_sym)
|
19
|
+
validate_actions(@actions)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def normalize_options(options)
|
25
|
+
options.each_pair { |k, v| options[k.to_sym] = options.delete(k) unless k.kind_of? Symbol }
|
26
|
+
options[:only] ||= []
|
27
|
+
options[:except] ||= []
|
28
|
+
options[:only] = Array(options[:only]).map(&:to_s)
|
29
|
+
options[:except] = Array(options[:except]).map(&:to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_actions(actions)
|
33
|
+
raise Auditor::Error.new "at least one action in #{Config.valid_actions.inspect} must be specified" if actions.empty?
|
34
|
+
raise Auditor::Error.new "#{Config.valid_actions.inspect} are the only valid actions" unless actions.all? { |a| Config.valid_actions.include?(a.to_sym) }
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/auditor/recorder.rb
CHANGED
@@ -1,44 +1,45 @@
|
|
1
|
-
require 'auditor/
|
1
|
+
require 'auditor/status'
|
2
2
|
|
3
3
|
module Auditor
|
4
4
|
class Recorder
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
include Status
|
6
|
+
|
7
|
+
def initialize(options, &blk)
|
8
|
+
@options = options
|
9
|
+
@blk = blk
|
8
10
|
end
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
|
12
|
+
[:create, :find, :update, :destroy].each do |action|
|
13
|
+
define_method("after_#{action}") do |model|
|
14
|
+
audit(model, action)
|
15
|
+
end
|
12
16
|
end
|
13
17
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@audit.
|
27
|
-
|
28
|
-
@audit.save
|
18
|
+
private
|
19
|
+
|
20
|
+
def audit(model, action)
|
21
|
+
return nil if auditing_disabled?
|
22
|
+
user = Auditor::User.current_user
|
23
|
+
|
24
|
+
audit = Audit.new
|
25
|
+
audit.auditable = model
|
26
|
+
audit.audited_changes = prepare_changes(model.changes) if changes_available?(action)
|
27
|
+
audit.action = action
|
28
|
+
audit.comment = @blk.call(model, user) if @blk
|
29
|
+
|
30
|
+
@options[:fail_on_error] ? audit.save! : audit.save
|
29
31
|
end
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
32
|
+
|
33
|
+
def prepare_changes(changes)
|
34
|
+
chg = changes.dup
|
35
|
+
chg = chg.delete_if { |key, value| @options[:except].include?(key) } unless @options[:except].empty?
|
36
|
+
chg = chg.delete_if { |key, value| !@options[:only].include?(key) } unless @options[:only].empty?
|
37
|
+
chg.empty? ? nil : chg
|
38
|
+
end
|
39
|
+
|
40
|
+
def changes_available?(action)
|
41
|
+
[:create, :update].include?(action)
|
42
|
+
end
|
43
|
+
|
43
44
|
end
|
44
|
-
end
|
45
|
+
end
|
data/lib/auditor/spec_helpers.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
module Auditor
|
2
2
|
module SpecHelpers
|
3
|
-
include Auditor::
|
4
|
-
|
3
|
+
include Auditor::Status
|
4
|
+
|
5
5
|
def self.included(base)
|
6
6
|
base.class_eval do
|
7
7
|
before(:each) do
|
8
|
-
|
8
|
+
disable_auditing
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
after(:each) do
|
12
|
-
|
12
|
+
enable_auditing
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Auditor
|
2
|
+
module Status
|
3
|
+
|
4
|
+
def auditing_disabled?
|
5
|
+
Thread.current[:auditor_disabled] == true
|
6
|
+
end
|
7
|
+
|
8
|
+
def auditing_enabled?
|
9
|
+
Thread.current[:auditor_disabled] == false
|
10
|
+
end
|
11
|
+
|
12
|
+
def disable_auditing
|
13
|
+
Thread.current[:auditor_disabled] = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def enable_auditing
|
17
|
+
Thread.current[:auditor_disabled] = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def without_auditing
|
21
|
+
previously_disabled = auditing_disabled?
|
22
|
+
|
23
|
+
begin
|
24
|
+
disable_auditing
|
25
|
+
result = yield if block_given?
|
26
|
+
ensure
|
27
|
+
enable_auditing unless previously_disabled
|
28
|
+
end
|
29
|
+
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_auditing
|
34
|
+
previously_disabled = auditing_disabled?
|
35
|
+
|
36
|
+
begin
|
37
|
+
enable_auditing
|
38
|
+
result = yield if block_given?
|
39
|
+
ensure
|
40
|
+
disable_auditing if previously_disabled
|
41
|
+
end
|
42
|
+
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
def audit_as(user)
|
47
|
+
previous_user = Audit::User.current_user
|
48
|
+
|
49
|
+
begin
|
50
|
+
Audit::User.current_user = user
|
51
|
+
result = yield if block_given?
|
52
|
+
ensure
|
53
|
+
Audit::User.current_user = previous_user
|
54
|
+
end
|
55
|
+
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|