Blog

Nov

Simple authorization in Ruby on Rails apps


Here at Elabs, we've been using CanCan for authorization in a number of applications. Ryan Bates managed to build an authorization system which is both simple and powerful. A step away from the bloated role based system available at the time, yet more sophisticated than simply tacking on methods on ActiveRecord models.

Over time though we've come against a few grievances with CanCan.

  • Ability files quickly become too big to manage, and there is no built in strategy for splitting up abilities across multiple files.
  • Even worse, there is no natural way to structure ability files. We usually resort to comments to divide the file into sections for different models.
  • All ability rules need to be evaluated for every request. While not a huge performance hit, it seems like a built in wastefulness.

And finally: at the time of writing, CanCan has 128 open issues, 28 open pull requests. Important functionality in the gem is broken, and attempts to fix it through pull requests are ignored. The test suite depends on ActiveRecord < 3.1 and won't even run with later versions of ActiveRecord, unless someone fixes this, we don't actually know if CanCan works at all with newer versions of AR.

In a recent project we worked on, we were running against bugs in CanCan which forced us to run a forked version, and we were fighting against an ability file which was growing out of control. We decided that we needed a new way to approach the problem.

Back to basics

We really like CanCan's simple approach. The ability file isolates all authorization logic, and it leaves you free to handle authorization however you want to. You are free to grow your authorization system from a single user role to whatever complexity you need. We were intent on keeping this flexibility.

We wanted something simpler though. Something which we can implement without really needing a library at all. We wanted to have full control over how the authorization system works.

We took inspiration from objectify and Bryan Helmkamp's excellent blog post 7 Patterns to Refactor Fat ActiveRecord Models among others and pared the whole thing down to creating a plain Ruby class for each domain model.

We call these classes policies and we put them in app/policies. They might look like this:

class PostPolicy
  attr_reader :user, :post

  def initialize(user, post)
    @user = user
    @post = post
  end

  def create?
    user.admin? or not post.published?
  end
end

Using these classes from the controller is fairly easy:

def create
  @post = Post.new(params[:post])
  raise NotAuthorizedError unless PostPolicy.new(current_user, @post).create?
  if @post.save
    redirect_to @post
  else
    render :new
  end
end

This works quite nicely, but unfortunately it's a lot more code to write in the controller. Controllers are un-DRY enough as it is; we need to make this easier. It's simple enough to introduce a helper method for fetching a policy for a given record:

def policy(record)
  "#{record.class}Policy".constantize.new(current_user, record)
end

Now we can simplify our create method somewhat.

def create
  @post = Post.new(params[:post])
  raise NotAuthorizedError unless policy(@post).create?
  …
end

We can easily wrap this pattern in another method:

def authorize(record)
  raise NotAuthorizedError unless policy(record).public_send(params[:action] + "?")
end

And we end up with this:

def create
  @post = Post.new(params[:post])
  authorize(@post)
  …
end

That looks a lot closer to what we have in CanCan.

This pattern works fine for 6 of the 7 restful actions, but what about #index?

CanCan can automatically construct a query based on the permissions you have specified, as long as the hash based syntax is used, anyway. Unfortunately we've found this magic to be error prone and sometimes insufficient. We really want to use scopes for this, but we don't want those to pollute our model objects. Again, taking inspiration from Bryan's blog post, we create a class for this:

class PostPolicy < Struct.new(:user, :post)
  class Scope < Struct.new(:user, :scope)
    def resolve
      if user.admin?
        scope
      else
        scope.where(:published => true)
      end
    end
  end

  …
end

Usage for this from the index action is also fairly easy:

def index
  @posts = PostPolicy::Scope.new(current_user, Post.scoped).resolve
end

Again, we can simplify this with a helper method:

def index
  @posts = policy_scope(Post.scoped)
end

Both the policy and policy_scope method are especially useful in views. We can do things like this:

<% policy_scope(@category.posts).each do |post| %>
  <li>
    <h2><%= post.title %></h2>
    <p><%= link_to "Edit", [:edit, post] if policy(post).edit? %></p>
  </li>
<% end %>

Our views are kept quite nice and DRY, just like with CanCan.

We have bundled up these helpers in a very simple gem we're calling Pundit. It has a few more tricks up its sleeves, but basically it does exactly what this post outlines. We found that we could replace CanCan with this pattern very effectively. The resulting code is simpler, easier to understand and easier to test.

Conclusion

While we do think that Pundit has been useful to us, there is a bigger takeaway from this. We had a problem, and we threw a large library with a complicated DSL at the problem, and as the old saying goes, we now had two problems. Sometimes the simpler solution is better. Sometimes it makes sense to leverage Ruby, over creating your own mini language.

Spend some time reconsidering the dependencies you have in your application and whether they are actually helping you, or if you're spending more time fighting them than you're getting out.

blog comments powered by Disqus