Complete Guide To Managing User Permissions In Rails Apps

honeybadger_staff

Honeybadger Staff

Posted on April 14, 2022

Complete Guide To Managing User Permissions In Rails Apps

This article was originally written by Renata Marques on the Honeybadger Developer Blog.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Our controller with the policy object looks like this:

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
honeybadger_staff
Honeybadger Staff

Posted on April 14, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related