-
Notifications
You must be signed in to change notification settings - Fork 3
rails authorization
Last updated 16 September 2014
How to control access in a Rails application. An overview of Rails authorization, including role-based authorization, with a comparison of the CanCan and Pundit gems.
This article offers an overview of role-based authorization in Rails. The RailsApps project provides example applications and tutorials demonstrating authorization:
rails-devise-roles
rails-devise-pundit
You can create the example applications in a few minutes using Rails Composer.
This is an article from the RailsApps project. The RailsApps project provides example applications that developers use as starter apps. Hundreds of developers use the apps, report problems as they arise, and propose solutions. Rails changes frequently; each application is known to work and serves as your personal “reference implementation.” Support for the project comes from subscribers. If this article is helpful, please join the RailsApps project.
Originally, everyone was anonymous on the web. Browsers requested web pages without identifying the user. In 1997, cookies were introduced to the web to keep track of user sessions, and soon applications were developed that allowed users to create accounts, and sign in to their accounts to initiate sessions. The features that allow users to create accounts (and edit or delete their profiles) are called user management features. Allowing users to sign in and identify themselves is called authentication. Typically, we request an email address and a password to authenticate the user, so we can be sure whoever is signing in is the same person who created the account.
User management and authentication are not core features of Rails but it is easy to add authentication and user management to a Rails application, either by writing the code or adding a gem. If you would like your users to sign in with an account they’ve already established on a popular site such as Twitter or Facebook, you can use the OmniAuth gem. If you’d like visitors to register and sign in with an email address and password, you can use the Devise gem. Both OmniAuth and Devise are robust and full-featured, so most developers use the gems, rather than implementing authentication features themselves. The RailsApps project offers an OmniAuth Tutorial and a Devise Tutorial to get you started.
It’s important to distinguish authentication, which identifies a user, from authorization, which controls what a user is allowed to do. In this article, we look at ways to implement authorization, anticipating that you’ve already added user management and authentication.
Almost every web application needs an authorization system, if there are parts of the website that are restricted to some users. Most websites set access restrictions based on roles; that is, users are grouped by privilege. The web application checks the user’s role to determine if access is allowed. We call this role-based authorization.
In the simplest implementation, we check if a user has a specific role (such as administrator) and either allow access or redirect with an “Access Denied” message. Roles are attributes associated with a user account, and often implemented in a User model. We’ll look at ways to implement roles, but first let’s consider situations where role-based authorization is not suitable.
Role-based authorization is suitable for simple applications without complex access rules. A big advantage is easy conceptualization; it is easy to imagine personas, each with different (but uniform) privileges. If all you need are role-based rules, use them.
You may encounter complex applications where role-based authorization is inadequate. In these cases, authorization is often based on matching requested activities with a database of privileges. For example, imagine an application that is used across a university to record and report student grades. A student can see his or her own grades for any class; a teaching assistant can enter a grade but not change it after the course ends but only for students in their own section; a professor can enter or change a grade for any student in the class until the next semester begins; the department chairperson can view but not change grades for any student enrolled in a department course; the registrar of records can view or change any grade for any student ever enrolled. Whew! In a real university, the requirements are even more complex, I’m sure. Not only do roles overlap (a professor may also be a department chairperson) but privileges are finer-grained than roles. A user with the role of professor should only have grade-changing privileges for the students in his or her course and it is impractical to create a new role for every new course every semester. This is a use case for building access rules based on permissions attached to activities, not roles.
If you’re building an application with this level of complexity, seek help from experienced developers. You’ll be treading in the territory of large enterprises where initiatives such as User-Managed Access hold sway. This tutorial doesn’t cover such complexity. We’ll focus on the majority of applications where role-based authorization is optimal.
Simple role-based authorization requires:
- attributes for roles, typically in a User model
- access rules added to controller actions, restricting access to prohibited pages
- methods to check roles in view templates, displaying content conditionally
In an application with simple access restrictions, you can add authorization with a few lines of hand-crafted code. You’ll need to add a role attribute to a User model. You’ll use helper methods to construct conditional statements for access control in Rails controllers. And you can use the same helper methods to conditionally display content in views.
Many developers use the Pundit or CanCan gems (or its successor, CanCanCan). These gems help organize and centralize access rules in complex applications, keeping controllers “skinny.” As a framework, Rails allows you to add as much complexity to a controller as you wish. However, the Rails community has come to a consensus that complexity in controllers is a not a best practice. Authorization quickly adds complexity to controllers, which is why developers use Pundit or CanCan.
Given the advice, “Keep your controllers skinny,” some developers attempt to implement access rules as methods in a model. Access rules don’t belong in a model, given that a model is best used for retrieving and manipulating data, and not for logic that controls program flow through the application. Rather than build “fat models” with complex access rules for program flow, developers look for ways to keep both models and controllers free from excess authorization code. Pundit or CanCan are options.
You can use the Pundit gem to keep your controllers skinny. Pundit is an authorization system that uses simple Ruby objects for access rules. Pundit uses a folder named app/policies/ containing plain Ruby objects that implement access rules. Pundit is well-suited to the service-oriented architecture that is popular for large Rails applications, emphasizing object-oriented design with discrete Ruby objects providing specialized services.
Pundit policy objects are often described as POROs, or “plain old Ruby objects.” PORO simply means that a Pundit policy object doesn’t inherit from other classes or include code mixed in from elsewhere. In contrast, a Rails model that inherits from Active Record is not a PORO; the model inherits behavior that is defined in a parent class. Because Pundit policy objects are POROs, the code is simple and easy to understand.
CanCan was a popular gem for authorization developed by Ryan Bates (best known for RailsCasts) and abandoned prior to the release of Rails 4.0. Due to its popularity, the community-based CanCanCan project maintains an updated version of CanCan. CanCan provides a DSL (domain-specific language) that isolates all authorization logic in a single Ability class.
Before starting a project, developers often want to know what is best, CanCan or Pundit?
As an application grows in complexity, the CanCan Ability class can grow unwieldy. Also, every authorization request requires evaluation of the full CanCan Ability class, adding performance overhead. In its favor, CanCan is popular and well known among Rails developers. If you inherit a project that uses CanCan, you can upgrade to CanCanCan with minimal disruption. Many developers continue to be happy using CanCan.
Pundit also offers the advantage of segregating access rules into a central location, keeping controllers skinny. With Pundit the central location is not a single Ability file. Instead, it is a folder named app/policies/ containing plain Ruby objects that implement access rules. Adding authorization to a controller action requires one line of code that calls a helper method. Pundit uses meta-programming magic to instantiate a policy object that matches an access rule to the controller action. Pundit policy objects are lightweight, adding authorization logic without as much overhead as CanCan. If you wish, you can create a single Pundit policy object for use with all your controllers. More often, developers create a policy object that corresponds with a specific model (for example, a UserPolicy class to match a User model) and will use the policy object to control access for actions in a specific controller (for example, a Users controller).
I prefer Pundit when implementing authorization in a complex application. You can read the Rails and Pundit Tutorial to learn how to use Pundit.
For simple applications, CanCan or Pundit are not necessary and you can use simple role-based authorization without any extra authorization gems. The Role-Based Authorization Tutorial goes into detail.
Neither Pundit or CanCan implement roles. With either gem, or hand-rolled authorization approaches, you’ll need to implement roles, either by adding attributes to a User Model or adding a gem to manage roles. Rails has no convention for implementing roles. There is a wide range of approaches which we’ll survey here.
Just as Rails has no convention for implementing roles, there is no convention established in the framework for a “user.” Developers are free to create a model for an Account, a Member, a Profile, or anything else that meets their requirements. In most cases, developers create a User model, which is a practice we follow in the RailsApps example applications and tutorials.
Before you decide which approach you’ll use to implement roles, consider whether your users will each have a single role, or if you will need to assign multiple roles to a single user. Your implementation of roles will be different depending on whether you need a single role or multiple roles.
Let’s consider some examples. If a user can either be an ordinary user, or an administrator, and nothing else, each user has a single role. If a user can join with a bronze, silver, or gold plan, or be an administrator, you’ll only need one role per user. With a single role per user, privileges can be cumulative. You can create access rules so gold users get all the privileges of bronze or silver users (plus more). A single role per user is all you need for many web applications.
If privileges are not cumulative, you may need multiple roles per user. Consider concertgoers. Some may pay extra to sit close to the stage; there’s a “seating” role with values of “close” or “far.” Some may win a contest for a backstage pass; whether they sit close or far, each user will be assigned a value of “access” or “no access” for the “backstage” role. Each user will have multiple roles. Take the time to map out your system of privileges before deciding how you’ll implement roles.
The simplest use case is a user who can be either an administrator or an ordinary user. You can add a boolean attribute to the User model to indicate whether a user is an administrator or not. Then you can check the role with a simple method such as @user.admin?
.
This approach has drawbacks when you need multiple roles. You could add another boolean attribute to indicate if a user has a premium plan, but as soon as you add more plans, the approach gets unwieldy, as you’ll need to add a separate attribute to the model for every anticipated authorization level.
For a user who can have only a single role, you could add a role
attribute to the User model, and set a string representing a privilege level such as “admin,” “gold,” “silver,” or “bronze.” This approach requires only one column in the User data table. You may need some supporting code in the User model that makes sure only pre-defined roles can be used. You can check the role with if @user.role == 'admin'
. With a little extra code, you can implement methods in the User model such as @user.is_admin?
.
You may encounter several limitations with this approach. First, though you can easily add new roles, you can’t easily rename roles once you’ve got registered users (you’d have to change many records in the database). Second, a user cannot have multiple roles; only one role is possible for each user. Finally, this approach requires extra code in the model to implement convenience methods such as @user.is_admin?
. A better approach is Enum roles, described below.
A model attribute can encode a role using a bitmask function. Instead of representing the role as a string, the role attribute can take an integer value. The integer itself means nothing; instead, by decoding the integer as a binary number, each bit represents a role. This is more compact than creating a separate database column for each role.
In the early days of computing, when machine memory was limited and code had to be compact, bitmasks were commonly used to store configuration settings or other data. Today bitmasks are a clever trick that is best avoided. To implement bitmasks to encode roles, you’ll have to add complex methods to your User model (or use the bitmask_attributes gem). There is also no way to set up database indexes or simple queries to retrieve encoded roles. It’s the worst kind of hack; there is no performance benefit and your code becomes much less readable. There are better alternatives.
If your application requires that users have more than one role, a Role model provides the most flexible implementation. The User model and Role model will have a many-to-many association, so a user can have multiple roles and a role can be assigned to multiple users. You’ll need two database tables, one for the User model and another for the Role model. Additionally, to implement the many-to-many association, your database will need an intermediate “join” table named roles_users. Your models will implement the association using the has_and_belongs_to_many association:
An alternative approach uses the has_many :through. This requires an intermediate class such as Assignment. Unless you need to interact with the intermediate object, has_and_belongs_to_many is more appropriate.
With a Role model, each user can be assigned multiple roles. You’ll be able to construct access rules with conditions such as if @user.roles.include?('admin')
. If you want convenience methods such as @user.is_admin?
, you’ll need extra code in the User model.
I won’t show you the code you need to implement roles using the Role model because there are gems that provide this functionality. You don’t have to implement it yourself.
If your user needs multiple roles, for example, options to allow balcony seating plus a backstage pass, consider adding a gem that adds a roles table. You can implement it yourself, but a gem is more convenient.
Martin Nash’s Royce provides a simple and robust implementation for multiple roles. The Royce gem provides a generator that creates a migration to create a roles table, a users_roles join table, and appropriate database indexes. To add roles to the User model, simply add a method to the model, specifying which roles are available:
royce_roles %w[ user vip admin ]
Royce provides a full set of convenience methods without any extra coding, so you can use methods such as:
-
user.add_role :admin
– to assign an admin role to a user -
user.has_role? :admin
– to determine if a user is an administrator -
user.remove_role :admin
– to remove a role
As an alternative to Royce, you can use Florent Monbillard’s Rolify gem to add multiple roles to an application. Like Royce, it provides a generator to set up tables and indexes. Rolify is more complex than Royce. It lets you apply roles to resource classes or instances. For example, for a discussion forum, a user could be a moderator for some forums but not others. Rolify is well-documented on its wiki.
Both Royce and Rolify are convenient and well-tested, so there is little reason to implement your own Role model, if your application requires users with more than one role.
Most applications don’t need to assign more than one role to a user. In many applications, a single role for each user is sufficient, so you don’t need to use the Rolify gem. Instead you can use a feature of Active Record, Enum, introduced in Rails 4.1. Enums are the simplest way to add roles to a User model, with advantages over all the approaches described above.
An enum, or enumerated type, is stored in the database as an integer but represented in code as a string. Enums give us all the functionality we need to implement user roles. We can define the names of the roles, and if necessary, change the names as needed (the integer values stored with each user record remain unchanged). Active Record will restrict the assignment of the attribute to a collection of predefined values, so we don’t have to add any code to restrict the names of the roles to a defined set. Best of all, enums come with a set of convenience methods that allow us to directly query the role without any extra code. For an enum attribute named role
, with the values admin
, vip
, and user
, we can use these methods:
-
User.roles # => {"user"=>0, "vip"=>1, "admin"=>2}
– list all roles -
user.admin!
– make the user an administrator -
user.admin? # => true
– query if the user is an administrator -
user.role # => "admin"
– find out the user’s role -
@users = User.admin
– obtain an array of all users with the admin role -
user.role = 'foo' # ArgumentError: 'foo' is not a valid role
– we can’t set invalid roles
Active Record automatically gives you convenience methods to assign and query any role. For example, in a User model, if you have:
enum role: [:coach, :business, :first, :crew]
You’ll automatically get the following methods anywhere you’ve instantiated the user object:
-
user.coach!
– assign the user’s role user.business!
user.first!
user.crew!
-
user.coach?
– query the user’s role user.business?
user.first?
user.crew?
Active Record enums make it easy to add role-based authorization to a Rails application.
The following code samples are taken from the rails-devise-roles example application on GitHub. The Role-Based Authorization Tutorial explains how to build the complete application.
Thanks to the power of the Active Record enum attribute, we need only a few lines of code in the User model to provide a complete roles implementation for role-based authorization. Here is an example of a User model with the code needed to implement roles:
class User < ActiveRecord::Base enum role: [:user, :vip, :admin] after_initialize :set_default_role, :if => :new_record? def set_default_role self.role ||= :user end # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable end
The enum
method sets the role
attribute with a set of predefined role values: user
, vip
, and admin
. Refer to the documentation for Enum to see other options for setting role values. For example, you can use a hash to explicitly map enum values to integers:
enum role: {user: 0, bronze: 1, silver: 2, gold: 3, admin: 4}
Before you can use a User model with these roles, you’ll need to run a migration to add a new field to the Users table in the database:
$ rails generate migration AddRoleToUsers role:integer
The migration will look like this:
class AddRoleToUsers < ActiveRecord::Migration def change add_column :users, :role, :integer end end
You’ll access roles by name, such as user
, vip
, or admin
, but thanks to Active Record enum, the values are stored as integers in the Users table. The values that correspond to each integer are defined in the User class. Whether you use CanCan, Pundit, or simple role-based authorization, you can set up your roles in the User model using an Active Record enum attribute.
The following code samples are taken from the rails-devise-roles example application on GitHub. The Role-Based Authorization Tutorial provides additional details.
Here is an example of a User controller with authorization checks:
class UsersController < ApplicationController before_filter :authenticate_user! before_filter :admin_only, :except => :show def index @users = User.all end def show @user = User.find(params[:id]) unless current_user.admin? unless @user == current_user redirect_to :back, :alert => "Access denied." end end end def update @user = User.find(params[:id]) if @user.update_attributes(secure_params) redirect_to users_path, :notice => "User updated." else redirect_to users_path, :alert => "Unable to update user." end end def destroy user = User.find(params[:id]) user.destroy redirect_to users_path, :notice => "User deleted." end private def admin_only unless current_user.admin? redirect_to :back, :alert => "Access denied." end end def secure_params params.require(:user).permit(:role) end end
We’ll apply a before_filter
that calls a private admin_only
method on all actions except show
.
The admin_only
method checks if the current user is an administrator. If the user is not an administrator, the action redirects to the previous page and displays a flash message, “Access denied.”
The index
action displays a page with a list of users. We only want administrators to have access to the Users#index page. We want the update
and destroy
actions to only be used by administrators.
The show
action displays a user’s profile page, with details about the user. We want administrators to have access to any user’s profile. And we want a user to have access to their own profile, but not the profile of any other user.
The following code samples are taken from the rails-devise-roles example application on GitHub. The Role-Based Authorization Tutorial provides additional details.
Here’s an example of a view file that uses role-based authorization to display different messages to an ordinary user, VIP, or administrator:
<% if user_signed_in? %> <% case current_user.role %> <% when 'user' %> <h3>Welcome</h3> <% when 'vip' %> <h3>Welcome, VIP</h3> <% when 'admin' %> <h3>Welcome, Administrator</h3> <% end %> <% else %> <h3>Welcome</h3> <% end %> <p><%= link_to 'Users:', users_path %> <%= User.count %> registered</p>
The Active Record Enum gives us a method we can use to determine the name of the role. We use current_user.role
to find the user’s role. Depending on the role, we display a different welcome message. You can imagine that view templates can quickly become cumbersome if we try to accommodate a large number of roles or complex access rules. If your view templates become complex, use partials to reduce the code complexity, or use Pundit to implement access control logic.
The following code samples are taken from the rails-devise-pundit example application on GitHub. The Rails and Pundit Tutorial provides additional details.
Here is an example of a User controller that uses methods supplied by Pundit:
class UsersController < ApplicationController before_filter :authenticate_user! after_action :verify_authorized def index @users = User.all authorize User end def show @user = User.find(params[:id]) authorize @user end def update @user = User.find(params[:id]) authorize @user if @user.update_attributes(secure_params) redirect_to users_path, :notice => "User updated." else redirect_to users_path, :alert => "Unable to update user." end end def destroy user = User.find(params[:id]) authorize user user.destroy redirect_to users_path, :notice => "User deleted." end private def secure_params params.require(:user).permit(:role) end end
The statement after_action :verify_authorized
isn’t strictly necessary. It is a second line of defense, offered by Pundit, that ensures you haven’t forgotten to confirm authorization in every controller action where you want it. If you include after_action :verify_authorized
and a controller action isn’t protected with the authorize
method, the user will see an exception when the unprotected action is called.
The index
action is protected with Pundit using the authorize User
method call. We add the keyword authorize
to any controller action that requires authorization for access. The authorize
method takes an argument that tells Pundit where to find access rules. The authorize
helper method finds a UserPolicy class and instantiates it, passing the current_user
object and either the User class or an instance of the User model, and calling an index?
method to return true or false.
Here’s an example of the corresponding Pundit policy object:
class UserPolicy attr_reader :current_user, :model def initialize(current_user, model) @current_user = current_user @user = model end def index? @current_user.admin? end def show? @current_user.admin? or @current_user == @user end def update? @current_user.admin? end def destroy? return false if @current_user == @user @current_user.admin? end end
The Pundit documentation recommends placing policy objects in the app/policies folder. Notice the class definition class UserPolicy
. It doesn’t inherit from any parent class. It is a plain old Ruby object, which means there are no inherited methods other than what is found in any simple Ruby object. We have to implement everything we need. That’s a benefit; there is no hidden API or domain specific language to learn. Fortunately, the boilerplate needed to implement a policy object is very simple, so we don’t have to add much code to turn a simple Ruby object into a policy object.
The name of the class must correspond to an existing model class (one that inherits from ActiveRecord or ActiveModel). The name of the model class is combined with “Policy” to form the class name. This allows Pundit to find and instantiate the class from the authorize
method.
You’ll need to define a method that corresponds to each controller action that requires authorization. Use the name of the controller action combined with a ?
(question mark) character. By convention in Ruby, method names ending in question marks are expected to return true or false. We’ve been calling these methods “access rules,” which is descriptive of their function. There is nothing special about these methods. As long as the method has the name of a controller action followed by a question mark, and returns a boolean, it serves as an access rule for Pundit authorization.
This is a simple example of a Pundit policy object. You’ll have to judge for yourself whether your application is sufficiently complex to warrant use of Pundit. If you anticipate your application will grow in complexity, it is a good idea to use Pundit. Pundit has a steeper learning curve than simple role-based authorization; the Rails and Pundit Tutorial explains Pundit in depth.
Authorization is a requirement for many Rails applications. Role-based authorization is easy to conceptualize and can be added to a User model using an Active Record Enum attribute (use the Royce or Rolify gems if access is predicated on more than one assigned role). Simple role-based authorization may be all you need. If your controller gets overly complex, switch to Pundit to manage authorization.