Multitenancy in Rails

If you are a Rails developer, it is only a matter of time until a client approaches you to build a multitenant SaaS app.

Multitenancy applications are unique in the respect that data between tenants needs to separated by a virtual iron wall. In this post I will discuss the three common solutions for creating multitenant apps in Rails, what I dislike about each one, and I'll propose a new implementation which is simpler, cleaner, and very effective.

This is Marshmallow

To demonstrate the ideas in multitenancy, I will be creating a theoretical project management application which is a stripped down version of Basecamp. We have only three models, Company, User, and Project, with users and projects belonging to companies.

Because this is a multitenant app, we have three types of users:

  1. Admins: these are the clients who have hired you to build the site and have top-level access.
  2. Managers: these are the IT staff from each company. They manage the projects and users at the company level.
  3. Users: the rank-and-file end users of your site.

Each of these groups need their own section of the site with the controls, dashboards, and forms that they require, so we will have a namespaced Admin section and a namespaced Management section for our admins and managers respectively. (Standard users just use the regular space.)

Let's call this app Marshmallow, primarily because you weren't expecting it.

Why Bother?

So why do multitenant apps even need a special solution to deal with their security needs? (If you don't need convincing that multitenant apps are special, skip this section.)

It boils down to wanting to have bulletproof data separation.

The problem with multitenant apps is that unlike normal apps with two namespaces - regular users and admin users - these apps have a third namespace in the middle for the manager.

Consider these controllers.

# A normal admin controller
class Admin::ProjectsController < Admin::ApplicationController

  def show
    @project = Project.find(params[:id])
  end

end


# A normal end-user controller
class ProjectsController < ApplicationController

  def show
    @project = current_user.projects.find(params[:id])
  end

end

Those are not the problem. The problem is the mid-level management controller. A company's manager wants to edit a project. The project is not assigned to him - he is the creator and manager, the assigner, not the assignee. You would have to do something like this.

class Management::ProjectsController < Management::ApplicationController

  def show
    @company = current_user.company
    @project = @company.projects.find(params[:id])
  end

end

And what if you descend some levels into the fact that a project has many to-do's which each have many comments?

class Management::CommentController < Management::ApplicationController

  def show
    # Please don't actually ever do this; I am just making a point.
    company   = current_user.company
    project   = company.projects.find(params[:project_id])
    todo      = project.todos.find(params[:todo_id])
    @comment  = todo.comments.find(params[:id])
  end

end

The point is that you end up with three different sets of controllers, each with completely different logic for interacting with models. This is where bugs are born.

The Basecamp Solution

In an post from DHH about Concerns, he posted a snippet from Basecamp's code which controls access to data on the model level.

It is a concern - a module - that is included in each model. Adapting it to our example app, it looks like this:

module Visible
  extend ActiveSupport::Concern

  module ClassMethods
    def visible_to(person)
      where("#{table_name}.company_id IN (?)", person.company_id)
    end
  end
end

From your controllers you would then call: @project = Project.visible_to(current_user).find(params[:id]). That is a long line of code (and not so easy on the eyes) to be writing out in your controllers e-v-e-r-y-w-h-e-r-e. And what if a particular model has a unique rule regarding who can access the record? Do you then define it's own visible_to method in the model?

This implementation also opens the Pandora's box of whether this visible_to method, which deals with visibility of the current user, is the responsibility of the other models, and if it is acceptable to include the method in those models. DHH was using it to emphasize his view that it is okay, but this is the kind of discussion that developers go to war over.

Neither of these points are real issues - admittedly, they are just minor nitpicks. It's just that I'd prefer something simpler and cleaner.

The Ryan Bates Method

Ryan Bates demonstrates using default_scope to scope all queries to the current tenant. To do that, he assigns the current user's company ID to a class variable in the tenant model. Here in our Marshmallow app, it would go like this.

The Company model is set up to accept a current_id class-level accessor.

class Company < ActiveRecord::Base
  attr_accessible :name
  cattr_accessor :current_id
end

In the Application Controller, this Company.current_id class accessor is set to the user's company ID on each request.

class ApplicationController
  around_filter :scope_current_company

  private

    def scope_current_company
      Company.current_id = current_tenant.id
      yield
    ensure
      Company.current_id = nil
    end
end

Then a default_scope is added to all other models to scope them to that company.

class User < ActiveRecord::Base
  has_secure_password  
  attr_accessible :name, :email, :password, :password_confirmation  
  default_scope { where (company_id: Company.current_id) }
end

There is nothing wrong with this approach, but it makes me nervous. The whole concept of tracking the current request via an arbitrary class-level accessor and then using that to rely on default scopes just gives me the heebie-jeebies.

It's also not failsafe. Guess what happens if for some reason the Company.current_id doesn't get set? The call does not error out. The default scope does not error out. Instead, the default scope actually returns all records with a company_id of NULL.

Not to mention that your admin controllers, which are not supposed to be restricted, will need to call Project.unscoped everywhere to bypass the default scope. To top it off, multi-threaded apps need safeguarding. It's just too much work for my taste.

The Apartments Method

The Apartment gem creates separate databases for each of your tenants for true data separation. This could be very useful on a very large enterprise app where each tenant is accumulating hordes of data, but for your standard SaaS startup, this is overkill.

A New Approach: Introducing Goliath

Here is my approach, which I am dubbing Goliath, because, ya know, it's my approach so I get to name it whatever I want, and right now Goliath seems like a mighty fine name.

Let's start with a basic question: What is our true goal here? We want a simple uniform method we can use across all controllers - admin, management, and standard - which will just work. A method that will work everywhere without fuss and without bugs.

Let's do just that.

I am going to create a plain ruby object called Goliath that will act as a bouncer (which is the real reason I named him Goliath). All we have to do is invoke it and it will magically give us the records the current user is allowed to view.

I am going to start backwards, from the controllers. Here is what our controllers will look like.

# Our admin controller
class Admin::ProjectsController < Admin::ApplicationController

  def show
    @project = @goliath.projects.find(params[:id])
  end

end

# Our manager controller
class Management::ProjectsController < Management::ApplicationController

  def show
    @project = @goliath.projects.find(params[:id])
  end

end

# Our end-user controller
class ProjectsController < ApplicationController

  def show
    @project = @goliath.projects.find(params[:id])
  end

end

Hey, did you notice that? They're all identical! I'm feeling better already.

So where is this @goliath instance variable set? In a before_filter.

class ApplicationController

  before_filter :enable_goliath

  def enable_goliath
    @goliath = Goliath.new(current_user)
  end

end

And this is what our Goliath model looks like, saved in models/goliath.rb.

class Goliath

  def initialize user
    @user = user
  end

  def projects
    admin? ? Project.scoped : Project.where('company_id = ?', @user.company_id).scoped
  end

  private

    def admin?
      @user.is_admin?
    end

    def manager?
      @user.is_manager?
    end

end

It's really simple. We have a method for each model, and the method returns what the current user can see. If the current user is an admin, we return Project.scoped. The .scoped allows us to chain more methods onto the end of @goliath.projects. If the current user is not an admin, we return a subset of records restricted to the user's tenant, his company.

The beauty of this method is its simplicity. For every new model we add to our app, we add a one line method into Goliath with the rules of who can see that model's records. And everything works like magic.

An end note: You probably shouldn't actually name this model Goliath. I would use Guard. Or even Fetch. You could then call @fetch.projects.find. That looks good.