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.

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
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required