auditor_tenancy 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ 2.4.0
2
+ -----
3
+ * Added initial tenancy support (currently hard-coded; needs to become an option)
4
+
5
+ 2.3.2
6
+ -----
7
+ * Forked from https://github.com/nearinfinity/auditor
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.
@@ -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,20 @@
1
+ module Auditor
2
+ module SpecHelpers
3
+ include Auditor::Status
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ before(:each) do
8
+ disable_auditing
9
+ end
10
+
11
+ after(:each) do
12
+ enable_auditing
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+
20
+
@@ -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
@@ -0,0 +1,15 @@
1
+ module Auditor
2
+ module Tenant
3
+
4
+ def current_tenant
5
+ Thread.current[:auditor_tenant]
6
+ end
7
+
8
+ def current_tenant=(tenant)
9
+ Thread.current[:auditor_tenant] = tenant
10
+ end
11
+
12
+ module_function :current_tenant, :current_tenant=
13
+
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Auditor
2
+ module User
3
+
4
+ def current_user
5
+ Thread.current[:auditor_user]
6
+ end
7
+
8
+ def current_user=(user)
9
+ Thread.current[:auditor_user] = user
10
+ end
11
+
12
+ module_function :current_user, :current_user=
13
+
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Auditor
2
+ VERSION = "2.4.0"
3
+ end
@@ -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,9 @@
1
+ module Auditor
2
+ module Generators
3
+ class Base < Rails::Generators::NamedBase
4
+ def self.source_root
5
+ File.expand_path(File.join(File.dirname(__FILE__), 'auditor', generator_name, 'templates'))
6
+ end
7
+ end
8
+ end
9
+ 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,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
@@ -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
@@ -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
@@ -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
@@ -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
+
@@ -0,0 +1,4 @@
1
+ class User < ActiveRecord::Base; end
2
+ class Model < ActiveRecord::Base
3
+ belongs_to :user
4
+ end
@@ -0,0 +1,17 @@
1
+ module TransactionalSpecs
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+ around(:each) do |spec|
6
+ ActiveRecord::Base.transaction do
7
+ begin
8
+ spec.call
9
+ ensure
10
+ raise ActiveRecord::Rollback
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ end
@@ -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: []