Canable (by John Nunemaker) with a Role Based Access System
How the system used to work
The User class or whichever object needed to be authorized against an action would include Canable::Cans. Objects being manipulated that needed protection would include Canable::Ables, and you would override the viewable_by?(user) methods to implement the various permissions governing that resource and the user trying to access it.
How RBACanable works now
RBACanable is significantly different than the original Canable and unfortunately not compatible with existing code that uses Canable. The original Canable was oriented so that each resource (an object including Canable::Able) managed it's own permissions. RBACanable is oriented in the opposite way, where each object that wants to preform an action and needs authorization manages the permissions that govern it. This is necessary because each one of these action-performing objects (called Actors in RBACanable) get their governing rules from a series of Roles (any module including Canable::Role).
Whatever class(es) you want all permissions to run through should include Canable::Actor instad of Canable::Cans.
class User > ActiveRecord::Base include Canable::Actor
Actors represent objects in the system that want to preform an action. An actor's abilities are defined by the role(s) it plays.
The rules that govern your actors are defined in modules extending Canable::Role and/or each other.
module EmployeeRole include Canable::Role default_response false end module ManagerRole include Canable::Role default_response true end
class Article include MongoMapper::Document include Canable::Ables end
Including Canable::Ables adds the able methods to the class including it. In this instance, any article instance now has viewable_by?(actor), creatable_by?(actor), updatable_by?(actor) and destroyable_by?(actor). Now
Lets say an article can be viewed and created by anyone, but only updated or destroyed by the user that created the article. To do that, you could leave viewable_by? and creatable_by? alone as they default to true and just override the other methods.
class Article include MongoMapper::Document include Canable::Ables userstamps! # adds creator and updater def updatable_by?(user) creator == user end def destroyable_by?(user) updatable_by?(user) end end
Lets look at some sample code now:
john = User.create(:name => 'John') steve = User.create(:name =. 'Steve') ruby = Article.new(:title => 'Ruby') john.can_create?(ruby) # true steve.can_create?(ruby) # true ruby.creator = john ruby.save john.can_view?(ruby) # true steve.can_view?(ruby) # true john.can_update?(ruby) # true steve.can_update?(ruby) # false john.can_destroy?(ruby) # true steve.can_destroy?(ruby) # false
Now we can implement our permissions for each resource and then always check whether a user can or cannot do something. This makes it all really easy to test. Next, how would you use this in the controller.
class ApplicationController include Canable::Enforcers end
Including Canable::Enforcers adds an enforce permission method for each of the actions defined (by default view/create/update/destroy). It is the same thing as doing this for each Canable action:
class ApplicationController include Canable::Enforcers delegate :can_view?, :to => :current_user helper_method :can_view? # so you can use it in your views hide_action :can_view? private def enforce_view_permission(resource) raise Canable::Transgression unless can_view?(resource) end end
Which means you can use it like this:
class ArticlesController < ApplicationController def show @article = Article.find!(params[:id]) enforce_view_permission(@article) end end
If the user can_view? the article, all is well. If not, a Canable::Transgression is raised which you can decide how to handle (show 404, slap them on the wrist, etc.).
Adding Your Own Actions
You can add your own actions like this:
The first parameter is the can method (ie: can_publish?) and the second is the able method (ie: publishable_by?).
So, lets review: cans go on user model, ables go on everything, you override ables in each model where you want to enforce permissions, and enforcers go after each time you find or initialize an object in a controller. Bing, bang, boom.
Note on Patches/Pull Requests
Fork the project.
Make your feature addition or bug fix.
Add tests for it. This is important so I don't break it in a future version unintentionally.
Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
Send me a pull request. Bonus points for topic branches.
Copyright © 2010 John Nunemaker. See LICENSE for details.