diff --git a/README.md b/README.md index 386c19d8f..106bec409 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,28 @@ end post.audits.last.user # => 'console-user-username' ``` +### Current Tenant Tracking + +If you're using Audited in a Rails application, all audited changes made within a request will automatically be attributed to the current tenant. By default, Audited uses the `current_tenant` method in your controller. + +```ruby +class PostsController < ApplicationController + def create + current_tenant # => # + @post = Post.create(params[:post]) + @post.audits.last.tenant # => # + end +end +``` + +To use a method other than `current_tenant`, put the following in an initializer: + +```ruby +Audited.current_tenant_method = :my_current_tenant +``` + +The standard Audited install assumes your Tenant model has an integer primary key type. + ### Associated Audits Sometimes it's useful to associate an audit with a model other than the one being changed. For instance, given the following models: diff --git a/lib/audited.rb b/lib/audited.rb index 36baff418..51e3b571b 100644 --- a/lib/audited.rb +++ b/lib/audited.rb @@ -2,7 +2,7 @@ module Audited class << self - attr_accessor :ignored_attributes, :current_user_method, :max_audits, :auditing_enabled + attr_accessor :ignored_attributes, :current_user_method, :current_tenant_method, :max_audits, :auditing_enabled attr_writer :audit_class def audit_class @@ -21,6 +21,7 @@ def config @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on) @current_user_method = :current_user + @current_tenant_method = :current_tenant @auditing_enabled = true end diff --git a/lib/audited/audit.rb b/lib/audited/audit.rb index a1512ea3f..f89ea1634 100644 --- a/lib/audited/audit.rb +++ b/lib/audited/audit.rb @@ -5,6 +5,7 @@ module Audited # # * auditable: the ActiveRecord model that was changed # * user: the user that performed the change; a string or an ActiveRecord model + # * tenant: the tenant that the change was performed in; a string or an ActiveRecord model # * action: one of create, update, or delete # * audited_changes: a hash of all the changes # * comment: a comment set with the audit @@ -36,9 +37,10 @@ def dump(obj) class Audit < ::ActiveRecord::Base belongs_to :auditable, polymorphic: true belongs_to :user, polymorphic: true + belongs_to :tenant, polymorphic: true belongs_to :associated, polymorphic: true - before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address + before_create :set_version_number, :set_audit_user, :set_audit_tenant, :set_request_uuid, :set_remote_address cattr_accessor :audited_class_names self.audited_class_names = Set.new @@ -122,6 +124,25 @@ def user_as_string alias_method :user_as_model, :user alias_method :user, :user_as_string + # Allows tenant to be set to either a string or an ActiveRecord object + # @private + def tenant_as_string=(tenant) + # reset both either way + self.tenant_as_model = self.subdomain = nil + tenant.is_a?(::ActiveRecord::Base) ? + self.tenant_as_model = tenant : + self.subdomain = tenant + end + alias_method :tenant_as_model=, :tenant= + alias_method :tenant=, :tenant_as_string= + + # @private + def tenant_as_string + tenant_as_model || subdomain + end + alias_method :tenant_as_model, :tenant + alias_method :tenant, :tenant_as_string + # Returns the list of classes that are being audited def self.audited_classes audited_class_names.map(&:constantize) @@ -182,6 +203,11 @@ def set_audit_user nil # prevent stopping callback chains end + def set_audit_tenant + self.tenant ||= ::Audited.store[:current_tenant].try!(:call) # from Sweeper + nil # prevent stopping callback chains + end + def set_request_uuid self.request_uuid ||= ::Audited.store[:current_request_uuid] self.request_uuid ||= SecureRandom.uuid diff --git a/lib/audited/sweeper.rb b/lib/audited/sweeper.rb index aca68e36f..067086b0e 100644 --- a/lib/audited/sweeper.rb +++ b/lib/audited/sweeper.rb @@ -3,7 +3,8 @@ class Sweeper STORED_DATA = { current_remote_address: :remote_ip, current_request_uuid: :request_uuid, - current_user: :current_user + current_user: :current_user, + current_tenant: :current_tenant } delegate :store, to: ::Audited @@ -21,6 +22,10 @@ def current_user lambda { controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true) } end + def current_tenant + lambda { controller.send(Audited.current_tenant_method) if controller.respond_to?(Audited.current_tenant_method, true) } + end + def remote_ip controller.try(:request).try(:remote_ip) end diff --git a/lib/generators/audited/install_generator.rb b/lib/generators/audited/install_generator.rb index bbc5c49c5..ff28f84cc 100644 --- a/lib/generators/audited/install_generator.rb +++ b/lib/generators/audited/install_generator.rb @@ -14,6 +14,7 @@ class InstallGenerator < Rails::Generators::Base class_option :audited_changes_column_type, type: :string, default: "text", required: false class_option :audited_user_id_column_type, type: :string, default: "integer", required: false + class_option :audited_tenant_id_column_type, type: :string, default: "integer", required: false source_root File.expand_path("../templates", __FILE__) diff --git a/lib/generators/audited/templates/install.rb b/lib/generators/audited/templates/install.rb index 1d43f093c..f4e122c58 100644 --- a/lib/generators/audited/templates/install.rb +++ b/lib/generators/audited/templates/install.rb @@ -7,7 +7,10 @@ def self.up t.column :associated_type, :string t.column :user_id, :<%= options[:audited_user_id_column_type] %> t.column :user_type, :string + t.column :tenant_id, :<%= options[:audited_tenant_id_column_type] %> + t.column :tenant_type, :string t.column :username, :string + t.column :subdomain, :string t.column :action, :string t.column :audited_changes, :<%= options[:audited_changes_column_type] %> t.column :version, :integer, :default => 0 @@ -20,6 +23,7 @@ def self.up add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index' add_index :audits, [:associated_type, :associated_id], :name => 'associated_index' add_index :audits, [:user_id, :user_type], :name => 'user_index' + add_index :audits, [:tenant_id, :tenant_type], :name => 'tenant_index' add_index :audits, :request_uuid add_index :audits, :created_at end diff --git a/spec/audited/audit_spec.rb b/spec/audited/audit_spec.rb index 2f0008557..e9b492ecd 100644 --- a/spec/audited/audit_spec.rb +++ b/spec/audited/audit_spec.rb @@ -4,6 +4,7 @@ describe Audited::Audit do let(:user) { Models::ActiveRecord::User.new name: "Testing" } + let(:tenant) { Models::ActiveRecord::Tenant.new subdomain: "testing" } describe "audit class" do around(:example) do |example| @@ -135,6 +136,44 @@ class TempModel < ::ActiveRecord::Base end end + describe "tenant=" do + it "should be able to set the tenant to a model object" do + subject.tenant = tenant + expect(subject.tenant).to eq(tenant) + end + + it "should be able to set the tenant to nil" do + subject.tenant_id = 1 + subject.tenant_type = 'Models::ActiveRecord::Tenant' + subject.subdomain = 'my_tenant' + + subject.tenant = nil + + expect(subject.tenant).to be_nil + expect(subject.tenant_id).to be_nil + expect(subject.tenant_type).to be_nil + expect(subject.subdomain).to be_nil + end + + it "should be able to set the tenant to a string" do + subject.tenant = 'test_tenant' + expect(subject.tenant).to eq('test_tenant') + end + + it "should clear model when setting to a string" do + subject.tenant = tenant + subject.tenant = 'testing_tenant' + expect(subject.tenant_id).to be_nil + expect(subject.tenant_type).to be_nil + end + + it "should clear the username when setting to a model" do + subject.subdomain = 'test_tenant' + subject.tenant = tenant + expect(subject.subdomain).to be_nil + end + end + describe "revision" do it "should recreate attributes" do user = Models::ActiveRecord::User.create name: "1" diff --git a/spec/audited/sweeper_spec.rb b/spec/audited/sweeper_spec.rb index 8bce0bccf..c144ae3fd 100644 --- a/spec/audited/sweeper_spec.rb +++ b/spec/audited/sweeper_spec.rb @@ -4,6 +4,7 @@ class AuditsController < ActionController::Base before_action :populate_user + before_action :populate_tenant attr_reader :company @@ -20,9 +21,11 @@ def update private attr_accessor :current_user + attr_accessor :current_tenant attr_accessor :custom_user def populate_user; end + def populate_tenant; end end describe AuditsController do @@ -31,13 +34,16 @@ def populate_user; end before do Audited.current_user_method = :current_user + Audited.current_tenant_method = :current_tenant end let(:user) { create_user } + let(:tenant) { create_tenant } describe "POST audit" do it "should audit user" do controller.send(:current_user=, user) + controller.send(:current_tenant=, tenant) expect { post :create }.to change( Audited::Audit, :count ) @@ -45,15 +51,24 @@ def populate_user; end expect(controller.company.audits.last.user).to eq(user) end - it "does not audit when method is not found" do + it "does not audit when user method is not found" do controller.send(:current_user=, user) - Audited.current_user_method = :nope + Audited.current_user_method = :nope_user expect { post :create }.to change( Audited::Audit, :count ) expect(controller.company.audits.last.user).to eq(nil) end + it "does not audit when tenant method is not found" do + controller.send(:current_tenant=, tenant) + Audited.current_tenant_method = :nope_tenant + expect { + post :create + }.to change( Audited::Audit, :count ) + expect(controller.company.audits.last.tenant).to eq(nil) + end + it "should support custom users for sweepers" do controller.send(:custom_user=, user) Audited.current_user_method = :custom_user @@ -77,6 +92,7 @@ def populate_user; end it "should record a UUID for the web request responsible for the change" do allow_any_instance_of(ActionDispatch::Request).to receive(:uuid).and_return("abc123") controller.send(:current_user=, user) + controller.send(:current_tenant=, tenant) post :create @@ -94,11 +110,24 @@ def populate_user; end expect(controller.company.audits.last.user).to eq(user) end + + it "should call current_tenant after controller callbacks" do + expect(controller).to receive(:populate_tenant) do + controller.send(:current_tenant=, tenant) + end + + expect { + post :create + }.to change( Audited::Audit, :count ) + + expect(controller.company.audits.last.tenant).to eq(tenant) + end end describe "PUT update" do it "should not save blank audits" do controller.send(:current_user=, user) + controller.send(:current_tenant=, tenant) expect { put :update, Rails::VERSION::MAJOR == 4 ? {id: 123} : {params: {id: 123}} diff --git a/spec/audited_spec_helpers.rb b/spec/audited_spec_helpers.rb index 105c5fbf1..a5d47a44a 100644 --- a/spec/audited_spec_helpers.rb +++ b/spec/audited_spec_helpers.rb @@ -8,6 +8,14 @@ def build_user(attrs = {}) Models::ActiveRecord::User.new({name: 'darth', username: 'darth', password: 'noooooooo'}.merge(attrs)) end + def create_tenant(attrs = {}) + Models::ActiveRecord::Tenant.create({subdomain: 'some_tenant'}.merge(attrs)) + end + + def build_tenant(attrs = {}) + Models::ActiveRecord::Tenant.new({subdomain: 'another_tenant'}.merge(attrs)) + end + def create_versions(n = 2, attrs = {}) Models::ActiveRecord::User.create(name: 'Foobar 1', **attrs).tap do |u| (n - 1).times do |i| diff --git a/spec/support/active_record/models.rb b/spec/support/active_record/models.rb index 09424fa38..fc235a769 100644 --- a/spec/support/active_record/models.rb +++ b/spec/support/active_record/models.rb @@ -3,6 +3,27 @@ module Models module ActiveRecord + class Tenant < ::ActiveRecord::Base + audited + attribute :non_column_attr if Rails.version >= '5.1' + + def subdomain=(val) + write_attribute(:subdomain, CGI.escapeHTML(val)) + end + end + + class AccessibleAfterDeclarationTenant < ::ActiveRecord::Base + self.table_name = :tenants + audited + attr_accessible :subdomain if respond_to?(:attr_accessible) + end + + class AccessibleBeforeDeclarationTenant < ::ActiveRecord::Base + self.table_name = :tenants + attr_accessible :subdomain if respond_to?(:attr_accessible) # declare attr_accessible before calling aaa + audited + end + class User < ::ActiveRecord::Base audited except: :password attribute :non_column_attr if Rails.version >= '5.1' diff --git a/spec/support/active_record/schema.rb b/spec/support/active_record/schema.rb index 4e59772f2..e6510c1a1 100644 --- a/spec/support/active_record/schema.rb +++ b/spec/support/active_record/schema.rb @@ -30,6 +30,10 @@ ActiveRecord::Base.establish_connection ActiveRecord::Schema.define do + create_table :tenants do |t| + t.column :subdomain, :string + end + create_table :users do |t| t.column :name, :string t.column :username, :string @@ -66,7 +70,10 @@ t.column :associated_type, :string t.column :user_id, :integer t.column :user_type, :string + t.column :tenant_id, :integer + t.column :tenant_type, :string t.column :username, :string + t.column :subdomain, :string t.column :action, :string t.column :audited_changes, :text t.column :version, :integer, default: 0 @@ -79,6 +86,7 @@ add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index' add_index :audits, [:associated_id, :associated_type], name: 'associated_index' add_index :audits, [:user_id, :user_type], name: 'user_index' + add_index :audits, [:tenant_id, :tenant_type], name: 'tenant_index' add_index :audits, :request_uuid add_index :audits, :created_at end diff --git a/test/db/version_1.rb b/test/db/version_1.rb index fe1d24cb5..e2432210d 100644 --- a/test/db/version_1.rb +++ b/test/db/version_1.rb @@ -4,7 +4,10 @@ t.column :auditable_type, :string t.column :user_id, :integer t.column :user_type, :string + t.column :tenant_id, :integer + t.column :tenant_type, :string t.column :username, :string + t.column :subdomain, :string t.column :action, :string t.column :changes, :text t.column :version, :integer, default: 0 @@ -13,5 +16,6 @@ add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index' add_index :audits, [:user_id, :user_type], name: 'user_index' + add_index :audits, [:tenant_id, :tenant_type], name: 'tenant_index' add_index :audits, :created_at end diff --git a/test/db/version_2.rb b/test/db/version_2.rb index 6396fc350..bfe928e68 100644 --- a/test/db/version_2.rb +++ b/test/db/version_2.rb @@ -4,7 +4,10 @@ t.column :auditable_type, :string t.column :user_id, :integer t.column :user_type, :string + t.column :tenant_id, :integer + t.column :tenant_type, :string t.column :username, :string + t.column :subdomain, :string t.column :action, :string t.column :changes, :text t.column :version, :integer, default: 0 @@ -14,5 +17,6 @@ add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index' add_index :audits, [:user_id, :user_type], name: 'user_index' + add_index :audits, [:tenant_id, :tenant_type], name: 'tenant_index' add_index :audits, :created_at end diff --git a/test/db/version_3.rb b/test/db/version_3.rb index e04cf4336..bde111818 100644 --- a/test/db/version_3.rb +++ b/test/db/version_3.rb @@ -4,7 +4,10 @@ t.column :auditable_type, :string t.column :user_id, :integer t.column :user_type, :string + t.column :tenant_id, :integer + t.column :tenant_type, :string t.column :username, :string + t.column :subdomain, :string t.column :action, :string t.column :audited_changes, :text t.column :version, :integer, default: 0 @@ -14,6 +17,7 @@ add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index' add_index :audits, [:user_id, :user_type], name: 'user_index' + add_index :audits, [:tenant_id, :tenant_type], name: 'tenant_index' add_index :audits, :created_at end diff --git a/test/db/version_4.rb b/test/db/version_4.rb index 64cfeb464..9fefa3429 100644 --- a/test/db/version_4.rb +++ b/test/db/version_4.rb @@ -4,7 +4,10 @@ t.column :auditable_type, :string t.column :user_id, :integer t.column :user_type, :string + t.column :tenant_id, :integer + t.column :tenant_type, :string t.column :username, :string + t.column :subdomain, :string t.column :action, :string t.column :audited_changes, :text t.column :version, :integer, default: 0 @@ -15,6 +18,7 @@ add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index' add_index :audits, [:user_id, :user_type], name: 'user_index' + add_index :audits, [:tenant_id, :tenant_type], name: 'tenant_index' add_index :audits, :created_at end diff --git a/test/db/version_5.rb b/test/db/version_5.rb index 1d3bb2677..233de831b 100644 --- a/test/db/version_5.rb +++ b/test/db/version_5.rb @@ -4,7 +4,10 @@ t.column :auditable_type, :string t.column :user_id, :integer t.column :user_type, :string + t.column :tenant_id, :integer + t.column :tenant_type, :string t.column :username, :string + t.column :subdomain, :string t.column :action, :string t.column :audited_changes, :text t.column :version, :integer, default: 0 diff --git a/test/db/version_6.rb b/test/db/version_6.rb index 281ea9227..a72f5fdde 100644 --- a/test/db/version_6.rb +++ b/test/db/version_6.rb @@ -4,7 +4,10 @@ t.column :auditable_type, :string t.column :user_id, :integer t.column :user_type, :string + t.column :tenant_id, :integer + t.column :tenant_type, :string t.column :username, :string + t.column :subdomain, :string t.column :action, :string t.column :audited_changes, :text t.column :version, :integer, default: 0