metautonomo.us

Confounding URL typists since 2007.

Easy Role-Based Authorization in Rails

Posted by Ernie on September 30, 2008 at 4:16 pm

Once user authentication has been added to your Rails app, authorization isn’t far behind. In fact, very basic authorization functionality exists the moment you implement user authentication. At that point, users who are logged in will have authorization to access areas of your application that others do not. The next common step is to add a boolean attribute to the User model to track whether a user is a "normal" user or someone who should have access to administer the application as well, yielding a convenient syntax like ?.

Adding an attribute to track a user’s administrator status may well be enough for a simple application, but at some point you will want something more flexible. After all, you don’t want to go adding a new column to your user table for every single possible authorization level, do you? Here’s one very easy way to handle things.

A good role model

So our user might be an admin. But perhaps he is just a plain old user, or a deactivated user, or a superhero, or… You get the idea. We want to be able to add new "roles" as the need for them arises, so we will generate a Role model with a few basic attributes:

script/generate model Role name:string description:string

Once we’ve added a role_id column to the user table and told Rails that User belongs_to :role, we’ll now be able to do something like == 'admin'. Well, that’s functional, but really ugly. It’d be a lot better if we could say something like ?. That’s not too tricky at all.

Our old friend method_missing

Since we have no idea what kind of roles we may eventually be adding to the database, it doesn’t make sense to code a special method for each and every one. Let’s use method_missing instead.

app/models/user.rb:

  def method_missing(method_id, *args)
    if match = matches_dynamic_role_check?(method_id)
      tokenize_roles(match.captures.first).each do |check|
        return true if role.name.downcase == check
      end
      return false
    else
      super
    end
  end
 
  private
 
  def matches_dynamic_role_check?(method_id)
    /^is_an?_([a-zA-Z]\w*)\?$/.match(method_id.to_s)
  end
 
  def tokenize_roles(string_to_split)
    string_to_split.split(/_or_/)
  end


A quick regular expression check of the method called lets us capture the last part of any method starting with is_a_ or is_an_ and a quick split on "_or_" lets us do something like ?. Pretty slick, and very simple!

But wait, there’s more!

So, that’s kind of nice. But of course, as your app grows, it’s likely that you’ll want to expose certain functionality to users with a bunch of different roles. For instance, users who are disabled shouldn’t be allowed to log in at all, but everyone else should, and you don’t want to go around writing code like this:

def login
  if .is_a_user_or_admin_or_superhero_or_demigod_or_chuck_norris?
    # log in
  else
    # do something else
  end
end


"No problem," you say. "I’ll just use method_missing to handle is_not_a_whatever?!" That, my friend, is a slippery slope. There is a better way.

Permission to come aboard

So we have certain roles, and they have permission to do certain things, which sometimes overlap, but not always. Why not create a Permission model, then create an association between roles and permissions? Let’s do that.

script/generate model Permission name:string description:string
script/generate migration CreatePermissionsRoles

Then, in the migration:

  def self.up
    create_table :permissions_roles, :id => false do |t|
      t.integer :permission_id
      t.integer :role_id
    end
  end

And in the models:

class Role < ActiveRecord::Base
  has_and_belongs_to_many :permissions
end
 
class Permission < ActiveRecord::Base
  has_and_belongs_to_many :roles
end

So now we can get to whatever permissions we have assigned to the role of the user by doing something like and do a find_by_name, or whatever our hearts desire. I think you see where we’re going from here.

First, while it’s entirely accurate to say that the user’s role has certain permissions, isn’t it also accurate to say that the user himself has those permissions? Let’s add a little bit of syntactical sugar by using delegate:

app/models/user.rb:

class User < ActiveRecord::Base
  belongs_to :role
  delegate :permissions, :to => :role
  # ...
end

Now we can say , which is a bit more readable. Now, let’s modify our dynamic role check to handle permissions as well:

  def method_missing(method_id, *args)
    if match = matches_dynamic_role_check?(method_id)
      tokenize_roles(match.captures.first).each do |check|
        return true if role.name.downcase == check
      end
      return false
    elsif match = matches_dynamic_perm_check?(method_id)
      return true if permissions.find_by_name(match.captures.first)
    else
      super
    end
  end
 
  private
 
  # previous methods omitted
  def matches_dynamic_perm_check?(method_id)
    /^can_([a-zA-Z]\w*)\?$/.match(method_id.to_s)
  end

Done! Now, we can ask our user objects to tell us what they can do, such as ?, ?, or, in the case of our superhero role, maybe we’ll clean up our view a little bit:

<%= link_to_if(can_fly?, 'Fly!',
               {:controller => 'users', :action => 'fly' }) %>

Filed under Blog
Tagged as , ,
You can leave a comment, or trackback from your own site.