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.

其他提示

You have set ability just for Topic. So, it will check only for topic object.

To have a check for tenant level you need to set something like this:

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    can :manage, Tenant do |tenant|
      if user.admin?
        `you code goes here`
      else
      end
    end
    can :read, Tenant
    can :read, Topic
  end
end

Му example of ability for multitenent app

class Ability
  def initialize(admin,  tenant = nil)
     user ||= User.new
     if user.admin?
       can :manage, Topic
     else
       can :manage, PostState, tenant: tenant
     end
  end
end

You may not pass tenant and use Tenant.current_id

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top