Skip to content

Commit

Permalink
Migrated code from the older big_brother project.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff Kunkle committed Jan 6, 2011
0 parents commit 425c68d
Show file tree
Hide file tree
Showing 28 changed files with 875 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage
rdoc
auditor-*.gem
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2009 Near Infinity Corporation

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
69 changes: 69 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
= Auditor

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.

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.

= Installation

To use it with your Rails 3 project, add the following line to your Gemfile

gem 'auditor'

Auditor can also be installed as a Rails plugin

rails plugin install git://github.com/nearinfinity/auditor.git

Generate the migration and create the audits table

rails generate auditor:migration
rake db:migrate

= Setup

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

class ApplicationController < ActionController::Base
before_filter :set_current_user

private

def set_current_user
Auditor::User.current_user = @current_user
end
end

= Examples

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.

class EditablePage < ActiveRecord::Base
include Auditor::ModelAudit

has_many :tags

audit(:create, :update) { |model, user| "Editable page modified by #{user.display_name}" }
audit(:destroy) { |model, user| "#{user.display_name} deleted editable page #{model.id}" }
end

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:

* auditable_id - the primary key of the table belonging to the audited model object
* auditable_type - the class type of the audited model object
* auditable_version - the version number of the audited model object (if versioning is tracked)
* user_id - the primary key of the table belonging to the user being audited
* user_type - the class type of the model object representing users in your application
* action - a string indicating the action that was audited (create, update, destroy, or find)
* message - the custom message returned by any block passed to the audit call
* edits - a YAML string containing the before and after state of any model attributes that changed
* created_at - the date and time the audit record was recorded

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

# Prevent SSN and passwords from being saved in the audit table
audit(:create, :destroy, :except => [:ssn, :password])

# Only audit edits to the title column when destroying/deleting
audit(:destroy, :only => :title)

Copyright (c) 2010 Near Infinity Corporation, released under the MIT license
37 changes: 37 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
$:.unshift File.expand_path("../lib", __FILE__)

require 'rake'
require 'rake/rdoctask'
require 'rspec/core/rake_task'

desc 'Default: run specs'
task :default => :spec

desc "Run specs"
RSpec::Core::RakeTask.new do |t|
t.rspec_opts = %w(-fs --color)
end

desc "Run specs with RCov"
RSpec::Core::RakeTask.new(:rcov) do |t|
t.rspec_opts = %w(-fs --color)
t.rcov = true
t.rcov_opts = %w(--exclude "spec/*,gems/*")
end

desc 'Generate documentation for the gem.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Auditor'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end

task :build do
system "gem build auditor.gemspec"
end

task :release => :build do
system "gem push auditor-#{Auditor::VERSION}"
end
24 changes: 24 additions & 0 deletions auditor.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib/', __FILE__)
$:.unshift lib unless $:.include?(lib)

require 'auditor/version'

Gem::Specification.new do |s|
s.name = "auditor"
s.version = Auditor::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ["Jeff Kunkle", "Matt Wizeman"]
s.homepage = "http://github.com/nearinfinity/auditor"
s.summary = "Rails 3 plugin for auditing access to your ActiveRecord model objects"
s.description = "Auditor allows you to declaratively specify what CRUD operations should be audited and save the audit data to the database."
s.license = "MIT"

s.required_rubygems_version = ">= 1.3.6"

s.add_development_dependency "rspec"

s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.rdoc)
s.test_files = Dir.glob("{spec}/**/*")
s.require_path = 'lib'
end
1 change: 1 addition & 0 deletions init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'auditor'
9 changes: 9 additions & 0 deletions lib/auditor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require 'auditor/audit'
require 'auditor/integration'
require 'auditor/model_audit'
require 'auditor/user'
require 'auditor/version'

module Auditor
class Error < StandardError; end
end
6 changes: 6 additions & 0 deletions lib/auditor/audit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require 'active_record'

class Audit < ActiveRecord::Base
validates_presence_of :auditable_id, :auditable_type, :user_id, :user_type, :action
serialize :edits
end
36 changes: 36 additions & 0 deletions lib/auditor/config_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Auditor
class ConfigParser

def self.extract_config(args)
options = (args.delete_at(args.size - 1) if args.last.kind_of?(Hash)) || {}
normalize_config args, options
validate_config args, options
options = normalize_options(options)

[args, options]
end

private

def self.normalize_config(actions, options)
actions.each_with_index { |item, index| actions[index] = item.to_sym }
options.each_pair { |k, v| options[k.to_sym] = options.delete(k) unless k.kind_of? Symbol }
end

def self.normalize_options(options)
return { :except => [], :only => [] } if options.nil? || options.empty?
options[:except] = options[:except] || []
options[:only] = options[:only] || []
options[:except] = Array(options[:except]).map(&:to_s)
options[:only] = Array(options[:only]).map(&:to_s)
options
end

def self.validate_config(actions, options)
raise Auditor::Error.new "at least one :create, :find, :update, or :destroy action must be specified" if actions.empty?
raise Auditor::Error.new ":create, :find, :update, and :destroy are the only valid actions" unless actions.all? { |a| [:create, :find, :update, :destroy].include? a }
raise Auditor::Error.new "only one of :except and :only can be specified" if options.size > 1
end

end
end
49 changes: 49 additions & 0 deletions lib/auditor/integration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'auditor/thread_status'

module Auditor
module Integration

def without_auditor
previously_disabled = auditor_disabled?
disable_auditor

begin
result = yield if block_given?
ensure
enable_auditor unless previously_disabled
end

result
end

def with_auditor
previously_disabled = auditor_disabled?
enable_auditor

begin
result = yield if block_given?
ensure
disable_auditor if previously_disabled
end

result
end

def disable_auditor
Auditor::ThreadStatus.disable
end

def enable_auditor
Auditor::ThreadStatus.enable
end

def auditor_disabled?
Auditor::ThreadStatus.disabled?
end

def auditor_enabled?
Auditor::ThreadStatus.enabled?
end

end
end
47 changes: 47 additions & 0 deletions lib/auditor/model_audit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'auditor/thread_status'
require 'auditor/config_parser'
require 'auditor/recorder'

module Auditor
module ModelAudit

def self.included(base)
base.extend ClassMethods
end

# ActiveRecord won't call the after_find handler unless it see's a specific after_find method defined
def after_find; end

def auditor_disabled?
Auditor::ThreadStatus.disabled? || @auditor_disabled
end

module ClassMethods
def audit(*args, &blk)
actions, options = Auditor::ConfigParser.extract_config(args)

actions.each do |action|
unless action.to_sym == :find
callback = "auditor_before_#{action}"
define_method(callback) do
@auditor_auditor = Auditor::Recorder.new(action, self, options, &blk)
@auditor_auditor.audit_before unless auditor_disabled?
true
end
send "before_#{action}".to_sym, callback
end

callback = "auditor_after_#{action}"
define_method(callback) do
@auditor_auditor = Auditor::Recorder.new(action, self, options, &blk) if action.to_sym == :find
@auditor_auditor.audit_after unless auditor_disabled?
true
end
send "after_#{action}".to_sym, callback

end
end
end

end
end
44 changes: 44 additions & 0 deletions lib/auditor/recorder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'auditor/user'

module Auditor
class Recorder

def initialize(action, model, options, &blk)
@action, @model, @options, @blk = action.to_sym, model, options, blk
end

def audit_before
@audit = Audit.new(:edits => prepare_edits(@model.changes, @options))
end

def audit_after
@audit ||= Audit.new

@audit.attributes = {
:auditable_id => @model.id,
:auditable_type => @model.class.to_s,
:user_id => user.id,
:user_type => user.class.to_s,
:action => @action.to_s
}

@audit.auditable_version = @model.version if @model.respond_to? :version
@audit.message = @blk.call(@model, user) if @blk

@audit.save
end

private
def user
Auditor::User.current_user
end

def prepare_edits(changes, options)
chg = changes.dup
chg = chg.delete_if { |key, value| options[:except].include? key } unless options[:except].empty?
chg = chg.delete_if { |key, value| !options[:only].include? key } unless options[:only].empty?
chg.empty? ? nil : chg
end

end
end
20 changes: 20 additions & 0 deletions lib/auditor/spec_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Auditor
module SpecHelpers
include Auditor::Integration

def self.included(base)
base.class_eval do
before(:each) do
disable_auditor
end

after(:each) do
enable_auditor
end
end
end

end
end


Loading

0 comments on commit 425c68d

Please sign in to comment.