Aegis: Role-based Permissions for your Ruby on Rails application

Creative Commons License Wrote

This is a guest post by our friends over at makandra, a cool Ruby on Rails development shop. Today they announce a great new Ruby gem for dealing with role-based permissions.
You know the game!
Each time you start a new application the same procedure starts over again: You set up your tools like git, trac & co. and prepare to get going. Then you look into your backlog and plan your first iteration:

  • “As a moderator I should be able to edit and delete all posts in case somebody …”
  • “As a superuser I want to create, edit and delete moderators and users.”
  • “As a department director I should be able to allow or deny requests for leave for employees in my department.”

In almost every project we at makandra were involved with during the past year, some kind of role-based permission-system was required.

Over and over and over again…

After we had implemented a custom role system for the third time, it was enough – and we have learned some things about how complex those kinds of permissions can get. We extracted everything necessary and turned it into a gem. Today we proudly present Aegis – role-based permissions for your user models.

We put it all on github so you can use, modify and fork our little gem: http://github.com/makandra/aegis

To install Aegis into your project, add the following to your Initializer.run block in your environment.rb:

config.gem 'makandra-aegis', :lib => 'aegis', :source => 'http://gems.github.com'

Afterwards run sudo rake gems:install to fetch all gems including Aegis.

Defining your permissions

Create a Permissions-model in app/models, make it inherit from Aegis::Permissions and define the roles and permissions you need:

# app/models/permissions.rb
class Permissions  :allow

  permission :edit_post do |user, post|
    allow :registered_user do
      post.creator == user # registered users may only edit their own posts
    end
    allow :moderator # moderators may edit any post
  end

  permission :read_post do |user, post|
    allow :everyone
    deny :guest do
      post.private? # guests may not read private posts
    end
  end

end

See how nicely all permission related logic is gathered in one central place?
No more need to scatter a million ifs all over your application.

To tell Aegis which models are equipped with roles, you add a string column role_name to the users table. Then open the User model and add has_role to it:

# app/models/user.rb
class User < ActiveRecord::Base
  has_role
end

Checking and asserting permissions

In your views and controllers you can now call may_read_post? and may_read_post! on instances of the user model. The soft may_read_post? simply returns true or false while the less forgiving may_read_post! throws an exception when the user is missing the required permission.

In views you will often use the soft check to decide whether to show or hide a GUI element:

# app/views/posts/index.html.erb
@posts.each do |post|
  
    
    
      
    
  

(current_user is a helper method we’re using to point to the currently signed in user. If you are using Clearance for authentication you already have it.)

You rarely want those soft checks in controllers.
What you want is to simply assert that the user has sufficient permissions at a given
point in your code, and raise an error otherwise:

# app/controllers/posts_controller.rb
class PostsController

  def update
    @post = Post.find(params[:id])
    current_user.may_edit_post! @post # raises an Aegis::PermissionError for unauthorized access
    # ...
  end
  
end

Presenting permission errors to the world

We often use an around filter to convert Aegis::PermissionError to a 403 forbidden status code to be
a good citizen of HTTP and make Webrat see failures in our integration tests:

around_filter :convert_permission_error

def convert_permission_error
  yield
rescue Aegis::PermissionError => e
  render :text => e.message, :status => :forbidden
end

The same around filter can be used to show a nicer “access denied” message
to your users.

Feedback?

If you find Aegis useful, have comments or need help with your Ruby on Rails projects,
do not hesitate to drop us a line at
info@makandra.de.

20 thoughts on “Aegis: Role-based Permissions for your Ruby on Rails application

  1. Good stuff! Wondering if this depends on ActiveRecord or if it can work with other model-managing libraries like DataMapper or some of the more experimental ones?

    Like

  2. Hi Charlie,

    thanks for your comment – at the moment Aegis indeed depends on AR.
    In case you couple Aegis with DataMapper, drop us a line, we’re also interested too see it!

    Greetings from Cercedilla
    Thomas

    Like

  3. I really like the approach you guys have chosen. I am a beginner with Ruby on Rails, and haven taken a look at all the http://ruby-toolbox.com/categories/rails_authorization.html.

    Instead of Clearance, I’m using Authlogic for authentication. Do you know any problems with this paring?

    And another noob question that is a bit off topic. Do you know a good way to assign users to a specific role at registration? Thank you!

    Like

  4. Thanks for this gem! I’m working on a site which will be mostly accessible to non-registered users as well as registered users. I’m using current_user.may_edit_page? checks in my views, but I keep having to check current_user.nil? first so I don’t get a NoMethod error. Is there a more efficient way to use permissions such that when nobody is logged in, permissions are set to :everyone? I’m thinking about using a wrapper method around current_user, which will return a new user if current_user.nil?

    Like

  5. @Chris: The best way is to always have a current_user. When no particular user is signed in, use an unsaved User instance with a role name like “guest” or “anonymous”. Here is a typical before_filter in our ApplicationControllers:

      def find_current_user
        @current_user = User.find_by_id(session["user_id"]) || User.new(:role_name => "guest")
      end
    

    Like

  6. @Shan: Aegis should work just fine with Clearance or Authlogic.

    A simple way to have a default role name for new user’s is to use fnando’s excellent has_defaults plugin and then change your User model like this:

    class User
      has_defaults :role_name => "registered_user"
    end
    

    Like

  7. Looks nice. Simple and powerful, exactly what I was looking for.

    But it doesn’t seem to be working when I define roles like “role :guest”. I changed the role names to strings (role ‘guest’) instead of the symbols and it helped.

    Like

  8. @Yuriy: You should be able to use symbols to refer to roles.

    Make sure that the role_name attribute in your user model receives the role name as a string though. ActiveRecord can only store strings since the database has no concept of Ruby symbols.

    Like

  9. Has anyone used this with devise? I’ve started looking at devise and I like the idea of a rack based auth solution. It has roles *kindof* in that you can use authentication with any model you like (admin, user etc) but that’s not ideal for users with multiple roles. I’d be interested to see how this stacks up in devise vs, say, clearance

    Like

  10. I agree, I think one User model with access rules is much better than a model per role. So do you know how this would work with one Devise User model? I’m assuming it’s fine?

    Like

  11. @Brad: Just use the “has_role” directive in the same user model where you set up Devise. Like Clearance and Authlogic, Devise provides a “current_user” getter for your controller on which you can invoke aegis methods like “current_user.may_create_orders?”.

    Aegis really doesn’t care which authentication plugin you’re using.

    Like

  12. Hello,
    i am completly new to ruby on rails and just experimenting with it at the moment to check if i wanna go future with it, so i am trying out some basics every application need kinda so installed me clearance which is working fine and also found your gem which seems to fit my needs fine, other are either too simple or too complex.
    But i just dont get this to work i kinda made the stuff as in the sample but i keep getting all the time
    undefined local variable or method `has_role’ for #
    I guess its something really simple not mentioned in the guide which isnt a problem for a experienced rails user but for newbs like me.

    Like

  13. @Henning Koch,
    can you elaborate a bit on the code you gave to Chris about setting up a guest user:

    def find_current_user @current_user = User.find_by_id(session[“user_id”]) || User.new(:role_name => “guest”) end

    I am using Authlogic but having trouble finding where I should create the temporary guest user

    Thanks!

    Like

  14. @Dave: When you implement authentication in Rails, it is useful to define a helper method in your ApplicationController that returns the user currently signed in. This method is often called current_user.

    My suggestion to Chris was to enhance his current_user method, so it returns a new, unsaved guest user in case no user is signed in for this session.

    Like

  15. Interesting.

    But what if the user wants to configure the permissions on each role. With this hard coded into a .rb file… how can it be modified by anyone other than a developer doing a redployment?

    Like

  16. @S.: You can grant a user permission under the condition that a piece of Ruby code yields true. You do this by calling `allow` with a block. Inside that block you can look at anything you like, including configuration settings that live in the database and can be configured in the UI.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.