Introduction
Most application developers will encounter the fundamental concepts of authentication and authorization pretty early in their career. These are essential components in securing a web application, or general software system, or indeed any system.
In this article I want to examine some of the popular options available for implementing
authorization in a Rails project. I will consider the two most popular libraries, and look at how these authorization
solutions could be incorporated into the ubiquitous PostsController
. I hope to give you a
feel for how each of the libraries fulfills its function, and weigh up the pros and cons of each.
Authentication and Authorization
Authentication describes how we verify the identity of the users of our system, i.e. who the user is. Some applications will expose functionality or content to anonymous users, in this case these users do not need to complete authentication. But many applications need to know who the user is. Authentication is often achieved using credentials like username and password, or a secret token.
Authorization describes what the user is allowed to do. The question of what the user can do will usually come after we know who the user is. So for most applications effective authorization will be dependent on authentication.
As a mature web framework, there are a number of different authentication solutions that will integrate with your Rails project. Common libraries include authlogic, clearance and devise. Each of these libraries make it easy for you to require that a user be logged-in before they can access a particular controller action, or endpoint. For example, from the clearance docs:
class ArticlesController < ApplicationController
before_action :require_login
def index
current_user.articles
end
end
Each of these authentication libraries make it relatively easy for the application developer to define that a controller action
should only be accessible to a logged-in user, and the identity of that logged-in user can usually be retrieved via a
current_user
helper method, exposed within the controller.
This is what authentication gives us. But the real focus of this post is to look at the authorization options for Rails. As for authentication, there are several options available, but we will focus our consideration on the two most popular options:
Before we look at specifics for these particular gems, let's discuss what authorization is in a general sense, and why we need it.Principles of authorization
Once our authentication system has identified who the user is, the next question is what should they be allowed to do.
With Rails the public interface that your application exposes is defined by your routes file, typically found at
config/routes.rb
. One can apply constraints within the routes file, but for our purposes we consider that
the routes file will simply map URL patterns, in a request, to the controller endpoints that should respond.
In this way we will consider the controller as the first point where our own application code will interact with the request. Once the request hits our controller action we want to ensure that the user has the proper credentials to access that action.
The system under consideration
Let's consider a concrete example which we will reuse during this article. The source code for all that follows can be
found on GitHub. We will start with the
standard rails project obtained by running rails new authorization-rails
. On top of
the standard project we will add the following components for our exploration.
We have a basic, unprotected PostsController
which exposes index
, show
and destroy
actions as follows:
class PostsController < ApplicationController
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id].to_i)
end
def destroy
@post = Post.find(params[:id].to_i)
@post.destroy
redirect_to posts_path, flash: { warning: "Post has been successfully deleted" }
end
end
A corresponding resource
will be added to the config/routes.rb
file to expose these
controller endpoints:
Rails.application.routes.draw do
resource :session, only: [:update]
resources :posts, only: [:index, :show, :destroy]
…
end
As well as this standard route for the PostsController
you can also see a small utility route for the
SessionsController
, which just allows us to toggle the logged-in user in our demo. To support our investigation of authorization options, we will stub out authentication with a simple User
class that looks like this:
class User
attr_reader :name, :is_admin
USERS = []
def initialize(name, is_admin=false)
@name = name
@is_admin = is_admin
USERS << self
end
def self.jack
User.find("jack")
end
def self.jill
User.find("jill")
end
def self.find(name)
USERS.find{ |u| u.name == name }
end
end
User.new("jack", false)
User.new("jill", true)
The User
model is simply made up of a name and a flag to indicate if the user is an admin. We also
hardcode two users, Jack and Jill, where the latter is an admin.
We will store the current user's name in a session variable and offer a separate endpoint to allow us to toggle between the
different users (SessionsController
).
The currently logged-in user will be exposed through a helper method on the base ApplicationController
:
class ApplicationController < ActionController::Base
prepend_before_action :set_current_user
helper_method :current_user
def current_user
@current_user ||= User.find(session[:current_user].to_s)
end
private
def set_current_user
session[:current_user] ||= User.jack.name
end
end
Exposing a current_user
method to the controller, in this way, is a pretty standard technique among the
authentication options.
Finally, in preparation for what follows we are going to add a couple more routes to config/routes.rb
.
These additional routes will provide namespaced-URLs to access equivalent PostsController
s, where the
authorization has been implemented using pundit
and cancancan
, respectively:
Rails.application.routes.draw do
resource :session, only: [:update]
resources :posts, only: [:index, :show, :destroy]
namespace :pundit do
resources :posts, only: [:index, :show, :destroy]
end
namespace :can_can do
resources :posts, only: [:index, :show, :destroy]
end
end
Now let's take a look at how we can implement authorization for these controllers.
Pundit
Pundit provides a simple framework of helper methods, along with some conventions, which encourage the use of plain Ruby objects to define your authorization rules in an object-oriented fashion.
It requires that you create a policy file for each domain class that you wish to authorize actions against. This is a
plain Ruby object that is instantiated with a user object and an instance of the class to which the policy relates. In our
case we will construct a PostPolicy
. Within the PostPolicy
you will define separate
instance methods that should return truthy or falsey values to indicate if the user has permission to execute the action
on the defined Post
instance.
In addition to this you can define a Scope
class. The intention of this class is to encapsulate the logic that
defines which instances the user should be allowed to list. This is typically defined as an inner class of the policy class.
The Scope
class should be initialized with the user instance and an initial scope which you will
chain to, based on the particular user passed. The Scope
class should then define a resolve
method that will define the list of model instances that the defined user has access to.
Implementation
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def show?
post.published ||
user.is_admin ||
(user.name == post.author)
end
def destroy?
user.name == post.author
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
if user.is_admin
scope.all
else
scope.where(published: true).or(scope.where(author: user.name))
end
end
end
end
The instance methods defined in the policy will use the User
and Post
instances to make
the decision on whether the user should be allowed to execute the action on that model. The logic in these methods
will reflect your business logic for the user and domain model involved.
For example, in our case we have said that the user should be able to #show?
the Post
instance
if the post is public or if the user is an admin or if the user is the author of the post.
def show?
post.published ||
user.is_admin ||
(user.name == post.author)
end
By contrast the Scope
does not concern itself with instances of the Post
class, instead
it uses the ActiveRecord
query API to define which set of posts an individual user should be able to list.
In our case the admin can list all posts, while other users should only see posts which are published, or which
they have authored themselves.
def resolve
if user.is_admin
scope.all
else
scope.where(published: true).or(scope.where(author: user.name))
end
end
We make use of this policy within the Pundit::PostsController
. It exposes the same controller end-points
as the original, unauthorized controller but this time we make use of the authorize
helper-method
included via Pundit. This method will instantiate an instance of our PostPolicy
and will use
that to decide if the current_user
has access to the defined action on the defined model instance.
When the user should not have access to the action then a Pundit::NotAuthorizedError
is raised to abort
the action.
In addition to the authorize
method we can see the policy_scope
helper method is being
used in the index
. This will be used to retrieve only the Post
instances to which the
current_user
has access.
class Pundit::PostsController < ApplicationController
include Pundit::Authorization
def index
@posts = policy_scope(Post)
end
def show
@post = Post.find(params[:id].to_i)
authorize @post
end
def destroy
@post = Post.find(params[:id].to_i)
authorize @post
@post.destroy
redirect_to pundit_posts_path, flash: { warning: "Post has been successfully deleted" }
end
end
It can be instructive to imagine how the authorize
helper-method might be implemented by Pundit. Something
like this should provide the correct behaviour:
def authorize(model, action)
unless PostPolicy.new(current_user, model).public_send(action)
raise Pundit::NotAuthorizedError
end
end
Obviously things are a little more complicated that this, as Pundit will need to infer the policy class from the model instance passed,
also the action can be inferred from the controller action being executed. But these details aside, this is the essence of how Pundit
enforces authorization using our simple policy classes.
Comments
This simple example shows some of the basics of how Pundit can be used to enforce authorization of your controller end-points. Some essential comments about this usage:
- You will need to define a policy class for each domain class that you wish to authorize actions against. This can lead to a lot of new policy classes being required, but it encourages simple well-defined policies, which are easily tested.
-
Within your controller you will use
authorize
andpolicy_scope
helper methods to authorize your actions and scope your queries, respectively. -
Pundit also adds a method to your controllers called
verify_authorized
. This method will raise an exception ifauthorize
has not yet been called, which might be a useful safety check in some cases. -
We have looked at the methods used to secure your controller actions, but Pundit also offers a
policy
helper method which you can access in your views to conditionally display elements which the user should have access to. For example:<%- if policy(@post).edit? &> <%= link_to t('general.edit'), edit_blog_post_path(@post) %> <%- end %>
CanCanCan
This gem is an active community fork of the original cancan gem which is no longer maintained. I will use the shorter name to reference it from here on.
With CanCan we define a model called Ability
that encapsulates all of the permissions that a user should
have in your application. The initialize
method on the Ability
will take a user instance from
your application, and uses a declarative syntax to define the different actions that this user is allow to execute on each
of your domain models, specifically the can
and cannot
methods.
Within controllers (and views) there are are helper methods, like can?
and authorize!
.
These methods will use an instance of the Ability
class, parameterized with the
current_user
, and they will query this Ability
model to determine if defined actions can be
executed against particular model instances
Implementation
For ourPost
model the permissions can be declared in our Ability
class as follows (found in
app/models/ability.rb
):
class Ability
include CanCan::Ability
def initialize(user)
can :read, Post, published: true
return unless user.present? # additional permissions for logged in users (they can read and destroy their own posts)
can :read, Post, author: user.name
can :destroy, Post, author: user.name
return unless user.is_admin # additional permissions for administrators
can :read, Post
end
end
We are capturing the same authorization logic in our Ability
class as we did in our
PostPolicy
considered previously. In this case the can
method is used to declare which
actions the user should be able to access. The unauthenticated (anonymous) user can read any published posts, whilst an
authenticated user can also read their own draft posts, or destroy any posts which they have authored. Finally an admin
user can read any post without condition.
It should be noted that CanCan also supports declarations using the cannot(action, model)
method.
I avoid use of cannot
declarations in order to keep this demonstration simple. Which is also, incidentally,
the reason why I generally avoid its use in real projects.
Within the CanCan::PostsController
the abilities of the current user can be accessed via the
current_ability
helper-method. The authorize!(action, model)
method will use this
current_ability
to decide if the user has access to the defined action
on the
model
instance. If the user does not have the requiste authorization then a
CanCan::AccessDenied
error is raised to abort the action.
class CanCan::PostsController < ApplicationController
def index
@posts = Post.accessible_by(current_ability)
end
def show
@post = Post.find(params[:id].to_i)
authorize! :read, @post
end
def destroy
@post = Post.find(params[:id].to_i)
authorize! :destroy, @post
@post.destroy
redirect_to can_can_posts_path, flash: { warning: "Post has been successfully deleted" }
end
end
The scoping of the query in the index
action is achieved using the accessible_by
scope on
the model class, and passing the current_ability
. This uses the existing rule declarations in your
Ability
class to build the SQL for retrieving the Post
instances where the
current_user
has :index
or :read
access.
As we did for Pundit, it can be useful to imagine how the authorize!
method might be implemented in CanCan:
def authorize!(action, model)
ability = Ability.new(current_user)
unless ability.can?(action, model)
raise CanCan::AccessDenied
end
end
This is a bit of a cheat because we have just used the built-in can?
method on the Ability
class. Nonetheless you can imagine that this method will need to iterate over the permissions registerd by the can
declarations in the Ability
initializer, and return true
should any of these permissions match.
Comments
Again this simple PostsController
example gives us a feel for how CanCan is integrated into your Rails
project and how to apply authorization to your controller end points. Some key points:
-
By default authorization rules for all your domain models are declared in the
Ability
initializer. It can be useful to have all these rules managed in one place, but becomes unwieldy when you have a large number of domain models. There are documented techniques to split your ability file, but this is not the default pattern. -
Within your controller you will typically use the
authorize!
helper method to protect each action. CanCan also provides a controller methodload_and_authorize_resource
which will run abefore_action
to instantiate the model instance and ensure that thecurrent_user
has permission to execute theaction
on the model instance. An exception is raised where the user is not authorized to carry out the action. This controller method can be useful to avoid repeated checks in each controller action, but you need to be sure that you have abilities defined that match your controller actions, in particular this can catch you out if you have any non-RESTful actions. -
Again we have focussed on protecting our controller actions, however CanCan also provides the
can?
helper method which you can use in your views to conditionally show content based on thecurrent_user
's access permissions:<%- if can? :edit, @post %> <%= link_to t('general.edit'), edit_blog_post_path(@post) %> <%- end %>
-
The
accessible_by
magic doesn't work when you declare permissions logic using block syntax, it needs to be declared as a hash. Sometimes it is just easier to use the block syntax, so for me this is a real limitation of theaccessible_by
feature, and I generally tend to write an explicitly scoped query where this is needed.
Summary
In this article we have briefly introduced the concepts of authentication and authorization, and discussed how authentication (i.e. knowing who the user is) is often a precursor to authorization checks (i.e. checking what the user is allowed to do).
We then examined two of the most popular libraries for adding authorization to a Rails application, namely
pundit
and cancancan
. We compared how these two libraries could be used to apply
authorization checks to a simple controller.
With the Ability
class CanCan offers a central place to declare the access permissions for your users, across all
of your different domain models. It has a neat syntax for declaring rules, and for checking permissions in your
controllers and views. As a result of these features, getting up and running with CanCan is pretty easy and it is a great
choice if you have a relatively small number of domain models that you need to manage permissions for. However, as
your number of domain models increases having a single Ability
file to manage all the different
permissions can get very complicated.
By contrast, Pundit takes a little more effort to get started. Each domain model that you wish to authorize against will
need to have a policy class defined. These policies are plain Ruby objects, which are easy to digest and easy to test, which
means that this system scales a lot better to more complicated domains. In addition, the Scope
class for
managing the querying of user-accessible objects is a lot more transparent than the accessible_by
magic
offered by CanCan, in my opinion.
Comments
There are no existing comments
Got your own view or feedback? Share it with us below …