auditor 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +69 -0
- data/lib/auditor.rb +9 -0
- data/lib/auditor/audit.rb +6 -0
- data/lib/auditor/config_parser.rb +36 -0
- data/lib/auditor/integration.rb +49 -0
- data/lib/auditor/model_audit.rb +47 -0
- data/lib/auditor/recorder.rb +44 -0
- data/lib/auditor/spec_helpers.rb +20 -0
- data/lib/auditor/thread_local.rb +18 -0
- data/lib/auditor/thread_status.rb +34 -0
- data/lib/auditor/user.rb +16 -0
- data/lib/auditor/version.rb +3 -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 +19 -0
- data/spec/config_parser_spec.rb +53 -0
- data/spec/model_audit_spec.rb +83 -0
- data/spec/recorder_spec.rb +120 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/support/auditor_helpers.rb +29 -0
- data/spec/thread_local_spec.rb +14 -0
- data/spec/thread_status_spec.rb +16 -0
- data/spec/user_spec.rb +25 -0
- metadata +113 -0
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,69 @@
|
|
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
|
+
= Setup
|
23
|
+
|
24
|
+
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
|
25
|
+
|
26
|
+
class ApplicationController < ActionController::Base
|
27
|
+
before_filter :set_current_user
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def set_current_user
|
32
|
+
Auditor::User.current_user = @current_user
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
= Examples
|
37
|
+
|
38
|
+
Auditor works very similarly to Joshua Clayton's acts_as_auditable plugin. There are two audit calls in the example below. The first declares that create and update actions should be audited for the EditablePage model and the string returned by the passed block should be included as a custom message. The second audit call simply changes the custom message when auditing destroy (aka delete) actions.
|
39
|
+
|
40
|
+
class EditablePage < ActiveRecord::Base
|
41
|
+
include Auditor::ModelAudit
|
42
|
+
|
43
|
+
has_many :tags
|
44
|
+
|
45
|
+
audit(:create, :update) { |model, user| "Editable page modified by #{user.display_name}" }
|
46
|
+
audit(:destroy) { |model, user| "#{user.display_name} deleted editable page #{model.id}" }
|
47
|
+
end
|
48
|
+
|
49
|
+
All audit data is stored in a table named Audits, which is automatically created for you when you run the migration included with the plugin. However, there's a lot more recorded than just the custom message, including:
|
50
|
+
|
51
|
+
* auditable_id - the primary key of the table belonging to the audited model object
|
52
|
+
* auditable_type - the class type of the audited model object
|
53
|
+
* auditable_version - the version number of the audited model object (if versioning is tracked)
|
54
|
+
* user_id - the primary key of the table belonging to the user being audited
|
55
|
+
* user_type - the class type of the model object representing users in your application
|
56
|
+
* action - a string indicating the action that was audited (create, update, destroy, or find)
|
57
|
+
* message - the custom message returned by any block passed to the audit call
|
58
|
+
* edits - a YAML string containing the before and after state of any model attributes that changed
|
59
|
+
* created_at - the date and time the audit record was recorded
|
60
|
+
|
61
|
+
The edits column automatically serializes the before and after state of any model attributes that change during the action. If there are only a few attributes you want to audit or a couple that you want to prevent from being audited, you can specify that in the audit call. For example
|
62
|
+
|
63
|
+
# Prevent SSN and passwords from being saved in the audit table
|
64
|
+
audit(:create, :destroy, :except => [:ssn, :password])
|
65
|
+
|
66
|
+
# Only audit edits to the title column when destroying/deleting
|
67
|
+
audit(:destroy, :only => :title)
|
68
|
+
|
69
|
+
Copyright (c) 2011 Near Infinity Corporation, released under the MIT license
|
data/lib/auditor.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Auditor
|
2
|
+
class ConfigParser
|
3
|
+
|
4
|
+
def self.extract_config(args)
|
5
|
+
options = (args.delete_at(args.size - 1) if args.last.kind_of?(Hash)) || {}
|
6
|
+
normalize_config args, options
|
7
|
+
validate_config args, options
|
8
|
+
options = normalize_options(options)
|
9
|
+
|
10
|
+
[args, options]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def self.normalize_config(actions, options)
|
16
|
+
actions.each_with_index { |item, index| actions[index] = item.to_sym }
|
17
|
+
options.each_pair { |k, v| options[k.to_sym] = options.delete(k) unless k.kind_of? Symbol }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.normalize_options(options)
|
21
|
+
return { :except => [], :only => [] } if options.nil? || options.empty?
|
22
|
+
options[:except] = options[:except] || []
|
23
|
+
options[:only] = options[:only] || []
|
24
|
+
options[:except] = Array(options[:except]).map(&:to_s)
|
25
|
+
options[:only] = Array(options[:only]).map(&:to_s)
|
26
|
+
options
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.validate_config(actions, options)
|
30
|
+
raise Auditor::Error.new "at least one :create, :find, :update, or :destroy action must be specified" if actions.empty?
|
31
|
+
raise Auditor::Error.new ":create, :find, :update, and :destroy are the only valid actions" unless actions.all? { |a| [:create, :find, :update, :destroy].include? a }
|
32
|
+
raise Auditor::Error.new "only one of :except and :only can be specified" if options.size > 1
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'auditor/thread_status'
|
2
|
+
|
3
|
+
module Auditor
|
4
|
+
module Integration
|
5
|
+
|
6
|
+
def without_auditor
|
7
|
+
previously_disabled = auditor_disabled?
|
8
|
+
disable_auditor
|
9
|
+
|
10
|
+
begin
|
11
|
+
result = yield if block_given?
|
12
|
+
ensure
|
13
|
+
enable_auditor unless previously_disabled
|
14
|
+
end
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def with_auditor
|
20
|
+
previously_disabled = auditor_disabled?
|
21
|
+
enable_auditor
|
22
|
+
|
23
|
+
begin
|
24
|
+
result = yield if block_given?
|
25
|
+
ensure
|
26
|
+
disable_auditor if previously_disabled
|
27
|
+
end
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def disable_auditor
|
33
|
+
Auditor::ThreadStatus.disable
|
34
|
+
end
|
35
|
+
|
36
|
+
def enable_auditor
|
37
|
+
Auditor::ThreadStatus.enable
|
38
|
+
end
|
39
|
+
|
40
|
+
def auditor_disabled?
|
41
|
+
Auditor::ThreadStatus.disabled?
|
42
|
+
end
|
43
|
+
|
44
|
+
def auditor_enabled?
|
45
|
+
Auditor::ThreadStatus.enabled?
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'auditor/thread_status'
|
2
|
+
require 'auditor/config_parser'
|
3
|
+
require 'auditor/recorder'
|
4
|
+
|
5
|
+
module Auditor
|
6
|
+
module ModelAudit
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
# ActiveRecord won't call the after_find handler unless it see's a specific after_find method defined
|
13
|
+
def after_find; end
|
14
|
+
|
15
|
+
def auditor_disabled?
|
16
|
+
Auditor::ThreadStatus.disabled? || @auditor_disabled
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def audit(*args, &blk)
|
21
|
+
actions, options = Auditor::ConfigParser.extract_config(args)
|
22
|
+
|
23
|
+
actions.each do |action|
|
24
|
+
unless action.to_sym == :find
|
25
|
+
callback = "auditor_before_#{action}"
|
26
|
+
define_method(callback) do
|
27
|
+
@auditor_auditor = Auditor::Recorder.new(action, self, options, &blk)
|
28
|
+
@auditor_auditor.audit_before unless auditor_disabled?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
send "before_#{action}".to_sym, callback
|
32
|
+
end
|
33
|
+
|
34
|
+
callback = "auditor_after_#{action}"
|
35
|
+
define_method(callback) do
|
36
|
+
@auditor_auditor = Auditor::Recorder.new(action, self, options, &blk) if action.to_sym == :find
|
37
|
+
@auditor_auditor.audit_after unless auditor_disabled?
|
38
|
+
true
|
39
|
+
end
|
40
|
+
send "after_#{action}".to_sym, callback
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'auditor/user'
|
2
|
+
|
3
|
+
module Auditor
|
4
|
+
class Recorder
|
5
|
+
|
6
|
+
def initialize(action, model, options, &blk)
|
7
|
+
@action, @model, @options, @blk = action.to_sym, model, options, blk
|
8
|
+
end
|
9
|
+
|
10
|
+
def audit_before
|
11
|
+
@audit = Audit.new(:edits => prepare_edits(@model.changes, @options))
|
12
|
+
end
|
13
|
+
|
14
|
+
def audit_after
|
15
|
+
@audit ||= Audit.new
|
16
|
+
|
17
|
+
@audit.attributes = {
|
18
|
+
:auditable_id => @model.id,
|
19
|
+
:auditable_type => @model.class.to_s,
|
20
|
+
:user_id => user.id,
|
21
|
+
:user_type => user.class.to_s,
|
22
|
+
:action => @action.to_s
|
23
|
+
}
|
24
|
+
|
25
|
+
@audit.auditable_version = @model.version if @model.respond_to? :version
|
26
|
+
@audit.message = @blk.call(@model, user) if @blk
|
27
|
+
|
28
|
+
@audit.save
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def user
|
33
|
+
Auditor::User.current_user
|
34
|
+
end
|
35
|
+
|
36
|
+
def prepare_edits(changes, options)
|
37
|
+
chg = changes.dup
|
38
|
+
chg = chg.delete_if { |key, value| options[:except].include? key } unless options[:except].empty?
|
39
|
+
chg = chg.delete_if { |key, value| !options[:only].include? key } unless options[:only].empty?
|
40
|
+
chg.empty? ? nil : chg
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Auditor
|
2
|
+
module SpecHelpers
|
3
|
+
include Auditor::Integration
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
before(:each) do
|
8
|
+
disable_auditor
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:each) do
|
12
|
+
enable_auditor
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Auditor
|
2
|
+
class ThreadLocal
|
3
|
+
|
4
|
+
def initialize(initial_value)
|
5
|
+
@thread_symbol = "#{rand}#{Time.now.to_f}"
|
6
|
+
set initial_value
|
7
|
+
end
|
8
|
+
|
9
|
+
def set(value)
|
10
|
+
Thread.current[@thread_symbol] = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
Thread.current[@thread_symbol]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'auditor/thread_local'
|
2
|
+
|
3
|
+
module Auditor
|
4
|
+
module ThreadStatus
|
5
|
+
|
6
|
+
def self.enabled?
|
7
|
+
status
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.disabled?
|
11
|
+
!status
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.enable
|
15
|
+
set_status true
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.disable
|
19
|
+
set_status false
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def self.status
|
24
|
+
@status = Auditor::ThreadLocal.new(true) if @status.nil?
|
25
|
+
@status.get
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.set_status(status)
|
29
|
+
@status = Auditor::ThreadLocal.new(true) if @status.nil?
|
30
|
+
@status.set status
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/auditor/user.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Auditor
|
2
|
+
module User
|
3
|
+
def current_user
|
4
|
+
Thread.current[@@current_user_symbol]
|
5
|
+
end
|
6
|
+
|
7
|
+
def current_user=(user)
|
8
|
+
Thread.current[@@current_user_symbol] = user
|
9
|
+
end
|
10
|
+
|
11
|
+
module_function :current_user, :current_user=
|
12
|
+
|
13
|
+
private
|
14
|
+
@@current_user_symbol = :auditor_current_user
|
15
|
+
end
|
16
|
+
end
|
@@ -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,19 @@
|
|
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 :auditable_version, :integer
|
7
|
+
t.column :user_id, :integer, :null => false
|
8
|
+
t.column :user_type, :string, :null => false
|
9
|
+
t.column :action, :string, :null => false
|
10
|
+
t.column :message, :text
|
11
|
+
t.column :edits, :text
|
12
|
+
t.column :created_at, :datetime, :null => false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.down
|
17
|
+
drop_table :audits
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor'
|
3
|
+
|
4
|
+
describe Auditor::ConfigParser do
|
5
|
+
|
6
|
+
describe 'Configuration' do
|
7
|
+
it "should parse actions and options from a config array" do
|
8
|
+
config = Auditor::ConfigParser.extract_config([:create, 'update', {:only => :username}])
|
9
|
+
config.should_not be_nil
|
10
|
+
config.should have(2).items
|
11
|
+
config[0].should =~ [:create, :update]
|
12
|
+
config[1].should == {:only => ["username"], :except => []}
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should parse actions and options from a config array when options are absent" do
|
16
|
+
config = Auditor::ConfigParser.extract_config([:create, 'update'])
|
17
|
+
config.should_not be_nil
|
18
|
+
config.should have(2).items
|
19
|
+
config[0].should =~ [:create, :update]
|
20
|
+
config[1].should == {:only => [], :except => []}
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should parse actions" do
|
24
|
+
config = Auditor::ConfigParser.extract_config([:create])
|
25
|
+
config.should_not be_nil
|
26
|
+
config.should have(2).items
|
27
|
+
config[0].should =~ [:create]
|
28
|
+
config[1].should == {:only => [], :except => []}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'Configuration Validation' do
|
34
|
+
it "should raise a Auditor::Error if no action is specified" do
|
35
|
+
lambda {
|
36
|
+
Auditor::ConfigParser.instance_eval { validate_config([], {}) }
|
37
|
+
}.should raise_error(Auditor::Error)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should raise a Auditor::Error if an invalid action is specified" do
|
41
|
+
lambda {
|
42
|
+
Auditor::ConfigParser.instance_eval { validate_config([:create, :udate], {}) }
|
43
|
+
}.should raise_error(Auditor::Error)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should raise a Auditor::Error if both the except and only options are specified" do
|
47
|
+
lambda {
|
48
|
+
Auditor::ConfigParser.instance_eval { validate_config([:find], {:except => :ssn, :only => :username}) }
|
49
|
+
}.should raise_error(Auditor::Error)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor'
|
3
|
+
|
4
|
+
describe Auditor::ModelAudit do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Auditor::User.current_user = (Class.new do
|
8
|
+
def id; 1 end
|
9
|
+
end).new
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should audit find' do
|
13
|
+
c = new_model_class.instance_eval { audit(:find); self }
|
14
|
+
verify_standard_audits(c.new, :find)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should audit create' do
|
18
|
+
c = new_model_class.instance_eval { audit(:create); self }
|
19
|
+
verify_standard_audits(c.new, :create)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should audit update' do
|
23
|
+
c = new_model_class.instance_eval { audit(:update); self }
|
24
|
+
verify_standard_audits(c.new, :update)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should audit destroy' do
|
28
|
+
c = new_model_class.instance_eval { audit(:destroy); self }
|
29
|
+
verify_standard_audits(c.new, :destroy)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should allow multiple actions to be specified with one audit statment' do
|
33
|
+
c = new_model_class.instance_eval { audit(:create, :update); self }
|
34
|
+
verify_standard_audits(c.new, :create, :update)
|
35
|
+
|
36
|
+
c = new_model_class.instance_eval { audit(:create, :update, :destroy); self }
|
37
|
+
verify_standard_audits(c.new, :create, :update, :destroy)
|
38
|
+
|
39
|
+
c = new_model_class.instance_eval { audit(:create, :update, :destroy, :find); self }
|
40
|
+
verify_standard_audits(c.new, :create, :update, :destroy, :find)
|
41
|
+
end
|
42
|
+
|
43
|
+
def verify_standard_audits(instance, *audited_callbacks)
|
44
|
+
audited_callbacks.each do |action|
|
45
|
+
mock_auditor = mock('auditor')
|
46
|
+
Auditor::Recorder.should_receive(:new).and_return(mock_auditor)
|
47
|
+
mock_auditor.should_receive(:audit_before) unless action == :find
|
48
|
+
mock_auditor.should_receive(:audit_after)
|
49
|
+
instance.send action
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def new_model_class
|
54
|
+
Class.new(ActiveRecordMock) do
|
55
|
+
include Auditor::ModelAudit
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class ActiveRecordMock
|
60
|
+
def id; 1 end
|
61
|
+
|
62
|
+
[:create, :update, :destroy, :find].each do |action|
|
63
|
+
define_method(action) do
|
64
|
+
send "before_#{action}".to_sym unless action == :find
|
65
|
+
send "after_#{action}".to_sym
|
66
|
+
end
|
67
|
+
|
68
|
+
metaclass = class << self; self end
|
69
|
+
metaclass.instance_eval do
|
70
|
+
unless action == :find
|
71
|
+
define_method("before_#{action}") do |method|
|
72
|
+
define_method("before_#{action}") { send method }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
define_method("after_#{action}") do |method|
|
76
|
+
define_method("after_#{action}") { send method }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor'
|
3
|
+
|
4
|
+
describe Auditor::Recorder do
|
5
|
+
before(:each) do
|
6
|
+
@user = Auditor::User.current_user = new_model(7)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should create and save a new audit record' do
|
10
|
+
model = new_model(42, {'first_name' => ['old','new']})
|
11
|
+
|
12
|
+
auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => []}) { |m, u| "Model: #{m.id} User: #{u.id}" }
|
13
|
+
|
14
|
+
audit = do_callbacks(auditor, model)
|
15
|
+
|
16
|
+
audit.saved.should be_true
|
17
|
+
audit.action.should == 'create'
|
18
|
+
audit.edits.to_a.should =~ [['first_name', ['old', 'new']]]
|
19
|
+
audit.auditable_id.should == 42
|
20
|
+
audit.auditable_type.should == model.class.to_s
|
21
|
+
audit.user_id.should == @user.id
|
22
|
+
audit.user_type.should == @user.class.to_s
|
23
|
+
audit.auditable_version.should be_nil
|
24
|
+
audit.message.should == 'Model: 42 User: 7'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should capture the new id of a created record' do
|
28
|
+
model = new_model
|
29
|
+
|
30
|
+
auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => []})
|
31
|
+
|
32
|
+
audit = do_callbacks(auditor, model)
|
33
|
+
|
34
|
+
audit.saved.should be_true
|
35
|
+
audit.auditable_id.should == 42
|
36
|
+
audit.auditable_type.should == model.class.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should set message details to nil if they are not given' do
|
40
|
+
model = new_model
|
41
|
+
auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => []})
|
42
|
+
|
43
|
+
audit = do_callbacks(auditor, model)
|
44
|
+
|
45
|
+
audit.saved.should be_true
|
46
|
+
audit.message.should be_nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should not save change details for excepted attributes' do
|
50
|
+
model = new_model(42, {'first_name' => ['old','new'], 'last_name' => ['old','new']})
|
51
|
+
|
52
|
+
auditor = Auditor::Recorder.new(:create, model, {:except => ['last_name'], :only => []})
|
53
|
+
|
54
|
+
audit = do_callbacks(auditor, model)
|
55
|
+
|
56
|
+
audit.saved.should be_true
|
57
|
+
audit.edits.to_a.should =~ [['first_name', ['old', 'new']]]
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should only save change details for onlyed attributes' do
|
61
|
+
model = new_model(42, {'first_name' => ['old','new'], 'last_name' => ['old','new']})
|
62
|
+
|
63
|
+
auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => ['last_name']})
|
64
|
+
|
65
|
+
audit = do_callbacks(auditor, model)
|
66
|
+
|
67
|
+
audit.saved.should be_true
|
68
|
+
audit.edits.to_a.should =~ [['last_name', ['old', 'new']]]
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should not save attributes listed in both the only and except options' do
|
72
|
+
model = new_model(42, {'first_name' => ['old','new'], 'last_name' => ['old','new']})
|
73
|
+
|
74
|
+
auditor = Auditor::Recorder.new(:create, model, {:except => ['last_name'], :only => ['last_name']})
|
75
|
+
|
76
|
+
audit = do_callbacks(auditor, model)
|
77
|
+
|
78
|
+
audit.saved.should be_true
|
79
|
+
audit.edits.should be_nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def do_callbacks(auditor, model)
|
83
|
+
auditor.audit_before
|
84
|
+
audit = auditor.instance_variable_get(:@audit)
|
85
|
+
audit.saved.should be_false
|
86
|
+
|
87
|
+
model.changes = nil
|
88
|
+
model.id = 42 if model.id.nil?
|
89
|
+
|
90
|
+
auditor.audit_after
|
91
|
+
auditor.instance_variable_get(:@audit)
|
92
|
+
end
|
93
|
+
|
94
|
+
def new_model(id = nil, changes = {})
|
95
|
+
model = (Class.new do; attr_accessor :id, :changes; end).new
|
96
|
+
model.id, model.changes = id, changes
|
97
|
+
model
|
98
|
+
end
|
99
|
+
|
100
|
+
class Audit
|
101
|
+
attr_accessor :edits, :saved
|
102
|
+
attr_accessor :action, :auditable_id, :auditable_type, :user_id, :user_type, :auditable_version, :message
|
103
|
+
|
104
|
+
def initialize(attrs={})
|
105
|
+
@edits = attrs.delete(:edits)
|
106
|
+
@saved = false
|
107
|
+
raise "You can only set the edits field in this class" unless attrs.empty?
|
108
|
+
end
|
109
|
+
|
110
|
+
def attributes=(attrs={})
|
111
|
+
attrs.each_pair do |key, val|
|
112
|
+
self.send("#{key}=".to_sym, val)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def save
|
117
|
+
@saved = true
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
|
3
|
+
# Requires supporting files with custom matchers and macros, etc,
|
4
|
+
# in ./support/ and its subdirectories.
|
5
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
# If you're not using ActiveRecord you should remove these
|
9
|
+
# lines, delete config/database.yml and disable :active_record
|
10
|
+
# in your config/boot.rb
|
11
|
+
# config.use_transactional_fixtures = true
|
12
|
+
# config.use_instantiated_fixtures = false
|
13
|
+
|
14
|
+
# == Fixtures
|
15
|
+
#
|
16
|
+
# You can declare fixtures for each example_group like this:
|
17
|
+
# describe "...." do
|
18
|
+
# fixtures :table_a, :table_b
|
19
|
+
#
|
20
|
+
# Alternatively, if you prefer to declare them only once, you can
|
21
|
+
# do so right here. Just uncomment the next line and replace the fixture
|
22
|
+
# names with your fixtures.
|
23
|
+
#
|
24
|
+
# config.global_fixtures = :all
|
25
|
+
#
|
26
|
+
# If you declare global fixtures, be aware that they will be declared
|
27
|
+
# for all of your examples, even those that don't use them.
|
28
|
+
#
|
29
|
+
# You can also declare which fixtures to use (for example fixtures for test/fixtures):
|
30
|
+
#
|
31
|
+
# config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
|
32
|
+
#
|
33
|
+
# == Mock Framework
|
34
|
+
#
|
35
|
+
# RSpec uses it's own mocking framework by default. If you prefer to
|
36
|
+
# use mocha, flexmock or RR, uncomment the appropriate line:
|
37
|
+
#
|
38
|
+
# config.mock_with :mocha
|
39
|
+
# config.mock_with :flexmock
|
40
|
+
# config.mock_with :rr
|
41
|
+
#
|
42
|
+
# == Notes
|
43
|
+
#
|
44
|
+
# For more information take a look at Spec::Runner::Configuration and Spec::Runner
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AuditorHelpers
|
2
|
+
|
3
|
+
def current_user=(user)
|
4
|
+
Auditor::User.current_user = user
|
5
|
+
end
|
6
|
+
|
7
|
+
def current_user
|
8
|
+
Auditor::User.current_user
|
9
|
+
end
|
10
|
+
|
11
|
+
def clear_current_user
|
12
|
+
Auditor::User.current_user = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify_audit(audit, audited, user, action, edits_nil=false, message_nil=true)
|
16
|
+
audit.auditable_id.should == audited.id
|
17
|
+
audit.auditable_type.should == audited.class.name
|
18
|
+
audit.user_id.should == user.id
|
19
|
+
audit.user_type.should == user.class.name
|
20
|
+
audit.action.should == action.to_s
|
21
|
+
audit.message.should be_nil if message_nil
|
22
|
+
audit.message.should_not be_nil unless message_nil
|
23
|
+
audit.edits.should be_nil if edits_nil
|
24
|
+
audit.edits.should_not be_nil unless edits_nil
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor/thread_local'
|
3
|
+
|
4
|
+
describe Auditor::ThreadLocal do
|
5
|
+
it "should properly set and get thread-local variables" do
|
6
|
+
val = "val"
|
7
|
+
tl = Auditor::ThreadLocal.new(val)
|
8
|
+
tl.get.should == val
|
9
|
+
|
10
|
+
val2 = "val2"
|
11
|
+
tl.set val2
|
12
|
+
tl.get.should == val2
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'auditor'
|
3
|
+
|
4
|
+
describe Auditor::ThreadStatus do
|
5
|
+
it "should be enabled if set to enabled" do
|
6
|
+
Auditor::ThreadStatus.enable
|
7
|
+
Auditor::ThreadStatus.should be_enabled
|
8
|
+
Auditor::ThreadStatus.should_not be_disabled
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should be disabled if set to disabled" do
|
12
|
+
Auditor::ThreadStatus.disable
|
13
|
+
Auditor::ThreadStatus.should_not be_enabled
|
14
|
+
Auditor::ThreadStatus.should be_disabled
|
15
|
+
end
|
16
|
+
end
|
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,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: auditor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jeff Kunkle
|
14
|
+
- Matt Wizeman
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-01-06 00:00:00 -05:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: rspec
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
description: Auditor allows you to declaratively specify what CRUD operations should be audited and save the audit data to the database.
|
37
|
+
email:
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files: []
|
43
|
+
|
44
|
+
files:
|
45
|
+
- lib/auditor/audit.rb
|
46
|
+
- lib/auditor/config_parser.rb
|
47
|
+
- lib/auditor/integration.rb
|
48
|
+
- lib/auditor/model_audit.rb
|
49
|
+
- lib/auditor/recorder.rb
|
50
|
+
- lib/auditor/spec_helpers.rb
|
51
|
+
- lib/auditor/thread_local.rb
|
52
|
+
- lib/auditor/thread_status.rb
|
53
|
+
- lib/auditor/user.rb
|
54
|
+
- lib/auditor/version.rb
|
55
|
+
- lib/auditor.rb
|
56
|
+
- lib/generators/auditor/migration/migration_generator.rb
|
57
|
+
- lib/generators/auditor/migration/templates/migration.rb
|
58
|
+
- lib/generators/auditor.rb
|
59
|
+
- LICENSE
|
60
|
+
- README.rdoc
|
61
|
+
- spec/config_parser_spec.rb
|
62
|
+
- spec/model_audit_spec.rb
|
63
|
+
- spec/recorder_spec.rb
|
64
|
+
- spec/spec_helper.rb
|
65
|
+
- spec/support/auditor_helpers.rb
|
66
|
+
- spec/thread_local_spec.rb
|
67
|
+
- spec/thread_status_spec.rb
|
68
|
+
- spec/user_spec.rb
|
69
|
+
has_rdoc: true
|
70
|
+
homepage: http://github.com/nearinfinity/auditor
|
71
|
+
licenses:
|
72
|
+
- MIT
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
hash: 23
|
93
|
+
segments:
|
94
|
+
- 1
|
95
|
+
- 3
|
96
|
+
- 6
|
97
|
+
version: 1.3.6
|
98
|
+
requirements: []
|
99
|
+
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.3.7
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: Rails 3 plugin for auditing access to your ActiveRecord model objects
|
105
|
+
test_files:
|
106
|
+
- spec/config_parser_spec.rb
|
107
|
+
- spec/model_audit_spec.rb
|
108
|
+
- spec/recorder_spec.rb
|
109
|
+
- spec/spec_helper.rb
|
110
|
+
- spec/support/auditor_helpers.rb
|
111
|
+
- spec/thread_local_spec.rb
|
112
|
+
- spec/thread_status_spec.rb
|
113
|
+
- spec/user_spec.rb
|