Ask A Question

Notifications

You’re not receiving notifications from this thread.

Structure Guidance - Multi Tenancy Authentication / Authorization

Dan Tappin asked in Rails

I while back I was playing around building an app where you had multiple tenants (Company Model) and a single login (User Model). I then created roles (Role Model) to link the users to the tenants. These roles would typically include "guest", "contractor", "admin" etc. I would then use this to authenticate the users access to various resources associated to each tenant. So one user could be a guest for one company and an admin for the next. This way they had a single login etc.

It works pretty good but I was thinking that I should really try to use Devise for the authentication and CanCanCan for the authorization. After some Googling there seems to be a few approaches to this but nothing seems to just out at me as an elegant use of Devise and CanCanCan to accomplish this vs a from scratch solution.

It seems most of the solutions use subdomains which I could use but I would rather not. Devise-basecamper (https://github.com/digitalopera/devise-basecamper) seems like on the right track but its based pin CanCan and looks a bit stale.

Any ideas would be appreciated.

Thanks,

Dan

Reply

Hey Dan,

This is a great topic and there are a lot of different approaches like you mentioned. For me personally, I've always built this from scratch because I didn't want to force subdomain usage in each case. I'd used Harvest and Freshbooks and didn't like having to always go to my subdomain first.

The solution I made was pretty simple. I'll let you login to the site as normal and then have a simple method in the navigation for you to switch accounts (or companies in your case). I'll use this method to set the session[:company_id] which is tied with a current_company method. Basically something like this:

class AccountsController < ApplicationController
  before_action :authenticate_user!

  def switch
    # Load the company through the association so we know they have access to it
    @current_company ||= current_user.companies.find(params[:id])
    session[:company_id] = @current_company.id
    redirect_to root_path
  end
end

This will set you up with a switch action that can be used for switching accounts. Just pass in the ID in a url that routes to this action and you'll be all set.

Then you can put this in your application_controller.rb so you can also access this in the views.

def current_company
  @current_company ||= current_user.companies.find(session[:company_id])
end
helper_method :current_company

You can set the session[:company_id] to a default when they login, or allow them to use the site under a "Personal" account without a company.

At least with this setup you have full control over everything. I've been really pleased with how this turned out and it's really lightweight.

Reply

I think this is basically like I had set it up but I had the Role model to glue the users to the accounts. This is what authorized the users to switch to a given account and what type of user they were.

I need to dig into Devise and CanCanCan more but I am thinking that I should just be able to use Devise as planned and then add a layer of authorization with CanCanCan using the users Role to set the user type (guest, admin etc.)

Reply

Yeah, so you'll probably skip the regular CanCanCan load_resource and do it yourself using this code. Then once you have the company loaded and associated objects, you can pass them through to authorize to hit your proper authorization rules. That's where all the rules from the Role will come into play. You can even check the Role itself, but I'm guessing most of the time you'll want to authorize objects through the Role.

Reply

Hey guys,

New to GoRails and have been following the pundit screen cast to accomplish something similar to what you are discussing above. I'm creating an application that has several admin levels (user, admin, and superadmin). I've used pundit to setup the roles and am able to assign a user as an admin through the rails console. I'm stuck with how to create a toggle/check button that would allow me to assign users to a role through a view. Also, I have a users who belong to a classroom (product is for schools) and I have built the appropriate associations. Same problem, how do I allow an admin/super admin to assign users to a classroom. Any suggestions on how to do this?

Anything to point me the right direction that would be great. Thanks!

Reply

Hey Alex,

The easiest thing (if you're using a string column like I did in the episode) is to just do a dropdown because each option's value is text.

Check boxes make sense if you've got a join table and allow each user to have multiple roles.

Instead of a dropdown, you could do radio buttons for a bit more visible UI:

f.radio_button(:role, "user")
f.radio_button(:role, "admin")
f.radio_button(:role, "superadmin")

Does that make sense?

Reply

Yes it does make sense. I don't want to allow users to have multiple roles but I only want the super admin to be able to change the role of a user to user, admin, or super admin. I think this means I have to create the join table correct?

EDIT: scratch that question, I figured it out. I had to add :role to devise in the application controller. Now I need to figure out how to make it so the admin is the only one who can change the role :) thanks for the help!

Reply

In that case, I would stick to just a string column on the User model for role. That way it can only store one value (and you can add more later easily). No need for a join table here because your User will just contain the role.

# role :string
class User
  def user?; role == "user"; end
  def admin?; role == "admin"; end
  def superadmin?; role == "superadmin"; end
end

You can create some helper methods like that to determine what type of user they are.

Then to restrict who can change that, you can update your controller's strong params code for superadmins to add the role column as allowed for editing. The other types of users won't allow that field, so they can't change user's roles.

You can do that with Pundit pretty easily. First you'll create the policy for the User model and then you can have your controller ask the Policy which params are allowed:

# app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
  def permitted_attributes
    if user.superadmin?
      [:first_name, :last_name, :role]
    else
      [:first_name, :last_name]
    end
  end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to @user
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(*policy(@user || User).permitted_attributes)
  end
end
Reply

boom! that is exactly what I needed! Thanks for the help Chris!

Reply

Any thoughts on acts_as_tenant vs. apartment?

The multiple database per tenant seems bulletproof but would get ugly with lots of tenants. The odds of something going wrong with migrations etc. does not seem worth the risk.

acts_as_tenant seems to implement the system I was going to anyway.

Reply

If there are things like legal reasons why you should separate out the DBs, then it's good to, otherwise it's often just a lot more work and much harder to maintain. You're probably best off doing without multiple databases unless you know you need that feature. Of course, that means that every record should be linked to an some sort of Account model if you don't have separation through database.

Reply

Exactly - the more I think about it postgeSQL schemas or the apartment gem are a great way to segregate data but create a lot of work.

Here is a related StackExchange post of mine to consider:

http://stackoverflow.com/questions/28513849/rolify-and-acts-as-tenant-with-single-signon-with-some-devise-pundit-on-the-s

Reply

Hi everyone.

Just wanted weigh in on this. I've also been building a multi-tenant application and it's lead me down some interesting paths… I read Ryan Bigg’s book on the topic (“Multitenancy with Rails”) but decided to figure out my own solution in the end. The book strongly recommends against going the postgres schemas route—mostly because of performance issues. From my understanding it’s like having to host multiple rails apps for each user, at the same time, on your production server. Not so bad if you only have a couple users, but it can get pretty expensive as you scale up.

Ryan’s solution is to roll your own authentication system using Warden and use subdomains to scope your data. It all sounded pretty good but one of the problems with the book (at least for me) was that Ryan decided to write all of this stuff inside of a Rails engine which just added to a lot of confusion for me. As I was going through the book I decided not to do the engine approach and just use a starndard rails app. Long story short I completely botched the job! I couldn’t get the authentication / subdomains to work in tandem and I ended up with more confusion than anything. Complete failure!

In the end I decided to make my own solution using Devise for authentication (because…well…it’s just easy!), NO subdomains, and use Pundit to help scope the data.

How it works is pretty simple:

In Pundit all you have to do is make sure your user is scoping to the model of your choice. I eventually want to add multiple users that have access to the same data so I ended scoping everything to the Company model. (A User has_one :company, a Company has_many :users).

Pundit setup is standard except I added this to the application_policy:

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.where(company_id: user.company.id)
    end
  end

Then in your controllers all you need to do scope your data is use the pundit method policy_scope. That way everything is scoped to the company:

  def index
    @plans = policy_scope(Plan).all
    authorize @plans
 end

That's it! As you add models to to your app you just need to make sure they are all tied to the company. (belongs_to :company).

I like this solution because it’s simple. You can use Devise out of the box and you don’t have to go through the hassle of dealing with subdomains. Also I like the fact that Pundit has my back and will throw an error if I forget to scope something properly.

A couple gotcha’s though:

  • When scoping data you need to make double sure that your scopes are working and you’re not accidentally showing the wrong data to the wrong user. Good tests here are essential.

  • The routes can be a bit tricky because ideally you don’t want to have any ids in your web addresses. Example: You don’t want someone manually typing http://yourapp.com/client/3 into their browser if the third client in your database belongs to someone else. Mostly I’ve been able to avoid this by using a lot of resource (singular) :model in the routes. So far that seems to be working but I think I’m going to have to break that pretty soon. I’ll probably have to generate a GUID and use that in the routes instead of a regular id.

I’m still developing this app but so far it seems like a pretty good solution for me. I’m still super new to all this though so please let me know if this is a horrible idea!

Reply

Great post Andrew! That's a great approach and breakdown on the gotchas. You could either use a randomly generated ID of some sort, or you could add an incremental ID scoped to the company in as well. That can be nice so that you have more memorable ID numbers if you need them. This is how Github pull requests always start with #1. The real database ID is a separate number.

I'll be recording a couple episodes soon on how to set up multitenancy with subdomains. It's definitely not a simple thing, but really isn't too bad once you break it all down.

Reply

Hey guys, I've been following this thread. Great topic. Chris, can't wait for the new episodes you mentioned. This is a challenge I've run into a bunch lately.

Reply

Hi All,

I am using Postgresql and apartment gem and my database has many views. so I am using

config.use_schemas = true
config.use_sql = true

to use pg_dump but when I run migrations it tries to use normal user account through which I am logged in to my computer rather than the one I provided in my database.yml

Is there any way to provide correct credentials so that pg_dump command can run when I run migrations? Any help would be greatly appreciated.

Thanks

Reply

Hi,

If you want to tackle the problem Andrew described: You don’t want someone manually typing http://yourapp.com/client/3 into their browser.

you can do the following using Pundit and using a association model:

class User < ActiveRecord::Base
  has_many :associations
  has_many :clients, through: :associations
end

class Association < ActiveRecord::Base
    belongs_to :user
    belongs_to :client
end

class Item < ActiveRecord::Base
  has_many :associations
  has_many :users, through: :associations
end

and your client_policy.rb will look like this:

class ClientPolicy < ApplicationPolicy

    def initialize(user, client)
    @user = user
    @client = client
  end

  class Scope < Scope
    def resolve
     if user.admin?
        scope.all
      else
        scope.where(:id => user.associations.pluck(:client_id))
      end
    end
  end

    def index?
        user.present?
    end

    def show?
   user.present? && @user.associations.pluck(:client_id).include?(@client.id)
    end

  def create?
    user.present? && user.admin?
  end

  def new?
    create?
  end

  def update?
    user.present? && @user.associations.pluck(:client_id).include?(@client.id)
  end

  def edit?
    update?
  end

  def destroy?
    user.present? && user.admin?
  end

end

This way you will only be allowed to clients where you have access to.

Reply

Hi Chris and al.
I have been watching the video about pundit and i am trying to implement it. I have been reading Andrew's (above) blog post about it and separating content for users based on their appartenance to companies. It would be great to have a working example about this.

Reply

In addition to this, I was wondering what could be the idea for allowing a user to view and edit an item based on his/her email being part of the attribute (eg. assign_to). Is pundit appropriate to this as well?

Reply

You can set the session[:company_id] to a default when they login, or allow them to use the site under a "Personal" account without a company.

Hi folks, appreciate the commentary, it's extra ordinarily re-assuring. I want to do exactly this:

  • How can we scope to both a company, and also
  • a personal account, at the same time?

Some users will not want to be associated with a company, but they will want to have access to the same resources of a company.

Any pointers, however brief, will be received with thanks.

Any pointers would be much appreciated.

Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 81,842+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.

    Screencast tutorials to help you learn Ruby on Rails, Javascript, Hotwire, Turbo, Stimulus.js, PostgreSQL, MySQL, Ubuntu, and more.

    © 2024 GoRails, LLC. All rights reserved.