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?
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.