Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support to Apartment current_tenant #519

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 # => #<Tenant subdomain: "my_tenant">
@post = Post.create(params[:post])
@post.audits.last.tenant # => #<Tenant subdomain: "my_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:
Expand Down
3 changes: 2 additions & 1 deletion lib/audited.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
28 changes: 27 additions & 1 deletion lib/audited/audit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Audited
#
# * <tt>auditable</tt>: the ActiveRecord model that was changed
# * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
# * <tt>tenant</tt>: the tenant that the change was performed in; a string or an ActiveRecord model
# * <tt>action</tt>: one of create, update, or delete
# * <tt>audited_changes</tt>: a hash of all the changes
# * <tt>comment</tt>: a comment set with the audit
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion lib/audited/sweeper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/generators/audited/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
4 changes: 4 additions & 0 deletions lib/generators/audited/templates/install.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
39 changes: 39 additions & 0 deletions spec/audited/audit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -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"
Expand Down
33 changes: 31 additions & 2 deletions spec/audited/sweeper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

class AuditsController < ActionController::Base
before_action :populate_user
before_action :populate_tenant

attr_reader :company

Expand All @@ -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
Expand All @@ -31,29 +34,41 @@ 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 )

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
Expand All @@ -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

Expand All @@ -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}}
Expand Down
8 changes: 8 additions & 0 deletions spec/audited_spec_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
21 changes: 21 additions & 0 deletions spec/support/active_record/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
8 changes: 8 additions & 0 deletions spec/support/active_record/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
4 changes: 4 additions & 0 deletions test/db/version_1.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Loading