A Practical Guide to Rails Authorization: Master Pundit in 10 Minutes
A step-by-step tutorial on implementing production-grade authorization in Ruby on Rails using Pundit. Learn how to set up Policies for role-based access control, use Scopes for data filtering, and configure automated authorization verification to secure your application efficiently.

A Practical Guide to Rails Authorization: Master Pundit in 10 Minutes
Author's perspective: 8 years in Java backend, now working with Rails.
If you've built Rails applications, you've likely encountered this scenario: an "Article Management" module where admins can edit or delete any article, regular users can only modify their own drafts, and guests can only view public posts. Initially, you might scatter if current_user.admin? checks across your controllers. It works at first, but as your business logic evolves, authorization rules become fragmented across multiple files. Modifying a single permission requires tracing through several files, and writing tests becomes a nightmare.
Today, I'll show you how to clean up this mess using Pundit. With over 8.5k stars on GitHub, Pundit is one of the most popular authorization libraries in the Ruby/Rails ecosystem. Its core philosophy is brilliantly simple: map each model to a corresponding Policy class, and centralize all authorization logic in these plain Ruby classes. By the end of this guide, you'll be able to independently set up Pundit, define roles, filter data with Scopes, and implement automated verification for a production-ready authorization system.
1. Prerequisites
- Ruby 3.1+ and Rails 7.x
- A basic authentication system (e.g., Devise) that provides
current_userin your controllers. - Familiarity with Rails MVC architecture.
No metaprogramming or complex DSLs are required. Pundit embraces "Plain Old Ruby" — if you can read a standard Ruby class, you can use Pundit.
2. Quick Start: Get Up and Running in 3 Steps
Step 1: Installation
Add Pundit to your Gemfile or run the following command in your terminal:
sh
bundle add pundit
Step 2: Include the Authorization Module
In app/controllers/application_controller.rb, include the Pundit module:
ruby
class ApplicationController < ActionController::Base
include Pundit::Authorization
end
Why do this? Core methods like authorize and policy_scope reside in this module. Including it in ApplicationController makes them automatically available to all child controllers.
Step 3: Generate the Base Policy
Run the generator to create the foundational policy files:
sh
rails g pundit:install
This generates application_policy.rb inside app/policies/, providing a base ApplicationPolicy and ApplicationPolicy::Scope. Restart your Rails server after generation, otherwise Rails won't autoload the new directory.
💡 Pro Tip: If your project has many models, customize the generated
ApplicationPolicyupfront. Place common logic (like login checks or default-deny rules) in the base class. All subsequent policies will simply inherit from it.
3. Hands-On: Building a Blog Authorization System
Let's assume a standard Post model with three distinct role-based permissions:
| Role | View | Edit | Delete | View Index |
|---|---|---|---|---|
| Admin | All posts | All posts | All posts | All posts |
| Author | Own + Public | Own drafts | Own drafts | Own posts |
| Guest | Public only | ❌ | ❌ | Public only |
3.1 Writing Your First Policy
Create app/policies/post_policy.rb:
ruby
class PostPolicy < ApplicationPolicy
def show?
user.nil? ? record.public? : user.admin? || record.public? || user == record.author
end
def update?
user&.admin? || user == record.author
end
def destroy?
user&.admin?
end
end
Key Concepts:
userandrecordare automatically injected byApplicationPolicy, representing the current user and the target model instance, respectively.- Methods ending in
?(e.g.,show?,update?) automatically map to corresponding controller actions. - Simply return
trueorfalse. No need to raise exceptions or write guard clauses here.
3.2 Integrating into Controllers
ruby
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
authorize @post # Automatically calls PostPolicy#show?
end
def update
@post = Post.find(params[:id])
authorize @post # Automatically calls PostPolicy#update?
if @post.update(post_params)
redirect_to @post
else
render :edit
end
end
def destroy
@post = Post.find(params[:id])
authorize @post
@post.destroy
redirect_to posts_path
end
private
def post_params
params.require(:post).permit(:title, :body, :status)
end
end
See how clean it is? A single line authorize @post in the controller triggers Pundit to:
- Infer
PostPolicyfrom thePostmodel. - Automatically pass
current_userand@postto the policy. - Call the corresponding
xxx?method based on the action name. - Raise
Pundit::NotAuthorizedErrorif the check fails.
3.3 Using Scopes for Data Filtering
If you use Post.all for your index page, guests will see restricted data. Pundit's Scope solves this elegantly:
ruby
class PostPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
return scope.all if user&.admin?
if user
scope.where(author: user).or(scope.where(public: true))
else
scope.where(public: true)
end
end
end
# ... previous methods remain unchanged
end
Use it in your controller like this:
ruby
def index
@posts = policy_scope(Post) # Automatically calls PostPolicy::Scope#resolve
end
How Scopes Work: They consolidate "who sees what data" logic into the Policy class. The policy_scope method in the controller returns a filtered query result. This ensures list views, search results, and other read operations all share the same authorization rules.
3.4 Preventing Missed Authorizations
The biggest headache in authorization is forgetting to add authorize to a new action. Pundit provides built-in verification to catch this:
ruby
class ApplicationController < ActionController::Base
include Pundit::Authorization
after_action :verify_pundit_authorization
private
def verify_pundit_authorization
if action_name == "index"
verify_policy_scoped
else
verify_authorized
end
end
end
During development, if you forget to call authorize or policy_scope in an action, Rails will raise an error. This acts as a safety net before deployment.
4. FAQs & Common Pitfalls
Q1: What if my app doesn't use current_user?
If your authentication system uses Current.user or another variable, simply override pundit_user in ApplicationController:
ruby
def pundit_user
Current.user
end
Pundit will use this method instead of the default current_user.
Q2: How do I show user-friendly error messages on authorization failure?
Rescue Pundit::NotAuthorizedError in ApplicationController and redirect with a flash message:
ruby
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_back_or_to(root_path)
end
Q3: What if a single Policy class becomes too bloated?
Extract shared logic into modules and include them where needed. Since Pundit relies on plain Ruby classes, you can leverage standard DRY practices like inheritance, composition, or alias_method.
Q4: Can I integrate Pundit with Strong Parameters to control permitted fields?
Yes. You can use policy methods to dynamically define permitted parameters:
ruby
## Example controller usage (conceptual)
def post_params
params.require(:post).permit(*expected_attributes_for(@post))
end
Note: Pundit itself doesn't enforce parameter filtering out of the box, but you can easily bridge it with Strong Parameters by querying the policy.
5. Next Steps & Conclusion
We've successfully built the foundation of a robust authorization system. To take it further, consider:
- Writing Tests: Use
spec/policies/post_policy_spec.rbwith RSpec matchers to verify role-based edge cases. - Namespaced Policies: For admin panels, use
authorize([:admin, @post])to route toAdmin::PostPolicy, keeping frontend and backend permissions decoupled. - I18n Integration: Localize authorization error messages for better maintainability in multi-language apps.
Pundit's philosophy is "do the least, then let you run free." It avoids magical DSLs and doesn't dictate your permission model — it simply helps you organize authorization logic in clean, testable Ruby classes. Once you master the interplay between Policies and Scopes, you'll find that authorization stops being a maintenance burden, regardless of how complex your business rules become.
Have a specific scenario you're struggling to model? Drop it in the comments, and let's discuss.