adding conditions behavior to Ability#can and fetch with Ability#conditions - closes #53

This commit is contained in:
Ryan Bates 2010-04-15 16:50:47 -07:00
parent 23a5888fe0
commit baeef0b9dd
8 changed files with 122 additions and 34 deletions

View File

@ -1,5 +1,7 @@
1.1.0 (not released) 1.1.0 (not released)
* Adding conditions behavior to Ability#can and fetch with Ability#conditions - see issue #53
* Renaming :class option to :resource for load_and_authorize_resource which now supports a symbol for non models - see issue #45 * Renaming :class option to :resource for load_and_authorize_resource which now supports a symbol for non models - see issue #45
* Properly handle Admin::AbilitiesController in params[:controller] - see issue #46 * Properly handle Admin::AbilitiesController in params[:controller] - see issue #46

View File

@ -1,14 +1,14 @@
= CanCan = CanCan
RDocs[http://rdoc.info/projects/ryanb/cancan] | Wiki[http://wiki.github.com/ryanb/cancan] | Screencast[http://railscasts.com/episodes/192-authorization-with-cancan] | Metrics[http://getcaliper.com/caliper/project?repo=git%3A%2F%2Fgithub.com%2Fryanb%2Fcancan.git] | Tests[http://runcoderun.com/ryanb/cancan] RDocs[http://rdoc.info/projects/ryanb/cancan] | Wiki[http://wiki.github.com/ryanb/cancan] | Screencast[http://railscasts.com/episodes/192-authorization-with-cancan] | Metrics[http://getcaliper.com/caliper/project?repo=git%3A%2F%2Fgithub.com%2Fryanb%2Fcancan.git]
This is a simple authorization solution for Ruby on Rails to restrict what a given user is allowed to access in the application. This is completely decoupled from any role based implementation allowing you to define user roles the way you want. All permissions are stored in a single location for convenience. This is a simple authorization solution for Ruby on Rails to restrict what a given user is allowed to access in the application. This is completely decoupled from any role based implementation allowing you to define user roles the way you want. All permissions are stored in a single location and not duplicated across the controller, view, and database.
This assumes you already have authentication (such as Authlogic[http://github.com/binarylogic/authlogic]) which provides a current_user model. This assumes you already have authentication (such as Authlogic[http://github.com/binarylogic/authlogic] or Devise[http://github.com/plataformatec/devise]) which provides a +current_user+ model.
== Installation == Installation
You can set it up as a gem in your environment.rb file. You can set CanCan up as a gem in your environment.rb file.
config.gem "cancan" config.gem "cancan"
@ -86,13 +86,21 @@ You can pass an array for either of these parameters to match any one.
In this case the user has the ability to update or destroy both articles and comments. In this case the user has the ability to update or destroy both articles and comments.
You can pass a block to provide logic based on the article's attributes. You can pass a hash of conditions as the third argument.
can :update, Article do |article| can :read, Project, :active => true, :user_id => user.id
article && article.user == user
Here the user can only see active projects which he owns. See ControllerAdditions#conditions for a way to use this in database queries.
If the conditions hash does not give you enough control over defining abilities, you can use a block to write any Ruby code you want.
can :update, Project do |project|
project && project.groups.include?(user.group)
end end
If the block returns true then the user has that :update ability for that article, otherwise he will be denied access. It's possible for the passed in model to be nil if one isn't specified, so be sure to take that into consideration. If the block returns true then the user has that :update ability for that project, otherwise he will be denied access. It's possible for the passed in model to be nil if one isn't specified, so be sure to take that into consideration.
The downside to using a block is that it cannot be used to generate conditions for database queries.
You can pass :all to reference every type of object. In this case the object type will be passed into the block as well (just in case object is nil). You can pass :all to reference every type of object. In this case the object type will be passed into the block as well (just in case object is nil).

View File

@ -2,6 +2,9 @@ module CanCan
# This error is raised when a user isn't allowed to access a given # This error is raised when a user isn't allowed to access a given
# controller action. See ControllerAdditions#unauthorized! for details. # controller action. See ControllerAdditions#unauthorized! for details.
class AccessDenied < StandardError; end class AccessDenied < StandardError; end
# A general CanCan exception
class Error < StandardError; end
end end
require 'cancan/ability' require 'cancan/ability'

View File

@ -50,13 +50,9 @@ module CanCan
# end # end
# #
def can?(action, noun, *extra_args) def can?(action, noun, *extra_args)
(@can_definitions || []).reverse.each do |base_behavior, defined_action, defined_noun, defined_block| matching_can_definition(action, noun) do |base_behavior, defined_actions, defined_nouns, defined_conditions, defined_block|
defined_actions = expand_actions(defined_action) result = can_perform_action?(action, noun, defined_actions, defined_nouns, defined_conditions, defined_block, extra_args)
defined_nouns = [defined_noun].flatten return base_behavior ? result : !result
if includes_action?(defined_actions, action) && includes_noun?(defined_nouns, noun)
result = can_perform_action?(action, noun, defined_actions, defined_nouns, defined_block, extra_args)
return base_behavior ? result : !result
end
end end
false false
end end
@ -80,16 +76,26 @@ module CanCan
# #
# In this case the user has the ability to update or destroy both articles and comments. # In this case the user has the ability to update or destroy both articles and comments.
# #
# You can pass a block to provide logic based on the article's attributes. # You can pass a hash of conditions as the third argument.
# #
# can :update, Article do |article| # can :read, Project, :active => true, :user_id => user.id
# article && article.user == user #
# Here the user can only see active projects which he owns. See ControllerAdditions#conditions for a way to
# use this in database queries.
#
# If the conditions hash does not give you enough control over defining abilities, you can use a block to
# write any Ruby code you want.
#
# can :update, Project do |project|
# project && project.groups.include?(user.group)
# end # end
# #
# If the block returns true then the user has that :update ability for that article, otherwise he # If the block returns true then the user has that :update ability for that project, otherwise he
# will be denied access. It's possible for the passed in model to be nil if one isn't specified, # will be denied access. It's possible for the passed in model to be nil if one isn't specified,
# so be sure to take that into consideration. # so be sure to take that into consideration.
# #
# The downside to using a block is that it cannot be used to generate conditions for database queries.
#
# You can pass :all to reference every type of object. In this case the object type will be passed # You can pass :all to reference every type of object. In this case the object type will be passed
# into the block as well (just in case object is nil). # into the block as well (just in case object is nil).
# #
@ -112,9 +118,9 @@ module CanCan
# can :read, :stats # can :read, :stats
# can? :read, :stats # => true # can? :read, :stats # => true
# #
def can(action, noun, &block) def can(action, noun, conditions = nil, &block)
@can_definitions ||= [] @can_definitions ||= []
@can_definitions << [true, action, noun, block] @can_definitions << [true, action, noun, conditions, block]
end end
# Define an ability which cannot be done. Accepts the same arguments as "can". # Define an ability which cannot be done. Accepts the same arguments as "can".
@ -129,9 +135,9 @@ module CanCan
# product.invisible? # product.invisible?
# end # end
# #
def cannot(action, noun, &block) def cannot(action, noun, conditions = nil, &block)
@can_definitions ||= [] @can_definitions ||= []
@can_definitions << [false, action, noun, block] @can_definitions << [false, action, noun, conditions, block]
end end
# Alias one or more actions into another one. # Alias one or more actions into another one.
@ -179,8 +185,39 @@ module CanCan
@aliased_actions = {} @aliased_actions = {}
end end
# Returns a hash of conditions which match the given ability. This is useful if you need to generate a database
# query based on the current ability.
#
# can :read, Article, :visible => true
# conditions :read, Article # returns { :visible => true }
#
# For example, you can use this in Active Record find conditions to only fetch articles the user has permission to read.
#
# Article.where(current_ability.conditions(:read, Article))
#
# If the ability is not defined then false is returned so be sure to take that into consideration.
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
# determined from that.
def conditions(action, noun)
matching_can_definition(action, noun) do |base_behavior, defined_actions, defined_nouns, defined_conditions, defined_block|
raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{noun.inspect}" if defined_block
return defined_conditions || {}
end
false
end
private private
def matching_can_definition(action, noun, &block)
(@can_definitions || []).reverse.each do |base_behavior, defined_action, defined_noun, defined_conditions, defined_block|
defined_actions = expand_actions(defined_action)
defined_nouns = [defined_noun].flatten
if includes_action?(defined_actions, action) && includes_noun?(defined_nouns, noun)
return block.call(base_behavior, defined_actions, defined_nouns, defined_conditions, defined_block)
end
end
end
def default_alias_actions def default_alias_actions
{ {
:read => [:index, :show], :read => [:index, :show],
@ -199,16 +236,22 @@ module CanCan
end.flatten end.flatten
end end
def can_perform_action?(action, noun, defined_actions, defined_nouns, defined_block, extra_args) def can_perform_action?(action, noun, defined_actions, defined_nouns, defined_conditions, defined_block, extra_args)
if defined_block.nil? if defined_block
true
else
block_args = [] block_args = []
block_args << action if defined_actions.include?(:manage) block_args << action if defined_actions.include?(:manage)
block_args << (noun.class == Class ? noun : noun.class) if defined_nouns.include?(:all) block_args << (noun.class == Class ? noun : noun.class) if defined_nouns.include?(:all)
block_args << (noun.class == Class ? nil : noun) block_args << (noun.class == Class ? nil : noun)
block_args += extra_args block_args += extra_args
return defined_block.call(*block_args) defined_block.call(*block_args)
elsif defined_conditions
if noun.class != Class
defined_conditions.all? do |name, value|
noun.send(name) == value
end
end
else
true
end end
end end

View File

@ -154,6 +154,10 @@ module CanCan
::Ability.new(current_user) ::Ability.new(current_user)
end end
def cached_current_ability
@current_ability ||= current_ability
end
# Use in the controller or view to check the user's permission for a given action # Use in the controller or view to check the user's permission for a given action
# and object. # and object.
# #
@ -167,7 +171,7 @@ module CanCan
# #
# This simply calls "can?" on the current_ability. See Ability#can?. # This simply calls "can?" on the current_ability. See Ability#can?.
def can?(*args) def can?(*args)
(@current_ability ||= current_ability).can?(*args) cached_current_ability.can?(*args)
end end
# Convenience method which works the same as "can?" but returns the opposite value. # Convenience method which works the same as "can?" but returns the opposite value.
@ -175,7 +179,7 @@ module CanCan
# cannot? :destroy, @project # cannot? :destroy, @project
# #
def cannot?(*args) def cannot?(*args)
(@current_ability ||= current_ability).cannot?(*args) cached_current_ability.cannot?(*args)
end end
end end
end end

View File

@ -1,7 +1,7 @@
module CanCan module CanCan
class ControllerResource # :nodoc: class ControllerResource # :nodoc:
def initialize(controller, name, parent = nil, options = {}) def initialize(controller, name, parent = nil, options = {})
raise "The :class option has been renamed to :resource for specifying the class in CanCan." if options.has_key? :class raise CanCan::Error, "The :class option has been renamed to :resource for specifying the class in CanCan." if options.has_key? :class
@controller = controller @controller = controller
@name = name @name = name
@parent = parent @parent = parent

View File

@ -140,4 +140,32 @@ describe CanCan::Ability do
@ability.can?(:read, 2, 1).should be_true @ability.can?(:read, 2, 1).should be_true
@ability.can?(:read, 2, 3).should be_false @ability.can?(:read, 2, 3).should be_false
end end
it "should use conditions as third parameter and determine abilities from it" do
@ability.can :read, Array, :first => 1, :last => 3
@ability.can?(:read, [1, 2, 3]).should be_true
@ability.can?(:read, [1, 2, 3, 4]).should be_false
@ability.can?(:read, Array).should be_false
end
it "should return conditions for a given ability" do
@ability.can :read, Array, :first => 1, :last => 3
@ability.conditions(:show, Array).should == {:first => 1, :last => 3}
end
it "should raise an exception when a block is used on condition" do
@ability.can :read, Array do |a|
true
end
lambda { @ability.conditions(:show, Array) }.should raise_error(CanCan::Error, "Cannot determine ability conditions from block for :show Array")
end
it "should return an empty hash for conditions when there are no conditions" do
@ability.can :read, Array
@ability.conditions(:show, Array).should == {}
end
it "should return false when performed on an action which isn't defined" do
@ability.conditions(:foo, Array).should == false
end
end end

View File

@ -54,6 +54,6 @@ describe CanCan::ControllerResource do
it "should raise an exception when specifying :class option since it is no longer used" do it "should raise an exception when specifying :class option since it is no longer used" do
lambda { lambda {
CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person) CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person)
}.should raise_error }.should raise_error(CanCan::Error)
end end
end end