Adding the Pundit Gem

What does the Pundit gem do?

The Pundit gem is a Ruby library that is used for authorization in Ruby on Rails applications. It provides a set of tools for defining permissions and access control in your application.

Here’s how it works:

  • Policies: Pundit uses plain old Ruby objects, called “policies,” to define what actions a particular user can perform on a given resource. Each resource typically has its own policy class, and each action (like create, update, or destroy) is represented as a method within that class.
  • Scopes: Pundit also provides a mechanism for scoping queries to the database. For instance, if you have a list of posts, and a user should only see the ones they’ve authored, you can define a scope in the policy that filters the list accordingly.
  • Integration with Controllers: Pundit provides helper methods for use in controllers to enforce the policies that you have defined. It does this through methods like authorize and policy_scope.
  • Error Handling: If a user attempts an action they’re not authorized to perform, Pundit will raise a Pundit::NotAuthorizedError, which you can then handle in your application.

In essence, Pundit provides a flexible and straightforward way to add complex authorization rules to your Rails applications. It’s particularly suited to applications with complex permissions logic or multi-tenancy requirements.


How do I set Pundit up?

Here are the general steps you’d follow to set up Pundit in a Rails application:

  1. Add the Pundit gem to your Gemfile:

You can add the Pundit gem to your Gemfile like so:

gem 'pundit'

After adding the gem, you’ll need to run the bundle install command to install it.

  1. Include Pundit in your ApplicationController:

Next, you’ll need to include the Pundit module in your ApplicationController. This will make Pundit’s methods available to all of your controllers.

class ApplicationController < ActionController::Base
  include Pundit
end
  1. Create a Policy:

Now you can start creating policies for your models. Policies are just plain old Ruby objects, and they live in the app/policies directory. You can create a policy manually, or you can generate one with Rails’ generator.

For example, if you wanted to create a policy for a User model, you could run:

rails g pundit:policy User

This would generate a file at app/policies/post_policy.rb with a class PostPolicy.

You can then define methods in this class for each action you want to authorize. By convention, these methods should be named after the corresponding controller action, and should return true if the action is authorized and false otherwise. Here’s an example:

class PostPolicy < ApplicationPolicy
  def update?
    # Only allow the post's author to update it
    record.user == user
  end
end

In this example, record represents the Post instance that the user wants to update, and user represents the currently logged-in user.

  1. Use the Policy in your Controller:

Now you can use the authorize method provided by Pundit in your controller actions to enforce the policy you’ve defined. Here’s how you might use it in the PostsController:

class PostsController < ApplicationController
  def update
    @post = Post.find(params[:id])
    authorize @post
    # rest of the action...
  end
end

The authorize method will raise a Pundit::NotAuthorizedError if the policy method returns false.

  1. Handle Unauthorized Access:

You’ll probably want to rescue Pundit::NotAuthorizedError in your ApplicationController and display a helpful message to the user. Here’s one way you might do it:

class ApplicationController < ActionController::Base
  include Pundit

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    flash[:alert] = "You are not authorized to perform this action."
    redirect_to(request.referrer || root_path)
  end
end

This is just a simple introduction. Pundit has many more features, like policy scopes for controlling what objects a user can see. You should check out the Pundit documentation to learn more.


My question: When I ran #3 I got something unexpected. Could you explain this?

jg-work-computer@Jeremy-Work-MacBook-Pro affyex-devise-users % rails g pundit:policy User
      create  app/policies/user_policy.rb
      invoke  rspec
/Users/jg-work-computer/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/pundit-2.3.0/lib/generators/rspec/templates/policy_spec.rb:1:in `template': undefined method `exists?' for File:Class (NoMethodError)

@output_buffer = ''.dup; @output_buffer.concat "require '".freeze; @output_buffer.concat(( File.exists?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' ).to_s); @output_buffer.concat "'\n\nRSpec.describe ".freeze
                                                                                               ^^^^^^^^
Did you mean?  exist?

ChatGPT Response

The error you’re encountering appears to be a bug with the Pundit gem’s integration with RSpec, specifically in the policy generator. It seems the generator is using File.exists? which is deprecated in Ruby and this method has been removed in Ruby 3.1 and later versions, causing the NoMethodError. The correct method to use is File.exist? (without the ‘s’).

As of my knowledge cutoff in September 2021, Pundit version 2.3.0 was the latest. If the bug has been fixed in a later version, you can update your Pundit gem.

If the bug still persists in the latest version, you can manually create the policy and policy spec files. Here’s an example of what you might write in the policy spec file (spec/policies/user_policy_spec.rb):

require 'rails_helper'

RSpec.describe UserPolicy do
  let(:user) { User.new }

  subject { described_class }

  permissions :update? do
    it "denies access if user is not admin" do
      expect(subject).not_to permit(User.new, User.new)
    end

    it "grants access if user is an admin" do
      expect(subject).to permit(User.new(admin: true), User.new)
    end
  end
end

This is just an example, of course. You’d need to adjust it to match the actual behavior of your UserPolicy class.

It would also be beneficial to report the bug to the maintainers of the Pundit gem. You can do this by opening an issue on the Pundit GitHub page. Be sure to include the version of Ruby and Pundit you’re using, as well as the error message you’re getting.


Can you tell me what is wrong here? And how to fix it?

NameError in UsersController#edit
uninitialized constant ApplicationPolicy
Did you mean?
ApplicationJob
ApplicationHelper
Extracted source (around line #1):
1
2
3
4
5
6

class UserPolicy < ApplicationPolicy

  def show?
    # Only allow the user to see their own profile
    #record == user
  end

ChatGPT Response

The error message “uninitialized constant ApplicationPolicy” indicates that the ApplicationPolicy class hasn’t been defined, but you’re trying to inherit from it in your UserPolicy class.

In a typical Rails application with Pundit, there’s a base ApplicationPolicy class that other policy classes inherit from. The ApplicationPolicy provides a basic structure and default behaviors for your policy classes.

You can create an ApplicationPolicy with the following code:

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end
  end
end

Place this code in a file named application_policy.rb in the app/policies directory of your Rails application.

This default ApplicationPolicy:

Provides an initializer that takes a user and a record. These are used in the policy methods to determine permissions.
Defines a Scope class that can be used for scoping database queries according to the policy.
After creating the ApplicationPolicy, your UserPolicy should work as expected (assuming it’s correctly written and being used properly).

And remember, it’s important to override the resolve method in the Scope class on your UserPolicy if you want to scope the query for users (the current implementation of resolve in ApplicationPolicy returns all records).

Could you explain what @record is?

ChatGPT – Sure, in the context of Pundit policies, @record refers to the instance of the model that you’re checking permissions for.

Here’s how it works:

When you call the authorize method in a controller, you pass in an instance of a model. For example:

def update
  @post = Post.find(params[:id])
  authorize @post
  # ...
end

In this case, @post is an instance of the Post model that the user wants to update.

The authorize method then instantiates the appropriate policy class, passing the current user and the instance you passed in to the policy’s initialize method. This is where @user and @record get set:

def initialize(user, record)
  @user = user
  @record = record
end

When you define a method like update? in the policy, you can use @record to refer to the model instance that you’re checking permissions for.

So in the context of a UserPolicy, @record would be the user that the current user (represented by @user) wants to update or view.

In a PostPolicy, @record would be the post that the current user wants to create, update, view, or delete. And so on for each model in your application.

It’s also worth noting that @record doesn’t have to be a single instance of a model. If you’re authorizing a class-level action (like new? or create?), @record could be the class itself. Similarly, if you’re authorizing an action on a collection of records, @record could be an ActiveRecord relation or other enumerable object.


Can Pundit eliminate buttons if the user does not have permission to view those pages?

ChatGPT-4 answer about Pundit show/hide UI

Yes, Pundit can be used to show or hide certain parts of the UI based on the user’s permissions. You can use Pundit’s policy method in your views to achieve this. The policy method will instantiate the appropriate policy class for a given record, and you can call any of the policy’s methods on the resulting object.

Here’s how you might adjust your “Edit” and “Delete” links to only show up when the current user is allowed to update or destroy the given user, respectively:

<% @users.each do |user| %>
  <!-- ... -->
  <td>
    <%= link_to "Show", user_path(user), class: "btn btn-sm btn-outline-primary me-1" %>

    <% if policy(user).update? %>
      <%= link_to "Edit", edit_user_path(user), class: "btn btn-sm btn-outline-secondary me-1" %>
    <% end %>

    <% if policy(user).destroy? %>
      <%= link_to "Delete", 
                  user_path(user), 
                  method: :delete, 
                  "data-turbo-method": "delete",
                  "data-turbo-confirm": { confirm: "Are you sure?", turbo: false }, 
                  class: "btn btn-sm btn-outline-danger" %>
    <% end %>
  </td>
  <!-- ... -->
<% end %>

In this case, policy(user).update? and policy(user).destroy? will call the update? and destroy? methods of UserPolicy for the given user, returning true or false depending on whether the current user is allowed to perform the given action. If the method returns true, the corresponding link will be shown; if it returns false, the link will be hidden.

Please ensure that you have correctly defined the update? and destroy? methods in your UserPolicy class to reflect the permissions you want to enforce.

Find it here: Pundit Gem