Skip to content
This repository has been archived by the owner on Dec 12, 2021. It is now read-only.

Commit

Permalink
adding conditions behavior to Ability#can and fetch with Ability#cond…
Browse files Browse the repository at this point in the history
…itions - closes #53
  • Loading branch information
ryanb committed Apr 15, 2010
1 parent 23a5888 commit baeef0b
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 34 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rdoc
@@ -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
Expand Down
28 changes: 18 additions & 10 deletions README.rdoc
@@ -1,17 +1,17 @@
= 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"

And then install the gem. And then install the gem.


sudo rake gems:install sudo rake gems:install
Expand Down Expand Up @@ -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).


Expand Down
3 changes: 3 additions & 0 deletions lib/cancan.rb
Expand Up @@ -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'
Expand Down
83 changes: 63 additions & 20 deletions lib/cancan/ability.rb
Expand Up @@ -50,13 +50,9 @@ module Ability
# 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
Expand All @@ -79,17 +75,27 @@ def cannot?(*args)
# can [:update, :destroy], [Article, Comment] # can [:update, :destroy], [Article, Comment]
# #
# 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 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| # can :update, Project do |project|
# article && article.user == user # 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).
# #
Expand All @@ -112,9 +118,9 @@ def cannot?(*args)
# 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".
Expand All @@ -129,9 +135,9 @@ def can(action, noun, &block)
# 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.
Expand Down Expand Up @@ -179,8 +185,39 @@ def clear_aliased_actions
@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],
Expand All @@ -199,16 +236,22 @@ def expand_actions(actions)
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


Expand Down
8 changes: 6 additions & 2 deletions lib/cancan/controller_additions.rb
Expand Up @@ -154,6 +154,10 @@ def current_ability
::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.
# #
Expand All @@ -167,15 +171,15 @@ def current_ability
# #
# 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.
# #
# 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
Expand Down
2 changes: 1 addition & 1 deletion lib/cancan/controller_resource.rb
@@ -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
Expand Down
28 changes: 28 additions & 0 deletions spec/cancan/ability_spec.rb
Expand Up @@ -140,4 +140,32 @@
@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
2 changes: 1 addition & 1 deletion spec/cancan/controller_resource_spec.rb
Expand Up @@ -54,6 +54,6 @@
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

0 comments on commit baeef0b

Please sign in to comment.