diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 5f4c5e9c..025a8f07 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,5 +1,7 @@ 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 * Properly handle Admin::AbilitiesController in params[:controller] - see issue #46 diff --git a/README.rdoc b/README.rdoc index 0bf54855..03cb4e8c 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,17 +1,17 @@ = 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 -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" - + And then install the gem. sudo rake gems:install @@ -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. -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| - article && article.user == user + can :read, Project, :active => true, :user_id => user.id + +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 -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). diff --git a/lib/cancan.rb b/lib/cancan.rb index 855e31d3..b39cf933 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -2,6 +2,9 @@ module CanCan # This error is raised when a user isn't allowed to access a given # controller action. See ControllerAdditions#unauthorized! for details. class AccessDenied < StandardError; end + + # A general CanCan exception + class Error < StandardError; end end require 'cancan/ability' diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 9c7504c2..b4298c5a 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -50,13 +50,9 @@ module Ability # end # def can?(action, noun, *extra_args) - (@can_definitions || []).reverse.each do |base_behavior, defined_action, defined_noun, defined_block| - defined_actions = expand_actions(defined_action) - defined_nouns = [defined_noun].flatten - 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 + matching_can_definition(action, noun) do |base_behavior, defined_actions, defined_nouns, defined_conditions, defined_block| + result = can_perform_action?(action, noun, defined_actions, defined_nouns, defined_conditions, defined_block, extra_args) + return base_behavior ? result : !result end false end @@ -79,17 +75,27 @@ def cannot?(*args) # can [:update, :destroy], [Article, Comment] # # In this case the user has the ability to update or destroy both articles and comments. + # + # You can pass a hash of conditions as the third argument. # - # You can pass a block to provide logic based on the article's attributes. + # can :read, Project, :active => true, :user_id => user.id + # + # 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, Article do |article| - # article && article.user == user + # can :update, Project do |project| + # project && project.groups.include?(user.group) # 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, # 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). # @@ -112,9 +118,9 @@ def cannot?(*args) # can :read, :stats # can? :read, :stats # => true # - def can(action, noun, &block) + def can(action, noun, conditions = nil, &block) @can_definitions ||= [] - @can_definitions << [true, action, noun, block] + @can_definitions << [true, action, noun, conditions, block] end # Define an ability which cannot be done. Accepts the same arguments as "can". @@ -129,9 +135,9 @@ def can(action, noun, &block) # product.invisible? # end # - def cannot(action, noun, &block) + def cannot(action, noun, conditions = nil, &block) @can_definitions ||= [] - @can_definitions << [false, action, noun, block] + @can_definitions << [false, action, noun, conditions, block] end # Alias one or more actions into another one. @@ -179,8 +185,39 @@ def clear_aliased_actions @aliased_actions = {} 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 + 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 { :read => [:index, :show], @@ -199,16 +236,22 @@ def expand_actions(actions) end.flatten end - def can_perform_action?(action, noun, defined_actions, defined_nouns, defined_block, extra_args) - if defined_block.nil? - true - else + def can_perform_action?(action, noun, defined_actions, defined_nouns, defined_conditions, defined_block, extra_args) + if defined_block block_args = [] 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 ? nil : noun) 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 diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index 55e6b3f9..0499d13b 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -154,6 +154,10 @@ def current_ability ::Ability.new(current_user) 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 # and object. # @@ -167,7 +171,7 @@ def current_ability # # This simply calls "can?" on the current_ability. See Ability#can?. def can?(*args) - (@current_ability ||= current_ability).can?(*args) + cached_current_ability.can?(*args) end # Convenience method which works the same as "can?" but returns the opposite value. @@ -175,7 +179,7 @@ def can?(*args) # cannot? :destroy, @project # def cannot?(*args) - (@current_ability ||= current_ability).cannot?(*args) + cached_current_ability.cannot?(*args) end end end diff --git a/lib/cancan/controller_resource.rb b/lib/cancan/controller_resource.rb index 9a08a6af..39bd8197 100644 --- a/lib/cancan/controller_resource.rb +++ b/lib/cancan/controller_resource.rb @@ -1,7 +1,7 @@ module CanCan class ControllerResource # :nodoc: 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 @name = name @parent = parent diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index d986fcb4..103b4e37 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -140,4 +140,32 @@ @ability.can?(:read, 2, 1).should be_true @ability.can?(:read, 2, 3).should be_false 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 diff --git a/spec/cancan/controller_resource_spec.rb b/spec/cancan/controller_resource_spec.rb index 721cf462..64f639ce 100644 --- a/spec/cancan/controller_resource_spec.rb +++ b/spec/cancan/controller_resource_spec.rb @@ -54,6 +54,6 @@ it "should raise an exception when specifying :class option since it is no longer used" do lambda { CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person) - }.should raise_error + }.should raise_error(CanCan::Error) end end