Mastering Authorization in Rails with Pundit
One of the most critical aspects of building a web application is ensuring that users can only access the parts of the application they are authorized to use. Rails, a robust web framework, provides several tools and gems to help developers implement authorization efficiently. One such gem is Pundit, a simple, lightweight authorization library that integrates seamlessly with Rails.
What is Pundit?
Pundit is a Ruby gem that provides a set of helpers and conventions for managing authorization in Rails applications. Unlike other authorization libraries, such as CanCanCan, Pundit takes a different approach by focusing on policy objects. These policy objects encapsulate the authorization logic for a particular resource or model, keeping the code organized and easy to maintain.
Key Features of Pundit:
-
Clarity: Defines authorization logic in clear, concise Ruby classes, making it easier to understand and maintain.
-
Flexibility: Supports various authorization scenarios, including CRUD operations, custom actions, and complex permission structures.
-
Testability: Encourages writing unit tests for policies, ensuring your authorization logic is robust and reliable.
-
Integration: Integrates seamlessly with Rails controllers and views, providing helper methods for convenient authorization checks.
-
Community: Backed by a vibrant community with extensive documentation and resources.
Getting Started with Pundit
Create a new Rails application by executing the below command:
Create a new Rails app and install Pundit
> rails new demo_pundit
Navigate to the project
and
add pundit
gem to the project.
> cd demo_pundit
> bundle add pundit
You need to add Pundit::Authorization
in your application controller.
class ApplicationController < ActionController::Base
include Pundit::Authorization
end
You can quickly run the generator to set up an application policy with pre-configured defaults.
> rails g pundit:install
create app/policies/application_policy.rb
The generator command created an app/policies/
directory with a
file application_policy.rb
inside.
Create model
Let's create a User
model using the
devise
gem.
> bundle add devise
> rails g devise:install
> rails g devise user
> rake db:migrate
Adding policies
When we executed the rails g pundit:install
command,
application_policy.rb
was created.
It contains the below code:
# frozen_string_literal: true
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
@user = user
@record = record
end
def index?
false
end
def show?
false
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
raise NotImplementedError, "You must define #resolve in #{self.class}"
end
private
attr_reader :user, :scope
end
end
The model object is called a record
in the above ApplicationPolicy
file.
Pundit's ApplicationPolicy
defines
core authorization principles for your entire Rails application.
It is the starting point for all other policy classes,
offering several benefits like Default Permissions
,
Inheritance
,
Scopes
and
Flexibility
.
The most common
and
generic permissions can be defined in the ApplicationPolicy
.
You can override methods in specific policies to
have granular control over models
or
actions.
To have more control over User creation,
updation
and
deletion,
you can create a UserPolicy
using the pundit generator as below:
> rails generate pundit:policy User
create app/policies/user_policy.rb
invoke test_unit
create test/policies/user_policy_test.rb
You want to ensure only admins in your application
can create new users
and
destroy existing ones.
You can modify the method create?
and
destroy?
in the user_policy.rb
file.
class UserPolicy < ApplicationPolicy
def create?
user.admin?
end
def destroy?
user.admin?
end
end
To access these checks in your controller,
you need to add the authorize
method in your respective action
as follows:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
authorize @user
end
def destroy
@user = User.find(params[:id])
authorize @user
end
end
Behind the scenes,
authorize
simplifies authorization.
It automatically assumes a User
model has a corresponding UserPolicy
class.
It then instantiates the policy with the current user
and
the specific User
object.
It leverages the action name (e.g., create
) to call the
appropriate policy method create?
.
If the action name does not match the policy function name,
you can pass an additional argument to the authorize
method.
def deactivate
@user = User.find(params[:id])
authorize @user, :update?
@user.deactivate!
redirect_to @user
end
When the first argument to authorize isn't an object, you can pass the model class directly.
# app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
def admins?
user.admin?
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def admins
authorize User
end
end
Using policy in views
You can access a policy instance in both views
and
controllers using the policy
method.
This feature is precious for conditionally displaying links
or
buttons in the view:
<% if policy(@user).update? %>
<%= link_to "Edit User", edit_user_path(@user) %>
<% end %>
Scopes
Pundit does not have built-in support for defining scopes within policy files. However, you can still use Pundit in conjunction with ActiveRecord scopes to achieve the desired behaviour.
Let's say you add a Post
model to your Rails application.
A user can create
or
publish many posts.
You can restrict access to the posts in your system
by adding a Scope class in Pundit.
Here's a basic example of
how you might use a scope in combination with Pundit.
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def index?
true
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
if user.admin?
scope.all
else
scope.where(user_id: user.id)
end
end
end
end
In this example,
the Scope
class within the PostPolicy
defines a resolve method,
which returns a scoped relation based on the user's role.
If the user is an admin
,
they have access to all posts (scope.all
);
otherwise,
they only have access to posts that belong to them (scope.where(user_id: user.id)
).
In your controller,
you would use the policy_scope
method to apply the scope defined in your policy:
class PostsController < ApplicationController
def index
@posts = policy_scope(Post)
end
end
The policy_scope(Post)
call in the controller will apply the
scope defined in the PostPolicy::Scope
class to the Post
model,
ensuring that only authorized records are returned.
This approach allows you to use Pundit for authorization logic and ActiveRecord scopes for record-level restrictions, providing a flexible and powerful way to manage access control in your Rails application.
Verifying authorization policy and scope coverage
Unaddressed authorization checks in Pundit-powered applications can create security vulnerabilities. This emphasizes the importance of thoroughness from a security standpoint.
Fortunately,
Pundit includes a helpful feature that serves as a reminder
in case you overlook authorization.
Pundit keeps track of whether you have invoked authorize
within your controller action.
Additionally,
Pundit adds a method called verify_authorized
to your controllers.
This method will raise an exception if authorize
has not been called.
To ensure you remember to authorize
the action,
you should invoke this method in an after_action
hook, as shown.
class ApplicationController < ActionController::Base
include Pundit::Authorization
after_action :verify_authorized
end
Similarly,
Pundit also introduces verify_policy_scoped
to your controller.
This method functions similarly to verify_authorized
but monitors the use of policy_scope
instead of authorize
.
This is particularly valuable for controller actions such as index
,
which retrieve collections with a scope
and
do not authorize individual instances.
class ApplicationController < ActionController::Base
include Pundit::Authorization
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
end
Strong parameters
In Rails,
the controller manages mass-assignment protection.
However,
with Pundit,
you can determine which attributes a user can update by
defining rules in your policies.
To achieve this,
you can create a permitted_attributes
method in your policy.
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.author_of?(record)
[:title, :body, :categories]
else
[:categories]
end
end
end
You need to call the permitted_attributes
of the PostPolicy
in your controller as below:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
@post = Post.find(params[:id])
@post.update(post_params)
end
private
def post_params
params.
require(:post).
permit(policy(@post).permitted_attributes)
end
end
Benefits of Using Pundit:
- Improved Code Quality: Pundit's object-oriented approach leads to cleaner, more maintainable code.
- Enhanced Security: Explicitly defining authorization logic reduces the risk of security vulnerabilities.
- Increased Developer Productivity: Clear policies and helper methods simplify authorization checks.
- Scalability: Pundit adapts well to complex applications with evolving authorization requirements.
Conclusion
Pundit is a powerful and flexible authorization library for Rails that simplifies the process of implementing authorization logic in your application. By using policy objects and conventions, Pundit helps you keep your authorization code organized and maintainable. Whether you're building a small web application or a large-scale platform, Pundit can help you manage authorization effectively and securely.