Ernie Miller

No, I don't work in NYC, DC, or the valley, and I'm cool with that.

RSS Feed

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 @user.admin?.

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 @user.role.name == 'admin'. Well, that’s functional, but really ugly. It’d be a lot better if we could say something like @user.is_an_admin?. 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 @user.is_an_admin_or_superhero?. 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 @user.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 @user.role.permissions 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 @user.permissions, 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 @user.can_login?, @user.can_administer_users?, or, in the case of our superhero role, maybe we’ll clean up our view a little bit:

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

Filed under Blog
Tagged as , ,
You can leave a comment, or trackback from your own site.
  • http://www.codeofficer.com/ Russell Jones

    Funny, I built an identical system to this just recently! Good to see a different spin on it. I used roles to contain a hash of {:resource => :bitwise_permission}, where the permission string was a bitwise representation of each restful action. As well, I could do @user.can_edit_and_delete?(resource) … later though, i switched to doing checks straight off the model … @thing.is_editable_by?(@user) … as that made it easier to manage the business logic straight in the model.

  • http://thebalance.metautonomo.us Ernie

    Russell,

    Definitely a solid approach. Refactoring to @thing.is_editable_by?(@user) also makes a lot of sense in the event that you need to manage permissions down to the object level, sort of like *nix file permissions. Thanks for sharing!

  • Pingback: links for 2008-10-02 « Bloggitation

  • Noam B

    Hi,
    nice article.
    I wrote a plugin with roles and permissions, without the method_missing sugar (which is a cool thing I might add) but with a role hierarchy. Instead of delegate I simply allow a User to have permissions and @user.permissions triggers a search for his roles and the roles’ permissions in a recursive way.

  • http://www.pedropimentel.com Pedro Pimentel

    I was wondering if using method_missing that much doesn’t slow down significantly the application…
    I didn’t install the plugin yet, what’s your opinion ?

  • http://thebalance.metautonomo.us Ernie

    Sure, method_missing is slower than having a static method. In practice, though, you aren’t really going to see the impact of it unless you’re calling it in a tight loop.

    Still, if it’s a big concern, you can use class_eval in method_missing to add the new method to the class instead, as seen in ActiveRecord’s method_missing in base.rb.

  • http://blog.matthewrudy.com Matthew Rudy Jacobs

    At Jobs Go Public we have a very similar permissions model,
    but we also have permissions attached to individual controllers and actions.

    eg. for SearchesController#show we’d have the CanSearch permission attached,
    and as a before_filter we check that the current user has permission to use the current action.

    It’s massively complex,
    but perhaps necessary for the way we resell our services with a fine granularity.

    Nice, though.

  • http://www.szpil.com Joran

    Hi, thanks for an interesting post. I’ve been needing to find a solution to a similar problem. The goal is to have a fine-grained authorization scheme. Because an authorization scheme with roles and permissions still needs another dimension to have a kind of extra special zing, i.e. you need to be able to relate these roles and permissions to specific resources, (and then specific actions on those resources).

    Otherwise you’re still applying authorization at the controller level when where it’s really more important is further down at the data level.

    In other words, what you really want to do, is not have permissions or roles at all. I think the key is to figure out a way to say, right, this person owns this piece of data and can change it, and this other person over here can see it, but can’t change it, but none of them can delete it. Except of course the admin.

    This idea of data ownership is also more inline with REST and if you’re working on an API it’ll help keep things very simple for you and easy to document to clients. What’s also interesting is if it’s now about “data ownership” that starts to sounds scarily similar to “has_one” and “belongs_to” etc. Again, implying this kind of logic should go in the model with the controllers being able to handle authorization exceptions further up the chain if necessary. And, the magic of it, is that after a while, certain kinds of relationships (i.e. many-to-many, one-to-many, many-to-one etc) start showing specific authorization patterns for a particular app, and one can take advantage of this in writing more generic methods, that can be inherited, turtles all the way down, turtles all the way down.

  • http://thebalance.metautonomo.us Ernie

    @Joran:

    Yeah, that kind of fine-grained authorization would do well to take its inspiration from the *nix file permissions model. The only difference is that depending on the type of resources you’re talking about, your permission bits may consist of a lot more than rwx.

  • http://aufderyacht.de janprill

    Thanks for sharing this. I’m just at the moment in the situation to add this to my employers website and your approach will come in handy…

  • al2o3cr

    @Joran:

    Hobo (hobocentral.net) has a permisson model similar to what you’ve described – permissions are defined in the models, with the ability to restrict viewing and/or editing on a field-by-field basis.

  • Pingback: A simple rails permission framework « Matt on Rails

  • http://ruby.redpill.se Joe Siponen

    Instead of defining permissions in the models, we’ve chosen to use a technique in which permissions are described in the controllers (using a DSL we’ve developed). The authorization is implemented in modules, named according to roles in the application, which will extend a User instance depending on the role(s) it has been associated with. Further, these modules may also access data in the current controller in order to allow specific role implementations to be context aware.

    This technique for authorization/access-control is available as a plug-in and is described in more detail here:

    http://ruby.redpill.se/2008/9/29/redpill-linpro-releases-a-slick-access-control-plugin

  • Guillermo Velasquez

    A very nice blog post… And a very nice way to implement those nice readable methods… Thanks Ernie!

  • http://www.webconfection.com primordial

    thanks for this, i shall be adding it to a template i use for the sme.

  • http://steffenbartsch.com/blog Steffen

    Interesting recipe. Always great to see all those authorization aspects specific to each application.

    When looking for a authorization plugin, I compiled a commented list that you might like to check out.

    As many existing solutions rely on authorization rules being defined in the app code, I wrote another authorization plugin in the end. It takes those rules out of the code and automatically checks permissions and rewrites db queries. The authorization rules DSL proved very nice for talking to stakeholders and is less of a pain with changing requirements. You might want to have a look.

  • http://anilwadghule.com Anil

    Nice post.

  • Nico

    Any reason why not using

    has_many :permissions :through => :roles

    in the User model instead of delegating the perimissions to the role class?

  • http://thebalance.metautonomo.us Ernie

    Nico,

    Sure — in the example given, the User is in a belongs_to relationship to Role, not a has_many. The role_id is in the users table, not the other way around. Since has_many :through expects a join table to have the id of both ends of the relationship, and possibly additional data as well, a hm:t wouldn’t work.

  • Nico

    Erm, okay, thanx Ernie for the clarification. Makes sense! ;-)

  • Nico

    This is quite a nice authorization solution, however as I started thinking about implementing it myself, I found that I like to have a user to be able to have more than one role. Otherwise the system is rather limited I think. But then it becomes more difficult to get all permissions a user has. The delegate approach doesn’t seem to work anymore like that. Any ideas on this?

  • Nico

    The quick idea I came up is having another habtm or has_many :through association between roles and users so that the user can have more then one role. However, permission and role queries become more expensive then as potentially many records have to be fetched. A role query (is_a_…) potentially has to check the the whole roles array of that user, which seems okay. But a permission query (can_…) potentially has to look at all roles of a user AND at all permissions of that roles in turn.

    Any ideas? ;-)

  • Pingback: steffenbartsch — Rails Authorization Plugins

  • Mark

    Thanks for sharing Ernie! Really cool stuff. As a recent convert to Ruby, sometimes it is tough to wrap the mind around the unwritten and assumed code.

    Would you mind taking this one step further and showing a practical example? For example, blocking a user from seeing a link_to and then preventing the user from typing the URL directly http://www.website.com/posts/new

    Pardon my ignorance Ernie, I’m just trying to learn the practical use of @user.can_fly? method.

    Thanks for the great blog!
    ~Mark

  • NachoF

    so this means that all actions of every controller that have some kind of authorization restriction need to be assigned to each role in the database?

  • Harmen

    HI,

    maybe its because i am a Rails beginner but im not sure how to get this working , I have to setup some roles ofcourse ? but what to put in the tables ?

    you have some more info , like a more indepth tutorial ?

    Thanks :D

  • Harmen

    i have setup 3 roles in the roles table : Admin, moderator and user , but what to put in the permissions tables ?

  • Frank

    Harmen – You need to define some roles like ‘login’ or ‘moderate’ so you can do the user.can_moderate

    Also in the permissions_roles table you need to link the roles to the permissions. Hope that helps you.

    Brilliant article! I love it. However im getting a frustrating error, “permissions delegated to role.permissions, but role is nil”

    Any idea on how to solve this?

    Thanks!

  • German

    Nice article Ernie,

    i think querying the DB each time for permissions is a bit overhead. I’m trying to put permissions into the session after login. But since rails doesn’t allows accessing session from models the nice current_user.is_admin? or whatever is impossible. Storing permissions in the session only allows access them from controllers views. Any thoughts?

    greets

  • Chip

    Awesome article. New to Rails, I do have a question — how to maintain the relationship between Roles and Permissions. Do I create a third controller / set of views? What is best practice for creating these relationships? I’ve seen many tutorials about HABTM, but for the most part, they only go as far as setting up the foundation — I’ve not found anything for adding/removing the links. In most cases, the relationship seems to get established at the time of creation, but for Roles/Permissions, it seems that either could be created, and then a relationship gets added much later. For example, you could create a permission to “Read Posts”, add it to a generic User role, and if you created a Guest role later, you may want to add the “Read Posts” permission to it.

  • Trevor

    Looks like this is an old post, is something different in Ruby 1.9 or Rails 3 that would make this no longer work? I followed the instructions in this guide, but when I call “@user.is_an_admin? “I get a NoMethodError. Any suggestions?

  • http://www.clickfirstmedia.co.uk seo manchester

    Nice post with some interesting relevent information.

About

I'm Ernie Miller. But then, you probably knew that by looking at the page title, or the URL. I'm a Ruby programmer in Louisville, Kentucky. This blog used to be called "metautonomo.us", which I thought was kind of clever, but nobody, including me, could type it. Lesson learned.