Cancannible

Cancannible is a gem that extends CanCanCan with a range of capabilities:

  • database-persisted permissions
  • export CanCan methods to the model layer (so that permissions can be applied in model methods, and easily set in a test case)
  • permissions inheritance (so that, for example, a User can inherit permissions from Roles and/or Groups)
  • caching of abilities (so that they don't need to be recalculated on each web request)
  • general-purpose access refinements (so that, for example, CanCan will automatically enforce multi-tenant or other security restrictions)
  • battle-tested with Rails 3.2.x and 4.2.x

Two demo applications are available (with source) that show cancannible in action:

Limitations

Cancannible's origin was in a web application that's been in production for over 4 years. This gem is an initial refactoring as a separate component. It continues to be used in production, but there are some limitations and constraints that will ideally be removed or changed over time:

  • It only supports ActiveRecord for permissions storage (specifically, it has been tested with PostgreSQL and SQLite)
  • It currently assumes permissions are stored in a Permission model with a specific structure
  • It works with the CanCanCan gem.
  • It assumes your CanCan rules are setup with the default Ability class

Installation

Add this line to your application's Gemfile:

gem 'cancannible'

And then execute:

$ bundle

Or install it yourself as:

$ gem install cancannible

Configuration

A generator is provided to create:

  • a default initialization template
  • a Permission model and migration

After installing the gem, run the generator:

$ rails generate cancannible:install

Enable Cancannible support in a model

Include Cancannible::Grantee in each model that it will be valid to assign permissions to.

For example, if we have a User model associated with a Group, and both can have permissions assigned:

class User < ActiveRecord::Base
  belongs_to :group
  include Cancannible::Grantee
end

class Group < ActiveRecord::Base
  has_many :users
  include Cancannible::Grantee
end

Enabling Permissions inheritance

By default, permissions are not inherited from association. User the inherit_permissions_from class method to declare how permissions can be inherited.

For example:

class User < ActiveRecord::Base
  belongs_to :group
  include Cancannible::Grantee
  inherit_permissions_from :group
end

Or:

class User < ActiveRecord::Base
  belongs_to :group
  has_many :roles_users, class_name: 'RolesUsers'
  has_many :roles, through: :roles_users
  include Cancannible::Grantee
  inherit_permissions_from :group, :roles
end

The Cancannible initialization file

See the initialization file template for specific instructions. Use the initialization file to configure:

  • abilities caching
  • general-purpose access refinements

Configuring cached abilities storage

Cancannible does not implement any specific storage mechanism - that is up to you to provide if you wish.

Cached abilities storage is enabled by setting the get_cached_abilities and store_cached_abilities hooks with the appropriate implementation for your caching infrastructure.

For example, this is a simple scheme using Redis:

Cancannible.setup do |config|

  # Return an Ability object for +grantee+ or nil if not found
  config.get_cached_abilities = proc{|grantee|
    key = "user:#{grantee.id}:abilities"
    Marshal.load(@redis.get(key))
  }

  # Command: put the +ability+ object for +grantee+ in the cache storage
  config.store_cached_abilities = proc{|grantee,ability|
    key = "user:#{grantee.id}:abilities"
    @redis.set(key, Marshal.dump(ability))
  }

end

Testing the gem

The RSpec test suite runs as the default rake task:

rake
# same as:
rake spec

For convenience, guard is included in the development gem environment, so you can start automatic testing-on-change:

bundle exec guard

Appraisal is also included to run tests across Rails 3 and 4 environments:

appraisal rake spec

Contributing

  1. Fork it ( https://github.com/evendis/cancannible/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request