Skip to content

kristianmandrup/cancan-permits

Repository files navigation

CanCan Permits

Role specific Permits for use with CanCan authorization system.

Install

gem install cancan-permits

Usage in a Rails 3 app

Insert into Gemfile

gem 'cancan-permits'

Run bundler to bundle gems in the app

$ 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!

Status update: May 20, 2011

Cancan-permits have undergone major refactoring in the major-refactor branch. Please help out to make this gem much better and implement the ideas presented here.

You need help?

Please post ideas, questions etc. in the cancan-permits group on Google.
Inf you encounter bugs, raise an issue or even better: branch off, do the fix and make a pull request. Thanks!

If you have questions and/or ideas related to roles, please post in the rails-roles group or
see the roles generic project.

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
Note that configuration files for categories (of objects) and role groups are now also supported.

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_roles

end

def self.available_role_groups … end

end

CanCan permits will first try to assume it is used with Cream. If not it will fallback to try and get the available roles from the User model.
If all else fails, it will assume some defaults.

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 < RolePermit::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

Note:
The call owns user, Post looks ugly. I would prefer a DSL more like user.owns Post.
It should be possible to easily have Class of the role subject (user) include a method #owns which can perform the owns check (TODO).

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. Here also, we could envision that the user class include a method #in_role? which given a permit determines if it has that role.
user.in_role? self where self is the permit.

Note however that in most cases you don’t need this line to break out from the #permit? method. The latest design assumes that you only want to run the Permits
for the role and role groups of the role subject (the user)

TODO

Code should rename user to role_subject.
The method #role_list should return all roles of the role_subject, including those of the user’s role_groups (should be cached list).

Permit for Role group

Permit example:

class BloggersPermit < RoleGroupPermit::Base
def initialize ability, options = {}
super
end

def permit? user, options = {}
  1. return if !role_group_match? user
can :read, Blog can :manage, Article owns user, Post end

end

Here the name of the Role group ‘Bloggers’ is used as the prefix in the class name. In the #permit? method the #role_group_match? is used to ensure the permissions
are only granted if the user is a member of this group.

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.

h3.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 = {}
  1. 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

See Permits and License generators

Design overview

The starting point is Permits:Ability (see permits/ability.rb)
The Permits::Ability#initialize method takes the user (or role_subject) to evaluate permissions on and optionally some options (which can include the Request object etc.)
This initializer is more or less equivalent to the CanCan Ability#initialize method which also executes the permission logic.
The difference is the following code, which instead iterates over all the roles of the user and for each role executes an instance of the corresponding permit (using convention of Permit class name).

Permits::Ability#initialize

  # run permit executors
  permits_for(user).each do |permit|
    # execute the permit and break only if the execution returns the special :break symbol
    break if permit.execute(user, options) == :break
  end  

Each permit has a permit executor (see permit/executor folder) according to its base class (see fx permit_base.rb, the #execute and #executor methods).
To build each permit, the permit builder is used (see permits/builder folder).
The whole permits construct should NOT be rebuilt and reevaluated for each authorization!
Please see caching idea under the section ‘Design considerations’ below, in this document. Please help out to improve this!

The /license folder holds all the code license related code. The /loader, /parser and /configuration folders hold the code to load YAML files for static configuration of permits and licenses (currently only using yaml files).

Design considerations

Here are some design considerations for improvement:

Privileges

The roles project, has a role model where users can assign other users the privilege to perform a certain action.
Could we somehow integrate this privilege model into the current design and does that make sense?

Better Role Group design

Each user should be able to join different groups, hence the need for a #role_groups column or a separate RoleGroup model with table similar to the functionality of role.

When a user joins a role_group, he could automatically be given the roles of that group, but what if he then is taken out of that group? Then we can’t simply delete those same roles, as he might have had that role individually before. Bad idea!

Better then to just evaluate the role groups dynamically on demand. Later we could cache this result (fx by storing array of roles in the datastore), then invalidate the cache result for that user whenever his relationships with a role or role group changes, using event triggers? Note: We won’t have support for nested role groups!

  # when any role group changes which roles are in it
  on_role_group_change
    find all users referencing that group and update their cache result

  # whenever a role group is added or removed for a user
  on_add_user_role_group user
    # update roles of group to user (merge into Set)

  on_remove_user_role_group user
    # update cache of user

  on_remove_user_role user
    # update cache of user # can't simply remove role, since might be part of a group

  on_add_user_role user
    # add role to cache (merge into Set)  

Caching of roles_list for each user

From the cancan-permits code:

module License
  class Base
    attr_reader :permit, :licenses
    
    def initialize permit, licenses_file = nil
      @permit = permit
      @licenses = ::PermissionsLoader.load_licenses licenses_file
    end

So the licenses are only loaded when a Permit is initialized, but…


module Permits
  class Ability
    def initialize user, options = {}
      # put ability logic here!
      user ||= Guest.create
      all_permits = Permits::Ability.permits(self, options)
...

    # set up each Permit instance to share this same Ability 
    # so that the can and cannot operations work on the same permission collection!
    def self.permits ability, options = {}
         ...     
        permit = make_permit(role, ability, options)
        permits << permit if permit

    def self.make_permit role, ability, options = {}
      begin            
        permit_clazz = get_permit role
        permit_clazz.new(ability, options) if permit_clazz && permit_clazz.kind_of?(Class)

So any time a Permits::Ability instance is created, the whole shebang is loaded, which is any time the permission is checked (see cream source!).

Instead, this Permits::Ability object should be cached and only reloaded when a license file changes.

http://guides.rubyonrails.org/caching_with_rails.html

1) ActiveSupport::Cache::MemoryStore: A cache store implementation which stores everything into memory in the same process. If you’re running multiple Ruby on Rails server processes (which is the case if you’re using mongrel_cluster or Phusion Passenger), then this means that your Rails server process instances won’t be able to share cache data with each other. If your application never performs manual cache item expiry (e.g. when you‘re using generational cache keys), then using MemoryStore is ok. Otherwise, consider carefully whether you should be using this cache store.

MemoryStore is not only able to store strings, but also arbitrary Ruby objects.

ActionController::Base.cache_store = :memory_store

Then cache like this:

Rails.cache.write("permits", @permits)
Rails.cache.read("permits")

Also see: http://railscasts.com/episodes/115-caching-in-rails-2-1

Advanced Caching

Along with the built-in mechanisms outlined above, a number of excellent plugins exist to help with finer grained control over caching. These include Chris Wanstrath’s excellent cache_fu plugin (more info here ) and Evan Weaver’s interlock plugin (more info here ). Both of these plugins play nice with memcached and are a must-see for anyone seriously considering optimizing their caching needs.

Also the new Cache money plugin is supposed to be mad cool.

  1. Then how do we invalidate the Permits Ability cache?

After a change to the file, read the time last modified of the file (or timestamp of key in filestore?)

Rails.cache.read("permits-changed")

Then compare the dates? is the file more recent?

Rails.cache.write("permits-last-changed", time_changed)

And then regenerate the Permits::Ability object and save it to the cache again!

@permits = Permits::Ability.new ....
Rails.cache.write("permits", @permits)

I think this could work!

More static configuration

For the Group licenses to work, I propose something like this:

Group Models of similar kind of “type” (that often have shared permission)

  1. categories.yml
    categories:
    articles:
    – Article
    – Post

group_licenses.yml


bloggers:
can:
manage:
– Article
– Post

publishers:
can:
manage:
– @articles
cannot:
manage:
– User

Note here the special @articles which should point to a category in the categories.yml file.
Then we just need to add the logic to use/apply this new meta logic (partly done!).

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

Copyright © 2010 Kristian Mandrup. See LICENSE for details.

About

Role specific Permits for use with CanCan permission system

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages