Jan 20, 2016
Table of contents:
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.
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.
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
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
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.
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.
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.