

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 if
s 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.
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?
LikeLike
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
LikeLike
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!
LikeLike
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 checkcurrent_user.nil?
first so I don’t get aNoMethod
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 aroundcurrent_user
, which will return a new user ifcurrent_user.nil?
LikeLike
@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 typicalbefore_filter
in our ApplicationControllers:LikeLike
@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:
LikeLike
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.
LikeLike
@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.
LikeLike
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
LikeLike
@Brad: Using devise’s multiple-model-authentication to manage access rules would be rather inconvenient. I don’t think it was meant for fine-grained permission management.
LikeLike
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?
LikeLike
@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.
LikeLike
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.
LikeLike
@Enduras make sure you restart your server after installing the gem, I had the same problem and this resolved it.
LikeLike
@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!
LikeLike
@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.
LikeLike
I dig the post. I use the Devise + CanCan combo and wrote about it at the following two links if you or anyone is interested. It’s certainly worth evaluating before making your choice:
http://www.tonyamoyal.com/2010/09/29/rails-authentication-with-devise-and-cancan-part-2-restful-resources-for-administrators/
http://www.tonyamoyal.com/2010/07/28/rails-authentication-with-devise-and-cancan-customizing-devise-controllers/
LikeLike
We’re using Aegis in our new app and it works like a charm. Very cool. Thanks again, makandra.
LikeLike
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?
LikeLike
@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.
LikeLike