Mastering Authorization in Rails with Pundit

railsFebruary 27, 2024Dotby Alkesh Ghorpade

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.

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right