Work
with us

Tell us about your idea
and we will find a way
to make it happen.

Get estimate

Join our awesome team

Check offers

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

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.

For simplicity:

[link]

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.

Policies specification

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.

[link]

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.

Controllers

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:

[link]

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.

Roles

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.

[link]

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.

[link]

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:

[link]

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.

Conclusion

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!

Post tags: