pg_audit_log 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ *~
3
+ .DS_Store
4
+ .bundle
5
+ .idea
6
+ Gemfile.lock
7
+ doc
8
+ pkg/*
9
+ tags
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.2-p136@pg_audit_log
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ # Specify your gem's dependencies the .gemspec
4
+ gemspec
5
+
6
+ gem "pg"
7
+ gem "ZenTest"
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009-11 Case Commons, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,64 @@
1
+ = pg_audit_log
2
+
3
+ * http://github.com/casecommons/pg_audit_log/
4
+
5
+ == DESCRIPTION
6
+
7
+ PostgreSQL only database-level audit logging of all databases changes using a completely transparent stored procedure and triggers.
8
+ Comes with specs for your project and a rake task to generate the reverse SQL to undo changes logged}
9
+
10
+ All SQL INSERTs, UPDATEs, and DELETEs will be captured. Record columns that do not change do not generate an audit log entry.
11
+
12
+ == INSTALL
13
+
14
+ === Enable custom variables in your postgresql instance
15
+
16
+ To enable auditing you need to set a variable in your postgresql.conf file:
17
+
18
+ For macports: /opt/local/var/db/postgresql84/defaultdb/postgresql.conf
19
+ For homebrew: /usr/local/var/postgres/postgresql.conf
20
+ For linux: it varies from distro to distro.
21
+
22
+ At the bottom of the file add the following:
23
+
24
+ custom_variable_classes = 'audit'
25
+
26
+ Then restart your PostgreSQL server
27
+
28
+ === Rails 3
29
+
30
+ $ rails generate pg_audit_log:install
31
+
32
+ === Installing the PostgreSQL function and triggers for your project
33
+
34
+ $ rake pg_audit_log:install
35
+
36
+ == Using on your project
37
+
38
+ The PgAuditLog::Entry ActiveRecord model represents a single entry in the audit log table. Each entry represents a single change to a single field of a record in a table. So if you change 3 columns of a record, that will generate 3 corresponding PgAuditLog::Entry records.
39
+
40
+ === Migrations
41
+
42
+ TODO
43
+
44
+ === schema.rb and development_structure.sql
45
+
46
+ Since schema.rb cannot represent TRIGGERs or FUNCTIONs you will need to set your environment to generate SQL instead of ruby for your database schema and structure. In your application environment put the following:
47
+
48
+ config.active_record.schema_format = :sql
49
+
50
+ And you can generate this sql using:
51
+
52
+ $ rake db:structure:dump
53
+
54
+ === Uninstalling
55
+
56
+ $ rake pg_audit_log:uninstall
57
+
58
+ == REQUIREMENTS
59
+
60
+ * ActiveRecord
61
+
62
+ == LICENSE
63
+
64
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ task :autotest do
5
+ sh "bundle update && autotest -s rspec2"
6
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "pg_audit_log"
@@ -0,0 +1,11 @@
1
+ module PgAuditLog
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def install
7
+ directory "lib/tasks"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module PgAuditLog
2
+ module Generators
3
+ class RSpecGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def install
7
+ directory "spec/models"
8
+ copy_file "spec/models/pg_audit_log_spec.rb", "spec/models/pg_audit_log_spec.rb"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,76 @@
1
+ namespace :pg_audit_log do
2
+ IGNORED_TABLES = ["plugin_schema_migrations", "sessions", "schema_migrations"]
3
+
4
+ desc "Install audit_log triggers on all tables"
5
+ task :install => :environment do
6
+ unless all_tables.include?(PgAuditLog::Entry.table_name)
7
+ puts "Creating #{PgAuditLog::Entry.table_name} table..."
8
+ sql = File.read(File.join(sql_path, "create_audit_log_table.sql"))
9
+ connection.execute_without_auditing(sql)
10
+ end
11
+ puts "Installing audit_changes() function..."
12
+ sql = File.read(File.join(sql_path, "create_audit_changes.sql"))
13
+ connection.execute_without_auditing(sql)
14
+
15
+ puts "Installing all audit log triggers... "
16
+ run_on(audit_log_tables) do |table|
17
+ <<-SQL
18
+ CREATE TRIGGER audit_#{table}
19
+ AFTER INSERT OR UPDATE OR DELETE
20
+ ON #{table}
21
+ FOR EACH ROW
22
+ EXECUTE PROCEDURE audit_changes()
23
+ SQL
24
+ end
25
+ puts "Exporting development_structure.sql..."
26
+ Rake::Task["db:structure:dump"].reenable
27
+ Rake::Task["db:structure:dump"].invoke
28
+ end
29
+
30
+ desc "Uninstall audit log triggers on all tables"
31
+ task :uninstall => :environment do
32
+ puts "Dropping all audit_log triggers... "
33
+ run_on(audit_log_tables) do |table|
34
+ "DROP TRIGGER audit_#{table} ON #{table};"
35
+ end
36
+ puts "Uninstalling audit_changes() function..."
37
+ sql = File.read(File.join(sql_path, "uninstall_audit_changes.sql"))
38
+ connection.execute_without_auditing(sql)
39
+
40
+ puts "Exporting development_structure.sql..."
41
+ Rake::Task["db:structure:dump"].reenable
42
+ Rake::Task["db:structure:dump"].invoke
43
+ end
44
+
45
+ private
46
+
47
+ def connection
48
+ ActiveRecord::Base.connection
49
+ end
50
+
51
+ def all_tables
52
+ connection.tables
53
+ end
54
+
55
+ def audit_log_tables
56
+ all_tables - (IGNORED_TABLES + [PgAuditLog::Entry.table_name])
57
+ end
58
+
59
+ def sql_path
60
+ Rails.root.join("lib/tasks/pg_audit_log")
61
+ end
62
+
63
+ def run_on(tables, &block)
64
+ tables.sort.each do |table|
65
+ puts "* #{table}"
66
+ sql = yield(table)
67
+ begin
68
+ connection.execute_without_auditing(sql)
69
+ rescue => e
70
+ puts e.to_s
71
+ connection.reconnect!
72
+ end
73
+ end
74
+ end
75
+
76
+ end
@@ -0,0 +1,72 @@
1
+ CREATE OR REPLACE PROCEDURAL LANGUAGE plpgsql;
2
+ CREATE OR REPLACE FUNCTION audit_changes() RETURNS trigger
3
+ LANGUAGE plpgsql
4
+ AS $_$
5
+ DECLARE
6
+ col information_schema.columns %ROWTYPE;
7
+ new_value text;
8
+ old_value text;
9
+ primary_key_column varchar;
10
+ primary_key_value varchar;
11
+ user_identifier integer;
12
+ unique_name varchar;
13
+ column_name varchar;
14
+ BEGIN
15
+ FOR col IN SELECT * FROM information_schema.columns WHERE table_name = TG_RELNAME LOOP
16
+ new_value := NULL;
17
+ old_value := NULL;
18
+ primary_key_column := NULL;
19
+ primary_key_value := NULL;
20
+ user_identifier := current_setting('audit.user_id');
21
+ unique_name := current_setting('audit.user_unique_name');
22
+ column_name := col.column_name;
23
+
24
+ EXECUTE 'SELECT pg_attribute.attname
25
+ FROM pg_index, pg_class, pg_attribute
26
+ WHERE pg_class.oid = $1::regclass
27
+ AND indrelid = pg_class.oid
28
+ AND pg_attribute.attrelid = pg_class.oid
29
+ AND pg_attribute.attnum = any(pg_index.indkey)
30
+ AND indisprimary'
31
+ INTO primary_key_column USING TG_RELNAME;
32
+
33
+ IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
34
+ EXECUTE 'SELECT CAST($1 . '|| column_name ||' AS TEXT)' INTO new_value USING NEW;
35
+ IF primary_key_column IS NOT NULL THEN
36
+ EXECUTE 'SELECT CAST($1 . '|| primary_key_column ||' AS VARCHAR)' INTO primary_key_value USING NEW;
37
+ END IF;
38
+ END IF;
39
+ IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
40
+ EXECUTE 'SELECT CAST($1 . '|| column_name ||' AS TEXT)' INTO old_value USING OLD;
41
+ EXECUTE 'SELECT CAST($1 . '|| primary_key_column ||' AS VARCHAR)' INTO primary_key_value USING OLD;
42
+ END IF;
43
+
44
+ IF TG_RELNAME = 'users' AND column_name = 'last_accessed_at' THEN
45
+ NULL;
46
+ ELSE
47
+ IF TG_OP != 'UPDATE' OR new_value != old_value OR (TG_OP = 'UPDATE' AND ( (new_value IS NULL AND old_value IS NOT NULL) OR (new_value IS NOT NULL AND old_value IS NULL))) THEN
48
+ INSERT INTO audit_log("operation",
49
+ "table_name",
50
+ "primary_key",
51
+ "field_name",
52
+ "field_value_old",
53
+ "field_value_new",
54
+ "user_id",
55
+ "user_unique_name",
56
+ "occurred_at"
57
+ )
58
+ VALUES(TG_OP,
59
+ TG_RELNAME,
60
+ primary_key_value,
61
+ column_name,
62
+ old_value,
63
+ new_value,
64
+ user_identifier,
65
+ unique_name,
66
+ current_timestamp);
67
+ END IF;
68
+ END IF;
69
+ END LOOP;
70
+ RETURN NULL;
71
+ END
72
+ $_$;
@@ -0,0 +1,18 @@
1
+ CREATE SEQUENCE audit_log_id_seq
2
+ START WITH 1
3
+ INCREMENT BY 1;
4
+
5
+ CREATE TABLE audit_log (
6
+ id integer PRIMARY KEY DEFAULT nextval('audit_log_id_seq'),
7
+ user_id integer,
8
+ user_unique_name character varying(255),
9
+ operation character varying(255),
10
+ table_name character varying(255),
11
+ field_name character varying(255),
12
+ field_value_new text,
13
+ field_value_old text,
14
+ occurred_at timestamp without time zone,
15
+ primary_key character varying(255)
16
+ );
17
+
18
+ ALTER SEQUENCE audit_log_id_seq OWNED BY audit_log.id;
@@ -0,0 +1,71 @@
1
+ require File.join(File.dirname(__FILE), "pg_audit_log_spec_helper")
2
+
3
+ describe PgAuditLog do
4
+ include PgAuditLogSpecHelper
5
+
6
+ describe "logging for all models" do
7
+ get_all_klasses.each do |klass|
8
+ it "logs inserts for #{klass} (#{klass.table_name})" do
9
+ object, columns = create_object_for_klass(klass)
10
+
11
+ log_entries = PgAuditLog::Entry.find :all, :conditions => {:table_name => klass.table_name, :operation => "INSERT"}
12
+ columns.each do |column|
13
+ selected_entries = log_entries.select {|entry| entry.field_name == column.name}
14
+
15
+ selected_entries.size.should >= 1
16
+
17
+ entry = selected_entries.detect {|entry| entry.primary_key == object.id.to_s}
18
+ entry.field_value_old.should be_nil
19
+ entry.field_value_new.should == get_data(column, true).to_s
20
+ end
21
+
22
+ log_entries.map(&:field_name).uniq.should =~ columns.map(&:name)
23
+ end
24
+
25
+ it "logs updates for #{klass}" do
26
+ klass.delete_all
27
+ klass.count.should == 0
28
+ object, columns = create_object_for_klass(klass, [klass.primary_key, "type", "updated_at", "deleted_at"])
29
+ object.reload # needed for Rails bug around create_without_callbacks + boolean value going from true to false
30
+ columns.each do |column|
31
+ object.send("#{column.name}=", get_diff_data(column))
32
+ end
33
+
34
+ object.send(:update_without_callbacks)
35
+ log_entries = PgAuditLog::Entry.find :all, :conditions => ["table_name = ? AND operation = ? AND field_name != ?", klass.table_name, "UPDATE", "updated_at"]
36
+ columns.each do |column|
37
+ selected_entries = log_entries.select {|entry| entry.field_name == column.name}
38
+
39
+ selected_entries.size.should equal(1), "Expected to find entry for #{column.name} (#{column.type}) but found none!"
40
+
41
+ entry = selected_entries.first
42
+ entry.field_value_old.should == get_data(column, true).to_s
43
+ entry.field_value_new.should == get_diff_data(column, true).to_s
44
+ end
45
+
46
+ log_entries.map(&:field_name).should =~ columns.map(&:name)
47
+ end
48
+
49
+ it "logs deletes for #{klass}" do
50
+ object, columns = create_object_for_klass(klass)
51
+
52
+ ActiveRecord::Base.connection.execute("DELETE FROM #{klass.quoted_table_name} WHERE #{klass.primary_key} = #{object.id}")
53
+
54
+ log_entries = PgAuditLog::Entry.find :all, :conditions => {:table_name => klass.table_name, :operation => "DELETE"}
55
+ columns.each do |column|
56
+ next if column.name == "update_at"
57
+ selected_entries = log_entries.select {|entry| entry.field_name == column.name}
58
+ selected_entries.size.should equal(1), "Expected to find entry for #{column.name} (#{column.type}) but found none!"
59
+
60
+ entry = selected_entries.first
61
+ entry.field_value_old.should == get_data(column, true).to_s
62
+ entry.field_value_new.should be_nil
63
+ end
64
+
65
+ log_entries.map(&:field_name).should =~ columns.map(&:name)
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,105 @@
1
+ module PgAuditLogSpecHelper
2
+ EXCLUDED_CLASSES = [ActiveRecord::Base, PgAuditLog::Entry]
3
+ EXCLUDED_COLUMNS = ["last_accessed_at"]
4
+
5
+ def included(base)
6
+ base.include(InstanceMethods)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def self.find_all_constants(constant)
12
+ constant.constants.find_all.select do |constant|
13
+ klass = constant.is_a?(String) ? constant.constantize : constant
14
+ klass.respond_to?(:ancestors) &&
15
+ klass.ancestors.include?(ActiveRecord::Base) &&
16
+ !klass.abstract_class? &&
17
+ !EXCLUDED_CLASSES.include?(klass)
18
+ end.sort
19
+ end
20
+
21
+ def self.get_all_klasses
22
+ return @all_klasses if @all_klasses
23
+ @all_klasses ||= find_all_constants(Module).map(&:constantize)
24
+ end
25
+ end
26
+
27
+ module InstanceMethods
28
+ def get_data(column, format_without_timezone = false)
29
+ case column.type
30
+ when :boolean
31
+ true
32
+ when :date
33
+ Date.parse("1/1/2000")
34
+ when :datetime
35
+ if format_without_timezone
36
+ DateTime.parse("1/1/2000 1pm").utc.strftime("%Y-%m-%d %H:%M:%S")
37
+ else
38
+ DateTime.parse("1/1/2000 1pm").utc.to_s
39
+ end
40
+ when :integer
41
+ 7
42
+ when :decimal
43
+ "7.1234567891"
44
+ when :float
45
+ 7.1234
46
+ when :string
47
+ if column.name == "type"
48
+ "Object"
49
+ else
50
+ "Happy"
51
+ end
52
+
53
+ when :text
54
+ "Happy text"
55
+ else
56
+ raise "I don't know how to make data for '#{column.type}'!"
57
+ end
58
+ end
59
+
60
+ def get_diff_data(column, format_without_timezone = false)
61
+ case column.type
62
+ when :boolean
63
+ false
64
+ when :date
65
+ Date.parse("12/1/2000")
66
+ when :datetime
67
+ if format_without_timezone
68
+ DateTime.parse("12/1/2000 1pm").utc.strftime("%Y-%m-%d %H:%M:%S")
69
+ else
70
+ DateTime.parse("12/1/2000 1pm").utc.to_s
71
+ end
72
+ when :integer
73
+ 9
74
+ when :decimal
75
+ "9.1234567891"
76
+ when :float
77
+ 9.1234
78
+ when :string
79
+ if column.name == "type"
80
+ "Object"
81
+ else
82
+ "Sad"
83
+ end
84
+ when :text
85
+ "Sad text"
86
+ else
87
+ raise "I don't know how to make data for '#{column.type}'!"
88
+ end
89
+ end
90
+
91
+ def create_object_for_klass(klass, exclude_columns = [])
92
+ exclude_columns = exclude_columns | EXCLUDED_COLUMNS
93
+
94
+ object = klass.new
95
+ columns = klass.columns.reject {|column| exclude_columns.include? column.name }
96
+ columns.each do |column|
97
+ object.send("#{column.name}=", get_data(column))
98
+ end
99
+
100
+ object.send(:create_without_callbacks)
101
+ [object, columns]
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,8 @@
1
+ module PgAuditLog
2
+ end
3
+
4
+ require "active_record"
5
+ require "pg_audit_log/version"
6
+ require "pg_audit_log/extensions/postgresql_adapter.rb"
7
+ require "pg_audit_log/entry"
8
+
@@ -0,0 +1,23 @@
1
+ class PgAuditLog::Entry < ActiveRecord::Base
2
+ class CannotDeleteError < StandardError
3
+ def message
4
+ "Audit Logs cannot be deleted!"
5
+ end
6
+ end
7
+ set_table_name :audit_log
8
+
9
+ before_destroy do
10
+ raise CannotDeleteError
11
+ end
12
+
13
+ class << self
14
+ def delete(id)
15
+ raise CannotDeleteError
16
+ end
17
+
18
+ def delete_all
19
+ raise CannotDeleteError
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,54 @@
1
+ require "active_record/connection_adapters/postgresql_adapter"
2
+
3
+ # Did not want to reopen the class but sending an include seemingly is not working.
4
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
+ def execute_with_auditing(sql, name = nil)
6
+ current_user = Thread.current[:current_user]
7
+ user_unique_name = current_user.try(:unique_name) || "UNKNOWN"
8
+
9
+ log_user_id = %[SET audit.user_id = #{current_user.try(:id) || "-1"}]
10
+ log_user_unique_name = %[SET audit.user_unique_name = "#{user_unique_name}"]
11
+
12
+ log([log_user_id, log_user_unique_name, sql].join("; "), name) do
13
+ if @async
14
+ @connection.async_exec(log_user_id)
15
+ @connection.async_exec(log_user_unique_name)
16
+ @connection.async_exec(sql)
17
+ else
18
+ @connection.exec(log_user_id)
19
+ @connection.exec(log_user_unique_name)
20
+ @connection.exec(sql)
21
+ end
22
+ end
23
+ end
24
+
25
+ alias_method_chain :execute, :auditing
26
+
27
+ def begin_db_transaction
28
+ execute_without_auditing "BEGIN"
29
+ end
30
+
31
+ # Commits a transaction.
32
+ def commit_db_transaction
33
+ execute_without_auditing "COMMIT"
34
+ end
35
+
36
+ # Aborts a transaction.
37
+ def rollback_db_transaction
38
+ execute_without_auditing "ROLLBACK"
39
+ end
40
+
41
+ def create_savepoint
42
+ execute_without_auditing("SAVEPOINT #{current_savepoint_name}")
43
+ end
44
+
45
+ def rollback_to_savepoint
46
+ execute_without_auditing("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
47
+ end
48
+
49
+ def release_savepoint
50
+ execute_without_auditing("RELEASE SAVEPOINT #{current_savepoint_name}")
51
+ end
52
+
53
+ end
54
+
@@ -0,0 +1,3 @@
1
+ module PgAuditLog
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "pg_audit_log/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "pg_audit_log"
7
+ s.version = PgAuditLog::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Case Commons, LLC"]
10
+ s.email = ["casecommons-dev@googlegroups.com"]
11
+ s.homepage = "https://github.com/Casecommons/pg_audit_log"
12
+ s.summary = %q{postgresql only database-level audit logging of all databases changes}
13
+ s.description = %q{A completely transparent audit logging component for your application using a stored procedure and triggers. Comes with specs for your project and a rake task to generate the reverse SQL to undo changes logged}
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_dependency("rails", ">=2.3")
21
+ s.add_development_dependency('rspec')
22
+ s.add_development_dependency('with_model')
23
+ end
@@ -0,0 +1,18 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "the PostgreSQL database" do
4
+ after do
5
+ ActiveRecord::Base.connection.reconnect!
6
+ end
7
+
8
+ it "allows custom class variables for audit" do
9
+ lambda {
10
+ ActiveRecord::Base.connection.execute('SET audit.test = 1')
11
+ }.should_not raise_error(ActiveRecord::StatementInvalid), "Your postgres is not configured for auditing. See README.rdoc"
12
+ end
13
+
14
+ it "has an audit log table" do
15
+ ActiveRecord::Base.connection.table_exists?("audit_log").should be_true
16
+ end
17
+
18
+ end
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "the connection adapter" do
4
+ subject { ActiveRecord::Base.connection }
5
+
6
+ it { should respond_to(:execute_without_auditing) }
7
+
8
+ it "should work for both execute and execute_without_auditing" do
9
+ subject.execute("SELECT 1 = 1")
10
+ subject.execute_without_auditing("SELECT 1 = 1")
11
+ end
12
+
13
+ end
@@ -0,0 +1,24 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe PgAuditLog::Entry do
4
+
5
+ subject { PgAuditLog::Entry.create! }
6
+
7
+ describe ".delete" do
8
+ it "blows up because deleting audit logs is not allowed" do
9
+ proc { PgAuditLog::Entry.delete(subject.id) }.should raise_error(PgAuditLog::Entry::CannotDeleteError)
10
+ end
11
+ end
12
+
13
+ describe ".delete_all" do
14
+ it "blows up because deleting audit logs is not allowed" do
15
+ proc { PgAuditLog::Entry.delete_all }.should raise_error(PgAuditLog::Entry::CannotDeleteError)
16
+ end
17
+ end
18
+
19
+ describe "#destroy" do
20
+ it "blows up because deleting audit logs is not allowed" do
21
+ proc { subject.destroy }.should raise_error(PgAuditLog::Entry::CannotDeleteError)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,185 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe PgAuditLog do
4
+ describe "a model that is audited" do
5
+ with_model :audited_model do
6
+ table do |t|
7
+ t.string :str
8
+ t.text :txt
9
+ t.integer :int
10
+ t.date :date
11
+ t.datetime :dt
12
+ t.boolean :bool
13
+ end
14
+ end
15
+
16
+ before do
17
+ AuditedModel.connection.execute(<<-SQL)
18
+ CREATE TRIGGER audit_audited_models
19
+ AFTER INSERT OR UPDATE OR DELETE
20
+ ON #{AuditedModel.quoted_table_name}
21
+ FOR EACH ROW
22
+ EXECUTE PROCEDURE audit_changes()
23
+ SQL
24
+ end
25
+
26
+ after do
27
+ AuditedModel.connection.execute("DROP TRIGGER audit_audited_models ON #{AuditedModel.quoted_table_name};")
28
+ PgAuditLog::Entry.connection.execute("TRUNCATE #{PgAuditLog::Entry.quoted_table_name}")
29
+ end
30
+
31
+ let(:attributes) { { :str => "foo", :txt => "bar", :int => 5, :date => Date.today, :dt => Time.now.midnight } }
32
+
33
+ context "on create" do
34
+
35
+ before do
36
+ AuditedModel.create!(attributes)
37
+ end
38
+
39
+ describe "the audit log record" do
40
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "str" }) }
41
+
42
+ it { should be }
43
+ its(:occurred_at) { should be }
44
+ its(:table_name) { should == AuditedModel.table_name }
45
+ its(:field_name) { should == "str" }
46
+ its(:primary_key) { should == AuditedModel.last.id.to_s }
47
+ its(:operation) { should == "INSERT" }
48
+
49
+ context "when a user is present" do
50
+ before do
51
+ Thread.current[:current_user] = stub(:id => 1, :unique_name => "my current user")
52
+ AuditedModel.create!
53
+ end
54
+
55
+ after { Thread.current[:current_user] = nil }
56
+
57
+ its(:user_id) { should == 1 }
58
+ its(:user_unique_name) { should == "my current user" }
59
+ end
60
+
61
+ context "when no user is present" do
62
+ its(:user_id) { should == -1 }
63
+ its(:user_unique_name) { should == "UNKNOWN" }
64
+ end
65
+
66
+ it "captures all new values for all fields" do
67
+ attributes.each do |field_name, value|
68
+ if field_name == :dt
69
+ PgAuditLog::Entry.last(:conditions => { :field_name => field_name }).field_value_new.should == value.strftime("%Y-%m-%d %H:%M:%S")
70
+ else
71
+ PgAuditLog::Entry.last(:conditions => { :field_name => field_name }).field_value_new.should == value.to_s
72
+ end
73
+ PgAuditLog::Entry.last(:conditions => { :field_name => field_name }).field_value_old.should be_nil
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+
80
+ context "on update" do
81
+ before do
82
+ @model = AuditedModel.create!(attributes)
83
+ end
84
+
85
+ context "when going from a value to a another value" do
86
+ before { @model.update_attributes!(:str => "bar") }
87
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "str" }) }
88
+
89
+ its(:operation) { should == "UPDATE" }
90
+ its(:field_value_new) { should == "bar" }
91
+ its(:field_value_old) { should == "foo" }
92
+ end
93
+
94
+ context "when going from nil to a value" do
95
+ let(:attributes) { {:txt => nil} }
96
+ before { @model.update_attributes!(:txt => "baz") }
97
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "txt" }) }
98
+
99
+ its(:field_value_new) { should == "baz" }
100
+ its(:field_value_old) { should be_nil }
101
+ end
102
+
103
+ context "when going from a value to nil" do
104
+ before { @model.update_attributes!(:str => nil) }
105
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "str" }) }
106
+
107
+ its(:field_value_new) { should be_nil }
108
+ its(:field_value_old) { should == "foo" }
109
+ end
110
+
111
+ context "when the value does not change" do
112
+ before { @model.update_attributes!(:str => "foo") }
113
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "str", :operation => "UPDATE" }) }
114
+
115
+ it { should_not be }
116
+ end
117
+
118
+ context "when the value is nil and does not change" do
119
+ let(:attributes) { {:txt => nil} }
120
+ before { @model.update_attributes!(:txt => nil) }
121
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "txt", :operation => "UPDATE" }) }
122
+
123
+ it { should_not be }
124
+ end
125
+
126
+ context "when the value is a boolean" do
127
+
128
+ context "going from nil -> true" do
129
+ before { @model.update_attributes!(:bool => true) }
130
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "bool", :operation => "UPDATE" }) }
131
+
132
+ its(:field_value_new) { should == "true" }
133
+ its(:field_value_old) { should be_nil }
134
+ end
135
+
136
+ context "going from false -> true" do
137
+ let(:attributes) { {:bool => false} }
138
+ before do
139
+ @model.update_attributes!(:bool => true)
140
+ end
141
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "bool", :operation => "UPDATE" }) }
142
+
143
+ its(:field_value_new) { should == "true" }
144
+ its(:field_value_old) { should == "false" }
145
+ end
146
+
147
+ context "going from true -> false" do
148
+ let(:attributes) { {:bool => true} }
149
+
150
+ before do
151
+ @model.update_attributes!(:bool => false)
152
+ end
153
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "bool", :operation => "UPDATE" }) }
154
+
155
+ its(:field_value_new) { should == "false" }
156
+ its(:field_value_old) { should == "true" }
157
+ end
158
+
159
+ end
160
+ end
161
+
162
+ context "on delete" do
163
+ before do
164
+ @model = AuditedModel.create!(attributes)
165
+ @model.delete
166
+ end
167
+
168
+ subject { PgAuditLog::Entry.last(:conditions => { :field_name => "str" }) }
169
+
170
+ its(:operation) { should == "DELETE" }
171
+
172
+ it "captures all new values for all fields" do
173
+ attributes.each do |field_name, value|
174
+ if field_name == :dt
175
+ PgAuditLog::Entry.last(:conditions => { :field_name => field_name }).field_value_old.should == value.strftime("%Y-%m-%d %H:%M:%S")
176
+ else
177
+ PgAuditLog::Entry.last(:conditions => { :field_name => field_name }).field_value_old.should == value.to_s
178
+ end
179
+ PgAuditLog::Entry.last(:conditions => { :field_name => field_name }).field_value_new.should be_nil
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ end
@@ -0,0 +1,43 @@
1
+ require "bundler/setup"
2
+ require "pg_audit_log"
3
+ require "with_model"
4
+
5
+ connection = nil
6
+ begin
7
+ ActiveRecord::Base.establish_connection(:adapter => 'postgresql',
8
+ :database => 'pg_audit_log_test',
9
+ :min_messages => 'warning')
10
+ connection = ActiveRecord::Base.connection
11
+ connection.execute("SELECT 1")
12
+ rescue PGError => e
13
+ puts "-" * 80
14
+ puts "Unable to connect to database. Please run:"
15
+ puts
16
+ puts " createdb pg_audit_log_test"
17
+ puts "-" * 80
18
+ raise e
19
+ end
20
+
21
+ connection.execute("DROP TABLE IF EXISTS audit_log")
22
+
23
+ sql_path = File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "generators", "pg_audit_log", "templates", "lib", "tasks", "pg_audit_log"))
24
+
25
+ ["create_audit_changes.sql", "create_audit_log_table.sql"].each do |sql_file|
26
+ begin
27
+ connection.execute File.read(File.join(sql_path, sql_file))
28
+ rescue => e
29
+ puts "-" * 80
30
+ puts "Unable to install #{sql_file}"
31
+ puts "-" * 80
32
+ raise e
33
+ end
34
+ end
35
+
36
+ RSpec.configure do |config|
37
+ config.mock_with :rspec
38
+ config.extend WithModel
39
+
40
+ config.after(:each) do
41
+ ActiveRecord::Base.connection.reconnect!
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_audit_log
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Case Commons, LLC
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-03-28 00:00:00 -04:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rails
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "2.3"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :development
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: with_model
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id003
49
+ description: A completely transparent audit logging component for your application using a stored procedure and triggers. Comes with specs for your project and a rake task to generate the reverse SQL to undo changes logged
50
+ email:
51
+ - casecommons-dev@googlegroups.com
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - .gitignore
60
+ - .rvmrc
61
+ - Gemfile
62
+ - LICENSE
63
+ - README.rdoc
64
+ - Rakefile
65
+ - init.rb
66
+ - lib/generators/pg_audit_log/install_generator.rb
67
+ - lib/generators/pg_audit_log/rspec_generator.rb
68
+ - lib/generators/pg_audit_log/templates/lib/tasks/pg_audit_log.rake
69
+ - lib/generators/pg_audit_log/templates/lib/tasks/pg_audit_log/create_audit_changes.sql
70
+ - lib/generators/pg_audit_log/templates/lib/tasks/pg_audit_log/create_audit_log_table.sql
71
+ - lib/generators/pg_audit_log/templates/lib/tasks/pg_audit_log/uninstall_audit_changes.sql
72
+ - lib/generators/pg_audit_log/templates/spec/models/pg_audit_log_spec.rb
73
+ - lib/generators/pg_audit_log/templates/spec/models/pg_audit_log_spec_helper.rb
74
+ - lib/pg_audit_log.rb
75
+ - lib/pg_audit_log/entry.rb
76
+ - lib/pg_audit_log/extensions/postgresql_adapter.rb
77
+ - lib/pg_audit_log/version.rb
78
+ - pg_audit_log.gemspec
79
+ - spec/configuration_spec.rb
80
+ - spec/connection_adapter_spec.rb
81
+ - spec/model_spec.rb
82
+ - spec/pg_audit_log_spec.rb
83
+ - spec/spec_helper.rb
84
+ has_rdoc: true
85
+ homepage: https://github.com/Casecommons/pg_audit_log
86
+ licenses: []
87
+
88
+ post_install_message:
89
+ rdoc_options: []
90
+
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: "0"
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: "0"
105
+ requirements: []
106
+
107
+ rubyforge_project:
108
+ rubygems_version: 1.6.2
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: postgresql only database-level audit logging of all databases changes
112
+ test_files:
113
+ - spec/configuration_spec.rb
114
+ - spec/connection_adapter_spec.rb
115
+ - spec/model_spec.rb
116
+ - spec/pg_audit_log_spec.rb
117
+ - spec/spec_helper.rb