Skip to Content
Course content

128: Pundit for Authorization Policies

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Authorization is one of those things that seems trivial when you're starting a project. You've got a user, you've got a resource, and you just need to make sure the user is allowed to touch that resource. In the beginning, the most intuitive place to put that logic is right in the controller action. It feels direct, and for a tiny app, it works.

The sprawl of conditional checks

Imagine we're building a project management tool where users can edit tasks. If you're taking the naive approach, your TasksController probably looks something like this:

def edit
  @task = Task.find(params[:id])
  if current_user.admin? || @task.project.owner == current_user
    render :edit
  else
    redirect_to root_path, alert: "You aren't allowed to do that."
  end
end

def update
  @task = Task.find(params[:id])
  if current_user.admin? || @task.project.owner == current_user
    if @task.update(task_params)
      redirect_to @task
    else
      render :edit
    end
  else
    redirect_to root_path, alert: "You aren't allowed to do that."
  end
end

At first glance, this is clear. But look at the repetition. I've written the same if statement twice. Now, imagine this app grows. You add a destroy action, a change_status action, and maybe a separate ProjectController that needs similar checks. Suddenly, your authorization logic is scattered across a dozen files. If the business requirement changes—say, we now allow "Project Managers" to edit tasks too—you have to hunt down every single one of those if blocks and update them. It's a recipe for a security hole because you'll inevitably miss one.

Moving the logic into Policy objects

This is where Pundit comes in. Instead of asking the controller to figure out if a user can do something, Pundit suggests we move that question into a plain old Ruby object (PORO) called a Policy. The idea is simple: for every model you have, you create a corresponding policy class.

Here is how we'd rewrite that task logic. We create a TaskPolicy that takes a user and the record they are trying to manipulate:

class TaskPolicy < ApplicationPolicy
  def update?
    user.admin? || record.project.owner == user
  end

  def edit?
    update?
  end

  def destroy?
    user.admin?
  end
end

Now, back in the controller, we don't write conditionals. We just call authorize. Pundit looks at the class of the object you passed in (a Task), finds the TaskPolicy, and calls the method that matches the current controller action (update?).

def update
  @task = Task.find(params[:id])
  authorize @task
  
  if @task.update(task_params)
    redirect_to @task
  else
    render :edit
  end
end

I love this approach because the controller is now only responsible for the flow of the request, not the rules of the business. If I need to change who can edit a task, I go to one file—the policy—and I change it once. The controller doesn't even need to know the rules changed; it just knows it asked Pundit and Pundit said "yes" or "no".

The trade-off: Boilerplate vs. Maintainability

To be fair, Pundit adds a bit of overhead. You're creating more files. You're adding a dependency to your Gemfile. If you're building a prototype that will only ever have two users, this is probably overkill. You're adding a layer of indirection that a junior dev might find confusing for the first ten minutes.

But the moment your app has more than three roles or more than five models, that "boilerplate" becomes your safety net. More importantly, these policy objects are incredibly easy to test. Since they're just Ruby classes, you don't have to spin up a full integration test or mock a session to check if an admin can delete a task; you just instantiate the policy with a mock user and a mock task and assert that update? returns true. That kind of test speed is what keeps a project maintainable over years, not weeks.




📋 Practical Task

Implementing a Document Access Policy

You are working on a document sharing app. Currently, the DocumentsController has authorization logic leaked into the show and destroy actions. Your goal is to move this logic into a Pundit policy to clean up the controller.

The Requirements:

  • Any user can show a document if it is marked as public: true.
  • A user can show a document if they are the author of that document.
  • Only the author of the document or a user with the admin role can destroy a document.

Your Task:

  1. Create a DocumentPolicy class.
  2. Implement the show? method based on the public status and authorship rules.
  3. Implement the destroy? method based on the authorship and admin rules.
  4. Update the DocumentsController#show and DocumentsController#destroy actions to use the authorize @document method, removing the existing if/else authorization blocks.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.