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_punditNavigate to the project
and
add pundit gem to the project.
> cd demo_pundit
> bundle add punditYou need to add Pundit::Authorization in your application controller.
class ApplicationController < ActionController::Base
include Pundit::Authorization
endYou can quickly run the generator to set up an application policy with pre-configured defaults.
> rails g pundit:install
create app/policies/application_policy.rbThe 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:migrateAdding 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
endThe 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.rbYou 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
endTo 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
endBehind 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
endWhen 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
endUsing 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
endIn 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
endThe 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
endSimilarly,
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
endStrong 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
endYou 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
endBenefits 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.