A Practical Guide to Rails Authorization: Master Pundit in 10 Minutes

11 views 0 likes 0 comments 18 minutesOriginalTutorial

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.

#Rails # Ruby # Pundit # Authorization # Backend Development # Practical Guide
A Practical Guide to Rails Authorization: Master Pundit in 10 Minutes

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_user in 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 ApplicationPolicy upfront. 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:

  • user and record are automatically injected by ApplicationPolicy, 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 true or false. 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:

  1. Infer PostPolicy from the Post model.
  2. Automatically pass current_user and @post to the policy.
  3. Call the corresponding xxx? method based on the action name.
  4. Raise Pundit::NotAuthorizedError if 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:

  1. Writing Tests: Use spec/policies/post_policy_spec.rb with RSpec matchers to verify role-based edge cases.
  2. Namespaced Policies: For admin panels, use authorize([:admin, @post]) to route to Admin::PostPolicy, keeping frontend and backend permissions decoupled.
  3. 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.

Last Updated:2026-07-06 10:06:24

Comments (0)

Post Comment

Loading...
0/500
Loading comments...