Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

adding conditions behavior to Ability#can and fetch with Ability#cond…

…itions - closes #53
  • Loading branch information...
commit baeef0b9dd2019f78f5aeb3e097d6067b537c561 1 parent 23a5888
@ryanb authored
View
2  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
View
28 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).
View
3  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'
View
83 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
View
8 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
View
2  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
View
28 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
View
2  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
Please sign in to comment.
Something went wrong with that request. Please try again.