6am is our blog about developing Roll Call

our web-app for businesses that revolve around talent, teams and projects.

We hope this blog will be about launching Roll Call very soon.

Adding multi-tenancy to your Rails app: acts_as_tenant Erwin Oct 03

Roll Call is implemented as a multi-tenant application: each user gets their own instance of the app, content is strictly scoped to a user’s instance. In Rails, this can be achieved in various ways. Guy Naor did a great job of diving into the pros and cons of each option in his 2009 Acts As Conference talk. If you are doing multi-tenancy in Rails, you should watch his video.

With a multi-db or multi-schema approach, you only deal with the multi-tenancy-aspect in a few specific spots in your app (Jerod Santo recently wrote an excellent post on implementing a multi-schema strategy). Compared to the previous two strategies, a shared database strategy has the downside that the ‘multi-tenancy’-logic is something you need to actively be aware of and manage in almost every part of your app.

Using a Shared Database strategy is alot of work!

For various other reasons we opted for a shared database strategy. However, for us the prospect of dealing with the multi-tenancy-logic throughout our app, was not appealing. Worse, we run the risk of accidently exposing content of one tenant to another one, if we mismanage this logic. While researching this topic I noticed there are no real ready made solutions available that get you on your way, Ryan Sonnek wrote his ‘multitenant’ gem and Mark Connel did the same. Neither of these solution seemed “finished” to us. So, we wrote our own implementation.

First, how does multi-tenancy with a shared database strategy work

A shared database strategy manages the multi-tenancy-logic through Rails associations. A tenant is represented by an object, for example an Account. All other objects are associated with a tenant: belongs_to :account. Each request starts with finding the @current_account. After that, each find is scoped through the tenant object: current_account.projects.all. This has to be remembered everywhere: in model method declarations and in controller actions. Otherwise, you’re exposing content of other tenants.

In addition, you have to actively babysit other parts of your app: validates_uniqueness_of requires you to scope it to the current tenant. You also have to protect agaist all sorts of form-injections that could allow one tenant to gain access or temper with the content of another tenant (see Paul Gallaghers presentation for more on these dangers).

Enter acts_as_tenant

I wanted to implement all the concerns above in an easy to manage, out of the way fashion. We should be able to add a single declaration to our model and that should implement:

  1. scoping all searches to the current Account
  2. scoping the uniqueness validator to the current Account
  3. protecting against various nastiness trying to circumvent the scoping.

The result is acts_as_tenant (github), a rails gem that will add multi tenancy using a shared database to your rails app in an out-of-your way fashion.

In the README, you will find more information on using acts_as_tenant in your projects, so we’ll give you a high-level overview here. Let’s suppose that you have an app to which you want to add multi tenancy, tenants are represented by the Account model and Project is one of the models that should be scoped by tenant:

1
2
3
4
5
6
7
8
9
class Addaccounttoproject < ActiveRecord::Migration
  def up
    add_column :projects, :account_id, :integer
  end

class Project < ActiveRecord::Base
  acts_as_tenant(:account)
  validates_uniqueness_to_tenant :name
end

What does adding these two methods accomplish:

  1. it ensures every search on the project model will be scoped to the current tenant,
  2. it adds validation for every association confirming the associated object does indeed belong to the current tenant,
  3. it validates the uniqueness of :name to the current tenant,
  4. it implements a bunch of safeguards preventing all kinds of nastiness from exposing other tenants data (mainly form-injection attacks).

Ofcourse, all the above assumes acts_as_tenant actually knows who the current tenant is. Two strategies are implemented to help with this.

Using the subdomain to workout the current tenant

1
2
3
4
class ApplicationController < ActionController::Base
   
   set_current_tenant_by_subdomain(:account, :subdomain)
end

Adding the above methods to your application controller tells acts_as_tenant that

  1. the current tenant should be found based on the subdomain (e.g. account1.myappdomain.com),
  2. tenants are respresented by the Account model and
  3. the Account model has a column named subdomain that should be used the lookup the current account, using the current subdomain.

Passing the current account to acts_as_tenant yourself

1
2
3
4
5
class ApplicationController < ActionController::Base

  current_account = Account.method_to_find_the_current_account
  set_current_tenant_to(current_account)
end

Acts_as_tenant also adds a handy helper to your controllers current_tenant, containing the current tenant object.

Great! Anything else I should know? A few caveats:

  • scoping of models only works if acts_as_tenant has a current_tenant available. If you do not set one by one of the methods described above, no scope will be applied!
  • for validating uniqueness within a tenant scope you must use the validates_uniqueness_to_tenant method. This method takes all the options the regular validates_uniqueness_of method takes.
  • it’s probably best to add the acts_as_tenant declaration after any other default_scope declarations you add to a model (i’m not exactly sure how rails 3 handles the chaining. If someone can enlighten me, thanks!).

We have been testing acts_as_tenant within Roll Call during recent weeks and it seems to be behaving well. Having said that, we welcome any feedback. This is my first real attempt at a plugin and the possibility of various improvements is almost a given.

Next post i’ll describe how we let this all play nice with other gems such as Devise and acts_as_taggable.

20 comments so far (Add your comment)
  1. jay

    Good job, thanks. Does the act_as_tenant gem work with mongoid.

  2. ErwinM

    @jay I'm afraid it won't work with mongoid, as the gem is written as an extension to ActiveRecord models.

  3. Andy

    Very useful to make data much secure. I'll create a new git branch in my app and try to integrate acts-as-tenant into the app.

  4. jay

    @erwin ok, thanks. Still a great work. I will try and make it mongoid compatible and if it work, i will commit it back. In the mean time, i look forward to your next post on how to make it play nice with other gems such as Devise and acts_as_taggable and please cancan or better still an admin panel like rails-admin that come s with cancan integration.

  5. Lennard Timm

    This really looks like a fine gem and could replace custom written code in a few apps I'm managing. There are some SaaS gems that promise to help with multi-tenancy and such but haven't tested them yet.

    Are there any best practices you're following in your apps when it comes to handling single or multi-user accounts? Are you using the Account model as a connector between associated models only or does it hold unique information like payment data?

  6. Andrew Coleman

    Hey guys,

    This looks a lot like my acts_as_restricted_subdomain plugin. I decided to go with the shared database route, mainly for migrations.

    Your plugin does handle validates_uniqueness_of much better than my plugin does, however. But, by comparison, i can do:

    1
    
    Patient.find(45)
    

    And i don't have to worry about scoping to the current account (we use Agency as the model). The request comes in and the plugin scopes all models configured to that subdomain by forcing activerecord conditions into the query. So, if you are connecting to the app and use the subdomain 'demo' and patient 45 belongs to the subdomain 'othersubdomain', you get a 404 not found.

    In the two and a half years of running it, i don't think we have ever found a way to get around the scope and let other sites access any others' data.

  7. ErwinM

    @Andy Do let me know how that goes and if you run into any issues!

    @Lennard I am currently writing up a post that explains how we deal with user accounts in the context of our multi-tenancy app. I'll make sure to explain how we decided to handle Users and Accounts. And why.

    @Andrew Very interesting! I hadn't come across your plugin in my research. In reaction to your comparison: acts_as_tenant behaves exactly the same as you describe in your example: it will only return patient 45 if that patient belongs to the current tenant. Acts_as_tenant scopes all searches to the current tenant by applying a default_scope to every search.

  8. Andrew Coleman

    @ErwinM sweet, so Rails3 compatibility? I am still on Rails 2 as it stands. I have well over 50KLOC to audit before i can make the jump. Sounds like a good place to start looking when we do update.

    On another note, i was talking to someone at RubyConf last week and they were describing a setup where they do subdomain division like we do, but with MySQL sharding. The application connects to the 'main' database where the accounts live, then some Rack middleware scopes all further database connections to the appropriate MySQL shard in a block. I thought that was a really cool way of handling the multi-database conundrum.

    Although, we have joins. As you can see here, suck it.

  9. ErwinM

    @Andrew Rails 3.1 required in fact, it won't work with any earlier version. The sharding you describe sounds a lot like the pg multi-schema approach.

  10. Pliny

    Are you guys still developing this?

  11. James Woodard

    Greetings Erwin,
    was wondering if the posts about integration with devise & tags were finished, it'd help me with my practice & demo (non-commercial), just for fun/learning app.
    You made an amazing gem btw, I've been reading about how to handle everything in relational database & spending hours feeling more at sea.
    From your post -it makes so much sense & if you could explain the Devise implementation that'd be so helpful. =)

  12. Chintan

    Hi, Thanks for the gem. I have been trying to understand how it works....

    I dont use subdomains to identify the current tenant. Instead I want to set the current tenant based on user who is logged in (e.g. User.tenant assuming user has a tenant_id field).
    How would I use the set_current_tenant_to method ? How would you pass the current tenant object ?

    class ApplicationController < ActionController::Base

    # This doesnt work because current_user is not set yet
    current_account = current_user.tenant
    set_current_tenant_to(current_account)

    end


    Thanks...

  13. Chintan

    coming back to my earlier question - instead of passing in current_tenant object to set_current_tenant_to() method, wouldnt it be better if we could pass in a lambda to dynamically evaluate the current tenant every request ?

    e.g.

    set_current_tenant_to ( lambda {current_user.tenant } )

    instead of

    set_current_tenant_to (current_tenant)

  14. ErwinM

    @ Pliny: Yes we are! In November we fell in the Rails upgrade trap and we lost some time. That said, the upgrading is done and we are ticking off the last things on our development schedule. Closed beta soon!

    @James: I'll make sure to post on Devise soon. I promised that some time ago (reason for delay see above)

  15. ErwinM

    @Chintan: On the example in your first comment: why is current_user not set yet?

  16. Peter Butterworth

    Sounds like a great gem and just what I need - Thanks
    but I'm a Rails newbie using 3.1 and having problem getting started with it
    1. Created rails app
    2. Ran scaffold to create Account including subdomain
    3. Ran scaffold to create Contact including account_id
    4. Ran rake db:migrate
    5. Added 2 accounts for subdomains acme & xyz
    6. Added gem 'acts-as-tenant' to gemfile
    7. Ran Bundle install which ran ok and showed intall of acts_as_tenant 0.2.6
    8. Added acts_as_tenant(:account) to Contact model
    9. Added set_current_tenant_by_subdomain(:account, :subdomain) to top of application controller

    10. Using lvh.me (as per Ryan Bates railscast 221) so i can try out subdomains on local host
    11. if i try acme.lvh.me:3000 get ROUTING ERROR - undefined method 'set_current_tenant_by_subdomain' for ApplicationController:Class

    I realise this might be a basic rails 3.1 issue as I've only recently started building in rails but your help would be most appreciated.
    Peter

  17. Chintan

    I am trying to set the current tenant based on an attribute (tenant_id) defined by the currently logged in user object. This current user object is returned by calling instance method "current_user" made available by DEVISE, AUTHLOGIC authentication gems. The following code doesnt work because "current_user" is an instance method.

    class ApplicationController < ActionController::Base

    current_account = current_user.tenant
    set_current_tenant_to(current_account)

    def my_method
    end
    end

  18. Steve

    I have read through this blog and would like to try your gem out with a small app I am building - would like to integrate it with Authlogic - I have configured all the models according to your docu - I think I have it all working ok - however am having issues with the Authlogic and session creating when a user logs in who is has the correct subdomain/account - authlogic throws an error "undefined method logged_out?".. your thoughts on how to use this gem with authlogic would be very much appreciated.

    Thanks

  19. David

    Hey Erwin,

    I was wondering when the article about acts_as_tenant + devise will be ready? It'll be hugely useful and I can't wait to read it! =) Thanks for the great work!

    Regards,

    David

  20. David

    Hi Erwin,

    Thanks for this great gem! I use it along with Devise and everything works fine so far… But I would like to hear your opinion on this:

    When a user signs in, the current_tenant is set using the set_current_tenant_through_filter method. When he signs out and another user tries to sign in, it doesn't work because the current_tenant is already set and the query on the Devise model is scoped to it.

    For now, I solved the problem using this in my ApplicationController before_filter, but I'm pretty sure it's not the cleanest way to do it:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    class ApplicationController < ActionController::Base
      # Use a before_filter to set the current tenant
      set_current_tenant_through_filter
      before_filter :set_current_account
      
      def set_current_account
        # Devise helper to check if a user is signed in
        if signed_in?
          current_account = Account.find(current_user.account_id)
          set_current_tenant(current_account)
        else
          # Unset current_tenant so another user can sign in
          ActsAsTenant.current_tenant = nil
        end
      end
    end
    

    Thank you in advance for your advice! Best regards,

    David


Add comment


(lesstile enabled - surround code blocks with ---)

Comments for this post are being moderated. Before you see your comment appear, we'll first have a quick look at it.

T7S82J7E4QRR