Complete Guide To Managing User Permissions In Rails Apps

There are many excellent authorization libraries for Rails, but did you know it's not that hard to build this functionality yourself? In this article, Renata Marques shows us how to use the Policy Object Pattern to implement access control in our Rails apps.

A common requirement of web applications is the ability to assign specific roles and permissions.

Many types of web applications distinguish between admins and regular users in providing restricted access. This is often performed using a simple boolean that determines whether the user is an admin. However, roles and permissions can become much more complex.

The value of your application lies in restricting access to certain data and actions. It's definitely something you don't want to mess up. In this post, we will explain how to implement roles and permissions in a basic Ruby on Rails application.

Do I need a gem to manage permissions?

No, you don't need a gem, especially if your application is small and you want to avoid adding more dependencies to your code base. However, if you are looking for alternatives, here are the most popular gems that deal with roles and permissions:

  • Devise Devise is a gem for authentication and roles management, and it is a really complex and robust solution. With 21.7k stars on GitHub, it is the most popular repo in this post, but it does more than roles management. It is known as an authentication solution, so only apply it to your codebase if you need a very robust library.

  • Pundit: Pundit is a gem that uses simple Ruby objects, and it is probably the simplest policy gem we will cover. Is simple to use, has minimal authorization, and is similar to using pure Ruby. With 7.3k stars on GitHub, it is currently the most popular policy gem.

  • CanCan: CanCan is an authorization library that restricts the resources a given user is allowed to access. However, CanCan has been abandoned for years and only works with Rails 3 and earlier releases.

  • CanCanCan: CanCanCan is another authorization library for Ruby and Ruby on Rails. It is an alternative to CanCan and is currently being maintained. With 4.9k stars on GitHub, it is the least popular, but it works pretty well and is well maintained.

All of these gems are great, but it's not too hard to build permissions yourself in plain Ruby. I will show you how to manage permissions without a gem, using a strategy called policy object pattern.

Policy object pattern

Policy Object is a design pattern used to deal with permissions and roles. You can use it each time you have to check whether something or someone is allowed to perform an action. It encapsulates complex business rules and can easily be replaced by other policy objects with different rules. All the external dependencies are injected into the policy object, encapsulating the permission check logic, which results in a clean controller and model. Gems like Pundit, Cancan, and Cancancan implement this pattern.

Pure policy object rules

  • The return has to be a boolean value
  • The logic has to be simple
  • Inside the method, we should only call methods on the passed objects

Implementation

Let's start with the naming convention; the filename has the _policy suffix applied and the class and policy at the end. In this method, names always end with the ? character (e.g.,UsersPolicy#allowed?).

Here is some example code:

class UsersPolicy
  def initialize(user)
    @user = user
  end

  def allowed?
    admin? || editor?
  end

  def editor?
    @user.where(editor: true)
  end

  def admin?
    @user.where(admin: true)
  end
end

In which scenarios should I use them?

When your app has more than one type of restricted access and restricted actions. For example, posts can be created with the following:

  • at least one tag,
  • a restriction that only admins and editors can create them, and
  • a requirement that editors need to be verified.

Here’s an example controller without a policy object:

class PostsController < ApplicationController
  def create
    if @post.tag_ids.size > 0
    && (current_user.role == ‘admin’
    || (current_user.role == ‘editor’ && current_user.verified_email))
      # create
    end
  end
end

Because the above condition checks are long, ugly, and unreadable, the policy object pattern should be applied.

Let’s begin by creating PostsCreationPolicy.

class PostsCreationPolicy
  attr_reader :user, :post

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

  def self.create?(user, post)
    new(user, post).create?
  end

  def create?
    with_tags? && author_is_allowed?
  end

  private

  def with_tags?
    post.tag_ids.size > 0
  end

  def author_is_allowed?
    is_admin? || editor_is_verified?
  end

  def is_admin?
    user.role == ‘admin’
  end

  def editor_is_verified?
    user.role == ‘editor` && user.verified_email
  end
end

Our controller with the policy object looks like this:

class PostsController < ApplicationController
  def create
    if PostsCreationPolicy.create?(current_user, @post)
      # create
    end
  end
end

How to use policy objects in Rails

Create a policy directory inside app /policies and place all of your policy classes there. When you need to call the controller, you can do so directly in an action or use a before_action:

class PostsController < ApplicationController
  before_action :authorized?, only: [:edit, :create, :update, :destroy]

  def authorized?
    unless ::PostsCreationPolicy.create?(current_user, @post)
      render :file => "public/404.html", :status => :unauthorized
    end
  end
end

How to test policy objects

It's simple to test the behavior in the controller:

require 'rails_helper'

RSpec.describe "/posts", type: :request do
  describe "when user is not allowed" do
    let(:user_not_allowed) { create(:user, admin: false, editor: false) }
    let(:tag) { create(:tag) }
    let(:valid_attributes) { attributes_for(:post, tag_id: tag.id) }

    before do
      sign_in user_not_allowed
    end

    describe "GET /index" do
      it "return code 401" do
        diet = Post.create! valid_attributes
        get edit_post_url(post)
        expect(response).to have_http_status(401)
      end
    end
  end
end

Testing the policy is simple, too; we have a lots of small methods with only one responsibility.

require 'rails_helper'

RSpec.describe PostsCreationPolicy do
  describe "when user is not allowed" do
    let(:user) { create(:user, editor: false, admin: false) }
    let(:user_editor) { create(:user, editor: true, email: verified) }
    let(:tag) { create(:tag) }
    let(:post) { create(:post, tag_id: tag.id) }

    describe ".create?" do
      context "when user is allowed" do
        it "creates a new post" do
          expect(described_class.create?(user_editor, post)).to eq(true)
        end
      end

      context "when user is not allowed" do
        it "does not create a new post" do
          expected(described_class.create?(user, post)).to eq(false)
        end
      end
    end

    # ...more test cases
  end
end

We test whether the object is allowed to be created in every scenario.

Conclusion

The policy pattern concept is small but produces big results. Consider applying a policy object each time you have to deal with simple or complex permissions. When it comes to testing with RSpec, you don’t need to use database records; your policies are purely Ruby objects, and your testing will be simple and fast.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Renata Marques

    Renata is a software engineer and computer scientist focused on Ruby and Javascript. She enjoys helping the developer community by contributing to open source projects. In her free time she likes watching and discussing movies and TV shows, reading books, listening to music, photography, and making videos.

    More articles by Renata Marques
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial