At some point in my work, I needed to restrict certain areas of the app for selected users. My first choice for this was to use well-known gem CanCan, but unfortunately, as of November 3rd 2014, it was inactive for over a year. That’s why I chose Pundit.
Before we start
Please consider downloading sample app I provided. I made some assumptions for the sake of simplicity:
User can modify or delete only the entries she/he owns.
Admin can modify and delete all entries.
User can create unpublished entries, and only she/he can see it.
Admin can see all entries.
Please note that I have left all entries in the index view and links to edit and destroy visible, just to make it easier for you to test it.
Policies are simple Ruby classes that allow showing which parts of application can be accessed by which users or groups. You should put them inside the app/policies directory. Let’s take a look at ApplicationPolicy, which I encourage you to use as a base for the rest of policies.
class ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end def index? check_if_admin(@user) || available_to_show?(@user, @record) end def show? check_if_admin(@user) || available_to_show?(@user, @record) end def create? true end def new? create? end def update? check_if_admin(@user) || proper_owner?(@user, @record) end def edit? update? end def destroy? check_if_admin(@user) || proper_owner?(@user, @record) end def check_if_admin(user) return false unless user user.admin? end end
Every method corresponds to a method inside its controller. When a method returns True, it means that the given user is authorized to access that method. Creating a base policy allows you to simplify making policies for any given model.
Now, when you have the base policy in place, it’s time to create specific policies for our model. If your app follows the assumptions above, the only thing you need to do, is to create a new class. Let’s call it EntryPolicy, as the it should be named after the the model. You need to define 3 methods inside: initialize which will only call the ApplicationPolicy constructor, available_to_show?, and proper_owner?. Let’s take a look at the example I created.
class EntryPolicy < ApplicationPolicy def initalize(user, entry) super end def proper_owner?(user, entry) entry.proper_owner?(user) end def available_to_show?(user, entry) return true if entry.published entry.proper_owner?(user) end end
Typically, if you want to override any of the ApplicationPolicy methods, just go ahead and do it :) As you can see, it’s possible to define many policies without too much work with this approach.
Using Pundit is really simple. To check whether a user is authorized to execute a particular action, just call authorize @object_name command. Pundit will search for proper policy and check authorization. For example:
def destroy @entry = Entry.find(params[:id]) authorize @entry @entry.destroy redirect_to entries_path, notice: 'Entry deleted' end
When you take a look at ApplicationPolicy::initialize(user, record), you will notice that it needs 2 arguments. First one is user, and Pundit is clever enough to retrieve current_user value from the Devise library. The second one is record, for which you can call authorize @object_name command inside the controller.
As I mentioned before, Pundit allows checking roles. To implement them first you need to create a migration for the user model. Simply add and integer field called role. You can download it from my sample here. Now just add an enum to user model.
enum role: [:user, :admin]
To make sure that each new user has the user role, just add following lines to your model.
after_initialize :set_default_role, :if => :new_record? def set_default_role self.role ||= :user end
Of course you can add more roles if you like, the sky’s the limit :)
Checking user role
Checking whether users have roles is simple. This is the check_if_admin method that I didn’t use above.
It’s that simple. Just call model.role_name?, and there you go.
Making it user-friendly
Right now, every unauthorized action raises an exception. In my opinion, it’s not the best way to show users that something is wrong. In the sample app I used, this is the approach shown in Pundit’s readme:
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized def user_not_authorized redirect_to(request.referrer || root_path, alert: 'Not autorized') end
Just put those lines inside ApplicationController, and that’s it. Now users will be redirected to referrer path, if not authorized.
Hopefully, I was able to show you how to use Pundit to make authorization inside your app in a relatively quick manner. If you have any questions, please do not hesitate to ask, and thank you for reading!