auditor 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ coverage
2
+ rdoc
3
+ *.gem
4
+ .bundle
5
+ Gemfile.lock
6
+ pkg/*
7
+ tmp/*
8
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in auditor.gemspec
4
+ gemspec
@@ -37,28 +37,24 @@ Auditor needs to know who the current user is, but with no standard for doing so
37
37
 
38
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
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}" }
40
+ class Page < ActiveRecord::Base
41
+ audit(:create, :update) { |model, user| "Page modified by #{user.display_name}" }
42
+ audit(:destroy) { |model, user| "#{user.display_name} deleted page #{model.id}" }
47
43
  end
48
44
 
49
45
  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
46
 
51
47
  * auditable_id - the primary key of the table belonging to the audited model object
52
48
  * 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
49
  * user_id - the primary key of the table belonging to the user being audited
55
50
  * user_type - the class type of the model object representing users in your application
56
51
  * 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
52
+ * audited_changes - a YAML string containing the before and after state of any model attributes that changed
53
+ * comment - the custom message returned by any block passed to the audit call
54
+ * version - an auditor-internal revision number for the audited model
59
55
  * created_at - the date and time the audit record was recorded
60
56
 
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
57
+ 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
62
58
 
63
59
  # Prevent SSN and passwords from being saved in the audit table
64
60
  audit(:create, :destroy, :except => [:ssn, :password])
@@ -66,4 +62,55 @@ The edits column automatically serializes the before and after state of any mode
66
62
  # Only audit edits to the title column when destroying/deleting
67
63
  audit(:destroy, :only => :title)
68
64
 
65
+ = Make Auditing Important
66
+
67
+ 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.
68
+
69
+ class Page < ActiveRecord::Base
70
+ audit!(:create, :update) { |model, user| "Page modified by #{user.display_name}" }
71
+ audit!(:destroy) { |model, user| "#{user.display_name} deleted page #{model.id}" }
72
+ end
73
+
74
+ = Auditable Versioning
75
+
76
+ 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.
77
+
78
+ p = Page.create(:title => "Revision 1")
79
+ p.audits.last.attribute_snapshot
80
+ > {:title => "Revision 1"}
81
+ time = Time.now
82
+ p.author = "Jeff"
83
+ p.save
84
+ p.audits.last.attribute_snapshot
85
+ > {:title => "Revision 1", :author => "Jeff"}
86
+ p.attributes_at(time)
87
+ > {:title => "Revision 1"}
88
+
89
+ = Integration
90
+ 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.
91
+
92
+ class PagesController < ApplicationController
93
+ include Auditor::Status
94
+
95
+ def update
96
+ page = Page.find(params[:id])
97
+ without_auditing { page.update_attributes(params[:page]) } # Auditor is disabled for the entire block
98
+ end
99
+ end
100
+ end
101
+
102
+ You can also force Auditor to audit any actions within a block as a specified user.
103
+
104
+ class PagesController < ApplicationController
105
+ include Auditor::Status
106
+
107
+ def update
108
+ page = Page.find(params[:id])
109
+ # Auditor will attribute update to 'another user'
110
+ audit_as(another_user) { page.update_attributes(params[:page]) }
111
+ end
112
+ end
113
+ end
114
+
115
+
69
116
  Copyright (c) 2011 Near Infinity Corporation, released under the MIT license
@@ -0,0 +1,33 @@
1
+ $:.unshift File.expand_path("../lib", __FILE__)
2
+
3
+ require 'rake'
4
+ require 'rake/rdoctask'
5
+ require 'rspec/core/rake_task'
6
+ require 'bundler'
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ desc 'Default: run specs'
11
+ task :default => :spec
12
+
13
+ desc "Run specs"
14
+ RSpec::Core::RakeTask.new do |t|
15
+ t.rspec_opts = %w(-fs --color)
16
+ end
17
+
18
+ desc "Run specs with RCov"
19
+ RSpec::Core::RakeTask.new(:rcov) do |t|
20
+ t.rspec_opts = %w(-fs --color)
21
+ t.rcov = true
22
+ t.rcov_opts = %w(--exclude "spec/*,gems/*")
23
+ end
24
+
25
+ desc 'Generate documentation for the gem.'
26
+ Rake::RDocTask.new(:rdoc) do |rdoc|
27
+ rdoc.rdoc_dir = 'rdoc'
28
+ rdoc.title = 'Auditor'
29
+ rdoc.options << '--line-numbers' << '--inline-source'
30
+ rdoc.rdoc_files.include('README.rdoc')
31
+ rdoc.rdoc_files.include('lib/**/*.rb')
32
+ end
33
+
@@ -0,0 +1,23 @@
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"
7
+ s.version = Auditor::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jeff Kunkle"]
10
+ s.homepage = "http://github.com/nearinfinity/auditor"
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")
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_development_dependency('rspec', '2.5.0')
21
+ s.add_development_dependency('sqlite3-ruby', '1.3.3')
22
+ s.add_development_dependency('activerecord', '> 3.0.0')
23
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'auditor'
@@ -1,9 +1,21 @@
1
1
  require 'auditor/audit'
2
- require 'auditor/integration'
3
- require 'auditor/model_audit'
4
- require 'auditor/user'
5
- require 'auditor/version'
2
+ require 'auditor/auditable'
6
3
 
7
4
  module Auditor
8
5
  class Error < StandardError; end
9
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
+
14
+ ActionController::Base.class_eval do
15
+ before_filter do
16
+ Auditor::User.current_user = self.current_user if self.respond_to?(:current_user)
17
+ end
18
+ end
19
+
20
+ end
21
+
@@ -1,6 +1,53 @@
1
1
  require 'active_record'
2
+ require 'auditor/config'
2
3
 
3
4
  class Audit < ActiveRecord::Base
4
- validates_presence_of :auditable_id, :auditable_type, :user_id, :user_type, :action
5
- serialize :edits
5
+ belongs_to :auditable, :polymorphic => true
6
+ belongs_to :user, :polymorphic => true
7
+
8
+ before_create :set_version_number
9
+ before_create :set_user
10
+
11
+ serialize :audited_changes
12
+
13
+ default_scope order(:version, :created_at)
14
+ scope :modifying, lambda { where('action in (?)', Auditor::Config.modifying_actions) }
15
+ scope :trail, lambda { |audit|
16
+ where('auditable_id = ? and auditable_type = ? and version <= ?',
17
+ audit.auditable_id, audit.auditable_type, audit.version)
18
+ }
19
+
20
+ def attribute_snapshot
21
+ attributes = {}.with_indifferent_access
22
+ self.class.modifying.trail(self).each do |predecessor|
23
+ attributes.merge!(predecessor.new_attributes)
24
+ end
25
+ attributes
26
+ end
27
+
28
+ protected
29
+
30
+ # Returns a hash of the changed attributes with the new values
31
+ def new_attributes
32
+ (audited_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
33
+ attrs[attr] = values.is_a?(Array) ? values.last : values
34
+ attrs
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def set_version_number
41
+ max = self.class.where(
42
+ :auditable_id => auditable_id,
43
+ :auditable_type => auditable_type
44
+ ).maximum(:version) || 0
45
+
46
+ self.version = Auditor::Config.modifying_actions.include?(self.action.to_sym) ? max + 1 : max
47
+ end
48
+
49
+ def set_user
50
+ self.user = Auditor::User.current_user if self.user_id.nil?
51
+ end
52
+
6
53
  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,38 @@
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 self.modifying_actions
11
+ @modifying_actions ||= [:create, :update, :destroy]
12
+ end
13
+
14
+ def initialize(*args)
15
+ @options = (args.pop if args.last.kind_of?(Hash)) || {}
16
+ normalize_options(@options)
17
+
18
+ @actions = args.map(&:to_sym)
19
+ validate_actions(@actions)
20
+ end
21
+
22
+ private
23
+
24
+ def normalize_options(options)
25
+ options.each_pair { |k, v| options[k.to_sym] = options.delete(k) unless k.kind_of? Symbol }
26
+ options[:only] ||= []
27
+ options[:except] ||= []
28
+ options[:only] = Array(options[:only]).map(&:to_s)
29
+ options[:except] = Array(options[:except]).map(&:to_s)
30
+ end
31
+
32
+ def validate_actions(actions)
33
+ raise Auditor::Error.new "at least one action in #{Config.valid_actions.inspect} must be specified" if actions.empty?
34
+ raise Auditor::Error.new "#{Config.valid_actions.inspect} are the only valid actions" unless actions.all? { |a| Config.valid_actions.include?(a.to_sym) }
35
+ end
36
+
37
+ end
38
+ end
@@ -1,44 +1,45 @@
1
- require 'auditor/user'
1
+ require 'auditor/status'
2
2
 
3
3
  module Auditor
4
4
  class Recorder
5
-
6
- def initialize(action, model, options, &blk)
7
- @action, @model, @options, @blk = action.to_sym, model, options, blk
5
+ include Status
6
+
7
+ def initialize(options, &blk)
8
+ @options = options
9
+ @blk = blk
8
10
  end
9
-
10
- def audit_before
11
- @audit = Audit.new(:edits => prepare_edits(@model.changes, @options))
11
+
12
+ [:create, :find, :update, :destroy].each do |action|
13
+ define_method("after_#{action}") do |model|
14
+ audit(model, action)
15
+ end
12
16
  end
13
17
 
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
18
+ private
19
+
20
+ def audit(model, action)
21
+ return nil if auditing_disabled?
22
+ user = Auditor::User.current_user
23
+
24
+ audit = Audit.new
25
+ audit.auditable = model
26
+ audit.audited_changes = prepare_changes(model.changes) if changes_available?(action)
27
+ audit.action = action
28
+ audit.comment = @blk.call(model, user) if @blk
29
+
30
+ @options[:fail_on_error] ? audit.save! : audit.save
29
31
  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
-
32
+
33
+ def prepare_changes(changes)
34
+ chg = changes.dup
35
+ chg = chg.delete_if { |key, value| @options[:except].include?(key) } unless @options[:except].empty?
36
+ chg = chg.delete_if { |key, value| !@options[:only].include?(key) } unless @options[:only].empty?
37
+ chg.empty? ? nil : chg
38
+ end
39
+
40
+ def changes_available?(action)
41
+ [:create, :update].include?(action)
42
+ end
43
+
43
44
  end
44
- end
45
+ end
@@ -1,19 +1,19 @@
1
1
  module Auditor
2
2
  module SpecHelpers
3
- include Auditor::Integration
4
-
3
+ include Auditor::Status
4
+
5
5
  def self.included(base)
6
6
  base.class_eval do
7
7
  before(:each) do
8
- disable_auditor
8
+ disable_auditing
9
9
  end
10
-
10
+
11
11
  after(:each) do
12
- enable_auditor
12
+ enable_auditing
13
13
  end
14
14
  end
15
15
  end
16
-
16
+
17
17
  end
18
18
  end
19
19
 
@@ -0,0 +1,60 @@
1
+ module Auditor
2
+ module Status
3
+
4
+ def auditing_disabled?
5
+ Thread.current[:auditor_disabled] == true
6
+ end
7
+
8
+ def auditing_enabled?
9
+ Thread.current[:auditor_disabled] == false
10
+ end
11
+
12
+ def disable_auditing
13
+ Thread.current[:auditor_disabled] = true
14
+ end
15
+
16
+ def enable_auditing
17
+ Thread.current[:auditor_disabled] = false
18
+ end
19
+
20
+ def without_auditing
21
+ previously_disabled = auditing_disabled?
22
+
23
+ begin
24
+ disable_auditing
25
+ result = yield if block_given?
26
+ ensure
27
+ enable_auditing unless previously_disabled
28
+ end
29
+
30
+ result
31
+ end
32
+
33
+ def with_auditing
34
+ previously_disabled = auditing_disabled?
35
+
36
+ begin
37
+ enable_auditing
38
+ result = yield if block_given?
39
+ ensure
40
+ disable_auditing if previously_disabled
41
+ end
42
+
43
+ result
44
+ end
45
+
46
+ def audit_as(user)
47
+ previous_user = Audit::User.current_user
48
+
49
+ begin
50
+ Audit::User.current_user = user
51
+ result = yield if block_given?
52
+ ensure
53
+ Audit::User.current_user = previous_user
54
+ end
55
+
56
+ result
57
+ end
58
+
59
+ end
60
+ end