Creating a Custom Permission Structure with Pundit
In this guide, we are going to implement a custom policy to start building out our app's permission structure.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

In this guide, we are going to implement a custom policy to start building out our app's permission structure.

To start, let's create a file called post_policy.rb in app/policies.

In this file, we want our PostPolicy class to inherit from ApplicationPolicy. By default the ApplicationPolicy has defined a number of methods. To begin our PostPolicy will override the update method.

Before we build this feature, if you open the application_policy.rb file you'll see two variables at the top called user and record. You'll have access to these variables in the inherited classes that we'll create for each site component that we want to add authorization to. User refers to the current user that is attempting to access the site component and record refers to the current record that is being edited, which in this case, is the post.

Now, in post_policy.rb we want to define a rule that states that only the user who created a record to edit it. As for admins, we want them to be able to approve or reject a post.

First we are going to check if the owner of a record is the current user.

# app/policies/post_policy.rb

def update?
  record.user_id == user.id
end

Next, open post_controller.rb, and update the edit and update methods.

# app/controllers/posts_controller.rb

def edit
  authorize @post
end

def update
  authorize @post

  if @post.update(post_params)
    redirect_to @post, notice: 'Your post was edited successfully'
  else
    render :edit
  end
end

This will authorize the record, and allow the user that created the record to edit it.

Let's see how it all works in the browser. After creating a new record and clicking on the edit link, I get an error.

large

This is because I didn't include pundit in our application controller. In order for helper methods like authorize to work, we need to include Pundit in our application so that all of the controllers have access to the methods. Let's open our application_controller.rb and update it to include a call to the Pundit module:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Pundit

  protect_from_forgery with: :exception
  before_action :authenticate_user!
end

Now, if you go back to the browser and try to edit the post the page should open properly. You can edit and make changes to the record.

Next, if I try to edit an existing record that was created by a different user, the browser throws an error:

large

That's not the behavior that we're looking for since we are preventing an admin user from accessing a post. We want admin users to be able to access the page and edit the status. To do that, let's update the code in our post_policy.rb file.

# app/policies/post_policy.rb

def update?
  record.user_id == user.id || user.type == 'AdminUser'
end

In this code, we are allowing admin users as well as the owner of the record to update a record using an or statement.

If you open up the browser you'll see that it's working properly.

Let's check this functionality with a regular user. Everything is fine.

Before we close this out let's perform a small refactor, I don't like to hard code "AdminUser" into the query. Let's reuse the code we implemented earlier in our admin dashboard's application_controller.rb file.

First let's create a method where we can list all of the potential admin types in an array and place it in the ApplicationPolicy parent class (this way we can use this list in any other policies, eventually we'll want to create a single place that lists out potential admin types for the entire application).

# app/policies/application_policy.rb

def admin_types
  ['AdminUser']
end

Now let's update the update? method in the PostPolicy:

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def update?
    record.user_id == user.id || admin_types.include?(user.type)
  end
end

If we check this in the browser, it works!

Resources