CanCan Permits

Role specific Permits for use with CanCan permission system.

Install

gem install cancan-permits

Usage Rails 3 app

Gemfile gem 'cancan-permits'

Update gem bundle in terminal: $ bundle install

See Generator section below.

See CanCan permits demo app for more info on how to set up a Rails 3 app with CanCan permits!

Rails 3 configuration

Note: This description does not apply to how CanCan-permits is used with Cream

Create a rails initializer with the following code:

module Cream
  # specify all roles available in your app!
  def self.available_roles
    [:guest, :admin]
  end
end  

Modify the User model in 'models/user.rb' (optional)

class User
  def self.roles
    Cream.available_roles
  end   
  
  def has_role? role
    (self.role || 'guest').to_sym == role.to_sym
  end       
end

Permits configuration

Permits can be configured using permits configuration files

Users, roles and permissions

CanCan permits requires that you have some kind of 'role system' in place and that User#has_role? returns whether the user has a given role (pass role argument as symbol or string). You can either add a 'role' directly to the User class or fx use a Roles Generic role strategy.

Application configuration for CanCan Permits

  • Define roles that Users can have
  • Define which roles are available
  • Define a Permit for each role.
  • For each Permit, define what Users with a role matching the permit can do

To add roles to your app, you might consider using a roles gem such as Roles Generic or any of the ORM specific variants.

CanCan permits is integrated with CanCan REST links, letting you easily control which users have access to which models in your app.

Note that Cream has a full_config generator that automatically configures all this for you in a standard configuration which integrates all the various parts (and even supports multiple ORMs) !!!

Define which roles are available

CanCan permits uses the following strategy to discover which roles are available in the app.

Default configuration:

module Permits::Roles
  def self.available
    if defined? ::Cream
      Cream.available_roles
    elsif defined? ::User
      User.roles
    else
      [:admin, :guest]
    end
  end
end

CanCan permits will first try to assume it is used with Cream. If not it will fallback to try and get the roles from User#roles. If all else fails, it will assume only the :guest and :admin roles are available.

You can always monkeypatch this configuration implementation to suit your own needs.

Define a Permit for each Role.

You can use the Permits generator to generate your permits. Permits should be placed in the app/permits folder.

Permit example:

class AdminPermit < Permit::Base
  def initialize(ability, options = {})
    super
  end

  def permit?(user, options = {})    
    return if !role_match? user    

    can(:read, Blog)
    can(:manage, Article)
    owns(user, Post)        
  end  
end

Alternatively you can use return if !super user, :in_role to exit if the user doesn't have a role that matches the Permit. This will in effect execute the same test.

Ownership permission:

The owns call is a special built-in way to define ownership permission. The #owns call can also pe used inside Permits. If a user owns an object instance that user will automatically have :manage permissions to that object instance.

Special permits

The Permits system uses some special permits that can be configured for avanced permission scenarios as described in the wiki.

Licenses

Permits support creation of more fine-grained permits through the use of Licenses.
Licenses are a way to group logical fragments of permission statements to be reused across multiple Permits.

You can use the License generator to generate your licenses. Lincenses should be placed in the app/licenses folder.

License example:

class BloggingLicense < License::Base
  def initialize name
    super
  end

  def enforce!
    can(:read, Blog)
    can(:create, Post)
    owns(user, Post)
  end
end  

Licenses usage example:

class GuestPermit < Permit::Base
    def initialize(ability, options = {})
      super
    end

    def permit?(user, options = {}) 
      super    
      return if !role_match? user

      licenses :user_admin, :blogging
    end
  end
end

The permits system will try to find a license named UserAdminLicense and BloggingLicense in this example and then call #enforce! on each license.

Using Permits with an ORM

The easiest option is to directly set the orm as a class variable. An appropriate 'ownership strategy' will be selected accordingly for the ORM.


  Permits::Ability.orm = :data_mapper

The ORMs currently supported (and tested) are :active_record, :data_mapper, :mongoid, :mongo_mapper

For more fine grained control, you can set a :strategy option directly on the Ability instance. This way the ownership strategy is set explicitly. The current valid values are :default and :string.

The strategy option :string can be used for most ORMs. Setting orm_ to :active_record or :generic makes use of the :default strategy. All the other ORMs use the :string ownership strategy,

Note: You can dive into the code and implement your own strategy if needed.

Setting the ownership strategy directly:

ability = Permits::Ability.new(@editor, :strategy => :string)

Advanced Permit options

Note that the options hash (second argument of the initializer) can also be used to pass custom data for the permission system to use to determine whether an action should be permitted. An example use of this is to pass in the HTTP request object. This approach is used in the default SystemPermit generated.

The ability would most likely be configured with the current request in a view helper or directly from within the controller.

editor_ability = Permits::Ability.new(@editor, :request => request)

A Permit can then use this extra information

Advanced #permit? functionality:

def permit?(user, options = {}) 
  request = options[:request]
  if request && request.host.localhost? && localhost_manager?
    can(:manage, :all) 
    return :break
  end    
end  

Configuring global management permission for localhost

The Permits system allows a global setting in order to allow localhost to manage all objects. This can be useful in development or administration mode.

To configure permits to allow localhost to manage objects: Permits::Configuration.localhost_manager = true

Assuming the following:

  • a request object is present
  • the host of the request is 'localhost'
  • Permits::Configuration has been configured to allow localhost to manage objects:

Then the user is allowed to manage all objects and no other Permits will be evaluated to restrict further.

Note: In the code above, the built in #localhost_manager? method is used.

Please provide suggestions and feedback on how to improve this :)

Generators

The gem comes with the following generators

  • cancan:permits - generate multiple permits
  • cancan:permit - generate a single permit
  • cancan:licenses - generate multiple licenses
  • cancan:license - generate a single license

The generators are described in detail here

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 (c) 2010 Kristian Mandrup. See LICENSE for details.