auditor_tenancy 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +135 -0
- data/auditor_tenancy.gemspec +24 -0
- data/init.rb +1 -0
- data/lib/auditor/audit.rb +66 -0
- data/lib/auditor/auditable.rb +44 -0
- data/lib/auditor/config.rb +34 -0
- data/lib/auditor/recorder.rb +56 -0
- data/lib/auditor/spec_helpers.rb +20 -0
- data/lib/auditor/status.rb +62 -0
- data/lib/auditor/tenant.rb +15 -0
- data/lib/auditor/user.rb +15 -0
- data/lib/auditor/version.rb +3 -0
- data/lib/auditor_tenancy.rb +23 -0
- data/lib/generators/auditor.rb +9 -0
- data/lib/generators/auditor/migration/migration_generator.rb +26 -0
- data/lib/generators/auditor/migration/templates/migration.rb +27 -0
- data/lib/generators/auditor/upgrade/templates/upgrade.rb +13 -0
- data/lib/generators/auditor/upgrade/upgrade_generator.rb +26 -0
- data/spec/audit_spec.rb +61 -0
- data/spec/auditable_spec.rb +127 -0
- data/spec/config_spec.rb +49 -0
- data/spec/recorder_spec.rb +115 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/status_spec.rb +44 -0
- data/spec/support/db_setup.rb +52 -0
- data/spec/support/model_setup.rb +4 -0
- data/spec/support/transactional_specs.rb +17 -0
- data/spec/user_spec.rb +25 -0
- metadata +125 -0
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Near Infinity Corporation
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
= Auditor
|
2
|
+
|
3
|
+
Auditor is a Rails 3 plugin for auditing access to your ActiveRecord model objects. It allows you to declaratively specify what CRUD operations should be audited and store that audit data in the database. You can also specify what attributes of model objects should automatically be audited and which ones should be ignored.
|
4
|
+
|
5
|
+
To audit your model objects you must specify which operations should be audited and which model attributes should be tracked. This "specify what you want to collect" approach avoids being overwhelmed with data and makes you carefully consider what is most important to audit.
|
6
|
+
|
7
|
+
= Installation
|
8
|
+
|
9
|
+
To use it with your Rails 3 project, add the following line to your Gemfile
|
10
|
+
|
11
|
+
gem 'auditor'
|
12
|
+
|
13
|
+
Auditor can also be installed as a Rails plugin
|
14
|
+
|
15
|
+
rails plugin install git://github.com/nearinfinity/auditor.git
|
16
|
+
|
17
|
+
Generate the migration and create the audits table
|
18
|
+
|
19
|
+
rails generate auditor:migration
|
20
|
+
rake db:migrate
|
21
|
+
|
22
|
+
= Upgrading
|
23
|
+
|
24
|
+
You will need to run the upgrade migration if coming from a version earlier than 2.1.0
|
25
|
+
|
26
|
+
rails generate auditor:upgrade
|
27
|
+
rake db:migrate
|
28
|
+
|
29
|
+
= Setup
|
30
|
+
|
31
|
+
Auditor needs to know who the current user is, but with no standard for doing so you'll have to do a little work to set things up. You simply need to set your current user model object as the Auditor current user before any CRUD operations are performed. For example, in a Rails application you could add the following to your application_controller.rb
|
32
|
+
|
33
|
+
class ApplicationController < ActionController::Base
|
34
|
+
before_filter :set_current_user
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def set_current_user
|
39
|
+
Auditor::User.current_user = @current_user
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
= Examples
|
44
|
+
|
45
|
+
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.
|
46
|
+
|
47
|
+
class Page < ActiveRecord::Base
|
48
|
+
audit(:create, :update) { |model, user, action| "Page modified by #{user.display_name}" }
|
49
|
+
audit(:destroy) { |model, user, action| "#{user.display_name} deleted page #{model.id}" }
|
50
|
+
end
|
51
|
+
|
52
|
+
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:
|
53
|
+
|
54
|
+
* auditable_id - the primary key of the table belonging to the audited model object
|
55
|
+
* auditable_type - the class type of the audited model object
|
56
|
+
* owner_id - the primary key of the of the model that owns this audit record
|
57
|
+
* owner_type - the class type of the owner model object
|
58
|
+
* user_id - the primary key of the table belonging to the user being audited
|
59
|
+
* user_type - the class type of the model object representing users in your application
|
60
|
+
* action - a string indicating the action that was audited (create, update, destroy, or find)
|
61
|
+
* audited_changes - a YAML string containing the before and after state of any model attributes that changed
|
62
|
+
* comment - the custom message returned by any block passed to the audit call
|
63
|
+
* version - an auditor-internal revision number for the audited model
|
64
|
+
* created_at - the date and time the audit record was recorded
|
65
|
+
|
66
|
+
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
|
67
|
+
|
68
|
+
# Prevent SSN and passwords from being saved in the audit table
|
69
|
+
audit(:create, :destroy, :except => [:ssn, :password])
|
70
|
+
|
71
|
+
# Only audit edits to the title column when destroying/deleting
|
72
|
+
audit(:destroy, :only => :title)
|
73
|
+
|
74
|
+
# Associate the audit records with a related model, which becomes the owner
|
75
|
+
audit(:update, :on => :book)
|
76
|
+
|
77
|
+
# Associate the audit records with a related model, multiple levels up.
|
78
|
+
# Here, we're auditing a great-grandchild where :parent will be the owner. Order is important.
|
79
|
+
audit(:update, :on => [:grandchild, :child, :parent])
|
80
|
+
|
81
|
+
= Make Auditing Important
|
82
|
+
|
83
|
+
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.
|
84
|
+
|
85
|
+
class Page < ActiveRecord::Base
|
86
|
+
audit!(:create, :update) { |model, user, action| "Page modified by #{user.display_name}" }
|
87
|
+
audit!(:destroy) { |model, user, action| "#{user.display_name} deleted page #{model.id}" }
|
88
|
+
end
|
89
|
+
|
90
|
+
= Auditable Versioning
|
91
|
+
|
92
|
+
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.
|
93
|
+
|
94
|
+
p = Page.create(:title => "Revision 1")
|
95
|
+
p.audits.last.attribute_snapshot
|
96
|
+
> {:title => "Revision 1"}
|
97
|
+
time = Time.now
|
98
|
+
p.author = "Jeff"
|
99
|
+
p.save
|
100
|
+
p.audits.last.attribute_snapshot
|
101
|
+
> {:title => "Revision 1", :author => "Jeff"}
|
102
|
+
p.attributes_at(time)
|
103
|
+
> {:title => "Revision 1"}
|
104
|
+
|
105
|
+
= Integration
|
106
|
+
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.
|
107
|
+
|
108
|
+
class PagesController < ApplicationController
|
109
|
+
include Auditor::Status
|
110
|
+
|
111
|
+
def update
|
112
|
+
page = Page.find(params[:id])
|
113
|
+
without_auditing { page.update_attributes(params[:page]) } # Auditor is disabled for the entire block
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
You can also force Auditor to audit any actions within a block as a specified user.
|
119
|
+
|
120
|
+
class PagesController < ApplicationController
|
121
|
+
include Auditor::Status
|
122
|
+
|
123
|
+
def update
|
124
|
+
page = Page.find(params[:id])
|
125
|
+
# Auditor will attribute update to 'another user'
|
126
|
+
audit_as(another_user) { page.update_attributes(params[:page]) }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
= License
|
132
|
+
|
133
|
+
Auditor is released under the MIT license.
|
134
|
+
|
135
|
+
Copyright (c) 2011 Near Infinity. http://www.nearinfinity.com
|
@@ -0,0 +1,24 @@
|
|
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_tenancy"
|
7
|
+
s.version = Auditor::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Jeff Kunkle", "Geoff Mayes"]
|
10
|
+
s.homepage = "http://github.com/mayesgr/auditor_tenancy"
|
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").reject { |path| path =~ /^(Gemfile|.gitignore|Rakefile)/ }
|
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_dependency('activerecord', '~> 3.0')
|
21
|
+
|
22
|
+
s.add_development_dependency('rspec', '2.5.0')
|
23
|
+
s.add_development_dependency('sqlite3-ruby', '1.3.3')
|
24
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'auditor_tenancy'
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'auditor/config'
|
3
|
+
|
4
|
+
class Audit < ActiveRecord::Base
|
5
|
+
belongs_to :auditable, :polymorphic => true
|
6
|
+
belongs_to :owner, :polymorphic => true
|
7
|
+
belongs_to :user, :polymorphic => true
|
8
|
+
belongs_to :tenant
|
9
|
+
|
10
|
+
before_create :set_version_number
|
11
|
+
before_create :set_user
|
12
|
+
before_create :set_tenant
|
13
|
+
|
14
|
+
serialize :audited_changes
|
15
|
+
|
16
|
+
default_scope order("id DESC")
|
17
|
+
scope :modifying, lambda {
|
18
|
+
where( [
|
19
|
+
'audited_changes is not ? and audited_changes not like ?',
|
20
|
+
nil, # ActiveRecord 3.0
|
21
|
+
nil.to_yaml # ActiveRecord 3.1
|
22
|
+
] )
|
23
|
+
}
|
24
|
+
scope :trail, lambda { |audit|
|
25
|
+
where('auditable_id = ? and auditable_type = ? and version <= ?',
|
26
|
+
audit.auditable_id, audit.auditable_type, audit.version)
|
27
|
+
}
|
28
|
+
|
29
|
+
def attribute_snapshot
|
30
|
+
attributes = {}.with_indifferent_access
|
31
|
+
self.class.modifying.trail(self).each do |predecessor|
|
32
|
+
attributes.merge!(predecessor.new_attributes)
|
33
|
+
end
|
34
|
+
attributes
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
# Returns a hash of the changed attributes with the new values
|
40
|
+
def new_attributes
|
41
|
+
(audited_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
|
42
|
+
attrs[attr] = values.is_a?(Array) ? values.last : values
|
43
|
+
attrs
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def set_version_number
|
50
|
+
max = self.class.where(
|
51
|
+
:auditable_id => auditable_id,
|
52
|
+
:auditable_type => auditable_type
|
53
|
+
).maximum(:version) || 0
|
54
|
+
|
55
|
+
self.version = self.audited_changes == nil ? max : max + 1
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_user
|
59
|
+
self.user = Auditor::User.current_user if self.user_id.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_tenant
|
63
|
+
self.tenant = Auditor::Tenant.current_tenant if self.tenant_id.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
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,34 @@
|
|
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 initialize(*args)
|
11
|
+
@options = (args.pop if args.last.kind_of?(Hash)) || {}
|
12
|
+
normalize_options(@options)
|
13
|
+
|
14
|
+
@actions = args.map(&:to_sym)
|
15
|
+
validate_actions(@actions)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def normalize_options(options)
|
21
|
+
options.each_pair { |k, v| options[k.to_sym] = options.delete(k) unless k.kind_of? Symbol }
|
22
|
+
options[:only] ||= []
|
23
|
+
options[:except] ||= []
|
24
|
+
options[:only] = Array(options[:only]).map(&:to_s)
|
25
|
+
options[:except] = Array(options[:except]).map(&:to_s)
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_actions(actions)
|
29
|
+
raise Auditor::Error.new "at least one action in #{Config.valid_actions.inspect} must be specified" if actions.empty?
|
30
|
+
raise Auditor::Error.new "#{Config.valid_actions.inspect} are the only valid actions" unless actions.all? { |a| Config.valid_actions.include?(a.to_sym) }
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'auditor/status'
|
2
|
+
|
3
|
+
module Auditor
|
4
|
+
class Recorder
|
5
|
+
include Status
|
6
|
+
|
7
|
+
def initialize(options, &blk)
|
8
|
+
@options = options
|
9
|
+
@blk = blk
|
10
|
+
end
|
11
|
+
|
12
|
+
[:create, :find, :update, :destroy].each do |action|
|
13
|
+
define_method("after_#{action}") do |model|
|
14
|
+
audit(model, action)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def audit(model, action)
|
21
|
+
return nil if auditing_disabled?
|
22
|
+
user = Auditor::User.current_user
|
23
|
+
tenant = Auditor::Tenant.current_tenant
|
24
|
+
|
25
|
+
audit = Audit.new
|
26
|
+
audit.tenant_id = tenant.id
|
27
|
+
audit.auditable_id = model.id
|
28
|
+
audit.auditable_type = model.class.name
|
29
|
+
audit.audited_changes = prepare_changes(model.changes) if model.changed?
|
30
|
+
audit.action = action
|
31
|
+
|
32
|
+
return if noop?(audit)
|
33
|
+
|
34
|
+
audit.comment = @blk.call(model, user, action) if @blk
|
35
|
+
|
36
|
+
without_auditing do
|
37
|
+
owner = @options[:on] ? Array(@options[:on]).inject(model) { |owner, parent| owner.send(parent.to_sym) } : model
|
38
|
+
audit.owner_id = owner.id
|
39
|
+
audit.owner_type = owner.class.name
|
40
|
+
end
|
41
|
+
|
42
|
+
@options[:fail_on_error] ? audit.save! : audit.save
|
43
|
+
end
|
44
|
+
|
45
|
+
def prepare_changes(changes)
|
46
|
+
chg = changes.dup
|
47
|
+
chg = chg.delete_if { |key, value| @options[:except].include?(key) } unless @options[:except].blank?
|
48
|
+
chg = chg.delete_if { |key, value| !@options[:only].include?(key) } unless @options[:only].blank?
|
49
|
+
chg.empty? ? nil : chg
|
50
|
+
end
|
51
|
+
|
52
|
+
def noop?(audit)
|
53
|
+
audit.action == :update && !audit.audited_changes.present?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'auditor/user'
|
2
|
+
|
3
|
+
module Auditor
|
4
|
+
module Status
|
5
|
+
|
6
|
+
def auditing_disabled?
|
7
|
+
Thread.current[:auditor_disabled] == true
|
8
|
+
end
|
9
|
+
|
10
|
+
def auditing_enabled?
|
11
|
+
Thread.current[:auditor_disabled] == false
|
12
|
+
end
|
13
|
+
|
14
|
+
def disable_auditing
|
15
|
+
Thread.current[:auditor_disabled] = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def enable_auditing
|
19
|
+
Thread.current[:auditor_disabled] = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def without_auditing
|
23
|
+
previously_disabled = auditing_disabled?
|
24
|
+
|
25
|
+
begin
|
26
|
+
disable_auditing
|
27
|
+
result = yield if block_given?
|
28
|
+
ensure
|
29
|
+
enable_auditing unless previously_disabled
|
30
|
+
end
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_auditing
|
36
|
+
previously_disabled = auditing_disabled?
|
37
|
+
|
38
|
+
begin
|
39
|
+
enable_auditing
|
40
|
+
result = yield if block_given?
|
41
|
+
ensure
|
42
|
+
disable_auditing if previously_disabled
|
43
|
+
end
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def audit_as(user)
|
49
|
+
previous_user = Auditor::User.current_user
|
50
|
+
|
51
|
+
begin
|
52
|
+
Auditor::User.current_user = user
|
53
|
+
result = yield if block_given?
|
54
|
+
ensure
|
55
|
+
Auditor::User.current_user = previous_user
|
56
|
+
end
|
57
|
+
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
data/lib/auditor/user.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'auditor/audit'
|
2
|
+
require 'auditor/auditable'
|
3
|
+
|
4
|
+
module Auditor
|
5
|
+
class Error < StandardError; end
|
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
|
+
require 'auditor/tenant'
|
14
|
+
|
15
|
+
ActionController::Base.class_eval do
|
16
|
+
before_filter do |c|
|
17
|
+
Auditor::User.current_user = c.send(:current_user) if c.respond_to?(:current_user)
|
18
|
+
Auditor::Site.current_tenant = c.send(:current_tenant) if c.respond_to?(:current_tenant)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Auditor
|
5
|
+
module Generators
|
6
|
+
class MigrationGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
desc "Create migration for Auditor audits table"
|
10
|
+
|
11
|
+
source_root File.expand_path("../templates", __FILE__)
|
12
|
+
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
if ActiveRecord::Base.timestamped_migrations
|
15
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
16
|
+
else
|
17
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_migration_file
|
22
|
+
migration_template 'migration.rb', 'db/migrate/create_audits_table.rb'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class CreateAuditsTable < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :audits, :force => true do |t|
|
4
|
+
t.column :auditable_id, :integer, :null => false
|
5
|
+
t.column :auditable_type, :string, :null => false
|
6
|
+
t.column :owner_id, :integer, :null => false
|
7
|
+
t.column :owner_type, :string, :null => false
|
8
|
+
t.column :tenant_id, :integer, :null => false
|
9
|
+
t.column :user_id, :integer, :null => false
|
10
|
+
t.column :user_type, :string, :null => false
|
11
|
+
t.column :action, :string, :null => false
|
12
|
+
t.column :audited_changes, :text
|
13
|
+
t.column :version, :integer, :default => 0
|
14
|
+
t.column :comment, :text
|
15
|
+
t.column :created_at, :datetime, :null => false
|
16
|
+
end
|
17
|
+
|
18
|
+
add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
|
19
|
+
add_index :audits, [:user_id, :user_type], :name => 'user_index'
|
20
|
+
add_index :audits, :tenant_id
|
21
|
+
add_index :audits, :created_at
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.down
|
25
|
+
drop_table :audits
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class UpgradeAuditsTable < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_column :audits, :owner_id, :integer
|
4
|
+
add_column :audits, :owner_type, :string
|
5
|
+
|
6
|
+
add_index :audits, [:owner_id, :owner_type], :name => 'owner_index'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.down
|
10
|
+
remove_column :audits, :owner_type
|
11
|
+
remove_column :audits, :owner_id
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Auditor
|
5
|
+
module Generators
|
6
|
+
class UpgradeGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
desc "Create upgrade migration for Auditor audits table"
|
10
|
+
|
11
|
+
source_root File.expand_path("../templates", __FILE__)
|
12
|
+
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
if ActiveRecord::Base.timestamped_migrations
|
15
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
16
|
+
else
|
17
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_migration_file
|
22
|
+
migration_template 'upgrade.rb', 'db/migrate/upgrade_audits_table.rb'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/audit_spec.rb
ADDED
@@ -0,0 +1,61 @@
|
|
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, :owner => @auditable, :audited_changes => { :name => [nil, 'new']}, :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, :owner => @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, :owner => @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, :owner => @auditable, :user => @user, :action => :create)
|
26
|
+
audit2 = Audit.create(:auditable => @auditable, :owner => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => [nil, 'n1'], 'value' => [nil, 'v1']})
|
27
|
+
audit3 = Audit.create(:auditable => @auditable, :owner => @auditable, :user => @user, :action => :find)
|
28
|
+
audit4 = Audit.create(:auditable => @auditable, :owner => @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, :owner => @auditable, :user => @user, :action => :create, :audited_changes => {'name' => [nil, 'n0']})
|
39
|
+
audit2 = Audit.create(:auditable => @auditable, :owner => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => ['n0', 'n1'], 'value' => [nil, 'v1']})
|
40
|
+
audit3 = Audit.create(:auditable => @auditable, :owner => @auditable, :user => @user, :action => :find)
|
41
|
+
audit4 = Audit.create(:auditable => @auditable, :owner => @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, :owner => @auditable, :user => @user, :action => :create)
|
52
|
+
audit2 = Audit.create(:auditable => @auditable, :owner => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => [nil, 'n1'], 'value' => [nil, 'v1']})
|
53
|
+
audit3 = Audit.create(:auditable => auditable2, :owner => auditable2, :user => @user, :action => :find)
|
54
|
+
audit4 = Audit.create(:auditable => @auditable, :owner => @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
|
+
|
@@ -0,0 +1,127 @@
|
|
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
|
+
@original_model = Model
|
11
|
+
Auditor::User.current_user = @user
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:each) do
|
15
|
+
reset_model
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should audit find' do
|
19
|
+
redefine_model { audit!(:find) }
|
20
|
+
m = without_auditing { Model.create }
|
21
|
+
|
22
|
+
Model.find(m.id)
|
23
|
+
|
24
|
+
verify_audit(Audit.last, m)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should audit create' do
|
28
|
+
redefine_model { audit!(:create) }
|
29
|
+
|
30
|
+
m = Model.create(:name => 'new')
|
31
|
+
|
32
|
+
verify_audit(Audit.last, m, { 'name' => [nil, 'new'], 'id' => [nil, m.id] })
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should audit update' do
|
36
|
+
redefine_model { audit!(:update) }
|
37
|
+
m = without_auditing { Model.create(:name => 'new') }
|
38
|
+
|
39
|
+
m.update_attributes(:name => 'newer')
|
40
|
+
|
41
|
+
verify_audit(Audit.last, m, { 'name' => ['new', 'newer'] })
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should audit destroy' do
|
45
|
+
redefine_model { audit!(:destroy) }
|
46
|
+
m = without_auditing { Model.create }
|
47
|
+
|
48
|
+
m.destroy
|
49
|
+
|
50
|
+
verify_audit(Audit.last, m)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should allow multiple actions to be specified with one audit statment' do
|
54
|
+
redefine_model { audit!(:create, :destroy) }
|
55
|
+
|
56
|
+
m = Model.create
|
57
|
+
m.reload
|
58
|
+
m = Model.find(m.id)
|
59
|
+
m.update_attributes({:name => 'new'})
|
60
|
+
m.destroy
|
61
|
+
|
62
|
+
Audit.count.should == 2
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should record the comment returned from a comment block' do
|
66
|
+
redefine_model { audit!(:create) { 'comment' } }
|
67
|
+
Model.create
|
68
|
+
Audit.last.comment.should == 'comment'
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should provide the model object and user as parameters to the comment block' do
|
72
|
+
id = without_auditing { Model.create }.id
|
73
|
+
user = @user
|
74
|
+
redefine_model {
|
75
|
+
audit!(:find) { |model, user|
|
76
|
+
model.id.should == id
|
77
|
+
user.should == user
|
78
|
+
}
|
79
|
+
}
|
80
|
+
Model.find(id)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should provide a snapshot of the object attributes at a given date or time' do
|
84
|
+
redefine_model { audit!(:create, :find, :update, :destroy) }
|
85
|
+
m = Model.create(:name => '1')
|
86
|
+
ts1 = Time.now
|
87
|
+
m = Model.find(m.id)
|
88
|
+
ts2 = Time.now
|
89
|
+
m.update_attributes(:name => '2')
|
90
|
+
ts3 = Time.now
|
91
|
+
m.destroy
|
92
|
+
ts4 = Time.now
|
93
|
+
|
94
|
+
m.attributes_at(ts1).should == {'name' => '1', 'id' => m.id}
|
95
|
+
m.attributes_at(ts2).should == {'name' => '1', 'id' => m.id}
|
96
|
+
m.attributes_at(ts3).should == {'name' => '2', 'id' => m.id}
|
97
|
+
m.attributes_at(ts4).should == {'name' => '2', 'id' => m.id}
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should not save an update record that does not have any audited changes' do
|
101
|
+
redefine_model { audit!(:update, :except => :name) }
|
102
|
+
|
103
|
+
lambda {
|
104
|
+
m = Model.create(:name => 'new')
|
105
|
+
m.update_attributes({:name => 'newer'})
|
106
|
+
}.should_not change(Audit, :count)
|
107
|
+
end
|
108
|
+
|
109
|
+
def verify_audit(audit, model, changes=nil)
|
110
|
+
audit.should_not be_nil
|
111
|
+
audit.auditable.should == model unless audit.action == 'destroy'
|
112
|
+
audit.user.should == @user
|
113
|
+
audit.audited_changes.should == changes unless changes.nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
def redefine_model(&blk)
|
117
|
+
clazz = Class.new(ActiveRecord::Base, &blk)
|
118
|
+
Object.send :remove_const, 'Model'
|
119
|
+
Object.send :const_set, 'Model', clazz
|
120
|
+
end
|
121
|
+
|
122
|
+
def reset_model
|
123
|
+
Object.send :remove_const, 'Model'
|
124
|
+
Object.send :const_set, 'Model', @original_model
|
125
|
+
end
|
126
|
+
|
127
|
+
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
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor/user'
|
3
|
+
|
4
|
+
describe Auditor::Recorder do
|
5
|
+
before(:each) do
|
6
|
+
@user = Auditor::User.current_user = User.create
|
7
|
+
end
|
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
|
37
|
+
audit.auditable_type.should == model.class.to_s
|
38
|
+
audit.owner_id.should == model.id
|
39
|
+
audit.owner_type.should == model.class.to_s
|
40
|
+
audit.user_id.should == @user.id
|
41
|
+
audit.user_type.should == @user.class.to_s
|
42
|
+
audit.comment.should == 'comment'
|
43
|
+
audit.audited_changes.should == {'name' => [nil, 'changed'] } if [:create, :update].include?(action)
|
44
|
+
|
45
|
+
audit.user.should == @user
|
46
|
+
audit.auditable.should == model
|
47
|
+
audit.owner.should == model
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should set comment details to nil if they are not given' do
|
51
|
+
model = Model.create
|
52
|
+
config = Auditor::Config.new(:create)
|
53
|
+
|
54
|
+
recorder = Auditor::Recorder.new(config.options)
|
55
|
+
recorder.after_create(model)
|
56
|
+
audit = Audit.last
|
57
|
+
|
58
|
+
audit.comment.should be_nil
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should not save change details for excepted attributes' do
|
62
|
+
model = Model.create
|
63
|
+
model.name = 'changed'
|
64
|
+
model.value = 'newval'
|
65
|
+
config = Auditor::Config.new(:create, :except => :name)
|
66
|
+
|
67
|
+
recorder = Auditor::Recorder.new(config.options)
|
68
|
+
recorder.after_create(model)
|
69
|
+
audit = Audit.last
|
70
|
+
|
71
|
+
audit.audited_changes.should == {'value' => [nil, 'newval'] }
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should only save change details for onlyed attributes' do
|
75
|
+
model = Model.create
|
76
|
+
model.name = 'changed'
|
77
|
+
model.value = 'newval'
|
78
|
+
config = Auditor::Config.new(:create, :only => :name)
|
79
|
+
|
80
|
+
recorder = Auditor::Recorder.new(config.options)
|
81
|
+
recorder.after_create(model)
|
82
|
+
audit = Audit.last
|
83
|
+
|
84
|
+
audit.audited_changes.should == {'name' => [nil, 'changed'] }
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should associate audit records with an owner' do
|
88
|
+
model = Model.create
|
89
|
+
config = Auditor::Config.new(:create)
|
90
|
+
recorder = Auditor::Recorder.new(config.options)
|
91
|
+
recorder.after_create(model)
|
92
|
+
audit = Audit.last
|
93
|
+
audit.owner.should == model
|
94
|
+
|
95
|
+
owner = User.create
|
96
|
+
model.user = owner
|
97
|
+
config = Auditor::Config.new(:create, :on => :user)
|
98
|
+
recorder = Auditor::Recorder.new(config.options)
|
99
|
+
recorder.after_create(model)
|
100
|
+
audit = Audit.last
|
101
|
+
audit.owner.should == owner
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should pass the model, user, and action to any supplied block' do
|
105
|
+
model = Model.create
|
106
|
+
config = Auditor::Config.new(:create)
|
107
|
+
recorder = Auditor::Recorder.new(config.options) do |model, user, action|
|
108
|
+
model.should == model
|
109
|
+
user.should == @user
|
110
|
+
action.should == :create
|
111
|
+
end
|
112
|
+
recorder.after_create(model)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'auditor'
|
3
|
+
|
4
|
+
# Requires supporting files with custom matchers and macros, etc,
|
5
|
+
# in ./support/ and its subdirectories.
|
6
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.include TransactionalSpecs
|
10
|
+
|
11
|
+
# RSpec uses it's own mocking framework by default. If you prefer to
|
12
|
+
# use mocha, flexmock or RR, uncomment the appropriate line:
|
13
|
+
#
|
14
|
+
# config.mock_with :mocha
|
15
|
+
# config.mock_with :flexmock
|
16
|
+
# config.mock_with :rr
|
17
|
+
#
|
18
|
+
# == Notes
|
19
|
+
#
|
20
|
+
# For more information take a look at Spec::Runner::Configuration and Spec::Runner
|
21
|
+
end
|
data/spec/status_spec.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor/status'
|
3
|
+
|
4
|
+
describe Auditor::Status do
|
5
|
+
it "should be enabled if set to enabled" do
|
6
|
+
obj = Class.new { include Auditor::Status }.new
|
7
|
+
obj.enable_auditing
|
8
|
+
obj.should be_auditing_enabled
|
9
|
+
obj.should_not be_auditing_disabled
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should be disabled if set to disabled" do
|
13
|
+
obj = Class.new { include Auditor::Status }.new
|
14
|
+
obj.disable_auditing
|
15
|
+
obj.should_not be_auditing_enabled
|
16
|
+
obj.should be_auditing_disabled
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should allow auditing as a specified user for a block of code" do
|
20
|
+
obj = Class.new { include Auditor::Status }.new
|
21
|
+
user1 = "user1"
|
22
|
+
user2 = "user2"
|
23
|
+
Auditor::User.current_user = user1
|
24
|
+
|
25
|
+
obj.audit_as(user2) { Auditor::User.current_user.should == user2 }
|
26
|
+
Auditor::User.current_user.should == user1
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should allow a block of code to be executed with auditing disabled" do
|
30
|
+
obj = Class.new { include Auditor::Status }.new
|
31
|
+
obj.enable_auditing
|
32
|
+
obj.should be_auditing_enabled
|
33
|
+
obj.without_auditing { obj.should be_auditing_disabled }
|
34
|
+
obj.should be_auditing_enabled
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should allow a block of code to be executed with auditing enabled" do
|
38
|
+
obj = Class.new { include Auditor::Status }.new
|
39
|
+
obj.disable_auditing
|
40
|
+
obj.should be_auditing_disabled
|
41
|
+
obj.with_auditing { obj.should be_auditing_enabled }
|
42
|
+
obj.should be_auditing_disabled
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
require 'active_record'
|
3
|
+
require 'generators/auditor/migration/templates/migration'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
tmpdir = File.join(File.dirname(__FILE__), '..', '..', 'tmp')
|
7
|
+
FileUtils.mkdir(tmpdir) unless File.exist?(tmpdir)
|
8
|
+
test_db = File.join(tmpdir, 'test.db')
|
9
|
+
|
10
|
+
connection_spec = {
|
11
|
+
:adapter => 'sqlite3',
|
12
|
+
:database => test_db
|
13
|
+
}
|
14
|
+
|
15
|
+
# Delete any existing instance of the test database
|
16
|
+
FileUtils.rm test_db, :force => true
|
17
|
+
|
18
|
+
# Create a new test database
|
19
|
+
ActiveRecord::Base.establish_connection(connection_spec)
|
20
|
+
|
21
|
+
# ActiveRecord::Base.connection.initialize_schema_migrations_table
|
22
|
+
|
23
|
+
class CreateUser < ActiveRecord::Migration
|
24
|
+
def self.up
|
25
|
+
create_table :users, :force => true do |t|
|
26
|
+
t.column :username, :string
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.down
|
31
|
+
drop_table :users
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class CreateModel < ActiveRecord::Migration
|
36
|
+
def self.up
|
37
|
+
create_table :models, :force => true do |t|
|
38
|
+
t.column :name, :string
|
39
|
+
t.column :value, :string
|
40
|
+
t.column :user_id, :integer
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.down
|
45
|
+
drop_table :models
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
CreateUser.up
|
50
|
+
CreateModel.up
|
51
|
+
CreateAuditsTable.up
|
52
|
+
|
data/spec/user_spec.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor/user'
|
3
|
+
|
4
|
+
describe Auditor::User do
|
5
|
+
it "should return the same user that's set on the same thread" do
|
6
|
+
user = "user"
|
7
|
+
Auditor::User.current_user = user
|
8
|
+
Auditor::User.current_user.should == user
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should not return the same user from a different thread" do
|
12
|
+
user = "user"
|
13
|
+
user2 = "user2"
|
14
|
+
|
15
|
+
Auditor::User.current_user = user
|
16
|
+
|
17
|
+
Thread.new do
|
18
|
+
Auditor::User.current_user.should be_nil
|
19
|
+
Auditor::User.current_user = user2
|
20
|
+
Auditor::User.current_user.should == user2
|
21
|
+
end
|
22
|
+
|
23
|
+
Auditor::User.current_user.should == user
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: auditor_tenancy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.4.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jeff Kunkle
|
9
|
+
- Geoff Mayes
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-12-30 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '3.0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rspec
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - '='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 2.5.0
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - '='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 2.5.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sqlite3-ruby
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.3.3
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - '='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.3.3
|
63
|
+
description: Auditor allows you to declaratively specify what CRUD operations should
|
64
|
+
be audited and save the audit data to the database.
|
65
|
+
email:
|
66
|
+
executables: []
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- CHANGELOG.md
|
71
|
+
- LICENSE
|
72
|
+
- README.rdoc
|
73
|
+
- auditor_tenancy.gemspec
|
74
|
+
- init.rb
|
75
|
+
- lib/auditor/audit.rb
|
76
|
+
- lib/auditor/auditable.rb
|
77
|
+
- lib/auditor/config.rb
|
78
|
+
- lib/auditor/recorder.rb
|
79
|
+
- lib/auditor/spec_helpers.rb
|
80
|
+
- lib/auditor/status.rb
|
81
|
+
- lib/auditor/tenant.rb
|
82
|
+
- lib/auditor/user.rb
|
83
|
+
- lib/auditor/version.rb
|
84
|
+
- lib/auditor_tenancy.rb
|
85
|
+
- lib/generators/auditor.rb
|
86
|
+
- lib/generators/auditor/migration/migration_generator.rb
|
87
|
+
- lib/generators/auditor/migration/templates/migration.rb
|
88
|
+
- lib/generators/auditor/upgrade/templates/upgrade.rb
|
89
|
+
- lib/generators/auditor/upgrade/upgrade_generator.rb
|
90
|
+
- spec/audit_spec.rb
|
91
|
+
- spec/auditable_spec.rb
|
92
|
+
- spec/config_spec.rb
|
93
|
+
- spec/recorder_spec.rb
|
94
|
+
- spec/spec_helper.rb
|
95
|
+
- spec/status_spec.rb
|
96
|
+
- spec/support/db_setup.rb
|
97
|
+
- spec/support/model_setup.rb
|
98
|
+
- spec/support/transactional_specs.rb
|
99
|
+
- spec/user_spec.rb
|
100
|
+
homepage: http://github.com/mayesgr/auditor_tenancy
|
101
|
+
licenses:
|
102
|
+
- MIT
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 1.8.24
|
122
|
+
signing_key:
|
123
|
+
specification_version: 3
|
124
|
+
summary: Rails 3 plugin for auditing access to your ActiveRecord model objects
|
125
|
+
test_files: []
|