Rails audit system with both ActiveResource and ActiveRecord - ruby

I have a huge project with both of ActiveRecord and ActiveResource models. I need to implement logging of user activity with these models and also to log changes of model attributes (save object state or somthing like that). Changes can made by users or cron rake tasks.
I also must have possibility to search any data by date , any field ..etc
Will be nice also to generate readable messages with last activity , for example
User Bob change his password to * and email to ** at 2011-08-12 08:12
Staff Jeff added new partner: Company name at 2011-08-12 08:13
Admin Jack deleted product : Product name at 2011-09-12 11:11
Client Sam ordered new service : Service name at 2011-09-12 11:12
Does anybody implement such logging? Ideas? Advices?
should I use gems or can I do all the logic with observers not changing models?
I liked gem https://github.com/airblade/paper_trail can anybody say how can I make it work with activeresource ?

You are looking for
https://github.com/collectiveidea/acts_as_audited
Few open source projects use that plugin I think Red Mine as well as The Foreman.
Edit: Unfortunately it can do only ActiveRecord, not ActiveResource.

Fivell, I just saw this question and don't have time to work up alterations this evening before the bounty expires, so I'll give you my auditing code that works with ActiveRecord and should work with ActiveResource, perhaps with a few tweaks (I don't use ARes often enough to know offhand). I know the callbacks we use are there, but I'm not sure if ARes has ActiveRecord's dirty attribute changes tracking.
This code logs each CREATE/UPDATE/DELETE on all models (excepting CREATEs on the audit log model and any other exceptions you specify) with the changes stored as JSON. A cleaned backtrace is also stored so you can determine what code made the change (this captures any point in your MVC as well as rake tasks and console usage).
This code works for console usage, rake tasks, and http requests, although generally only the last one logs the current user. (If I recall correctly, the ActiveRecord observer that this replaced did not work in rake tasks or the console.) Oh, this code comes from a Rails 2.3 app - I have a couple Rails 3 apps, but I haven't needed this kind of auditing for them yet.
I don't have code that builds a nice display of this information (we only dig into the data when we need to look into an issue), but since the changes are stored as JSON it should be fairly straightforward.
First, we store the current user in User.current so it is accessible everywhere, so in app/models/user.rb:
Class User < ActiveRecord::Base
cattr_accessor :current
...
end
The current user is set in the application controller for each request like so (and does not cause concurrency issues):
def current_user
User.current = session[:user_id] ? User.find_by_id(session[:user_id]) : nil
end
You could set User.current in your rake tasks if it made sense to.
Next, we define the model to store the audit info app/models/audit_log_entry.rb - you'll want to customize IgnoreClassesRegEx to fit any models you don't want audited:
# == Schema Information
#
# Table name: audit_log_entries
#
# id :integer not null, primary key
# class_name :string(255)
# entity_id :integer
# user_id :integer
# action :string(255)
# data :text
# call_chain :text
# created_at :datetime
# updated_at :datetime
#
class AuditLogEntry < ActiveRecord::Base
IgnoreClassesRegEx = /^ActiveRecord::Acts::Versioned|ActiveRecord.*::Session|Session|Sequence|SchemaMigration|CronRun|CronRunMessage|FontMetric$/
belongs_to :user
def entity (reload = false)
#entity = nil if reload
begin
#entity ||= Kernel.const_get(class_name).find_by_id(entity_id)
rescue
nil
end
end
def call_chain
return if call_chain_before_type_cast.blank?
if call_chain_before_type_cast.instance_of?(Array)
call_chain_before_type_cast
else
JSON.parse(call_chain_before_type_cast)
end
end
def data
return if data_before_type_cast.blank?
if data_before_type_cast.instance_of?(Hash)
data_before_type_cast
else
JSON.parse(data_before_type_cast)
end
end
def self.debug_entity(class_name, entity_id)
require 'fastercsv'
FasterCSV.generate do |csv|
csv << %w[class_name entity_id date action first_name last_name data]
find_all_by_class_name_and_entity_id(class_name, entity_id,
:order => 'created_at').each do |a|
csv << [a.class_name, a.entity_id, a.created_at, a.action,
(a.user && a.user.first_name), (a.user && a.user.last_name), a.data]
end
end
end
end
Next we add some methods to ActiveRecord::Base to make the audits work. You'll want to look at the audit_log_clean_backtrace method and modify for your needs. (FWIW, we put additions to existing classes in lib/extensions/*.rb which are loaded in an initializer.) In lib/extensions/active_record.rb:
class ActiveRecord::Base
cattr_accessor :audit_log_backtrace_cleaner
after_create :audit_log_on_create
before_update :save_audit_log_update_diff
after_update :audit_log_on_update
after_destroy :audit_log_on_destroy
def audit_log_on_create
return if self.class.name =~ /AuditLogEntry/
return if self.class.name =~ AuditLogEntry::IgnoreClassesRegEx
audit_log_create 'CREATE', self, caller
end
def save_audit_log_update_diff
#audit_log_update_diff = changes.reject{ |k,v| 'updated_at' == k }
end
def audit_log_on_update
return if self.class.name =~ AuditLogEntry::IgnoreClassesRegEx
return if #audit_log_update_diff.empty?
audit_log_create 'UPDATE', #audit_log_update_diff, caller
end
def audit_log_on_destroy
return if self.class.name =~ AuditLogEntry::IgnoreClassesRegEx
audit_log_create 'DESTROY', self, caller
end
def audit_log_create (action, data, call_chain)
AuditLogEntry.create :user => User.current,
:action => action,
:class_name => self.class.name,
:entity_id => id,
:data => data.to_json,
:call_chain => audit_log_clean_backtrace(call_chain).to_json
end
def audit_log_clean_backtrace (backtrace)
if !ActiveRecord::Base.audit_log_backtrace_cleaner
ActiveRecord::Base.audit_log_backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
ActiveRecord::Base.audit_log_backtrace_cleaner.add_silencer { |line| line =~ /\/lib\/rake\.rb/ }
ActiveRecord::Base.audit_log_backtrace_cleaner.add_silencer { |line| line =~ /\/bin\/rake/ }
ActiveRecord::Base.audit_log_backtrace_cleaner.add_silencer { |line| line =~ /\/lib\/(action_controller|active_(support|record)|hoptoad_notifier|phusion_passenger|rack|ruby|sass)\// }
ActiveRecord::Base.audit_log_backtrace_cleaner.add_filter { |line| line.gsub(RAILS_ROOT, '') }
end
ActiveRecord::Base.audit_log_backtrace_cleaner.clean backtrace
end
end
Finally, here are the tests we have on this - you'll need to modify the actual test actions of course. test/integration/audit_log_test.rb
require File.dirname(__FILE__) + '/../test_helper'
class AuditLogTest < ActionController::IntegrationTest
def setup
end
def test_audit_log
u = users(:manager)
log_in u
a = Alert.first :order => 'id DESC'
visit 'alerts/new'
fill_in 'alert_note'
click_button 'Send Alert'
a = Alert.first :order => 'id DESC', :conditions => ['id > ?', a ? a.id : 0]
ale = AuditLogEntry.first :conditions => {:class_name => 'Alert', :entity_id => a.id }
assert_equal 'Alert', ale.class_name
assert_equal 'CREATE', ale.action
end
private
def log_in (user, password = 'test', initial_url = home_path)
visit initial_url
assert_contain 'I forgot my password'
fill_in 'email', :with => user.email
fill_in 'password', :with => password
click_button 'Log In'
end
def log_out
visit logout_path
assert_contain 'I forgot my password'
end
end
And test/unit/audit_log_entry_test.rb:
# == Schema Information
#
# Table name: audit_log_entries
#
# id :integer not null, primary key
# class_name :string(255)
# action :string(255)
# data :text
# user_id :integer
# created_at :datetime
# updated_at :datetime
# entity_id :integer
# call_chain :text
#
require File.dirname(__FILE__) + '/../test_helper'
class AuditLogEntryTest < ActiveSupport::TestCase
test 'should handle create update and delete' do
record = Alert.new :note => 'Test Alert'
assert_difference 'Alert.count' do
assert_difference 'AuditLogEntry.count' do
record.save
ale = AuditLogEntry.first :order => 'created_at DESC'
assert ale
assert_equal 'CREATE', ale.action, 'AuditLogEntry.action should be CREATE'
assert_equal record.class.name, ale.class_name, 'AuditLogEntry.class_name should match record.class.name'
assert_equal record.id, ale.entity_id, 'AuditLogEntry.entity_id should match record.id'
end
end
assert_difference 'AuditLogEntry.count' do
record.update_attribute 'note', 'Test Update'
ale = AuditLogEntry.first :order => 'created_at DESC'
expected_data = {'note' => ['Test Alert', 'Test Update']}
assert ale
assert_equal 'UPDATE', ale.action, 'AuditLogEntry.action should be UPDATE'
assert_equal expected_data, ale.data
assert_equal record.class.name, ale.class_name, 'AuditLogEntry.class_name should match record.class.name'
assert_equal record.id, ale.entity_id, 'AuditLogEntry.entity_id should match record.id'
end
assert_difference 'AuditLogEntry.count' do
record.destroy
ale = AuditLogEntry.first :order => 'created_at DESC'
assert ale
assert_equal 'DESTROY', ale.action, 'AuditLogEntry.action should be CREATE'
assert_equal record.class.name, ale.class_name, 'AuditLogEntry.class_name should match record.class.name'
assert_equal record.id, ale.entity_id, 'AuditLogEntry.entity_id should match record.id'
assert_nil Alert.find_by_id(record.id), 'Alert should be deleted'
end
end
test 'should not log AuditLogEntry create entry and block on update and delete' do
record = Alert.new :note => 'Test Alert'
assert_difference 'Alert.count' do
assert_difference 'AuditLogEntry.count' do
record.save
end
end
ale = AuditLogEntry.first :order => 'created_at DESC'
assert_equal 'CREATE', ale.action, 'AuditLogEntry.action should be CREATE'
assert_equal record.class.name, ale.class_name, 'AuditLogEntry.class_name should match record.class.name'
assert_equal record.id, ale.entity_id, 'AuditLogEntry.entity_id should match record.id'
assert_nil AuditLogEntry.first(:conditions => { :class_name => 'AuditLogEntry', :entity_id => ale.id })
if ale.user_id.nil?
u = User.first
else
u = User.first :conditions => ['id != ?', ale.user_id]
end
ale.user_id = u.id
assert !ale.save
assert !ale.destroy
end
end

https://github.com/collectiveidea/acts_as_audited
and
https://github.com/airblade/paper_trail
are both great solutions for ActiveRecord only, but since much of ActiveRecord has been extracted to ActiveModel, it's likely to be reasonable to extend either to support ActiveResource as well, at least for read-only support. I looked through the Github network graphs and googled around and there doesn't appear to be any ongoing development of such a solution, nevertheless I expect it will be easier to implement on top of one of these two plugins than starting from scratch. paper_trail appears to be under more active development and has some commits for Rails 3.1, so it may be more up to date with Rails internals and easier to extend, but that's just a gut instinct—I'm not familiar with the internals of either.

The acts_as_audited gem should work well for you:
https://github.com/collectiveidea/acts_as_audited
And as far as ActiveResource is considered, it will also be a model in some other application. You can use the gem at the server side, and you don't need to audit it at the client side. All the CRUD operations using ActiveResource would finally translate to CRUD operations on the ActiveRecord (on server side).
So probably you need to look at it from a distance, and the same solution would apply in both the cases, but at different places.

for tracking user activity(CRUD ), i've created a class inherits from Logger, and now I am planing to write a litle plugin for tracking user that i can use for any ROR application built. I have already checked if there is a plugin like that but I didn’t see. I guess there are many gem like paper-trail, acts_as_audited or itslog but i prefer to use a plugin. Any suggestions?
Here is a link that might help you : http://robaldred.co.uk/2009/01/custom-log-files-for-your-ruby-on-rails-applications/comment-page-1/#comment-342
nice coding

Related

active record with ruby (not rails)

I am using active record with ruby (but not rails). I am using sqlite3 which has a test.db on file (not just in-memory). When I run the following code snippet using user.create, it complains about argument error (and when I use use.save, it throws an active record exception. Any idea what I might be doing wrong? Thanks
require 'rubygems'
gem 'activerecord'
require 'sqlite3'
require 'active_record'
ActiveRecord::Base.logger = Logger.new(STDERR)
#ActiveRecord::Base.colorize_logging = false
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:host => "localhost",
:database => 'test.db'
)
class User < ActiveRecord::Base
#attr_accessible :email, :full_name
attr_accessor :email
attr_accessor :full_name
validates :email, presence: true, uniqueness: true
def initialize(email, full_name)
#email = email
#full_name = full_name
end
end
puts "full_name for user:"
full_name = gets.chomp
puts "email address:"
email = gets.chomp
user = User.new(email, full_name)
#user.save
user = User.create!(email: '', full_name: '')
Exception in first case (with User.create!):
main.rb:42:in `initialize': wrong number of arguments (1 for 2) (ArgumentError)
from /var/lib/gems/1.9.1/gems/activerecord-4.2.4/lib/active_record/inheritance.rb:61:in `new'
from /var/lib/gems/1.9.1/gems/activerecord-4.2.4/lib/active_record/inheritance.rb:61:in `new'
from /var/lib/gems/1.9.1/gems/activerecord-4.2.4/lib/active_record/persistence.rb:50:in `create!'
from main.rb:55:in `<main>'
It is complaining about the new method. According to the documentation: (http://api.rubyonrails.org/classes/ActiveRecord/Base.html), you don't need the initialize, because when you inherit from ActiveRecord::Base, you need to initialize your objects with a hash.
user = User.new({email: email, full_name: full_name})
# or
user = User.new(email: email, full_name: full_name)
# then
user.save
You need to drop the initialize and the attr_accessor from your code.
Try to comment User#initialize method and create new user like this:
User.create! email: 'halk#mail.com', full_name: 'Halk'
Explanation
When you declare AR model by heritage from ActiveRecord::Base class you don't need to define your own #initialize method. But you do. When you call User::create! method, you pass only one argument - Hash with two pairs (with email and full_name keys). But User#initialize define two parameters - email and full_name separately. So Ruby exception raise and talk about it:
wrong number of arguments (1 for 2) (ArgumentError)

cramp framework sync 'render' correct way using em-synchrony

To describe my problem I attach simple Cramp http://cramp.in/ class.
I add some modification but its mainly work like https://github.com/lifo/cramp-pub-sub-chat-demo/blob/master/app/actions/chat_action.rb
class ChatAction < Cramp::Websocket
use_fiber_pool
on_start :create_redis
on_finish :handle_leave, :destroy_redis
on_data :received_data
def create_redis
#redis = EM::Hiredis.connect('redis://127.0.0.1:6379/0')
end
def destroy_redis
#redis.pubsub.close_connection
#redis.close_connection
end
def received_data(data)
msg = parse_json(data)
case msg[:action]
when 'join'
handle_join(msg)
when 'message'
handle_message(msg)
else
# skip
end
end
def handle_join(msg)
#user = msg[:user]
subscribe
publish(:action => 'control', :user => #user, :message => 'joined the chat room')
end
def handle_leave
publish :action => 'control', :user => #user, :message => 'left the chat room'
end
def handle_message(msg)
publish(msg.merge(:user => #user))
# added only for inline sync tests
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
private
def subscribe
#redis.pubsub.subscribe('chat') do |message|
render(message)
end
end
def publish(message)
#redis.publish('chat', encode_json(message))
end
def encode_json(obj)
Yajl::Encoder.encode(obj)
end
def parse_json(str)
Yajl::Parser.parse(str, :symbolize_keys => true)
end
def render_json(hash)
render encode_json(hash)
end
end
More about what i try to do is in handle_message method.
I try send messages to client in correct order. First publish message to all subscribers, second render some internal info only for current connected client.
For above code client receives:
{"action":"message","user":"user1","message":"this info should appear after published message"}
{"action":"message","message":"simple message","user":"user1"}
Its not synchronized, because of em-hiredis defferable responses, probably.
So I try to synchronized it this way:
def handle_message(msg)
EM::Synchrony.sync publish(msg.merge(:user => #user))
EM::Synchrony.next_tick do # if I comment this block messages order is still incorrect
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
end
Now, client handle messages with correct order.
{"action":"message","message":"simple message","user":"user1"}
{"action":"message","user":"user1","message":"this info should appear after published message"}
My questions are:
When I comment EM::Synchrony.next_tick block, messages order is still incorrect. What meaning have EM::Synchrony.next_tick block in this example?
Is this good way to handle inline sync with Cramp or EventMachine ?
Is there a better, clearer way to handle it ?
Thank you!
I found solution of this problem, em-synchrony should work inline out of the box by requiring this library:
require 'em-synchrony/em-hiredis'
class ChatAction < Cramp::Websocket
Using EM::Synchrony.next_tick block is bad idea, with big help of em-synchrony community I add em-hiredis 0.2.1 compatibility patch on github
So now handle_message method looks like this:
def handle_message(msg)
publish(msg.merge(:user => #user))
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
Don`t forget to take this gem from github
gem 'em-synchrony', :git=> 'git://github.com/igrigorik/em-synchrony.git'
Hope it helps someone.

Trouble getting the timestamp attributes for Active Record

So I'm trying to get the updated at attribute for my database object, but it's not working. I can get the other attributes but not the updated_at or created_at attributes. I'm also noticing that I can't get the id attribute as well. It seems as if anything I haven't declared in my Pocket.new does not exist. Here's my code:
By the way, I'm not using rails. I'm using sinatra-activerecord and sinatra.
Model
class Pocket < ActiveRecord::Base
def retrieve(consumer_key)
url = "get/"
pocket_url = join_url(url)
# time = Time.now.to_i
token = self.token
puts consumer_key
puts self
puts time = self.updated_at #.to_i this is where it happens.
options = {
:access_token => token,
:consumer_key => consumer_key,
:since => (time if defined? time)
}
hello = RestClient.post pocket_url, options
# JSON.parse(hello)
end
Controller
get '/auth/pocket/callback' do
[…]
pocket = Pocket.new(token: #token, user: #user)
DB migration
class CreatePocket < ActiveRecord::Migration
def up
create_table :pockets do |t|
t.string :user
t.string :token
t.timestamps
end
end
def down
drop_table :users
end
end
id, created_at, and updated_at are going to be nil for un-persisted objects, which is what you have by just calling .new and not saving the object yet.
Those attributes will be sent as soon as you call .save (assuming there are no validation errors).
Actually that isn't 100% true. updated_at will not be set on the first create / save, but it will be set on subsequent saves.

Uninitialized constant (NameError) when using FactoryGirl in module

Here's the error I'm getting when I try to run my tests with RSpec:
C:/Ruby193/lib/ruby/gems/1.9.1/gems/activesupport-3.2.11/lib/active_support/infl
ector/methods.rb:230:in `block in constantize': uninitialized constant User (Nam
eError)
I'm trying to run FactoryGirl with RSpec but without Rails. Here are the files that take part in the testing:
user_spec.rb
require 'spec_helper'
module Bluereader
describe User do
describe 'login' do
user = FactoryGirl.build(:user)
end
describe 'logout' do
end
describe 'create_account' do
end
describe 'delete_account' do
end
end
end
spec/spec_helper
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'rspec'
require 'lib/bluereader'
require 'factory_girl'
FactoryGirl.find_definitions
spec/factories.rb
require 'digest/sha1'
FactoryGirl.define do
sequence(:username) { |n| "user-#{n}" }
factory :user do
username
encrypted_password Digest::SHA1.hexdigest('password')
full_name 'John Doe'
logged_in_at Time.now
logged_out_at 0
end
end
At this point I know that the factories.rb file is being loaded (I tried with the moronic print-debugging). When I remove the user = FactoryGirl.build(:user) line from user_spec.rb I get no errors (and the normal RSpec feedback telling me there are no tests, but no errors). If you are interested, here's my model:
require 'digest/sha1'
module Bluereader
class User < ActiveRecord::Base
has_many :categories, :foreign_key => :user_id
has_many :news, :foreign_key => :user_id
has_many :settings, :foreign_key => :user_id
attr_reader :full_name
class << self
def login(username, password)
encrypted_password = Digest::SHA1.hexdigest(password)
if not User.exists?(:username => username, :encrypted_password => encrypted_password)
user_id = User.id_from_username(username)
update(user_id, :logged_in_at => Time.now, :logged_out_at => 0)
end
end
def logout
update(current_user.id, :logged_out_at => Time.now)
end
def validate_account(username, password, full_name)
if username.empty? or password.empty or full_name.empty?
return 'Please fill in all the fields.'
end
if User.exists?(:username => username)
return 'That username is already in use.'
end
unless username =~ /^\w+$/
return 'Username field should contain only letters, numbers and underscores.'
end
''
end
def create_account(username, password, full_name)
encrypted_password = Digest::SHA1.hexdigest(password)
User.create(:username => username,
:encrypted_password => encrypted_password,
:full_name => full_name,
:logged_in_at => Time.now,
:logged_out_at => 0)
end
def delete_account
current_user.destroy
end
private
def id_from_username(username)
user = where(:username => username).first
user.nil? ? 0 : user.id
end
def current_user
where(:logged_out_at => 0).first
end
end
end
end
SOLUTION
The problem was that the class User was in a module, here's the solution:
factory :user, class: Bluereader::User do
You need to require the rails environment in your spec helper file. Add the following to spec/spec_helper.rb:
require File.expand_path("../../config/environment", __FILE__)
Update
Even if you're not using Rails, you'll still need to require the models in your spec helper.
Taken from the bottom of the question
The problem was that the class User was in a module, here's the solution:
factory :user, class: Bluereader::User do
For anyone clumsy like me, you may have FactoryGirl in your code where you meant to have FactoryBot

Sinatra: DB Authentication with Sessions

I am writing a small sinatra application that I am integrating with Authlogic (following https://github.com/ehsanul/Sinatra-Authlogic-Template)
Everything works except for when I try to login. I get the following error:
NameError at /login
undefined local variable or method `active' for #<User:0x000001040208f0>
I am including the authlogic gem versus including it as a vendor. So my Sinatra app is not exactly the same as the one on Github.
Any and all inquiries will be MUCH appreciated!! Thanks!
Found out my issue.
Here is the model according to the Github page:
class User < ActiveRecord::Base
acts_as_authentic do |c|
# Bcrypt is recommended
#crypto_provider = Authlogic::CryptoProviders::BCrypt
c.perishable_token_valid_for( 24*60*60 )
c.validates_length_of_password_field_options =
{:on => :update, :minimum => 6, :if => :has_no_credentials?}
c.validates_length_of_password_confirmation_field_options =
{:on => :update, :minimum => 6, :if => :has_no_credentials?}
end
def active?
active
end
def has_no_credentials?
crypted_password.blank? #&& self.openid_identifier.blank?
end
def send_activation_email
Pony.mail(
:to => self.email,
:from => "no-reply#domain.tld",
:subject => "Activate your account",
:body => "You can activate your account at this link: " +
"http://domain.tld/activate/#{self.perishable_token}"
)
end
def send_password_reset_email
Pony.mail(
:to => self.email,
:from => "no-reply#domain.tld",
:subject => "Reset your password",
:body => "We have recieved a request to reset your password. " +
"If you did not send this request, then please ignore this email.\n\n" +
"If you did send the request, you may reset your password using the following link: " +
"http://domain.tld/reset-password/#{self.perishable_token}"
)
end
end
I removed all of the mail methods but my script was failing on the active? method because it was looking for an active column in the users table. Since I am unable to append this column to the table (due to data integrity with another system) I simply told my method to return true
My User.rb
class UserSession < Authlogic::Session::Base
end
class User < ActiveRecord::Base
acts_as_authentic do |c|
end
def active?
return true
end
end
Hope this helps someone!

Resources