Hand Rolling MultiTenancy

MultiTenancy is great! It allows developers to build a single application with multiple clearance levels instead of shipping separate applications that serve different purposes for different user roles.

I imagine a multi-level building that has different security levels. You can enter the first floor and look around as a visitor. But then if you want access to some of the other floors, you need to sign up for an account. You can even go to “higher” floors depending on your security clearance.

That is Multi-tenancy, authorized clearance to certain parts of the app.

If you want it, here is the definition of a multi-tenant app:

A tenant is a group of users sharing the same view on the software they use. With a multitenant architecture, a software application is designed to provide every tenant its data, configuration, user management, and tenant individual functionality.

In my most recent project, I inherited a legacy Rails App and was challenged with adding a bunch of features. I wanted to add multi-tenancy and faced two immediate options:

  1. Implement a pre-built version such as [Devise](https://github.com/plataformatec/devise) or [CanCanCan](https://github.com/CanCanCommunity/cancancan)
  2. Hand-Roll my own version of MultiTenancy Authorization.

The great thing about the second option is the power of learning. For students, I think it is important to choose the path of transparency. In my opinion, adding the feature using a pre-built tool was a quick and easy way out.

So, to get started, here was my starting:

GlobalPursuit on Github || GlobalPursuit on Heroku

This app already has implemented BCrypt authorization. So we simply want to build around that. It works perfectly fine. Our first move was to add two new models.

Role

class Role < ActiveRecord::Base
validates :name, uniqueness: true
has_many :user_roles
has_many :users, through: :user_roles
end

User

class User < ActiveRecord::Base
has_secure_password

has_many :user_roles
has_many :roles, through: :user_roles

belongs_to :store
has_many :orders
end

UserRole

class UserRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
end

The `User` model already has Bcrypt. If you want to learn more about the `has_secure_password` method, check out my friend Torie explain it here.

So far, we have added relations between the three models, `User`, `Role`, and `UserRoles`. At this point, we haven’t done anything to secure our application.

Make sure to run migrations at this point.

Next, we want to add various roles to the `Role` Model. Lets add the following roles:

Role.create([{name:”platform_admin” }, {name: “store_admin” }, {name: “registered_user”}])Role.create([{name:”platform_admin” }, {name: “store_admin” }, {name: “registered_user”}])

Also, be sure to add some helper methods to your app/controllers/application_controller.rb:

protect_from_forgery with: :exception 
before_action :authorize! 
add_flash_types :success,
:info,
:warning,
:danger
helper_method :current_user 
def current_user 
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def current_permission  
@current_permission ||= PermissionService.new(current_user)
end
def authorize! 
unless authorized? redirect_to root_url, danger: “You are not authorized to visit this page”
end
end
def authorized? 
current_permission.allow?(params[:controller], params[:action])
end

This is the beginning of our tool set that we will use. But to continue, we will need to add one more model. Let’s create a `PermissionService` class.

class PermissionService 
extend Forwardable
  attr_reader :user,
:controller,
:action
def_delegators
:user,
:platform_admin?,
:store_admin?,
:registered_user?
  def initialize(user) 
@user = user || User.new
end
  def allow?(controller, action) 
@controller = controller
@action = action

case
when platform_admin? then platform_admin_permissions
when store_admin? then store_admin_permissions
when registered_user? then registered_user_permissions
else
guest_user_permissions
end
end
end

We will conclude this class with a series of private methods. Each method will be a central place in the Rails App to define security authentication levels. For example, I can define what areas of the Rails App each User can access based on their assigned User Role. It is really simple and allows for future changes. Let’s take a look `platform_admin_permissions` below:

def platform_admin_permissions 
return true if controller == “sessions”
return true if controller == “items” && action.in?(%w(index show))
return true if controller == “stores” && action.in?(%w(index show))
return true if controller == “orders” && action.in?(%w(index show))
return true if controller == “users” && action.in?(%w(index show))
end

Here, the controllers are specified by a string and each action of said controller is simply a part of an array. Simple and straightforward.

I hope that this is helpful. I would suggest trying it at least once as the learning that can be gained is very helpful to help a beginner understand what an authorization gem such as CanCanCan is doing under the hood.