How to Integrate Permissions for Editing Posts
This guide will explain how to integrate permissions into a Rails app to ensure posts can only be edited by the user that created the post.
Guide Tasks
  • Read Tutorial

With Pundit installed now we're able to start configuring permissions (also known as policies) for our application to have.

Let's start with a basic use case:

  • A post should only be able to be able to be edited by the user who created the post

I highlighted the terms: post, edited, and user who created, this is because when you're building a permission structure, it's important to make it as straightforward as possible. Authorization features can get messy very quickly, mainly due to developers not spending the time in the beginning to outline exactly how the permission should be configured. It's for this reason why I like using the Pundit gem for integrating authorization, due to its minimal nature.

When it comes to building a permission structure feature, ensuring that you have a comprehensive test suite is very important since it will clearly illustrate what scenarios are covered.

Using Pundit to Integrate an Edit Policy

We already have tests in the spec/features/post_spec.rb file, so we don't have to create new specs. The change we'll be working on is moving authorization away from the User class and instead we'll manage it from various policies.

Let's begin by creating a new file that will store our policies that are specific to posts:

touch app/policies/post_policy.rb

With that file created we can define a class that inherits from the ApplicationPolicy class and we'll integrate a sample method for managing permissions for the posts update action:

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def update?
    record.user_id == user.id
  end
end

In our PostPolicy class we're overriding the update? method originally declared in the ApplicationPolicy since it simply returns false (side note: this means that it will block every update action that it's added to throughout the application. The ApplicationPolicy class is mainly in place to give a structure/naming structure that we can use to override with our own unique requirements).

Our record.user_id == user.id call is simply stating that the only user that should be able to update a post is the user that created the post. If you're wondering where the record and user attributes are coming from, if you look at the ApplicationPolicy class you'll see that they're set as read only attributes and they represent whatever object that we're adding authorization to, such as a post in our app, and then the user. This is a great benefit to using Pundit since it gives us easy access to the items that we want to work with. When we add a TopicPolicy we'll be able to use very similar code to how we're working with posts.

Let's also override the edit? method:

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def edit?
    record.user_id == user.id
  end

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

If you're getting a bad feeling about this code, that's good, it's never a good idea to duplicate code, so let's DRY up our record.user_id == user.id calls and refactor them into their own method:

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def edit?
    user_who_can_access_post
  end

  def update?
    user_who_can_access_post
  end

  def user_who_can_access_post
    record.user_id == user.id
  end
end

This may seem like a counterintuitive refactor since we now have MORE code than we did before. However this is a better approach since:

  1. It removes the code duplication
  2. If we want to add other authorized users in the future, such as editors or admins we can easily make the change in a single method instead of having to make the same change in multiple places.
  3. The main reason I like this refactor is because it's very explicit and it reads better than before. If you took over an application, you may not immediately understand what the code record.user_id == user.id means, however the method user_who_can_access_post clearly describes the intent of the program flow.

Now let's refactor our PostsController, by using Pundit policies we can now get rid of some ugly authorization code in our edit action and then integrate a proper permission structure to the update action (which currently has no protection from unauthorized HTTP requests). The two methods should now look like this:

# app/controllers/topics/posts_controller.rb

  def edit
    authorize @post
  end

  def update
    @post = @topic.posts.find(params[:id])

    authorize @post

    if @post.update(post_params)
      redirect_to topic_post_path(topic_id: @post.topic_id, id: @post), notice: 'Your post was successfully updated.'
    else
      render :edit, notice: 'There was an error processing your request!'
    end
  end

I like this implementation much more than what we had before. If you run the specs you'll see that we have a failure, however it's not with the authorization feature itself, instead it has to do with the test expecting to send a user back to the post page.

large

By default Pundit simply raises an exception when a user attempts to access a page they're not authorized to view, let's test this out in the browser by creating a new user (that doesn't have any posts) and then try to edit a post.

large

So here we need to rescue the Pundit::NotAuthorizedError exception and tell the application how to handle it. We can do this in the ApplicationController like so:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Pundit
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  protect_from_forgery with: :exception

  def current_user
    super || OpenStruct.new(full_name: 'Guest')
  end

  private

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

Now if we navigate back to the website and test it out you'll see we're redirected to the homepage.

large

Let's also refactor our failing test so it expects the unauthorized user to be sent to the homepage instead of the post page:

# spec/features/post_spec.rb:109

    it 'does not allow a user to edit a post they did not create' do
      logout(:user)
      login_as(@second_user, :scope => :user)
      visit edit_topic_post_path(topic_id: @topic.id, id: @post.id)
      expect(current_path).to eq(root_path)
    end

With that change you'll see that all of the specs are back to passing and now our application is blocking unauthorized users from editing posts and our code is more streamlined than it was before.

large

Another benefit to the code we now have implemented is that the old implementation didn't block API requests, which means that hackers may have been able to edit posts using outside services. We blocked users from accessing the edit page on the website, but a HTTP request could have called the application server and passed in data to edit a post that they did not create. With the new policy we're properly blocking both web and HTTP requests.

Also, I hope you appreciated that having the authorization tests in place from earlier in the application build made it possible to be confident that our new permission structure is working properly. Since I knew we were already verifying that only authorized users were able to edit posts in the specs we were able to clean up the implementation with minimal changes to the RSpec tests, which is the sign of a good test suite.

You may have noticed that we're not rendering notifications to users. When a user tries to access a page they don't have authorization for, they should be shown a message such as "You are not authorized to access this page.", however they're simply being redirected. In the next guide we'll implement notifications throughout the application.

Resources