Coder Social home page Coder Social logo

acts_as_tenant's Introduction

Acts As Tenant

Build Status Gem Version

Row-level multitenancy for Ruby on Rails apps.

This gem was born out of our own need for a fail-safe and out-of-the-way manner to add multi-tenancy to our Rails app through a shared database strategy, that integrates (near) seamless with Rails.

acts_as_tenant adds the ability to scope models to a tenant. Tenants are represented by a tenant model, such as Account. acts_as_tenant will help you set the current tenant on each request and ensures all 'tenant models' are always properly scoped to the current tenant: when viewing, searching and creating.

In addition, acts_as_tenant:

  • sets the current tenant using the subdomain or allows you to pass in the current tenant yourself
  • protects against various types of nastiness directed at circumventing the tenant scoping
  • adds a method to validate uniqueness to a tenant, validates_uniqueness_to_tenant
  • sets up a helper method containing the current tenant

Note: acts_as_tenant was introduced in this blog post.

Row-level vs schema multitenancy

What's the difference?

Row-level multitenancy each model must have a tenant ID column on it. This makes it easy to filter records for each tenant using your standard database columns and indexes. ActsAsTenant uses row-level multitenancy.

Schema multitenancy uses database schemas to handle multitenancy. For this approach, your database has multiple schemas and each schema contains your database tables. Schemas require migrations to be run against each tenant and generally makes it harder to scale as you add more tenants. The Apartment gem uses schema multitenancy.

๐ŸŽฌ Walkthrough

Want to see how it works? Check out the ActsAsTenant walkthrough video:

ActsAsTenant Walkthrough Video

Installation

To use it, add it to your Gemfile:

gem 'acts_as_tenant'

Getting started

There are two steps in adding multi-tenancy to your app with acts_as_tenant:

  1. setting the current tenant and
  2. scoping your models.

Setting the current tenant

There are three ways to set the current tenant:

  1. by using the subdomain to lookup the current tenant,
  2. by setting the current tenant in the controller, and
  3. by setting the current tenant for a block.

Looking Up Tenants

By Subdomain to lookup the current tenant

class ApplicationController < ActionController::Base
  set_current_tenant_by_subdomain(:account, :subdomain)
end

This tells acts_as_tenant to use the last subdomain to identify the current tenant. In addition, it tells acts_as_tenant that tenants are represented by the Account model and this model has a column named 'subdomain' which can be used to lookup the Account using the actual subdomain. If ommitted, the parameters will default to the values used above.

By default, the last subdomain will be used for lookup. Pass in subdomain_lookup: :first to use the first subdomain instead.

By Domain to lookup the current tenant

class ApplicationController < ActionController::Base
  set_current_tenant_by_subdomain_or_domain(:account, :subdomain, :domain)
end

You can locate the tenant using set_current_tenant_by_subdomain_or_domain( :account, :subdomain, :domain ) which will check for a subdomain and fallback to domain.

By default, the last subdomain will be used for lookup. Pass in subdomain_lookup: :first to use the first subdomain instead.

Manually using before_action

class ApplicationController < ActionController::Base
  set_current_tenant_through_filter
  before_action :your_method_that_finds_the_current_tenant

  def your_method_that_finds_the_current_tenant
    current_account = Account.find_it
    set_current_tenant(current_account)
  end
end

Setting the current_tenant yourself, requires you to declare set_current_tenant_through_filter at the top of your application_controller to tell acts_as_tenant that you are going to use a before_action to setup the current tenant. Next you should actually setup that before_action to fetch the current tenant and pass it to acts_as_tenant by using set_current_tenant(current_tenant) in the before_action.

If you are setting the tenant in a specific controller (except application_controller), it should to be included AT THE TOP of the file.

class MembersController < ActionController::Base
  set_current_tenant_through_filter
  before_action :set_tenant
  before_action :set_member, only: [:show, :edit, :update, :destroy]

  def set_tenant
    set_current_tenant(current_user.account)
  end
end

This allows the tenant to be set before any other code runs so everything is within the current tenant.

Setting the current tenant for a block

ActsAsTenant.with_tenant(current_account) do
  # Current tenant is set for all code in this block
end

This approach is useful when running background processes for a specified tenant. For example, by putting this in your worker's run method, any code in this block will be scoped to the current tenant. All methods that set the current tenant are thread safe.

Note: If the current tenant is not set by one of these methods, Acts_as_tenant will be unable to apply the proper scope to your models. So make sure you use one of the two methods to tell acts_as_tenant about the current tenant.

Disabling tenant checking for a block

ActsAsTenant.without_tenant do
  # Tenant checking is disabled for all code in this block
end

This is useful in shared routes such as admin panels or internal dashboards when require_tenant option is enabled throughout the app.

Allowing tenant updating for a block

ActsAsTenant.with_mutable_tenant do
  # Tenant updating is enabled for all code in this block
end

This will allow you to change the tenant of a model. This feature is useful for admin screens, where it is ok to allow certain users to change the tenant on existing models in specific cases.

Require tenant to be set always

If you want to require the tenant to be set at all times, you can configure acts_as_tenant to raise an error when a query is made without a tenant available. See below under configuration options.

Scoping your models

class AddAccountToUsers < ActiveRecord::Migration
  def up
    add_column :users, :account_id, :integer
    add_index  :users, :account_id
  end
end

class User < ActiveRecord::Base
  acts_as_tenant(:account)
end

acts_as_tenant requires each scoped model to have a column in its schema linking it to a tenant. Adding acts_as_tenant to your model declaration will scope that model to the current tenant BUT ONLY if a current tenant has been set.

Some examples to illustrate this behavior:

# This manually sets the current tenant for testing purposes. In your app this is handled by the gem.
ActsAsTenant.current_tenant = Account.find(3)

# All searches are scoped by the tenant, the following searches will only return objects
# where account_id == 3
Project.all =>  # all projects with account_id => 3
Project.tasks.all #  => all tasks with account_id => 3

# New objects are scoped to the current tenant
@project = Project.new(:name => 'big project')    # => <#Project id: nil, name: 'big project', :account_id: 3>

# It will not allow the creation of objects outside the current_tenant scope
@project.account_id = 2
@project.save                                     # => false

# It will not allow association with objects outside the current tenant scope
# Assuming the Project with ID: 2 does not belong to Account with ID: 3
@task = Task.new  # => <#Task id: nil, name: nil, project_id: nil, :account_id: 3>

Acts_as_tenant uses Rails' default_scope method to scope models. Rails 3.1 changed the way default_scope works in a good way. A user defined default_scope should integrate seamlessly with the one added by acts_as_tenant.

Validating attribute uniqueness

If you need to validate for uniqueness, chances are that you want to scope this validation to a tenant. You can do so by using:

validates_uniqueness_to_tenant :name, :email

All options available to Rails' own validates_uniqueness_of are also available to this method.

Custom foreign_key

You can explicitly specifiy a foreign_key for AaT to use should the key differ from the default:

acts_as_tenant(:account, :foreign_key => 'accountID') # by default AaT expects account_id

Custom primary_key

You can also explicitly specifiy a primary_key for AaT to use should the key differ from the default:

acts_as_tenant(:account, :primary_key => 'primaryID') # by default AaT expects id

Has and belongs to many

You can scope a model that is part of a HABTM relationship by using the through option.

class Organisation < ActiveRecord::Base
  has_many :organisations_users
  has_many :users, through: :organisations_users
end

class User < ActiveRecord::Base
  has_many :organisations_users
  acts_as_tenant :organisation, through: :organisations_users
end

class OrganisationsUser < ActiveRecord::Base
  belongs_to :user
  acts_as_tenant :organisation
end

Configuration options

An initializer can be created to control (currently one) option in ActsAsTenant. Defaults are shown below with sample overrides following. In config/initializers/acts_as_tenant.rb:

ActsAsTenant.configure do |config|
  config.require_tenant = false # true

  # Customize the query for loading the tenant in background jobs
  config.job_scope = ->{ all }
end
  • config.require_tenant when set to true will raise an ActsAsTenant::NoTenant error whenever a query is made without a tenant set.

config.require_tenant can also be assigned a lambda that is evaluated at run time. For example:

ActsAsTenant.configure do |config|
  config.require_tenant = lambda do
    if $request_env.present?
      return false if $request_env["REQUEST_PATH"].start_with?("/admin/")
    end
  end
end

ActsAsTenant.should_require_tenant? is used to determine if a tenant is required in the current context, either by evaluating the lambda provided, or by returning the boolean value assigned to config.require_tenant.

When using config.require_tenant alongside the rails console, a nice quality of life tweak is to set the tenant in the console session in your initializer script. For example in config/initializers/acts_as_tenant.rb:

SET_TENANT_PROC = lambda do
  if defined?(Rails::Console)
    puts "> ActsAsTenant.current_tenant = Account.first"
    ActsAsTenant.current_tenant = Account.first
  end
end

Rails.application.configure do
  if Rails.env.development?
    # Set the tenant to the first account in development on load
    config.after_initialize do
      SET_TENANT_PROC.call
    end

    # Reset the tenant after calling 'reload!' in the console
    ActiveSupport::Reloader.to_complete do
      SET_TENANT_PROC.call
    end
  end
end

belongs_to options

acts_as_tenant :account includes the belongs_to relationship. So when using acts_as_tenant on a model, do not add belongs_to :account alongside acts_as_tenant :account:

class User < ActiveRecord::Base
  acts_as_tenant(:account) # YES
  belongs_to :account # REDUNDANT
end

You can add the following belongs_to options to acts_as_tenant: :foreign_key, :class_name, :inverse_of, :optional, :primary_key, :counter_cache, :polymorphic, :touch

Example: acts_as_tenant(:account, counter_cache: true)

Background Processing libraries

ActsAsTenant supports

  • ActiveJob - ActsAsTenant will automatically save the current tenant in ActiveJob arguments and set it when the job runs.

  • Sidekiq Add the following code to config/initializers/acts_as_tenant.rb:

require 'acts_as_tenant/sidekiq'

Testing

If you set the current_tenant in your tests, make sure to clean up the tenant after each test by calling ActsAsTenant.current_tenant = nil. Integration tests are more difficult: manually setting the current_tenant value will not survive across multiple requests, even if they take place within the same test. This can result in undesired boilerplate to set the desired tenant. Moreover, the efficacy of the test can be compromised because the set current_tenant value will carry over into the request-response cycle.

To address this issue, ActsAsTenant provides for a test_tenant value that can be set to allow for setup and post-request expectation testing. It should be used in conjunction with middleware that clears out this value while an integration test is processing. A typical Rails and RSpec setup might look like:

# test.rb
require_dependency 'acts_as_tenant/test_tenant_middleware'

Rails.application.configure do
  config.middleware.use ActsAsTenant::TestTenantMiddleware
end
# spec_helper.rb
config.before(:suite) do |example|
  # Make the default tenant globally available to the tests
  $default_account = Account.create!
end

config.before(:each) do |example|
  if example.metadata[:type] == :request
    # Set the `test_tenant` value for integration tests
    ActsAsTenant.test_tenant = $default_account
  else
    # Otherwise just use current_tenant
    ActsAsTenant.current_tenant = $default_account
  end
end

config.after(:each) do |example|
  # Clear any tenancy that might have been set
  ActsAsTenant.current_tenant = nil
  ActsAsTenant.test_tenant = nil
end

Bug reports & suggested improvements

If you have found a bug or want to suggest an improvement, please use our issue tracked at:

github.com/ErwinM/acts_as_tenant/issues

If you want to contribute, fork the project, code your improvements and make a pull request on Github. When doing so, please don't forget to add tests. If your contribution is fixing a bug it would be perfect if you could also submit a failing test, illustrating the issue.

Contributing to this gem

We use the Appraisal gem to run tests against supported versions of Rails to test for compatibility against them all. StandardRb also helps keep code formatted cleanly.

  1. Fork the repo
  2. Make changes
  3. Run test suite with bundle exec appraisal
  4. Run bundle exec standardrb to standardize code formatting
  5. Submit a PR

Author & Credits

acts_as_tenant is written by Erwin Matthijssen & Chris Oliver.

This gem was inspired by Ryan Sonnek's Multitenant gem and its use of default_scope.

License

Copyright (c) 2011 Erwin Matthijssen, released under the MIT license

acts_as_tenant's People

Contributors

aaronrenner avatar adrian-gomez avatar artplan1 avatar calebhearth avatar cmer avatar davekaro avatar dsarhadian avatar erwinm avatar excid3 avatar iangreenleaf avatar laurens avatar maslenkov avatar mikecmpbll avatar murugan-r avatar mylesboone avatar niborg avatar nunommc avatar olleolleolle avatar patrotom avatar pavloo avatar pcragone avatar petergoldstein avatar phfts avatar preth00nker avatar rtdp avatar scarhand avatar stgeneral avatar tmaier avatar tvongaza avatar yshmarov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

acts_as_tenant's Issues

Problem with complex has_many through

Models:

class Tenant < ActiveRecord::Base
  has_many :userroles
  has_many :users, through: :userroles
  has_many :roles, through: :userroles
  has_many :admins, -> { joins(:roles).where("roles.name = 'admin'").uniq }, through: :userroles, class_name: 'User', source: :user
end

class Role < ActiveRecord::Base
  has_paper_trail
  acts_as_paranoid
  has_many :userroles, :dependent => :destroy
  has_many :users, :through => :userroles
end

class Userrole < ActiveRecord::Base
  acts_as_tenant(:tenant)
  has_paper_trail
  belongs_to :user
  belongs_to :role
end

When current_tenant doesn't set my code work right, but if I set current_tenant, I got errors. In console I got those errors:

2.1.0 :001 > ActsAsTenant.current_tenant
 => nil 
2.1.0 :002 > t = Tenant.first
2.1.0 :004 > t.admins.count
   (1.5ms)  SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (roles.name = 'admin')  [["tenant_id", 1]]
 => 1 
2.1.0 :005 > ActsAsTenant.current_tenant = t
2.1.0 :006 > t.admins.count
   (2.6ms)  SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" AND (userroles.tenant_id = 1) INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (userroles.tenant_id = 1) AND (roles.name = 'admin')  [["tenant_id", 1]]
PG::UndefinedTable: ERROR:  invalid reference to FROM-clause entry for table "userroles"
LINE 1: ...erroles_users_join"."user_id" = "users"."id" AND (userroles....
                                                             ^
HINT:  Perhaps you meant to reference the table alias "userroles_users_join".
: SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" AND (userroles.tenant_id = 1) INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (userroles.tenant_id = 1) AND (roles.name = 'admin')
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  invalid reference to FROM-clause entry for table "userroles"

because

where("#{self.table_name}.#{fkey} = ?", ActsAsTenant.current_tenant.id)  if ActsAsTenant.current_tenant

https://github.com/ErwinM/acts_as_tenant/blob/master/lib/acts_as_tenant/model_extensions.rb#L56

What should I do? I need tenanted Userroles models, so I can't exclude acts_as_tenant from them (such as delete default scope).

Extending a rails engine to use acts_as_tenant

I am trying to use acts_as_tenant in a test/guide application that uses and rails gem/engine Plutus.

I've got it semi- working, but have ran into a few few problems that I posted on StackOverflow.

Don't know if this has been tried before (using AoT in an engine engine) but could't find anything in the issues or web. If you can point me in a direction I'd be grateful.

validating belongs_to assocations

Are the values in belongs_to association also validated to check if they belong to the same tenant ?

EXAMPLE :
Lets say there are 2 models as follows:

ARTICLE with id, tenant_id columns
COMMENT with id, article_id and tenant_id columns
ARTICLE has many COMMENTS and COMMENTS belongs to ARTICLE
There are 2 articles in database :
Article ( :id => 1, :tenant_id => 10)
Article ( :id => 2, :tenant_id => 8)

Now,
Comment.create! ( :article_id => 1, :tenant_id => 10) # This works since article #1 belongs to tenant #10

However,
Comment.create! ( :article_id => 2, :tenant_id => 10) # This still works even if article #2 does NOT belong to tenant #10
Shouldn't this fail since comment is being associated to an article that belongs to another tenant ?

Without this check, a malicious user can associate objects belonging to other tenants (and not within his tenant)....

Issue with no subdomain and filters

I have an issue where I can filter the data belonging to each tenant as expected, however, when there is no subdomain or www is used I can see all the data from all tenants. For example, if I create a new account with a subdomain of 'green' and create a new user and equipment all the data is associated with the account where "green" is the subdomain. Which is perfect. When I create another account with "sample" it works the same and no data from "green" is shown. This is perfect. However, when I use www. or no subdomain, I can log in as any user and see all the data from the tenants.

I would prefer that if someone goes to www.mydomain.com or mydomain.com they could not log in and see all the data. I can use a before_filter by the subdomain, but that renders www.mydomain.com and mydomain.com useless and there is by default no opportunity for a user to create an account. The only good thing about this is that only "green" and "sample" accounts can see their respective data.

I have search high and low for a solution to this but to no avail. Please advise.

What about a subtenant concept? or...

I'm fairly new to rails in general (i know... where have I been all these years) but am trying to understand the best way to implement this type of environment...

Say a user logs in and is under a subdomain like 'mybusiness.domainname.com' and that will filter the records he sees based on tenant_id... which i have working just fine...
However, what would it take to do some sort of selectable 'location' of mybusiness...

Say for instance, I determine this user has access to 2 of our business locations...
So he selects one of those locations, it would then set some sort of session saying he's within the context of that 'location' now and all queries should filter based on that location, instead of globally amongst both of his accessible locations? Make sense?

Would that be something I should look elsewhere for or could I some how merge this concept with it somehow?

Back and forth between subdomains/tenants invalidates sessions

My acts_as_tenant usage:

class Domain < ActiveRecord::Base
  has_many :users
end

class User < ActiveRecord::Base
  acts_as_tenant(:domain)
  # devise stuff
end

class ApplicationController < ActionController::Base
  set_current_tenant_through_filter
  before_filter :set_tenant
  protect_from_forgery

  def set_tenant
    @domain = Domain.find_or_create_by_name(request.host)
    set_current_tenant(@domain)
  end
end

Configuration

Configure session_store with active_record_store or cookie_store, with or without key and domain: '.domain.tld' does not change anything.

Step to reproduce:

  1. When I go to domain1.domain.tld
    When I sign-up or sign-in
    Then I am signed-in, OK
  2. When I go to domain2.domain.tld
    Then I am not signed-in, OK
  3. When I go again to domain1.domain.tld
    Then I should not be signed-in, KO

Ideal use case

  1. When I go to domain1.domain.tld
    When I sign-up or sign-in as Dummy1
    Then I am signed-in as Dummy1
  2. When I go to domain2.domain.tld
    Then I am not signed-in
    When I sign-up or sign-in as Dummy2
    Then I am signed-in as Dummy2
  3. When I go again to domain1.domain.tld
    Then I am signed-in as Dummy1

Need compound indexes?

Hello,

Do I need a compound index on each table? For example an index for account_id (tenant) and project_id on a Project model? Right now I have individual ones for each of them.

Can't set_current_tenant_to(current_user.tenant)

Okay, this is more about my implementation than an actual issue with the gem, but thought I would raise it here an see if anyone watching has a solution...
I am not using subdomains, but rather a relation between the current_user (I am using sorcery) and the tenant model, so the tenant is not set until the user logs in (I think the new basecamp takes a similar approach). The problem is that "current_user" method has not been set up at the point of calling set_current_tenant_to, so it fails. session[:user_id] doesn't work because the session is not set up and neither is "cookies". Any ideas?
Many Thanks!

raise error if tenant_id is set to something else than current_tenant

Hi,
Below is a rspec test that successfully passes. I am wondering is there a way to raise an error if account_id is set to something different from current_tenant.id

it "automatically sets tenant id" do
ActsAsTenant.current_tenant = Account.create!(:name => 'foo')
@account2 = Account.create!(:name => 'baz')
Project.create!(:name => "New Project", :account_id => @account2.id).account_id.should eq ActsAsTenant.current_tenant.id
end

Ritesh

no superclass method `organization'

I have the following two models:

class EmailTemplate < ActiveRecord::Base
  acts_as_tenant :organization
end

class Organization < ActiveRecord::Base
end

This worked when using ActsAsTenant version 0.2.9 but fails with the latest version 0.3.4

EmailTemplate.first.organization
# We expect this to return an Organization instance, which it did before

Now it simply raises an error:

NoMethodError: super: no superclass method `organization' for #<EmailTemplate:0x007fde7d8cda58>
from /Users/thomas/.rbenv/versions/1.9.3-p194-perf/lib/ruby/gems/1.9.1/gems/activemodel-3.1.12/lib/active_model/attribute_methods.rb:385:in `method_missing'

We're using rails 3.1.12 and ruby 1.9.3p194

Can't specify tenant key column explicitly

I'd like to be able to do something like

belongs_to :tenant, :foreign_key => "tenant_col"
acts_as_tenant :tenant

But no matter what I end up with "tenant_id" in the generated SQL WHERE clause.

Is there any way to get acts_as_tenant to use the :foreign_key specified on the existing belongs_to relationship?

with_tenant return value

I use with_tenant to calculate a value, scoped by a different tenant.

x = ActsAsTenant.with_tenant(@account) do
  return "some value"
end

However, the return statement inside the block will return from the entire with_tenant method and therefore prevent the tenant to be reset to the old tenant.

What other ways is there to get the value from the block?

ATM with_tenant itself returns the old_tenant.
Would it break the API if with_tenant would return the value of the block instead?

Disabling "acts_as_tenant" when under a certain "tenant" :)

I'm trying to make a "super tenant" that can see data from all tenants, not a parent child relationship, just a master system that has no rules.

I'm using activeadmin, and the only one I would want enforced for the "system" tenant is the admin_user model.

The login still needs to use acts_as_tenant, but the rest of them don't once you're authenticated...?

What would be the quickest way to pull this off you think?

Adding an association object when parent doesn't exist fails

Hi,

I am having the following issue (using the spec example from your code).

class Account < ActiveRecord::Base
    has_many :projects
end

class Project < ActiveRecord::Base
    has_many :tasks
    acts_as_tenant :account
end

class Task < ActiveRecord::Base
    belongs_to :project
    acts_as_tenant :account
end

If I attempt to add a task without it belonging to a project the following SQL is run

t = Task.new(:name => "test")
t.save

SELECT COUNT(*) FROM "projects" WHERE "projects"."id" IS NULL 
    => false 

and the task is not saved. This is happening as well when I use accepts_nested_attributes_for within my model. I'm assuming rails is saving the Task before the Project and therefor creating this error.

Thanks for the gem and your help!

Refactor before_validation setting account_id

https://github.com/ErwinM/acts_as_tenant/blob/master/lib/acts_as_tenant/model_extensions.rb#L57-L61

before_validation Proc.new {|m|
  if ActsAsTenant.current_tenant
    m.send "#{association}_id=".to_sym, ActsAsTenant.current_tenant.id
  end
}, :on => :create

Currently accociation_id is set with a before_validation call. I suggest to replace it with an after_initialize call. This way the association is available even earlier.

after_initialize do
  if ActsAsTenant.current_tenant
    m.send "#{association}_id=".to_sym, ActsAsTenant.current_tenant.id
  end unless m.send("#{association}_id".to_sym).present?
end

What do you think about this idea?

Superuser problem/question

I am using your great gem for multi tenancy purposes and also for switching users for superuser role users. I.e. superusers can act as any other tenant and create/edit/etc records for different account. But I faced the following problem: I am also recording created_by/updated_by. Obviously superusers belong to a particular account/tenant. And when created_by/updated_by populated (setting to superuser's tenant_id) it's either not visible by other users or it doesn't allow to update (throwing "Created by association is invalid [ActsAsTenant]" exception).

Ideally I need to have tenant_id = nil for superuser but I doubt it's gonna work. Any idea how to approach/fix the problem?

Necessary to have tenant_id in each table?

In my app, I have an Account, and a User that belongs_to Account. Account then has_many widgets, and Widget has_many Orders.

They form a sort of tree, and it seems a bit redundant to me to require any new model within that tree to have an account_id. My current ad-hoc solution is to simply go current_user.account.widgets to scope them, but I do see the downside that I may use Widget.find somewhere and expose some other Account's widgets.

I guess where my rambling is going, is I'm wondering if there is any way to modify acts_as_tenant to use my associations to do the scoping, instead of requiring an account_id. Or am I missing something else, and this is a terrible idea? I suppose there is a bit of a performance overhead of following the associations, but I plan to fetch them all in one query anywhere, where necessary.

When joining a non-tenant table with a tenant table, then ActsAsTenant does not scope

I have a non-tenant table ("called services") and a tenant-table ("called service_texts").
When I do a join, then the scope on the tenant is not triggered.

The Explain string is:

SELECT "services".* FROM "services" INNER JOIN "service_texts" ON "service_texts"."service_id" = "services"."id"

Is there a way, that the Gem also scopes on these kind of queries?

Calling the current_tenant in application_controller

I'm having real issues in trying to grab the current_user from devise, determine the account id from here and then pass this as a variable to the set_current_tenant_to method.

In my application controller I have:

class ApplicationController < ActionController::Base

  protect_from_forgery # See ActionController::RequestForgeryProtection for details      
  helper :all # include all helpers, all the time

  def get_current_account_id
    current_account_user = current_user
    current_account_id = current_account_user.account_id
    current_account_id
  end

  current_account = Account.find(get_current_account_id)
  set_current_tenant_to(current_account)

I know that the get_current_account_id deduces the correct account_id because when I place it in the a before_filter call I can see in the logs it outputs the correct figure. However when I run this I get the following error:

Routing Error

undefined local variable or method `get_current_account_id' for ApplicationController:Class

Any idea how I can get this to work?

How to build associated records with that attach to the tenant itself??

Posted this on stackoverflow too but figured I better get to the bottom of this on here too..
http://stackoverflow.com/questions/17196434/how-to-build-associated-records-before-tenant-class-is-saved-with-acts-as-tena

Example...

I have a class called client which is my tenant model.

I have associated records to save along with each new client that gets created.
Let's call them tasks.

I created an after_initialize callback inside of client that calls a method called build_defaults which does the following...

def build_defaults
self.tasks.build(
  Task.new({
    :name => 'task 1',
    :description => 'task 1 desc',
    :view_module => 'task_1_template'
  }),
  Task.new({
    :name => 'task 2',
    :description => 'task 2 desc',
    :view_module => 'task_2_template'
  }),
  Task.new({
    :name => 'task 3',
    :description => 'task 3 desc',
    :view_module => 'task_3_template'
  }),
  Task.new({
    :name => 'task 4',
    :description => 'task 4 desc',
    :view_module => 'task_4_template'
  }),
  Task.new({
    :name => 'task 5',
    :description => 'task 5 desc',
    :view_module => 'task_5_template'
  })
)
end

The task class is setup as acts_as_tenant :client

When I go to do @client = new Client( :name => "Test Client" )
It raises ActsAsTenant::Errors::NoTenantSet: ActsAsTenant::Errors::NoTenantSet

Is there a way to conditionally bypass acts_as_tenant's check when it's a new_record? or a better way to handle this type of thing?

I'm fairly new to rails / ruby as of a few months ago...?

UPDATE

Well, I figured out if I change it to an "after_create" and set ActsAsTenant.current_tenant = self within the method I can do self.tasks.create! calls... but not sure if overriding ActsAsTenant.current_tenant is a good idea?

  after_create :build_defaults
  def build_defaults
    ActsAsTenant.current_tenant = self
    self.tasks.create!({
      :name => 'Facebook > Page Like',
      :description => 'Request that they like your page.',
      :view_module => 'facebook_page_like'
    })
    self.tasks.create!({
      :name => 'Facebook > Share',
      :description => 'Share a link on Facebook',
      :view_module => 'facebook_share_link'
    })
    self.tasks.create!({
      :name => 'Twitter > Tweet',
      :description => 'Post a tweet on a user behalf.',
      :view_module => 'twitter_tweet'
    })
    self.tasks.create!({
      :name => 'Twitter > Follow',
      :description => 'Follow the company twitter user.',
      :view_module => 'twitter_follow'
    })
    self.tasks.create!({
      :name => 'Giveaway Share',
      :description => 'This allows you to earn 5 extra entries by re-sharing this giveaway.',
      :view_module => 'giveaway_share'
    })
  end

Set tenant id manually?

Hello,

I am creating an account and a user (using nested attributes, has_many through). The problem is that the account_id is not set in the user record. Is there a way to set account_id (tenant column) manually? I tried it but I get an immutable error.

Multiple tenants

Hi,

It doesn't look like AaT supports "multiple" tenants out of the box, unless I'm missing something. For instance, say you want to have an :account tenant as well as a :reseller tenant.

Is this something anyone else might want/need? I could fork AaT and try to add it. Any thoughts on a strategy? Seems like we may be able to add sort of "key" option to ActsAsTenant.current_tenant.

Thanks.

Error with has_and_belongs_to_many association

When I use the has_and_belongs_to_many association in the model having act_as_tenant, I got the internal server error. Actually, it is an NoMethodError (undefined method ... ). So, I don't the problem. Please help me to verify. Thanks

A question regarding acts_as_tenant

Hi,

I have a question regarding using acts_as_tenant.

I have a model that have organization and sub_organization in the same table.

  • OrganiazationModel
  • id - sub_id - name

The relationship to the organization/sub_organization model is done through

  • CarModel [acts_as_tenant :organiazation]
  • organization_id - name

Now I want to use acts_as_tenant but I don't know if I can use it in my purpose.

Now I do this:

  • acts_as_tenant.current_tenant = current_root_organiaztion

But this does not allow me to do anything with the sub_organization, only root_organization.

Is there something I can do so I don't need to add one extra column for CarModel

  • CarModel
  • id - organization_id - tenacy_id

If I'm using tenant for the wrong purpose then please tell me, and I will try to find another way.

Bypass tenant

I have a question. If I want to by-pass the tenant scope in some actions in controllers, what should I do? There are some queries that I don't want to be scoped. Please guide me to this problem. Thank you.

rails-api gem

Hi There I would like to use acts_as_tenant with the rails-api gem - currently when I inherit from ActionController::API only - the method
set_current_tenant_through_filter
is not recognised. My current work around is to use the whole module stack by inheriting from ActionController::Base - but this is not ideal for performance of my api. I have used the acts_as_tenant gem for the main app and the api I am currently working on is going to handle the mobile apps I am working on. I would really appreciate your input as to the exact modules I need to inherit from as a minimum rather than inheriting from ::Base - which is going to be much slower.

THANKS

Steve

Using set_current_tenant_through_filter with Devise destroys user sessions

When I use Devise and your gem with setting manually the current tenant in the controller,
then sessions of users of other tenants are destroyed when an user of a different tenant logs in.
I reproduced it a blank application. That's my ApplicationController:

class ApplicationController < ActionController::Base
protect_from_forgery
set_current_tenant_through_filter
before_filter :find_the_current_tenant
before_filter :authenticate_user!

def find_the_current_tenant
if current_user
set_current_tenant(current_user.tenant)
else
set_current_tenant(nil)
end

end
end

Support www.subdomain.domain.com

Hi,

The following line needs to be changed:

          ActsAsTenant.current_tenant = tenant_class.where(tenant_column => request.subdomains.first).first

to

          ActsAsTenant.current_tenant = tenant_class.where(tenant_column => request.subdomains.last).first

... if you want to support the correct subdomain in the set_current_tenant_by_subdomain( ) method of the ControllerExtensions module.

You may want to let the user choose this - either by overriding (with their own filter, assigning to ActsAsTenant.current_tenant) or monkey-patching, rather than modifying the source of acts_as_tenant.

Another option is to give the user another filter to call (set_current_tenant_by_subdomain_only( ) ? ) that switches to the other behaviour.

Cheers,

Nigel

How to ensure tenant scoped by subdomain can reset password with devise

I know more of a devise question but..

I'm using devise and it seems the devise controllers don't inherit from ApplicationController (???) so don't have access to set_current_tenant_by_subdomain

When the password reset link is clicked I get no Tenant scoping by subdomain.

So how do you handle this? Is there an easy way to open up the DeviseControllers to ensure
set_current_tenant_by_subdomain is called?

Thanks so much

Use current_tenant in Model?

I have a field called request_closed in my Tenant record.

In the Request model, I would like to be able to use that field.

I tried this:

statusclosed = ActsAsTenant.current_tenant.request_closed
scope :notclosed, where("statuscode_id < ?", statusclosed )

And

statusclosed = current_tenant.request_closed

How can I access a current tenant record in a model?

Using set_current_tenant_through_filter with Devise destroys user sessions

This still seems to be an issue after 0.2.8.

I'm using AaT(0.2.9), Rails (3.2.9), Ruby(1.9.3) and whenever I set my tenant in a before filter, all users are logged out in devise. Any thoughts?

Update One:
The offending code is here

default_scope lambda {
where({fkey => ActsAsTenant.current_tenant.id}) if ActsAsTenant.current_tenant
}

Update Two:
My scenario might not be typical, I need the user to determine the tenant. This led to a circular reference based on the current implementation of the Gem.

  • In order to build the current_user properly, we need the tenant.
  • In order to get the tenant, we need the current_user

Initially:
-Devise would try to build the current_user, and the tenant would be set to whatever the tenant was from the last request.
-The current_user wasn't a tenant of the last tenant, so the user session would be destroyed and the user would be redirected.

I fixed this by:

  • Setting the current_tenant to nil upon the beginning of each request.
  • Loading the current_user (since the tenant has been set to nil, the current_user can load properly)
  • Loading the tenant based on the current_user

It seems like current_tenant should not carry over though across requests....

two different scopes on two different levels

Is there a way to define scopes on two different levels?

E.g. in SAP software there is a distinction between a client view (same customizing and some other shared data) and the various companies of this client.

Some tables must be scoped by the client view and other tables must be scoped by the company view.

Is there a way to do it with ActsAsTenant?

undefined method `id' for nil:NilClass

I get the following error message in my tests since upgrading from v0.3.1 to v0.3.3

undefined method `id' for nil:NilClass

I guess the fault lies in https://github.com/ErwinM/acts_as_tenant/blob/master/lib/acts_as_tenant/model_extensions.rb#L97

define_method "#{ActsAsTenant.tenant_klass.to_s}" do
  return ActsAsTenant.current_tenant if send(fkey) == ActsAsTenant.current_tenant.id
  super()
end

ActsAsTenant.current_tenant could be nil

I would have another request to you which is not worth an issue: Could you please run git push origin --tags. I would like to use the following URL to check for changes on gems I depend on: v0.3.1...v0.3.3

acts_as_tenant with counter_cache

I would love be able to do things like that:

acts_as_tenant :account, counter_cache: true

Is it possible ? At the moment, I just explicit the belongs_to:

acts_as_tenant :account
belongs_to :account, counter_cache: true

Maybe stupid question...?

But I'm a n00b... is there like a variable that gets set for a Tenant when it's set?
Like, can I access the columns of the model somehow without re-querying for it?

Allow 'super' user to ignore tenancy?

Hello,

First, thanks for the gem. It works great at keeping things separate. However, that is also becoming my problem. I want to allow an user with the admin role to log on as another, non-admin user, for troubleshooting and whatnot. I found a gem that works great at that 'switch_user', but when it comes to impersonating a user on another subdomain, I can't do it.

Is there a way to force acts_as_tenant to ignore tenancy for a certain class/type of user so I can accomplish this?

Thank you!

Best practices to add Internationalization?

I need a way to have different email templates for different tenants and also different locale files per tenant. For example when a new tenant added - it should use the default translations. Any ideas?

validates_uniqueness_to_tenant with non-array scope

Hello,

A minor bug :

validates_uniqueness_to_tenant :attribute1, scope: :attribute2

does not work. It raises :

undefined method `<<' for :attribute2:Symbol`

so I have to change this to :

validates_uniqueness_to_tenant :attribute1, scope: [:attribute2]

Rails AR handle both symbol and array. Awesome gem by the way, thank you!

Thread.Current storage under unicorn/thin

When using thin or unicorn Thread.Current isn't guaranteed to be cleared between requests, so you could end up having a tenant set when you don't want it set. As per this SO Question

There seems to be two solutions here:

  1. Add a note to the readme to create a before_filter that sets the current_tenant to nil if you aren't explicitly setting a tenant for each request
  2. Use something like RequestStore as referenced in this rails issue thread

Thoughts?

Errors with nested models. has_many :through

I have models:

class User < ActiveRecord::Base
  has_many :userroles
  has_many :roles, :through => :userroles
end
class Role < ActiveRecord::Base
  has_many :userroles, :dependent => :destroy
  has_many :users, :through => :userroles
end

Only this model should be tenanted

class Userrole < ActiveRecord::Base
  acts_as_tenant(:tenant)

  belongs_to :user
  belongs_to :role
end

When I try get User.first.roles I get this:

2.0.0-p195 :001 > ActsAsTenant.current_tenant = Tenant.find 1
  Tenant Load (4.2ms)  SELECT "tenants".* FROM "tenants" WHERE "tenants"."id" = $1 LIMIT 1  [["id", 1]]
 => #<Tenant id: 1, name: "Tenant AAA", subdomain: "a", tenant_id: nil, created_at: "2013-10-11 09:23:53", updated_at: "2013-10-11 09:23:53">
2.0.0-p195 :002 > User.first.roles
  User Load (1.6ms)  SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL ORDER BY "users"."id" ASC LIMIT 1
  Role Load (1.7ms)  SELECT "roles".* FROM "roles" INNER JOIN "userroles" ON "roles"."id" = "userroles"."role_id" WHERE "roles"."deleted_at" IS NULL AND "roles"."tenant_id" = 1 AND "userroles"."user_id" = $1  [["user_id", 1]]
 => #<ActiveRecord::Associations::CollectionProxy []>
2.0.0-p195 :003 > User.first.roles.to_sql
  User Load (0.8ms)  SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL ORDER BY "users"."id" ASC LIMIT 1
 => "SELECT "roles".* FROM "roles" INNER JOIN "userroles" ON "roles"."id" = "userroles"."role_id" WHERE "roles"."deleted_at" IS NULL AND

"roles"."tenant_id" = 1 
AND "userroles"."user_id" = $1"
2.0.0-p195 :004 >

But it should be like this:

"SELECT "roles".* FROM "roles" INNER JOIN "userroles" ON "roles"."id" = "userroles"."role_id" WHERE "roles"."deleted_at" IS NULL AND
 "userroles"."tenant_id" = 1 
AND "userroles"."user_id" = $1"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.