adding conditions behavior to Ability#can and fetch with Ability#conditions - closes #53
This commit is contained in:
parent
23a5888fe0
commit
baeef0b9dd
|
@ -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
|
||||||
|
|
24
README.rdoc
24
README.rdoc
|
@ -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).
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user