Can Do

DSL-based permission rules for Rails

Example:

class Permission
  def self.define_rules
    CanDo.setup do
      can :index, User do
        rule("You must be logged in.") {User.current}
        rule("You must have an active account to do this.") {User.current.active?}
      end

      can :show, User do
        cascade :index          #inherit the logged in and active rules from :index
        rule("You may not view others' accounts if they are private.") do |user| 
          !user.private? || user == User.current || User.current.admin?
        end
      end

      can :update, User do
        cascade :index          #inherit the logged in and active rules from :index
        rule("You may not update others' accounts.") {|user| user == User.current || User.current.admin?}
      end

      can :delete, User do
        cascade :update
      end

      can :create, UserInterest do
        # You may create an interest if you have permission to update that user.
        cascade :update, {|interest| interest.user} 
      end
    end
  end
end

application.rb:

ActionDispatch::Callbacks.to_prepare do
  Permission.define_rules       #allows rules to be reloaded when classes are reloaded
end

users_controller.rb:

def index
  require_permission! :index, User  #this will raise a CanDo::PermissionError if permission is denied
  ...
end

def show
  @user = User.find(params[:id])
  require_permission! :show, user   #this will raise a CanDo::PermissionError if permission is denied
  ...
end

application_controller.rb:

rescue_from CanDo::PermissionError do |error|
  render :text => "Permission denied: #{error.message}"
end

before_filter :initialize_current_user

def initialize_current_user
  User.current = your_code_goes_here
end

user.rb

def self.current=(value)
  Thread.current["User.current"] = value
end

def self.current
  Thread.current["User.current"]
end

users/index.haml

%ul
  - @users.each do |user|
    - can?(:show, user) do
      %li
        = link_to user.name, user_path(user)
        - if can?(:update, user) do
          = link_to "Edit", edit_user_path(user)
- can?(:create, User) do
  = link_to "Add User", new_user_path

To test your permission logic, simply call CanDo.reason(:verb, object) and test that the reason is what you expect. Make sure to test all rules inherited from cascades as well. Without this, it’s easy for cascades to introduce unintended consequences.

Special thanks to cancan, upon which Can Do is loosely based. Important differences:

* For large permission sets, cancan slows down dramatically. Can Do uses hash-based lookups, which dramatically reduces performance overhead.
* Can Do is far more expressive, allowing user-friendly explanations for failures.
* Can Do has explicit support for cascading rules to reduce repetition.