Key Takeaways
- Pundit is a straightforward, Ruby-based authorization library for Rails, focusing on using plain Ruby classes and methods without special DSLs, making it easy to integrate and understand.
- Setting up Pundit involves adding the gem to your Gemfile, running a generator to create a base policy class, and including Pundit in the ApplicationController.
- Authorization rules in Pundit are defined in policy classes, where methods corresponding to different actions (like `create?`, `update?`, etc.) determine access rights based on user attributes.
- Pundit enhances view layer integration by providing helper methods (`policy`) to conditionally display elements based on authorization, ensuring a seamless user experience.
- Error handling in Pundit is managed by rescuing from `Pundit::NotAuthorizedError` and redirecting or displaying appropriate messages, which can be customized for different scenarios or localized.
This is a second article in the “Authorization with Rails” series. In the previous article, we discussed CanCanCan, a widely known solution started by Ryan Bates and now supported by a group of enthusiasts. Today I am going to introduce you a bit less popular, but still viable, library called Pundit created by the folks of ELabs.
“Pundit” means “a scholar”, “a clever guy” (sometimes used as a negative description), but you don’t need to be a genius to use it in your projects. Pundit is really easy to understand and, believe me, you’ll love it. I fell in love it when I browsed its documentation.
The idea behind Pundit is employing plain old Ruby classes and methods without involving any special DSLs. This gem only adds a couple of useful helpers, so, all in all, you can craft your system the way you see fit. This solution is a bit more low-level than CanCanCan, and it is really interesting to compare the two.
In this article we will discuss all of Pundit’s main features: working with access rules, using helper methods, and scoping and defining permitted attributes.
The working demo is available at sitepoint-pundit.herokuapp.com.
The source code can be found on GitHub.
Preparations
Our preparations will be super fast. Go ahead and create a new Rails app. Pundit works with Rails 3 and 4, but for this demo version 4.1 will be used.
Drop in the following gems into your Gemfile:
Gemfile
[...]
gem 'pundit'
gem 'clearance'
gem 'bootstrap-sass'
[...]
Clearance will be used to set up authentication very quickly. This gem was covered in my “Simple Rails Authentication with Clearance” article, so you can refer to it for more information.
Bootstrap will be used for basic styling, though you may skip this as always.
Don’t forget to run
$ bundle install
Now, run Clearance’s generator that is going to create a User
model and some basic configuration:
$ rails generate clearance:install
Modify the layout to include flash messages, as Clearance and Pundit rely on them to display information to the user:
layouts/application.html.erb
[...]
<div id="flash">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>"><%= value %></div>
<% end %>
</div>
[...]
Create a new scaffold for Post
:
$ rails g scaffold Post title:string body:text
We also want our users to log in before working with the app:
application_controller.rb
[...]
before_action :require_login
[...]
Set up the root route:
routes.rb
[...]
root to: 'posts#index'
[...]
Admin User
To finish setting up our lab environment, we need to add an admin
field inside the users
table, so modify the migration like this:
xxx_create_users.rb
[...]
t.boolean :admin, default: false, null: false
[...]
Run the migrations:
$ rake db:migrate
Lastly, let’s add a small button to easily switch between admin states:
layouts/application.html.erb
[...]
<% if current_user %>
<div class="well well-sm">
Admin: <strong><%= current_user.admin? %></strong><br>
<%= link_to 'Toggle admin rights', user_path(current_user), method: :patch, class: 'btn btn-info' %>
</div>
<% end %>
[...]
We have to check if current_user
is present, otherwise non-authenticated users will see an error.
Add a route:
routes.rb
[...]
resources :users, only: [:update]
[...]
and a controller:
users_controller.rb
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
@user.toggle!(:admin)
flash[:success] = 'OK!'
redirect_to root_path
end
end
That’s it! The lab environment is ready, so now we can start playing with Pundit.
Integrating Pundit
To start off, add the following line to ApplicationController
:
application_controller.rb
[...]
include Pundit
[...]
Next, run Pundit’s generator:
$ rails g pundit:install
This is going to create a base class with policies inside the app/policies folder. Policy classes are the core of Pundit and we are going to work with them extensively. The base policy class looks like this:
app/policies/application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
@user = user
@record = record
end
def index?
false
end
def show?
scope.where(:id => record.id).exists?
end
def create?
false
end
def new?
create?
end
# [...]
# some stuff omitted
class Scope
# [...]
end
end
Each policy is a basic Ruby class, but you have to keep in mind a couple of things:
- Policies should be named after a model they belong to, but prefixed with the word
Policy
. For example, usePostPolicy
for thePost
model. If you don’t have an associated model, you still can use Pundit – read more here. - The first argument to the
initialize
method is a user record. Pundit uses thecurrent_user
method to get it, but if you do not have such a method, this behavior can be changed by overridingpundit_user
. Read more here. - The second argument is a model object. It does not have to be an
ActiveModel
object though. - The policy class has to implement query methods like
create?
ornew?
to check access rights.
If you are using the base policy class and inherit from it, you don’t really need to worry about most of this stuff, but there may be times when you need a custom policy class (for example, when you don’t have a corresponding model).
Providing Access Rules
Now, let’s write our first access rule. For example, we only want admin users to be able to destroy posts. That’s easy to do! Create a new post_policy.rb file inside the policies folder and paste the following code:
policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def destroy?
user.admin?
end
end
As you can see, the destroy?
method is going to return true
or false
, indicating whether a user is authorized to perform an action.
Inside the controller we need to check our rule:
posts_controller.rb
[...]
def destroy
authorize @post
@post.destroy
redirect_to posts_url, notice: 'Post was successfully destroyed.'
end
[...]
authorize
accepts a second optional argument to provide the name of the rule to use. This is useful if your action’s name is different, for example:
def publish
authorize @post, :update?
end
You can also pass a class name instead of an instance, for example, if you don’t have a resource to work with:
authorize Post
If the user is not allowed to perform the action, an error will be raised. We need to rescue from it and display a useful message instead. The easiest solution will be to just render some basic text:
application_controller.rb
[...]
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:warning] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
end
[...]
However, you might need to set up a custom message for different cases or translate it for other languages. That’s possible as well:
application_controller.rb
[...]
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized(exception)
policy_name = exception.policy.class.to_s.underscore
flash[:warning] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
redirect_to(request.referrer || root_path)
end
[...]
Don’t forget to update translations file:
config/locales/en.yml
en:
pundit:
default: 'You cannot perform this action.'
post_policy:
destroy?: 'You cannot destroy this post!'
If a user cannot destroy a post, there is no point in rendering the “Destroy” button. Luckily, Pundit provides a special helper method:
views/posts/index.html.erb
[...]
<% if policy(post).destroy? %>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
[...]
Go ahead and check it out! Everything should be working nicely.
You might be thinking that inside the policies we don’t handle a case when a user is not authenticated at all. To fix it, simply add the following line:
policies/application_policy.rb
[...]
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
@user = user
@record = record
end
[...]
Just don’t forget to rescue from Pundit::NotAuthorizedError
error inside the ApplicationController
.
Setting Up Relations
Now, let’s set up a one-to-many relationship between posts and users. Create a new migration and apply it:
$ rails g migration add_user_id_to_posts user:references
$ rake db:migrate
Modify model files:
models/post.rb
[...]
belongs_to :user
[...]
models/user.rb
[...]
has_many :posts
[...]
Also, it would be great to populate the database with some demo records. Use seeds.rb for that:
20.times do |i|
Post.create({title: "Post #{i + 1}", body: 'test body', user_id: i > 10 ? 1 : 2})
end
We just create 20 posts belonging to different users. Feel free to modify this code as needed.
Run
$ rake db:seed
to populate the database.
Modify the policy to allow users to destroy their own posts:
policies/post_policy.rb
[...]
def destroy?
user.admin? || record.user == user
end
[...]
As you probably remember, record
is being set to the object with which we are working. This is nice, but we can do more. How about creating a scope to load only the posts that user owns? Pundit supports that, too!
Working with Scopes
First of all, create a new, non-RESTful action:
posts_controller.rb
def user_posts
end
routes.rb
resources :posts do
collection do
get '/user_posts', to: 'posts#user_posts', as: :user
end
end
Add a top menu:
layouts/application.html.erb
[...]
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'Pundit', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar">
<ul class="nav navbar-nav">
<li><%= link_to 'All posts', posts_path %></li>
<li><%= link_to 'Your posts', user_posts_path %></li>
</ul>
</div>
</div>
</nav>
[...]
We need to create an actual scope. Inside the application_policy.rb file there is a Scope
class that has the following code:
policies/application_policy.rb
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
scope
end
end
Just as with policies, there are a couple of things to take into consideration:
- The class should be named
Scope
and nested inside your policy class. - The first argument passed to the
initialize
method is user, just like with policies. - The second argument is scope (an instance of
ActiveRecord
orActiveRecord::Relation
or anything else). - The
Scope
class implements a method calledresolve
that returns an iterable result.
Just inherit from the base class and implement your own resolve
method:
policies/post_policy.rb
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.where(user: user)
end
end
[...]
end
This scope simply loads all the posts that belong to a user. Use this new scope inside your controller:
posts_controller.rb
[...]
def user_posts
@posts = policy_scope(Post)
end
[...]
You may want to extract some code from the index view into partials (and prettify the table a bit):
views/posts/_post.html.erb
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<% if policy(post).destroy? %>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
</tr>
views/posts/_list.html.erb
<table class="table table-bordered table-striped table-condensed table-hover">
<thead>
<tr>
<th>Title</th>
<th>Body</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<%= render @posts %>
</tbody>
</table>
<br>
<%= link_to 'New Post', new_post_path %>
views/posts/index.html.erb
<h1>Listing Posts</h1>
<%= render 'list' %>
Now, just create a new view:
views/posts/user_posts.html.erb
<h1>Your Posts</h1>
<%= render 'list' %>
Boot the server and observe the result!
Inside the views you may use the following code to select only the required records:
<% policy_scope(@user.posts).each do |post| %>
<% end %>
Enforcing Authorization
If you want to check that authorization did take place in your controller, use verify_authorized
as an after action. You can also choose what actions to check:
posts_controller.rb
[...]
after_action :verify_authorized, only: [:destroy]
[...]
The same can be done to ensure scoping took place:
posts_controller.rb
[...]
after_action :verify_policy_scoped, only: [:user_posts]
[...]
Under some circumstances, however, it may be unreasonable to perform the authorization check, so you might want to skip it. For example, if a record to destroy wasn’t found, we just return back without any further actions. For this skip_authorization
method is used:
[...]
def destroy
if @post.present?
authorize @post
@post.destroy
else
skip_authorization
end
redirect_to posts_url, notice: 'Post was successfully destroyed.'
end
private
def set_post
@post = Post.find_by(id: params[:id])
end
[...]
Permitted Parameters
The last feature we are going to discuss is an ability to define permitted parameters in Pundit’s policies. Note that you have to use Rails 4 or Rails 3 with the strong_params gem for this to work properly.
Suppose, we have some “special” parameter that only administrators should be able to modify. Let’s add the corresponding migration:
$ rails g migration add_special_to_posts special:boolean
Modify the migration a bit:
xxx_add_special_to_posts.rb
[...]
add_column :posts, :special, :boolean, default: false
[...]
and apply it:
$ rake db:migrate
Now, define a new method in your policies:
policies/post_policy.rb
[...]
def permitted_attributes
if user.admin?
[:title, :body, :special]
else
[:title, :body]
end
end
[...]
So, an administrator can set all attributes, whereas users can only modify title
and body
. Lastly, update the controller methods:
posts_controller.rb
[...]
def create
@post = Post.new
@post.update_attributes(permitted_attributes(@post))
if @post.save
redirect_to @post, notice: 'Post was successfully created.'
else
render :new
end
end
def update
if @post.update(permitted_attributes(@post))
redirect_to @post, notice: 'Post was successfully updated.'
else
render :edit
end
end
[...]
permitted_attributes
is a helper method that expects a resource to be passed. There is a small gotcha inside the create
method: you need to initialize @post
and only then set its attributes. If you do this:
@post = Post.new(permitted_attributes(@post))
an error will be raised, because you are trying to pass a non-existant object to permitted_attributes
.
Instead of employing permitted_attributes
, you can stick to the basic post_params
and modify it like this:
posts_controller.rb
[...]
def post_params
params.require(:post).permit(policy(@post).permitted_attributes)
end
[...]
Lastly, modify the views:
views/posts/_form.html.erb
[...]
<div class="field">
<%= f.label :special %><br>
<%= f.check_box :special %>
</div>
[...]
views/posts/_list.html.erb
[...]
<thead>
<tr>
<th>Title</th>
<th>Body</th>
<th>Special?</th>
<th colspan="3"></th>
</tr>
</thead>
[...]
views/posts/_post.html.erb
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= post.special? %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<% if policy(post).destroy? %>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
</tr>
Now boot the server and trying setting the “special” parameter to true while being a basic user – you shouldn’t be able to do so.
Conclusion
In this article we’ve discussed Pundit – a great authorization solution employing basic Ruby classes. We’ve taken a look at most of its features. I encourage you to browse its documentation, because you might be interested in how to provide additional context or manually specify the policy class.
Have you ever used Pundit before? Would you consider using it in future? Do you think it is more convenient than CanCanCan or is it just a different solution? Share your opinion in the comments!
As always, I thank you for staying with me. If you want me to cover a topic, don’t hesitate to ask. See you!
Frequently Asked Questions (FAQs) about Rails Authorization with Pundit
What is the Pundit gem in Ruby on Rails?
Pundit is a Ruby gem that provides a set of helpers to build a simple, robust and scalable authorization system in Ruby on Rails applications. It uses plain Ruby classes and object-oriented design patterns to make the authorization rules easy to read, write, and maintain. Pundit provides a flexible way to manage permissions and access control to resources in your application, based on the roles and privileges of the current user.
How do I install and set up Pundit in my Rails application?
To install Pundit, you need to add the gem to your Gemfile and run the bundle install command. After that, you can generate the Pundit installation by running the rails g pundit:install command. This will create an application policy with some basic setup. You can then create individual policies for your models using the rails g pundit:policy model_name command.
How does Pundit handle authorization in Rails?
Pundit uses policy classes to define the authorization rules. Each policy class corresponds to a model in your application and contains methods that correspond to the actions you want to authorize. These methods return a boolean value indicating whether the user is authorized to perform the action or not. You can use these methods in your controllers and views to control access to resources.
How can I use Pundit to authorize actions in my controllers?
You can use the authorize method provided by Pundit in your controller actions to check if the current user is authorized to perform the action. The authorize method takes two arguments: the record you want to authorize the action on, and the action you want to authorize. If the user is not authorized, Pundit will raise a Pundit::NotAuthorizedError.
How can I use Pundit to control access in my views?
Pundit provides the policy method that you can use in your views to check if the current user is authorized to perform a certain action. The policy method takes a record and returns the policy for that record. You can then call the authorization methods on the policy to check if the user is authorized.
How can I handle Pundit::NotAuthorizedError in my application?
You can handle Pundit::NotAuthorizedError in your application controller by rescuing the exception and redirecting the user to a safe location with a flash message. You can also customize the error message by defining a user_not_authorized method in your application policy.
How can I test my Pundit policies?
You can test your Pundit policies using any Ruby testing framework. Pundit provides a set of matchers that you can use in your tests to check if the policies authorize or deny the actions as expected.
Can I use Pundit with Devise or other authentication systems?
Yes, Pundit works well with any authentication system including Devise. Pundit uses the current_user method to get the user for authorization, so as long as your authentication system provides a current_user method, you can use Pundit.
Can I use Pundit for complex authorization scenarios?
Yes, Pundit is very flexible and can handle complex authorization scenarios. You can define any number of methods in your policy classes to handle different authorization rules. You can also use inheritance and composition to share common rules between policies.
How can I debug my Pundit policies?
You can debug your Pundit policies by raising exceptions in your policy methods and inspecting the error messages. You can also use the Rails console to test your policies interactively.
Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.