Skip to content

rails authorization

Daniel Kehoe edited this page Aug 31, 2014 · 7 revisions

Rails Authorization

by Daniel Kehoe

Last updated 31 August 2014

How to control access in a Rails application. An overview of Rails authorization, introducing role-based authorization, with a comparison of the CanCan or Pundit gems.

Examples

This article offers an overview of role-based authorization in Rails. The RailsApps project provides example applications and tutorials demonstrating authorization:

Join RailsApps

What is the RailsApps Project?

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.

Authentication

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 web application frameworks 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. The feature that allows 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.

Role-Based Authorization

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.

Alternatives to Role-Based Authorization

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.

Implementing Role-Based Authorization

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 if 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.

Pundit

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.

CanCan

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.

If you find that authorization rules are adding too much complexity to your controllers, read the Pundit Quickstart Guide to learn how to use Pundit for an improved application architecture.

Implementing Roles

Despite the emphasis in Rails on conventions for the sake of consistency, 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.

There is also no convention for implementing roles. We’ll consider several approaches you may see in other applications, and then look closely at the approach we’ve selected for this application.

Single or Multiple Roles

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. 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 these 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.

Binary Role

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.

String Roles

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?.

Bitmask Roles

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.

Role Model

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 is a gem that provides this functionality. You don’t have to implement it yourself.

Rolify Gem

Use Florent Monbillard’s Rolify gem to add multiple roles to an application. The gem is well-documented on its wiki. The Rolify gem provides a generator that creates a Role model with 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 rolify method to the model.

Rolify 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
  • @users = User.with_role :admin – obtain an array of all users with the admin role

Rolify is 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.

Enum Roles

Most applications don’t need to assign more than one role to a user. In our tutorial application, a single role for each user is sufficient, so we won’t use the Rolify gem. Instead we’ll 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 enums make it easy to add role-based authorization to a Rails application.

Clone this wiki locally