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?