Jan 27, 2017

Very simple permissions in Rails

I was working on a very simple Rails API, using devise_token_auth and I need to authorize controller actions based on user roles.

(Although, this should work fine for regular ol’ devise)

I started out using cancancan and I even tried out pundit but they weren’t simple enough for my needs.

Requirements

  • I needed a simple hash to define which role can access which controller actions.
  • I needed my controllers to check this hash to determine if a user can access the action he’s trying to access.

Solution

I created a concern controllers/concerns/check_permission.rb

module CheckPermission
  extend ActiveSupport::Concern


  included do
    before_action :authenticate_user!, unless: :devise_controller?
    before_action :check_permission, unless: :devise_controller?
  end

  def permissions
    {
        tickets: {
            registered: [:index, :create, :reply, :close, :reopen],
            support: [:index, :reply, :close, :reopen],
            admin: []
        },

        users: {
            admin: [:create, :update, :index, :show, :destroy]
        }
    }
  end


  def check_permission
    controller_permissions = permissions[controller_name.to_sym]
    role = current_user.role.to_sym
    if controller_permissions.keys.include?(role)
      if !controller_permissions[role].include?(action_name.to_sym)
        head 403
      end
    else
      head 403
    end
  end
end

Then I included this concern in my ApplicationController

 
class ApplicationController < ActionController::API
  include DeviseTokenAuth::Concerns::SetUserByToken
  include CheckPermission

end

The method permissions returns a hash with the controllers, actions and roles in following format.

{
    controller_name: {
        role: [:action, :another_action],
        another_role: [:action...],
    },

    another_controller_name: {
        role: [:action, :another_action],
    }

    ...
}

Also, whatcheck_permissions does is pretty simple. It checks if the current user’s role array includes the action that is currently being attempted. If not, it returns a 403.

P.S

This obviously works only in situations where a User has only one role. But it is easy to adjust check_permission to work for a many-to-many relationship between User & Role.

def check_permission
  controller_permissions = permissions[controller_name.to_sym]
  roles = current_user.roles.pluck(:name).map &:to_sym
  if !(controller_permissions.keys & roles).empty?
    actions = []
    (controller_permissions.keys & roles).each { |x| actions | controller_permissions[x] }
    head 403 if !actions.include?(action_name.to_sym)
  else
    head 403
  end
end

This will first check if any of the user’s roles is present in the permitted roles of that controller.

Then it will create a union array of all the permitted actions of all the user’s roles and check if the current action_name exists in that array.

Simple, right?

Copied to clipboard!