auditor 1.0.0 → 2.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.
- 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/lib/auditor/user.rb
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
module Auditor
|
2
2
|
module User
|
3
|
+
|
3
4
|
def current_user
|
4
|
-
Thread.current[
|
5
|
+
Thread.current[:auditor_user]
|
5
6
|
end
|
6
7
|
|
7
8
|
def current_user=(user)
|
8
|
-
Thread.current[
|
9
|
+
Thread.current[:auditor_user] = user
|
9
10
|
end
|
10
|
-
|
11
|
+
|
11
12
|
module_function :current_user, :current_user=
|
12
13
|
|
13
|
-
private
|
14
|
-
@@current_user_symbol = :auditor_current_user
|
15
14
|
end
|
16
|
-
end
|
15
|
+
end
|
data/lib/auditor/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Auditor
|
2
|
-
VERSION = "
|
3
|
-
end
|
2
|
+
VERSION = "2.0.0"
|
3
|
+
end
|
@@ -5,11 +5,11 @@ module Auditor
|
|
5
5
|
module Generators
|
6
6
|
class MigrationGenerator < Rails::Generators::Base
|
7
7
|
include Rails::Generators::Migration
|
8
|
-
|
8
|
+
|
9
9
|
desc "Create migration for Auditor audits table"
|
10
|
-
|
10
|
+
|
11
11
|
source_root File.expand_path("../templates", __FILE__)
|
12
|
-
|
12
|
+
|
13
13
|
def self.next_migration_number(dirname)
|
14
14
|
if ActiveRecord::Base.timestamped_migrations
|
15
15
|
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
@@ -23,4 +23,4 @@ module Auditor
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
26
|
-
end
|
26
|
+
end
|
@@ -3,17 +3,21 @@ class CreateAuditsTable < ActiveRecord::Migration
|
|
3
3
|
create_table :audits, :force => true do |t|
|
4
4
|
t.column :auditable_id, :integer, :null => false
|
5
5
|
t.column :auditable_type, :string, :null => false
|
6
|
-
t.column :auditable_version, :integer
|
7
6
|
t.column :user_id, :integer, :null => false
|
8
7
|
t.column :user_type, :string, :null => false
|
9
8
|
t.column :action, :string, :null => false
|
10
|
-
t.column :
|
11
|
-
t.column :
|
9
|
+
t.column :audited_changes, :text
|
10
|
+
t.column :version, :integer, :default => 0
|
11
|
+
t.column :comment, :text
|
12
12
|
t.column :created_at, :datetime, :null => false
|
13
13
|
end
|
14
|
+
|
15
|
+
add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
|
16
|
+
add_index :audits, [:user_id, :user_type], :name => 'user_index'
|
17
|
+
add_index :audits, :created_at
|
14
18
|
end
|
15
19
|
|
16
20
|
def self.down
|
17
21
|
drop_table :audits
|
18
22
|
end
|
19
|
-
end
|
23
|
+
end
|
data/spec/audit_spec.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor/audit'
|
3
|
+
|
4
|
+
describe Audit do
|
5
|
+
before(:each) do
|
6
|
+
@auditable = Model.create
|
7
|
+
@user = User.create
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should set the version number on save' do
|
11
|
+
audit = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
|
12
|
+
audit.version.should == 1
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should provide access to the audited model object' do
|
16
|
+
audit = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
|
17
|
+
audit.auditable.should == @auditable
|
18
|
+
end
|
19
|
+
it 'should provide access to the user associated with the audit' do
|
20
|
+
audit = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
|
21
|
+
audit.user.should == @user
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should create a snapshot of the audited objects attributes at the time of the audit' do
|
25
|
+
audit1 = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
|
26
|
+
audit2 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => [nil, 'n1'], 'value' => [nil, 'v1']})
|
27
|
+
audit3 = Audit.create(:auditable => @auditable, :user => @user, :action => :find)
|
28
|
+
audit4 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'value' => [nil, 'v2']})
|
29
|
+
|
30
|
+
audit1.attribute_snapshot.should == {}
|
31
|
+
audit2.attribute_snapshot.should == {'name' => 'n1', 'value' => 'v1'}
|
32
|
+
audit3.attribute_snapshot.should == {'name' => 'n1', 'value' => 'v1'}
|
33
|
+
audit4.attribute_snapshot.should == {'name' => 'n1', 'value' => 'v2'}
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'modifying scope' do
|
37
|
+
it 'should return all audit records that were a result of modifying the audited object attributes' do
|
38
|
+
audit1 = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
|
39
|
+
audit2 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => [nil, 'n1'], 'value' => [nil, 'v1']})
|
40
|
+
audit3 = Audit.create(:auditable => @auditable, :user => @user, :action => :find)
|
41
|
+
audit4 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'value' => [nil, 'v2']})
|
42
|
+
|
43
|
+
Audit.modifying.should include(audit1, audit2, audit4)
|
44
|
+
Audit.modifying.should_not include(audit3)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'predecessors scope' do
|
49
|
+
it 'should return all previous audit records for the same auditable' do
|
50
|
+
auditable2 = Model.create
|
51
|
+
audit1 = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
|
52
|
+
audit2 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => [nil, 'n1'], 'value' => [nil, 'v1']})
|
53
|
+
audit3 = Audit.create(:auditable => auditable2, :user => @user, :action => :find)
|
54
|
+
audit4 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'value' => [nil, 'v2']})
|
55
|
+
|
56
|
+
Audit.trail(audit4).should include(audit1, audit2, audit4)
|
57
|
+
Audit.trail(audit4).should_not include(audit3)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Model < ActiveRecord::Base; end
|
63
|
+
class User < ActiveRecord::Base; end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor/user'
|
3
|
+
require 'auditor/status'
|
4
|
+
|
5
|
+
describe Auditor::Auditable do
|
6
|
+
include Auditor::Status
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
@user = User.create
|
10
|
+
Auditor::User.current_user = @user
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should audit find' do
|
14
|
+
redefine_model { audit!(:find) }
|
15
|
+
m = without_auditing { Model.create }
|
16
|
+
|
17
|
+
Model.find(m.id)
|
18
|
+
|
19
|
+
verify_audit(Audit.last, m)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should audit create' do
|
23
|
+
redefine_model { audit!(:create) }
|
24
|
+
|
25
|
+
m = Model.create(:name => 'new')
|
26
|
+
|
27
|
+
verify_audit(Audit.last, m, { 'name' => [nil, 'new'], 'id' => [nil, m.id] })
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should audit update' do
|
31
|
+
redefine_model { audit!(:update) }
|
32
|
+
m = without_auditing { Model.create(:name => 'new') }
|
33
|
+
|
34
|
+
m.update_attributes(:name => 'newer')
|
35
|
+
|
36
|
+
verify_audit(Audit.last, m, { 'name' => ['new', 'newer'] })
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should audit destroy' do
|
40
|
+
redefine_model { audit!(:destroy) }
|
41
|
+
m = without_auditing { Model.create }
|
42
|
+
|
43
|
+
m.destroy
|
44
|
+
|
45
|
+
verify_audit(Audit.last, m)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should allow multiple actions to be specified with one audit statment' do
|
49
|
+
redefine_model { audit!(:create, :destroy) }
|
50
|
+
|
51
|
+
m = Model.create
|
52
|
+
m.reload
|
53
|
+
m = Model.find(m.id)
|
54
|
+
m.update_attributes({:name => 'new'})
|
55
|
+
m.destroy
|
56
|
+
|
57
|
+
Audit.count.should == 2
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should record the comment returned from a comment block' do
|
61
|
+
redefine_model { audit!(:create) { 'comment' } }
|
62
|
+
Model.create
|
63
|
+
Audit.last.comment.should == 'comment'
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should provide the model object and user as parameters to the comment block' do
|
67
|
+
id = without_auditing { Model.create }.id
|
68
|
+
user = @user
|
69
|
+
redefine_model {
|
70
|
+
audit!(:find) { |model, user|
|
71
|
+
model.id.should == id
|
72
|
+
user.should == user
|
73
|
+
}
|
74
|
+
}
|
75
|
+
Model.find(id)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should provide a snapshot of the object attributes at a given date or time' do
|
79
|
+
redefine_model { audit!(:create, :find, :update, :destroy) }
|
80
|
+
m = Model.create(:name => '1')
|
81
|
+
ts1 = Time.now
|
82
|
+
m = Model.find(m.id)
|
83
|
+
ts2 = Time.now
|
84
|
+
m.update_attributes(:name => '2')
|
85
|
+
ts3 = Time.now
|
86
|
+
m.destroy
|
87
|
+
ts4 = Time.now
|
88
|
+
|
89
|
+
m.attributes_at(ts1).should == {'name' => '1', 'id' => m.id}
|
90
|
+
m.attributes_at(ts2).should == {'name' => '1', 'id' => m.id}
|
91
|
+
m.attributes_at(ts3).should == {'name' => '2', 'id' => m.id}
|
92
|
+
m.attributes_at(ts4).should == {'name' => '2', 'id' => m.id}
|
93
|
+
end
|
94
|
+
|
95
|
+
def verify_audit(audit, model, changes=nil)
|
96
|
+
audit.should_not be_nil
|
97
|
+
audit.auditable.should == model unless audit.action == 'destroy'
|
98
|
+
audit.user.should == @user
|
99
|
+
audit.audited_changes.should == changes unless changes.nil?
|
100
|
+
end
|
101
|
+
|
102
|
+
def redefine_model(&blk)
|
103
|
+
clazz = Class.new(ActiveRecord::Base, &blk)
|
104
|
+
Object.send :remove_const, 'Model'
|
105
|
+
Object.send :const_set, 'Model', clazz
|
106
|
+
end
|
107
|
+
|
108
|
+
class ::User < ActiveRecord::Base; end
|
109
|
+
class ::Model < ActiveRecord::Base; end
|
110
|
+
|
111
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor/config'
|
3
|
+
|
4
|
+
describe Auditor::Config do
|
5
|
+
|
6
|
+
describe 'Configuration' do
|
7
|
+
it "should parse actions and options from a config array" do
|
8
|
+
config = Auditor::Config.new(:create, 'update', {:only => :username})
|
9
|
+
config.actions.should_not be_nil
|
10
|
+
config.options.should_not be_nil
|
11
|
+
config.actions.should have(2).items
|
12
|
+
config.actions.should =~ [:create, :update]
|
13
|
+
config.options.should == {:only => ["username"], :except => []}
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should parse actions and options from a config array when options are absent" do
|
17
|
+
config = Auditor::Config.new(:create, 'update')
|
18
|
+
config.actions.should_not be_nil
|
19
|
+
config.actions.should have(2).items
|
20
|
+
config.actions.should =~ [:create, :update]
|
21
|
+
config.options.should == {:only => [], :except => []}
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should parse actions" do
|
25
|
+
config = Auditor::Config.new(:create)
|
26
|
+
config.actions.should_not be_nil
|
27
|
+
config.actions.should have(1).item
|
28
|
+
config.actions.should =~ [:create]
|
29
|
+
config.options.should == {:only => [], :except => []}
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'Configuration Validation' do
|
35
|
+
it "should raise a Auditor::Error if no action is specified" do
|
36
|
+
lambda {
|
37
|
+
Auditor::Config.new
|
38
|
+
}.should raise_error(Auditor::Error)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should raise a Auditor::Error if an invalid action is specified" do
|
42
|
+
lambda {
|
43
|
+
Auditor::Config.new(:create, :view)
|
44
|
+
}.should raise_error(Auditor::Error)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/spec/recorder_spec.rb
CHANGED
@@ -1,120 +1,86 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
-
require 'auditor'
|
2
|
+
require 'auditor/user'
|
3
3
|
|
4
|
-
describe Auditor::Recorder do
|
4
|
+
describe Auditor::Recorder do
|
5
5
|
before(:each) do
|
6
|
-
@user = Auditor::User.current_user =
|
6
|
+
@user = Auditor::User.current_user = User.create
|
7
7
|
end
|
8
|
-
|
9
|
-
it 'should create
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
8
|
+
|
9
|
+
it 'should create an audit record for create actions' do
|
10
|
+
verify_action(:create)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should create an audit record for find actions' do
|
14
|
+
verify_action(:find)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should create an audit record for update actions' do
|
18
|
+
verify_action(:update)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should create an audit record for destroy actions' do
|
22
|
+
verify_action(:destroy)
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify_action(action)
|
26
|
+
model = Model.create
|
27
|
+
model.reload
|
28
|
+
model.name = 'changed'
|
29
|
+
config = Auditor::Config.new(action)
|
30
|
+
|
31
|
+
recorder = Auditor::Recorder.new(config.options) { 'comment' }
|
32
|
+
recorder.send "after_#{action}", model
|
33
|
+
audit = Audit.last
|
34
|
+
|
35
|
+
audit.action.should == action.to_s
|
36
|
+
audit.auditable_id.should == model.id
|
20
37
|
audit.auditable_type.should == model.class.to_s
|
21
38
|
audit.user_id.should == @user.id
|
22
39
|
audit.user_type.should == @user.class.to_s
|
23
|
-
audit.
|
24
|
-
audit.
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
model = new_model
|
29
|
-
|
30
|
-
auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => []})
|
31
|
-
|
32
|
-
audit = do_callbacks(auditor, model)
|
33
|
-
|
34
|
-
audit.saved.should be_true
|
35
|
-
audit.auditable_id.should == 42
|
36
|
-
audit.auditable_type.should == model.class.to_s
|
40
|
+
audit.comment.should == 'comment'
|
41
|
+
audit.audited_changes.should == {'name' => [nil, 'changed'] } if [:create, :update].include?(action)
|
42
|
+
|
43
|
+
audit.user.should == @user
|
44
|
+
audit.auditable.should == model
|
37
45
|
end
|
38
|
-
|
39
|
-
it 'should set
|
40
|
-
model =
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
audit.
|
46
|
-
|
46
|
+
|
47
|
+
it 'should set comment details to nil if they are not given' do
|
48
|
+
model = Model.create
|
49
|
+
config = Auditor::Config.new(:create)
|
50
|
+
|
51
|
+
recorder = Auditor::Recorder.new(config.options)
|
52
|
+
recorder.after_create(model)
|
53
|
+
audit = Audit.last
|
54
|
+
|
55
|
+
audit.comment.should be_nil
|
47
56
|
end
|
48
|
-
|
57
|
+
|
49
58
|
it 'should not save change details for excepted attributes' do
|
50
|
-
model =
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
audit
|
59
|
+
model = Model.create
|
60
|
+
model.name = 'changed'
|
61
|
+
model.value = 'newval'
|
62
|
+
config = Auditor::Config.new(:create, :except => :name)
|
63
|
+
|
64
|
+
recorder = Auditor::Recorder.new(config.options)
|
65
|
+
recorder.after_create(model)
|
66
|
+
audit = Audit.last
|
67
|
+
|
68
|
+
audit.audited_changes.should == {'value' => [nil, 'newval'] }
|
58
69
|
end
|
59
|
-
|
70
|
+
|
60
71
|
it 'should only save change details for onlyed attributes' do
|
61
|
-
model =
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
audit
|
69
|
-
|
70
|
-
|
71
|
-
it 'should not save attributes listed in both the only and except options' do
|
72
|
-
model = new_model(42, {'first_name' => ['old','new'], 'last_name' => ['old','new']})
|
73
|
-
|
74
|
-
auditor = Auditor::Recorder.new(:create, model, {:except => ['last_name'], :only => ['last_name']})
|
75
|
-
|
76
|
-
audit = do_callbacks(auditor, model)
|
77
|
-
|
78
|
-
audit.saved.should be_true
|
79
|
-
audit.edits.should be_nil
|
80
|
-
end
|
81
|
-
|
82
|
-
def do_callbacks(auditor, model)
|
83
|
-
auditor.audit_before
|
84
|
-
audit = auditor.instance_variable_get(:@audit)
|
85
|
-
audit.saved.should be_false
|
86
|
-
|
87
|
-
model.changes = nil
|
88
|
-
model.id = 42 if model.id.nil?
|
89
|
-
|
90
|
-
auditor.audit_after
|
91
|
-
auditor.instance_variable_get(:@audit)
|
92
|
-
end
|
93
|
-
|
94
|
-
def new_model(id = nil, changes = {})
|
95
|
-
model = (Class.new do; attr_accessor :id, :changes; end).new
|
96
|
-
model.id, model.changes = id, changes
|
97
|
-
model
|
98
|
-
end
|
99
|
-
|
100
|
-
class Audit
|
101
|
-
attr_accessor :edits, :saved
|
102
|
-
attr_accessor :action, :auditable_id, :auditable_type, :user_id, :user_type, :auditable_version, :message
|
103
|
-
|
104
|
-
def initialize(attrs={})
|
105
|
-
@edits = attrs.delete(:edits)
|
106
|
-
@saved = false
|
107
|
-
raise "You can only set the edits field in this class" unless attrs.empty?
|
108
|
-
end
|
109
|
-
|
110
|
-
def attributes=(attrs={})
|
111
|
-
attrs.each_pair do |key, val|
|
112
|
-
self.send("#{key}=".to_sym, val)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def save
|
117
|
-
@saved = true
|
118
|
-
end
|
72
|
+
model = Model.create
|
73
|
+
model.name = 'changed'
|
74
|
+
model.value = 'newval'
|
75
|
+
config = Auditor::Config.new(:create, :only => :name)
|
76
|
+
|
77
|
+
recorder = Auditor::Recorder.new(config.options)
|
78
|
+
recorder.after_create(model)
|
79
|
+
audit = Audit.last
|
80
|
+
|
81
|
+
audit.audited_changes.should == {'name' => [nil, 'changed'] }
|
119
82
|
end
|
120
|
-
|
83
|
+
|
84
|
+
class Model < ActiveRecord::Base; end
|
85
|
+
class User < ActiveRecord::Base; end
|
86
|
+
end
|