cia 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +4 -0
- data/Appraisals +7 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +49 -0
- data/Rakefile +27 -0
- data/Readme.md +50 -0
- data/cia.gemspec +12 -0
- data/gemfiles/rails2.gemfile +11 -0
- data/gemfiles/rails2.gemfile.lock +36 -0
- data/gemfiles/rails3.gemfile +11 -0
- data/gemfiles/rails3.gemfile.lock +49 -0
- data/lib/cia.rb +30 -0
- data/lib/cia/attribute_change.rb +18 -0
- data/lib/cia/auditable.rb +21 -0
- data/lib/cia/create_event.rb +4 -0
- data/lib/cia/delete_event.rb +4 -0
- data/lib/cia/event.rb +24 -0
- data/lib/cia/null_transaction.rb +6 -0
- data/lib/cia/transaction.rb +23 -0
- data/lib/cia/update_event.rb +4 -0
- data/lib/cia/version.rb +3 -0
- data/spec/cia/attribute_change_spec.rb +18 -0
- data/spec/cia/event_spec.rb +8 -0
- data/spec/cia/transaction_spec.rb +75 -0
- data/spec/cia_spec.rb +106 -0
- data/spec/spec_helper.rb +89 -0
- metadata +78 -0
data/.travis.yml
ADDED
data/Appraisals
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
cia (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activemodel (3.2.2)
|
10
|
+
activesupport (= 3.2.2)
|
11
|
+
builder (~> 3.0.0)
|
12
|
+
activerecord (3.2.2)
|
13
|
+
activemodel (= 3.2.2)
|
14
|
+
activesupport (= 3.2.2)
|
15
|
+
arel (~> 3.0.2)
|
16
|
+
tzinfo (~> 0.3.29)
|
17
|
+
activesupport (3.2.2)
|
18
|
+
i18n (~> 0.6)
|
19
|
+
multi_json (~> 1.0)
|
20
|
+
appraisal (0.4.1)
|
21
|
+
bundler
|
22
|
+
rake
|
23
|
+
arel (3.0.2)
|
24
|
+
builder (3.0.0)
|
25
|
+
diff-lcs (1.1.3)
|
26
|
+
i18n (0.6.0)
|
27
|
+
multi_json (1.3.4)
|
28
|
+
rake (0.9.2)
|
29
|
+
rspec (2.6.0)
|
30
|
+
rspec-core (~> 2.6.0)
|
31
|
+
rspec-expectations (~> 2.6.0)
|
32
|
+
rspec-mocks (~> 2.6.0)
|
33
|
+
rspec-core (2.6.4)
|
34
|
+
rspec-expectations (2.6.0)
|
35
|
+
diff-lcs (~> 1.1.2)
|
36
|
+
rspec-mocks (2.6.0)
|
37
|
+
sqlite3 (1.3.6)
|
38
|
+
tzinfo (0.3.33)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
activerecord
|
45
|
+
appraisal
|
46
|
+
cia!
|
47
|
+
rake
|
48
|
+
rspec (~> 2)
|
49
|
+
sqlite3
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'appraisal'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
task :default do
|
5
|
+
sh "bundle exec rake appraisal:install && bundle exec rake appraisal spec"
|
6
|
+
end
|
7
|
+
|
8
|
+
task :spec do
|
9
|
+
sh "rspec spec/"
|
10
|
+
end
|
11
|
+
|
12
|
+
# extracted from https://github.com/grosser/project_template
|
13
|
+
rule /^version:bump:.*/ do |t|
|
14
|
+
sh "git status | grep 'nothing to commit'" # ensure we are not dirty
|
15
|
+
index = ['major', 'minor','patch'].index(t.name.split(':').last)
|
16
|
+
file = 'lib/cia/version.rb'
|
17
|
+
|
18
|
+
version_file = File.read(file)
|
19
|
+
old_version, *version_parts = version_file.match(/(\d+)\.(\d+)\.(\d+)/).to_a
|
20
|
+
version_parts[index] = version_parts[index].to_i + 1
|
21
|
+
version_parts[2] = 0 if index < 2 # remove patch for minor
|
22
|
+
version_parts[1] = 0 if index < 1 # remove minor for major
|
23
|
+
new_version = version_parts * '.'
|
24
|
+
File.open(file,'w'){|f| f.write(version_file.sub(old_version, new_version)) }
|
25
|
+
|
26
|
+
sh "bundle && git add #{file} Gemfile.lock && git commit -m 'bump version to #{new_version}'"
|
27
|
+
end
|
data/Readme.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Central Intelligent Auditing
|
2
|
+
|
3
|
+
Audit model events like update/create/delete + attribute changes.<br/>
|
4
|
+
- very normalized and queryable through table layout
|
5
|
+
```
|
6
|
+
1 Transaction (actor/ip/time/...)
|
7
|
+
-> has many events (updated subject + message)
|
8
|
+
-> has many attribute changes (changed password from foo to bar on subject)
|
9
|
+
```
|
10
|
+
- actors and subjects are polymorphic
|
11
|
+
- events come in different types like `ActiveAuditing::UpdateEvent`
|
12
|
+
- transactions wrap multiple events, a nice place to add debugging info like source/action/ip
|
13
|
+
- works on ActiveRecord 2 and 3
|
14
|
+
|
15
|
+
Install
|
16
|
+
=======
|
17
|
+
gem install active_auditing
|
18
|
+
Or
|
19
|
+
|
20
|
+
rails plugin install git://github.com/grosser/cia.git
|
21
|
+
|
22
|
+
|
23
|
+
Usage
|
24
|
+
=====
|
25
|
+
|
26
|
+
```Ruby
|
27
|
+
class User < ActiveRecord::Base
|
28
|
+
audited_attributes :email, :crypted_password
|
29
|
+
end
|
30
|
+
|
31
|
+
class ApplicationController < ActionController::Base
|
32
|
+
around_filter :scope_auditing
|
33
|
+
|
34
|
+
def scope_auditing
|
35
|
+
Auditing.audit :actor => current_user, :ip_address => request.remote_ip do
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
```
|
43
|
+
|
44
|
+
|
45
|
+
Author
|
46
|
+
======
|
47
|
+
[Michael Grosser](http://grosser.it)<br/>
|
48
|
+
michael@grosser.it<br/>
|
49
|
+
License: MIT<br/>
|
50
|
+
[![Build Status](https://secure.travis-ci.org/grosser/cia.png)](http://travis-ci.org/grosser/cia)
|
data/cia.gemspec
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
name = "cia"
|
3
|
+
require "#{name}/version"
|
4
|
+
|
5
|
+
Gem::Specification.new name, CIA::VERSION do |s|
|
6
|
+
s.summary = "Audit model events like update/create/delete + attribute changes + grouped them by transaction, in normalized table layout for easy query access."
|
7
|
+
s.authors = ["Michael Grosser"]
|
8
|
+
s.email = "michael@grosser.it"
|
9
|
+
s.homepage = "http://github.com/grosser/#{name}"
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.license = 'MIT'
|
12
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
PATH
|
2
|
+
remote: /Users/mgrosser/code/tools/cia
|
3
|
+
specs:
|
4
|
+
cia (0.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activerecord (2.3.14)
|
10
|
+
activesupport (= 2.3.14)
|
11
|
+
activesupport (2.3.14)
|
12
|
+
appraisal (0.4.1)
|
13
|
+
bundler
|
14
|
+
rake
|
15
|
+
diff-lcs (1.1.3)
|
16
|
+
rake (0.9.2.2)
|
17
|
+
rspec (2.10.0)
|
18
|
+
rspec-core (~> 2.10.0)
|
19
|
+
rspec-expectations (~> 2.10.0)
|
20
|
+
rspec-mocks (~> 2.10.0)
|
21
|
+
rspec-core (2.10.1)
|
22
|
+
rspec-expectations (2.10.0)
|
23
|
+
diff-lcs (~> 1.1.3)
|
24
|
+
rspec-mocks (2.10.1)
|
25
|
+
sqlite3 (1.3.6)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
ruby
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
activerecord (= 2.3.14)
|
32
|
+
appraisal
|
33
|
+
cia!
|
34
|
+
rake
|
35
|
+
rspec (~> 2)
|
36
|
+
sqlite3
|
@@ -0,0 +1,49 @@
|
|
1
|
+
PATH
|
2
|
+
remote: /Users/mgrosser/code/tools/cia
|
3
|
+
specs:
|
4
|
+
cia (0.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activemodel (3.2.3)
|
10
|
+
activesupport (= 3.2.3)
|
11
|
+
builder (~> 3.0.0)
|
12
|
+
activerecord (3.2.3)
|
13
|
+
activemodel (= 3.2.3)
|
14
|
+
activesupport (= 3.2.3)
|
15
|
+
arel (~> 3.0.2)
|
16
|
+
tzinfo (~> 0.3.29)
|
17
|
+
activesupport (3.2.3)
|
18
|
+
i18n (~> 0.6)
|
19
|
+
multi_json (~> 1.0)
|
20
|
+
appraisal (0.4.1)
|
21
|
+
bundler
|
22
|
+
rake
|
23
|
+
arel (3.0.2)
|
24
|
+
builder (3.0.0)
|
25
|
+
diff-lcs (1.1.3)
|
26
|
+
i18n (0.6.0)
|
27
|
+
multi_json (1.3.5)
|
28
|
+
rake (0.9.2.2)
|
29
|
+
rspec (2.10.0)
|
30
|
+
rspec-core (~> 2.10.0)
|
31
|
+
rspec-expectations (~> 2.10.0)
|
32
|
+
rspec-mocks (~> 2.10.0)
|
33
|
+
rspec-core (2.10.1)
|
34
|
+
rspec-expectations (2.10.0)
|
35
|
+
diff-lcs (~> 1.1.3)
|
36
|
+
rspec-mocks (2.10.1)
|
37
|
+
sqlite3 (1.3.6)
|
38
|
+
tzinfo (0.3.33)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
activerecord (= 3.2.3)
|
45
|
+
appraisal
|
46
|
+
cia!
|
47
|
+
rake
|
48
|
+
rspec (~> 2)
|
49
|
+
sqlite3
|
data/lib/cia.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'cia/version'
|
3
|
+
require 'cia/auditable'
|
4
|
+
require 'cia/null_transaction'
|
5
|
+
require 'cia/transaction'
|
6
|
+
require 'cia/event'
|
7
|
+
require 'cia/create_event'
|
8
|
+
require 'cia/update_event'
|
9
|
+
require 'cia/delete_event'
|
10
|
+
require 'cia/attribute_change'
|
11
|
+
|
12
|
+
module CIA
|
13
|
+
def self.audit(options = {})
|
14
|
+
Thread.current[:audit_transaction] = Transaction.new(options)
|
15
|
+
yield
|
16
|
+
ensure
|
17
|
+
Thread.current[:audit_transaction] = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.current_transaction
|
21
|
+
Thread.current[:audit_transaction] || NullTransaction
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.record_audit(event_type, object)
|
25
|
+
CIA.current_transaction.record(event_type, object)
|
26
|
+
rescue => e
|
27
|
+
Rails.logger.error("Failed to record audit: #{e}\n#{e.backtrace}")
|
28
|
+
raise e unless Rails.env.production?
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CIA
|
2
|
+
class AttributeChange < ActiveRecord::Base
|
3
|
+
self.table_name = "audit_attribute_changes"
|
4
|
+
|
5
|
+
belongs_to :event, :foreign_key => "audit_event_id"
|
6
|
+
belongs_to :source, :polymorphic => true
|
7
|
+
|
8
|
+
validates_presence_of :event, :attribute_name, :source
|
9
|
+
|
10
|
+
if ActiveRecord::VERSION::MAJOR > 2
|
11
|
+
scope :previous, :order => "id desc"
|
12
|
+
else
|
13
|
+
named_scope :previous, :order => "id desc"
|
14
|
+
end
|
15
|
+
|
16
|
+
delegate :created_at, :to => :event
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CIA
|
2
|
+
module Auditable
|
3
|
+
def self.included(base)
|
4
|
+
base.class_attribute :audited_attributes
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
base.after_create {|record| CIA.record_audit(CIA::CreateEvent, record) }
|
7
|
+
base.after_update {|record| CIA.record_audit(CIA::UpdateEvent, record) }
|
8
|
+
base.after_destroy {|record| CIA.record_audit(CIA::DeleteEvent, record) }
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def audit_attribute(*attributes)
|
13
|
+
self.audited_attributes = Set.new unless audited_attributes
|
14
|
+
self.audited_attributes += attributes.map(&:to_s)
|
15
|
+
|
16
|
+
has_many :audit_events, :class_name => "CIA::Event", :as => :source
|
17
|
+
has_many :audit_attribute_changes, :class_name => "CIA::AttributeChange", :as => :source
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/cia/event.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module CIA
|
2
|
+
class Event < ActiveRecord::Base
|
3
|
+
abstract_class
|
4
|
+
self.table_name = "audit_events"
|
5
|
+
|
6
|
+
belongs_to :source, :polymorphic => true
|
7
|
+
belongs_to :transaction, :foreign_key => :audit_transaction_id
|
8
|
+
has_many :attribute_changes, :foreign_key => :audit_event_id
|
9
|
+
|
10
|
+
validates_presence_of :transaction, :source, :type
|
11
|
+
|
12
|
+
# tested via transaction_test.rb
|
13
|
+
def record_attribute_changes!(changes)
|
14
|
+
changes.each do |attribute_name, (old_value, new_value)|
|
15
|
+
attribute_changes.create!(
|
16
|
+
:attribute_name => attribute_name,
|
17
|
+
:old_value => old_value,
|
18
|
+
:new_value => new_value,
|
19
|
+
:source => source
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module CIA
|
2
|
+
class Transaction < ActiveRecord::Base
|
3
|
+
self.table_name = "audit_transactions"
|
4
|
+
|
5
|
+
belongs_to :actor, :polymorphic => true
|
6
|
+
has_many :events, :foreign_key => :audit_transaction_id
|
7
|
+
|
8
|
+
def record(event_type, source)
|
9
|
+
changes = source.changes.slice(*source.class.audited_attributes)
|
10
|
+
message = source.audit_message if source.respond_to?(:audit_message)
|
11
|
+
|
12
|
+
return if not message and changes.empty? and event_type == CIA::UpdateEvent
|
13
|
+
|
14
|
+
event = event_type.create!(
|
15
|
+
:source => source,
|
16
|
+
:transaction => self,
|
17
|
+
:message => message
|
18
|
+
)
|
19
|
+
event.record_attribute_changes!(changes)
|
20
|
+
event
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/cia/version.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CIA::AttributeChange do
|
4
|
+
it "delegates create_at to event" do
|
5
|
+
t = Time.now
|
6
|
+
event = CIA::Event.new(:created_at => t)
|
7
|
+
change = CIA::AttributeChange.new(:event => event)
|
8
|
+
change.created_at.should == event.created_at
|
9
|
+
end
|
10
|
+
|
11
|
+
describe ".previous" do
|
12
|
+
it "finds by id desc" do
|
13
|
+
a = create_change
|
14
|
+
b = create_change
|
15
|
+
CIA::AttributeChange.previous.should == [b,a]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CIA::Transaction do
|
4
|
+
it "has many events" do
|
5
|
+
event = create_event
|
6
|
+
event.transaction.events.should == [event]
|
7
|
+
end
|
8
|
+
|
9
|
+
context "#record" do
|
10
|
+
def parse_event_changes(event)
|
11
|
+
event.attribute_changes.map { |c| [c.attribute_name, c.old_value, c.new_value] }
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:transaction){ CIA::Transaction.new(:actor => User.create!) }
|
15
|
+
|
16
|
+
it "records attribute creations" do
|
17
|
+
source = Car.create!
|
18
|
+
source.wheels = 4
|
19
|
+
event = transaction.record(CIA::UpdateEvent, source)
|
20
|
+
|
21
|
+
parse_event_changes(event).should == [["wheels", nil, "4"]]
|
22
|
+
end
|
23
|
+
|
24
|
+
it "records multiple attributes" do
|
25
|
+
source = CarWith3Attributes.create!
|
26
|
+
source.wheels = 4
|
27
|
+
source.drivers = 2
|
28
|
+
source.color = "red"
|
29
|
+
event = transaction.record(CIA::UpdateEvent, source)
|
30
|
+
|
31
|
+
parse_event_changes(event).should =~ [["wheels", nil, "4"], ["drivers", nil, "2"], ["color", nil, "red"]]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "records attribute changes" do
|
35
|
+
source = Car.create!(:wheels => 2)
|
36
|
+
source.wheels = 4
|
37
|
+
event = transaction.record(CIA::UpdateEvent, source)
|
38
|
+
parse_event_changes(event).should == [["wheels", "2", "4"]]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "records attribute deletions" do
|
42
|
+
source = Car.create!(:wheels => 2)
|
43
|
+
source.wheels = nil
|
44
|
+
event = transaction.record(CIA::UpdateEvent, source)
|
45
|
+
parse_event_changes(event).should == [["wheels", "2", nil]]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "does not record unaudited attribute changes" do
|
49
|
+
source = Car.create!
|
50
|
+
source.drivers = 2
|
51
|
+
event = nil
|
52
|
+
expect{
|
53
|
+
event = transaction.record(CIA::UpdateEvent, source)
|
54
|
+
}.to_not change{ CIA::Event.count }
|
55
|
+
|
56
|
+
event.should == nil
|
57
|
+
end
|
58
|
+
|
59
|
+
it "records audit_message as message even if there are no changes" do
|
60
|
+
source = CarWithAMessage.create!
|
61
|
+
source.audit_message = "Foo"
|
62
|
+
event = transaction.record(CIA::UpdateEvent, source)
|
63
|
+
|
64
|
+
event.message.should == "Foo"
|
65
|
+
parse_event_changes(event).should == []
|
66
|
+
end
|
67
|
+
|
68
|
+
it "record non-updates even without changes" do
|
69
|
+
source = Car.create!
|
70
|
+
event = transaction.record(CIA::CreateEvent, source)
|
71
|
+
|
72
|
+
parse_event_changes(event).should == []
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/spec/cia_spec.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CIA do
|
4
|
+
it "has a VERSION" do
|
5
|
+
CIA::VERSION.should =~ /^[\.\da-z]+$/
|
6
|
+
end
|
7
|
+
|
8
|
+
describe ".audit" do
|
9
|
+
it "has no transaction when it starts" do
|
10
|
+
CIA.current_transaction.should == CIA::NullTransaction
|
11
|
+
end
|
12
|
+
|
13
|
+
it "starts a new transaction" do
|
14
|
+
result = 1
|
15
|
+
CIA.audit({}) do
|
16
|
+
result = CIA.current_transaction
|
17
|
+
end
|
18
|
+
result.class.should == CIA::Transaction
|
19
|
+
end
|
20
|
+
|
21
|
+
it "stops the transaction after the block" do
|
22
|
+
CIA.audit({}){}
|
23
|
+
CIA.current_transaction.should == CIA::NullTransaction
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns the block content" do
|
27
|
+
CIA.audit({}){ 1 }.should == 1
|
28
|
+
end
|
29
|
+
|
30
|
+
it "is threadsafe" do
|
31
|
+
Thread.new do
|
32
|
+
CIA.audit({}) do
|
33
|
+
sleep 0.04
|
34
|
+
end
|
35
|
+
end
|
36
|
+
sleep 0.01
|
37
|
+
CIA.current_transaction.should == CIA::NullTransaction
|
38
|
+
sleep 0.04 # so next tests dont fail
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe ".record_audit" do
|
43
|
+
let(:object) { Car.new }
|
44
|
+
let(:transaction) { CIA.current_transaction }
|
45
|
+
|
46
|
+
around do |example|
|
47
|
+
CIA.audit :actor => User.create! do
|
48
|
+
example.call
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
before do
|
53
|
+
Rails.stub(:logger).and_return(mock(:error => ""))
|
54
|
+
Rails.env.stub(:production?).and_return(true)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "tracks create" do
|
58
|
+
expect{
|
59
|
+
object.save!
|
60
|
+
}.to change{ CIA::Event.count }.by(+1)
|
61
|
+
CIA::Event.last.class.should == CIA::CreateEvent
|
62
|
+
end
|
63
|
+
|
64
|
+
it "tracks delete" do
|
65
|
+
object.save!
|
66
|
+
expect{
|
67
|
+
object.destroy
|
68
|
+
}.to change{ CIA::Event.count }.by(+1)
|
69
|
+
CIA::Event.last.class.should == CIA::DeleteEvent
|
70
|
+
end
|
71
|
+
|
72
|
+
it "tracks update" do
|
73
|
+
object.save!
|
74
|
+
expect{
|
75
|
+
object.update_attributes(:wheels => 3)
|
76
|
+
}.to change{ CIA::Event.count }.by(+1)
|
77
|
+
CIA::Event.last.class.should == CIA::UpdateEvent
|
78
|
+
end
|
79
|
+
|
80
|
+
context "exceptions" do
|
81
|
+
before do
|
82
|
+
Rails.stub(:logger).and_return(mock(:error => ""))
|
83
|
+
Rails.env.stub(:production?).and_return(true)
|
84
|
+
transaction.stub(:record).and_raise(StandardError.new("foo"))
|
85
|
+
end
|
86
|
+
|
87
|
+
it "logs exceptions raised by the transaction" do
|
88
|
+
Rails.logger.should_receive(:error).with { |x| x =~ /Failed to record audit: foo/ }
|
89
|
+
object.save! rescue nil
|
90
|
+
end
|
91
|
+
|
92
|
+
it "re-raise exceptions when not in production" do
|
93
|
+
Rails.env.stub(:production?).and_return(false)
|
94
|
+
|
95
|
+
expect {
|
96
|
+
object.save!
|
97
|
+
}.to raise_error(StandardError)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "does not re-raise exceptions when in production" do
|
101
|
+
Rails.env.stub(:production?).and_return(true)
|
102
|
+
object.save!
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
$LOAD_PATH.unshift 'lib'
|
2
|
+
require 'cia'
|
3
|
+
|
4
|
+
ActiveRecord::Base.establish_connection(
|
5
|
+
:adapter => "sqlite3",
|
6
|
+
:database => ":memory:"
|
7
|
+
)
|
8
|
+
|
9
|
+
ActiveRecord::Schema.define(:version => 1) do
|
10
|
+
create_table :audit_transactions do |t|
|
11
|
+
t.integer :actor_id, :null => false
|
12
|
+
t.string :actor_type, :null => false
|
13
|
+
t.string :ip_address
|
14
|
+
t.timestamp :created_at
|
15
|
+
end
|
16
|
+
|
17
|
+
create_table :audit_events do |t|
|
18
|
+
t.string :type, :source_type, :null => false
|
19
|
+
t.integer :audit_transaction_id, :source_id, :null => false
|
20
|
+
t.string :message
|
21
|
+
t.timestamp :created_at
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table :audit_attribute_changes do |t|
|
25
|
+
t.integer :audit_event_id, :source_id, :null => false
|
26
|
+
t.string :attribute_name, :source_type, :null => false
|
27
|
+
t.string :old_value, :new_value
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table :cars do |t|
|
31
|
+
t.integer :wheels
|
32
|
+
t.integer :drivers
|
33
|
+
t.string :color
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table :users do |t|
|
37
|
+
t.string :email
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class User < ActiveRecord::Base
|
42
|
+
end
|
43
|
+
|
44
|
+
class Car < ActiveRecord::Base
|
45
|
+
include CIA::Auditable
|
46
|
+
audit_attribute :wheels
|
47
|
+
end
|
48
|
+
|
49
|
+
class CarWithAMessage < ActiveRecord::Base
|
50
|
+
self.table_name = "cars"
|
51
|
+
include CIA::Auditable
|
52
|
+
audit_attribute :wheels
|
53
|
+
attr_accessor :audit_message
|
54
|
+
end
|
55
|
+
|
56
|
+
class CarWith3Attributes < ActiveRecord::Base
|
57
|
+
self.table_name = "cars"
|
58
|
+
include CIA::Auditable
|
59
|
+
audit_attribute :wheels, :color
|
60
|
+
audit_attribute :drivers
|
61
|
+
end
|
62
|
+
|
63
|
+
class CarWithoutObservers < ActiveRecord::Base
|
64
|
+
self.table_name = "cars"
|
65
|
+
end
|
66
|
+
|
67
|
+
class CarWithoutObservers2 < ActiveRecord::Base
|
68
|
+
self.table_name = "cars"
|
69
|
+
end
|
70
|
+
|
71
|
+
def create_event
|
72
|
+
transaction = CIA::Transaction.create!(:actor => User.create!)
|
73
|
+
CIA::UpdateEvent.create!(:source => Car.create!, :transaction => transaction)
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_change
|
77
|
+
event = create_event
|
78
|
+
CIA::AttributeChange.create!(:event => event, :source => event.source, :attribute_name => "bar")
|
79
|
+
end
|
80
|
+
|
81
|
+
module Rails
|
82
|
+
def self.logger
|
83
|
+
raise "NOT STUBBED"
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.env
|
87
|
+
@@env ||= "NOT STUBBED"
|
88
|
+
end
|
89
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cia
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Grosser
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-21 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: michael@grosser.it
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- .travis.yml
|
21
|
+
- Appraisals
|
22
|
+
- Gemfile
|
23
|
+
- Gemfile.lock
|
24
|
+
- Rakefile
|
25
|
+
- Readme.md
|
26
|
+
- cia.gemspec
|
27
|
+
- gemfiles/rails2.gemfile
|
28
|
+
- gemfiles/rails2.gemfile.lock
|
29
|
+
- gemfiles/rails3.gemfile
|
30
|
+
- gemfiles/rails3.gemfile.lock
|
31
|
+
- lib/cia.rb
|
32
|
+
- lib/cia/attribute_change.rb
|
33
|
+
- lib/cia/auditable.rb
|
34
|
+
- lib/cia/create_event.rb
|
35
|
+
- lib/cia/delete_event.rb
|
36
|
+
- lib/cia/event.rb
|
37
|
+
- lib/cia/null_transaction.rb
|
38
|
+
- lib/cia/transaction.rb
|
39
|
+
- lib/cia/update_event.rb
|
40
|
+
- lib/cia/version.rb
|
41
|
+
- spec/cia/attribute_change_spec.rb
|
42
|
+
- spec/cia/event_spec.rb
|
43
|
+
- spec/cia/transaction_spec.rb
|
44
|
+
- spec/cia_spec.rb
|
45
|
+
- spec/spec_helper.rb
|
46
|
+
homepage: http://github.com/grosser/cia
|
47
|
+
licenses:
|
48
|
+
- MIT
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
hash: -3376077843642118516
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
hash: -3376077843642118516
|
71
|
+
requirements: []
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.8.24
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Audit model events like update/create/delete + attribute changes + grouped
|
77
|
+
them by transaction, in normalized table layout for easy query access.
|
78
|
+
test_files: []
|