Search code examples
ruby-on-railsauthorizationcancanmulti-tenant

Authorization in multi tenant app


In Railscasts Episode 388 - Multitenancy with Scopes, Ryan is adding a default scope to ensure security:

Alternatively we can use an authorization library such as CanCan to handle the scoping but this isn’t designed for a multi-tenant apps and it won’t solve this problem very well. This is one case where it’s acceptable to use a default scope so that’s what we’ll do.

class Tenant < ActiveRecord::Base
  attr_accessible :name, :subdomain
  has_many :topics
end

class Topic < ActiveRecord::Base
  attr_accessible :name, :content
  belongs_to :user
  has_many :posts

  default_scope { where(tenant_id: Tenant.current_id) }
end

My question is: I want to implement authorization (for example with Cancan) and would like to define abilities like these:

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.admin?
      can :manage, Topic
    else
      can :read, Topic
    end
  end
end

Does the user have the ability to manage the topics of all tenants or only within the tenants scope?

Or a more general question: what's the right method of authorization for multi tenant applications?


Solution

  • You are on the right track using CanCan, or CanCanCan since CanCan is deprecated, I think.

    I don't like the default_scope for the reason that it is not threadsafe. The user id is stored in a class variable, which means that two or more concurrent users in your app will break this unless you use Unicorn or some other web server that makes sure no more than one single client connection will access the same thread.

    You should therefore use something like Cancan.

    class Ability
      include CanCan::Ability
    
      def initialize(user)
        user ||= User.new # guest user (not logged in)
        if user.admin?
          # User's own Topics only:
          can :manage, Topic, user_id: user.id
          # or, with a Tenant
          can :manage, Topic, tenant_id: user.tenant.id if user.tenant # User belongs_to Tenant
          can :manage, Topic, tenant_id: user.tenants.map(&:id) if user.tenants.any? # User has_many Tenants
        else
          can :read, Topic # Anyone can read any topic.
        end
      end
    end
    

    Pick the strategy you need from the three examples above.


    EDIT Slightly more complicated example for Multi-Tenant Admins for @JoshDoody's question in the comments:


    class Admin < User; end
    
    class TenantAdmin
      belongs_to :tenant
      belongs_to :admin, class_name: User
    end
    
    class Ability
      include CanCan::Ability
    
      def initialize(user)
        user ||= User.new # guest user (not logged in)
        if user.admin?
          can :manage, Topic, tenant_id: TenantAdmin.where(admin: user).map(&:tenant_id)
        else
          can :read, Topic # Anyone can read any topic
        end
      end
    end
    

    Now, it might be that this is not as performant as you would like, but the general idea is that you have TenantAdmins who will be able to manage Topics within their Tenants.

    Hope this helps.