cia 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.
- 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
|
+
[](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: []
|