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
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.
Top comments (0)