Web applications involving user management has two parts to it, which is authentication and authorization. And you don't get to authorization without authentication, as we can't determine what you can do unless we know who you are in the first place.
Hand rolling out user authentication is a tedious task and majority of the Rails community has delegated out authentication to cool gems such as Devise.
So in this post, we will be talking about another awesome gem which you can leverage to delegate out authorization. And that is Pundit.
So what is Pundit?
When there arises need for restricting access to your application for certain users, role based authorization comes into play. This is where you can make leverage of Pundit. Pundit helps us to define policies which are PORC - Plain Old Ruby Classes - which means that the class does not inherit from other classes nor include in other modules from the framework. Thus makes it very easy to understand the code.
We would still need to define roles for our Users. But now the advantage is that we get to keep our controllers and models skinny. Policies that you define takes away code complexity from the model/controller which otherwise would have been used to determine access to a particular page. Makes our life easy, don't you think?
Setting up Pundit
It's very easy to set it up into your application. The documentation for the gem is well explained.
Nonetheless, let me put it down here:
- Add
gem 'pundit'
to yourGemfile
. - Within your application controller
include Pundit
. - Run command
bundle install
. - Optionally run
rails g pundit:install
which will set up an application policy with some useful defaults.
The Policies will be defined in app/policies/
directory. And don't forget to restart the Rails server so that Rails can pick up new classes that you define there.
Understanding Policies
Like mentioned earlier, policies are PORC, which houses the authorization for a particular page.
Let's look at a policy class example taken out from the documentation.
This is a policy defined to impose restriction for updating a post if the user is an admin, or if the post is unpublished.
Characteristics of Policy class
- The policy name should begin with the name of the model it corresponds to and should always be suffixed with
Policy
. So in the above example -PostPolicy
would be the policy forPost
model. - The initialize method of the policy would need the instance variable user and the model to be authorized. On a sidenote, we can also get by if the model is simply some other object we want to authorize. For example, say a service or form object which has conditions to be checked on it so as to perform the controller action.
- The method names should correspond to controller actions suffixed with a
?
. So for controller actions such asnew
,create
,edit
etc, the policy methodsnew?
,create?
,edit?
etc are to be defined
NOTE: Incase the controller does not have access to current_user method we can define a pundit_user method which will then be used instead.
We can further abstract this Policy if we run the generator rails g pundit:install
, which creates an Application policy with defaults for controller actions and also takes care of the initialization part. This can be inherited by other policies.
But hold on sec, what is a class Scope
doing in the generated ApplicationPolicy?
.
And that is what makes Pundit even more awesome, which we will be getting into soon.
With this generated base policy can simpify our PostPolicy
as
With this setup in place, let's see what changes at the controller level:
With this code in plate, update action of the controller when invoked is authorized and the authorize
method that we invoke here will retrieve the policy for the given record, initialize it with the record and current user and finally throw an error if the user is not authorized to perform the given action.
Understanding Scopes
Scopes are just like using the scopes you define for a model. But in our case, these scopes are done within the policy in context of the user's role for a particular controller action. Scopes are used to retrieve a subset of the records that we have. For example, in a blog app, a non admin user should be restricted to see only posts which has been published but not in draft state. I see you already imagining the controllers and models becoming thinner.
Let's rework our Post policy:
Here we have created a class which will scope the posts based on the user's role. And in order to use it in our controller, we just need to make use of the method policy_scope
.
Characteristics of Scope class
- They are too PORC, which are to be nested within the policy class.
- It needs to initialize with a user and a scope which can either a be ActiveRecord class or ActiveRecord::Relation.
- It needs to define a resolve method which scopes based on the user role.
So now, we revise our Post controller's index
to be like:
The index action will show only published posts unless the user is an admin.
Good Practices that can be leveraged using pundit
Keeping authorization explicit
Rather than making authorization or scoping implicit we rather be explict about it. We can add in checks at the ApplicationController
level so that exception is raised if we forget to add in authorize
or policy_scope
in our controller.
But still, we can make use of skip_authorization
or skip_policy_scope
in circumstances where you don't want to disable verification for the entire action.
Keeping a closed system
If we are making use of a Base policy such as ApplicationPolicy
. We can fail gracefully if at all an unauthenticated user makes through.
Handling errors on authorization
Since Pundit::NotAuthorizedError
will be raised if not authorized, we'd need to handle it gracefully.
This can be done by making use of rescue_from
directive for Pundit::NotAuthorizedError
and then pass in a method to handle the exception.
We can also go a step further and customize error messages based on which policy's action was not authorized.
And you can have your locale file to be like this:
This is a way to setup error messages for authorization as here we make use of the information NotAuthorizedError
provide ie. what query (e.g. :create?), what record (e.g. an instance of Post), and what policy (e.g. an instance of PostPolicy) caused the error to be raised. Ultimately, it's up to you on how you organize your locale files.
Alternatively, we can also serve them with 403 error page by configuring in application.rb
Extending policy with multiple roles
Often there comes in requirement that a particular CRUD action's authorization varies on multiple roles. In context of our example, say, there also comes in a role 'premium'. And now there exists posts which can be viewed by premium users and the admin only. No worries, just create a new 'premium' role and update our PostPolicy as below:
With the above changes now a normal user can't view premium posts in the index view listings as we are scoping it out and also we are authorizing the show page as to not allow non-premium users to see premium post content. Pretty neat isn't it? We no longer need to delegate the app execution flow to model or controller and let Pundit do all the heavy lifting.
This gives us fine granularity in controlling role based access and now that we understand how Pundit is structured and what conventions we need to follow, writing authorization code becomes intuitive. Skinny controllers and skinny models FTW!