cult3

Implementing Roles and Permissions in Ruby on Rails

Jan 20, 2016

Table of contents:

  1. The scenario we will be looking at
  2. Adding the Role model
  3. Creating the Assignment model
  4. Adding Pundit
  5. Writing Policies
  6. Conclusion

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

For example, many types of web application will have a distinction between admins and regular users. This can often be dealt with as a simple boolean on the user record.

But the granularity of roles and permissions can be much greater than this.

If access control is important to your application, it’s not something you want to mess up.

Your users are relying on you to restrict access to certain data and actions because that is where the value of your application lies.

In today’s tutorial we will be looking at implementing roles and permissions in a typical Ruby on Rails application.

The scenario we will be looking at

As I mentioned in the introduction to this post, there are a few different ways of dealing with roles and permissions in a web application.

For this application I’m going to be building a way for a user to have many roles within the application.

Each role will have slightly different permissions for the various resources of the application.

For example, an admin would be able to do everything, a member will have restricted access, and a guest will have read only access.

With that mapped out, lets take a look at what we need to build.

Adding the Role model

First we need to create the Role model that will for representing a role within the application.

The first thing we can do is to use the Rails generator to create the model:

bin/rails g model Role name:string

As you can see, this is a fairly simple model as a role only needs to have a name.

A role should always have a name and the name should always be unique, so we can add the following validations:

class Role < ActiveRecord::Base
  validates :name, presence: true, uniqueness: true
end

Finally we can add some simple tests to assert that this is working as it should:

class RoleTest < ActiveSupport::TestCase
  should validate_presence_of(:name)
  should validate_uniqueness_of(:name)
end

Creating the Assignment model

Next we need to create the Assignment model that will associate a Role to a User. This is your classic “has many through” relationship.

Once again we can use the Rails generator to generate the model:

bin/rails g model Assignment user:references role:references

Running this command should automatically generate the following Assignment model:

class Assignment < ActiveRecord::Base
  belongs_to :user
  belongs_to :role
end

We can add the following tests to the AssignmentTest class that was generated:

class AssignmentTest < ActiveSupport::TestCase
  should belong_to(:user)
  should belong_to(:role)
end

Next we can implement the relationships in the User and Role models and tests.

The User model will look like this:

class User < ActiveRecord::Base
  has_secure_password

  has_many :assignments
  has_many :roles, through: :assignments
end

And we can add the following tests:

class UserTest < ActiveSupport::TestCase
  should have_many(:assignments)
  should have_many(:roles).through(:assignments)
end

And the Role model should look like this:

class Role < ActiveRecord::Base
  has_many :assignments
  has_many :users, through: :assignments

  validates :name, presence: true, uniqueness: true
end

And we can add the following tests:

class RoleTest < ActiveSupport::TestCase
  should have_many(:assignments)
  should have_many(:users).through(:assignments)

  should validate_presence_of(:name)
  should validate_uniqueness_of(:name)
end

Finally we can add a role? method to the User class for checking to see if the user has a particular role:

def role?(role)
  roles.any? { |r| r.name.underscore.to_sym == role }
end

Here I’m checking to see if the given role matches any of the user’s roles.

Here is the test to assert that this is working as it should:

test 'user should have role' do
  assert_not(@subject.role? :admin)

  @subject.roles << Role.new(name: 'admin')

  assert(@subject.role? :admin)
end

Adding Pundit

Now that we our models set up for users, roles, and assignments, we need a way of defining “policies” for determining what should happen when a user tries to access a given resource.

For example, we will have a Article resource, and so we need to have a matching ArticlePolicy to determine what should happen when a user tries to perform an action on that resource.

Instead of reinventing the wheel, we can use a tried and tested Open Source gem called Pundit.

Pundit allows you to define and enforce policies for your resources using simple Ruby objects.

To install Pundit, add the following line to your Gemfile:

gem 'pundit'

And then run the following command in Terminal:

bundle install

Finally you can run the following command to generate the base policy:

bin/rails g pundit:install

This will create a new directory under app called policies where you can store your Policies.

Writing Policies

With Pundit installed, we can now start defining the policies for the application:

class ArticlePolicy < ApplicationPolicy
  def update?
    user.role? :admin or not record.published?
  end
end

Each Policy is instantiated with an instance of the current user and the resource that we’re checking against.

By inheriting from the ApplicationPolicy we can skip the boiler plate. The resource object is named record by default.

You can define the rules for each action of that resource. For example, here I’m only allowing admins to update articles if they have already been published

Now you can prevent users from taking certain actions or from being able to access certain data based upon their role. I’ll not go through using Pundit in your Controllers or Views as there is already a lot of documentation on using the gem on the Pundit Github page.

Conclusion

Roles and permissions is an important concept in a wide variety of web applications.

All most all business oriented applications will have some sort of roles and permissions requirements. But all applications are slightly different and so there is no one sized fits all solution.

In today’s tutorial we have looked at how to add the roles and assignment models to assign roles to a user.

We have also looked at using Pundit, a gem that allows us to define policies and scopes for accessing the resources of the application.

Pundit allows you to group the access control rules in central policy objects so that your business logic is easy to find, and evolve over time.

Pundit is also just plain old Ruby code without any magic, so you can be rest assured that your user’s permissions will be enforced correctly.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.