Merge remote branch 'upstream/master'

Conflicts:
	lib/cancan/ability.rb
	lib/cancan/active_record_additions.rb
	lib/cancan/can_definition.rb
	spec/cancan/ability_spec.rb
This commit is contained in:
Yura Sokolov 2010-05-24 15:17:10 +04:00
commit 46f03013f3
16 changed files with 320 additions and 238 deletions

View File

@ -1,3 +1,11 @@
* Load nested parent resources on collection actions such as "index" (thanks dohzya)
* Adding :name option to load_and_authorize_resource if it does not match controller - see issue #65
* Fixing issue when using accessible_by with nil can conditions (thanks jrallison) - see issue #66
* Pluralize table name for belongs_to associations in can conditions hash (thanks logandk) - see issue #62
* Support has_many association or arrays in can conditions hash * Support has_many association or arrays in can conditions hash
* Adding joins clause to accessible_by when conditions are across associations * Adding joins clause to accessible_by when conditions are across associations

View File

@ -16,7 +16,7 @@ module CanCan
# end # end
# #
module Ability module Ability
# Use to check the user's permission for a given action and object. # Use to check if the user has permission to perform a given action on an object.
# #
# can? :destroy, @project # can? :destroy, @project
# #
@ -47,6 +47,7 @@ module CanCan
# assert ability.cannot?(:destroy, Project.new) # assert ability.cannot?(:destroy, Project.new)
# end # end
# #
# Also see the RSpec Matchers to aid in testing.
def can?(action, subject, *extra_args) def can?(action, subject, *extra_args)
raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
matching_can_definition(action, subject) do |can_definition| matching_can_definition(action, subject) do |can_definition|
@ -80,8 +81,8 @@ module CanCan
# #
# can :read, Project, :active => true, :user_id => user.id # 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 # Here the user can only see active projects which he owns. See ActiveRecordAdditions#accessible_by
# use this in database queries. # for how 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 # 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. # write any Ruby code you want.
@ -122,7 +123,7 @@ module CanCan
can_definitions << CanDefinition.new(true, action, subject, conditions, block) can_definitions << CanDefinition.new(true, action, subject, conditions, block)
end end
# Define an ability which cannot be done. Accepts the same arguments as "can". # Defines an ability which cannot be done. Accepts the same arguments as "can".
# #
# can :read, :all # can :read, :all
# cannot :read, Comment # cannot :read, Comment
@ -198,14 +199,17 @@ module CanCan
# If the ability is not defined then false is returned so be sure to take that into consideration. # 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 # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
# determined from that. # determined from that.
def conditions(action, subject) def conditions(action, subject, options = {})
matched = matching_can_definition(action, subject) matched = matching_can_definition(action, subject)
unless matched.empty? unless matched.empty?
if matched.any?{|can_definition| can_definition.conditions.nil? && can_definition.block } if matched.any?{|can_definition|
cond = can_definition.conditions
(cond.nil? || cond.empty?) && can_definition.block
}
raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}"
end end
matched.map{|can_definition| matched.map{|can_definition|
[can_definition.base_behavior, can_definition.conditions] [can_definition.base_behavior, can_definition.conditions(options)]
} }
else else
false false
@ -226,8 +230,8 @@ module CanCan
# If there is just one :can ability, it conditions returned untouched. # If there is just one :can ability, it conditions returned untouched.
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
# determined from that. # determined from that.
def sql_conditions(action, subject) def sql_conditions(action, subject, options = {})
conds = conditions(action, subject) conds = conditions(action, subject, options)
return false if conds == false return false if conds == false
return (conds[0][1] || {}) if conds.size==1 && conds[0][0] == true # to match previous spec return (conds[0][1] || {}) if conds.size==1 && conds[0][0] == true # to match previous spec
@ -235,7 +239,7 @@ module CanCan
false_cond = subject.send(:sanitize_sql, ['?=?', true, false]) false_cond = subject.send(:sanitize_sql, ['?=?', true, false])
conds.reverse.inject(nil) do |sql, action| conds.reverse.inject(nil) do |sql, action|
behavior, condition = action behavior, condition = action
if condition if condition && !condition.empty?
condition = "#{subject.send(:sanitize_sql, condition)}" condition = "#{subject.send(:sanitize_sql, condition)}"
condition = "not (#{condition})" if behavior == false condition = "not (#{condition})" if behavior == false
else else

View File

@ -1,5 +1,5 @@
module CanCan module CanCan
# This module is automatically included into all Active Record. # This module is automatically included into all Active Record models.
module ActiveRecordAdditions module ActiveRecordAdditions
module ClassMethods module ClassMethods
# Returns a scope which fetches only the records that the passed ability # Returns a scope which fetches only the records that the passed ability
@ -20,7 +20,7 @@ module CanCan
# Here only the articles which the user can update are returned. This # Here only the articles which the user can update are returned. This
# internally uses Ability#conditions method, see that for more information. # internally uses Ability#conditions method, see that for more information.
def accessible_by(ability, action = :read) def accessible_by(ability, action = :read)
conditions = ability.sql_conditions(action, self) || {:id => nil} conditions = ability.sql_conditions(action, self, :tableize => true) || {:id => nil}
joins = ability.association_joins(action, self) joins = ability.association_joins(action, self)
if respond_to? :where if respond_to? :where
where(conditions).joins(joins) where(conditions).joins(joins)

View File

@ -1,16 +1,26 @@
module CanCan module CanCan
# This class is used internally and should only be called through Ability. # This class is used internally and should only be called through Ability.
# it holds the information about a "can" call made on Ability and provides
# helpful methods to determine permission checking and conditions hash generation.
class CanDefinition # :nodoc: class CanDefinition # :nodoc:
attr_reader :conditions, :block, :base_behavior, :definitive attr_reader :conditions, :block, :base_behavior, :definitive
include ActiveSupport::Inflector
# The first argument when initializing is the base_behavior which is a true/false
# value. True for "can" and false for "cannot". The next two arguments are the action
# and subject respectively (such as :read, @project). The third argument is a hash
# of conditions and the last one is the block passed to the "can" call.
def initialize(base_behavior, action, subject, conditions, block) def initialize(base_behavior, action, subject, conditions, block)
@base_behavior = base_behavior @base_behavior = base_behavior
@actions = [action].flatten @actions = [action].flatten
@subjects = [subject].flatten @subjects = [subject].flatten
@conditions = conditions @conditions = conditions || {}
@block = block @block = block
end end
# Accepts a hash of aliased actions and returns an array of actions which match.
# This should be called before "matches?" and other checking methods since they
# rely on the actions to be expanded.
def expand_actions(aliased_actions) def expand_actions(aliased_actions)
@expanded_actions = @actions.map do |action| @expanded_actions = @actions.map do |action|
aliased_actions[action] ? [action, *aliased_actions[action]] : action aliased_actions[action] ? [action, *aliased_actions[action]] : action
@ -27,9 +37,23 @@ module CanCan
@base_behavior ? result : !result @base_behavior ? result : !result
end end
# Returns a hash of conditions. If the ":tableize => true" option is passed
# it will pluralize the association conditions to match the table name.
def conditions(options = {})
if options[:tableize] && @conditions.kind_of?(Hash)
@conditions.inject({}) do |tableized_conditions, (name, value)|
name = tableize(name).to_sym if value.kind_of? Hash
tableized_conditions[name] = value
tableized_conditions
end
else
@conditions
end
end
def association_joins(conditions = @conditions) def association_joins(conditions = @conditions)
joins = [] joins = []
(conditions || []).each do |name, value| conditions.each do |name, value|
if value.kind_of? Hash if value.kind_of? Hash
nested = association_joins(value) nested = association_joins(value)
if nested if nested

View File

@ -59,6 +59,11 @@ module CanCan
# #
# load_resource :nested => [:publisher, :author] # load_resource :nested => [:publisher, :author]
# #
# [:+name+]
# The name of the resource if it cannot be determined from controller (string or symbol).
#
# load_resource :name => :article
#
# [:+resource+] # [:+resource+]
# The class to use for the model (string or constant). # The class to use for the model (string or constant).
# #
@ -102,6 +107,11 @@ module CanCan
# [:+except+] # [:+except+]
# Does not apply before filter to given actions. # Does not apply before filter to given actions.
# #
# [:+name+]
# The name of the resource if it cannot be determined from controller (string or symbol).
#
# load_resource :name => :article
#
# [:+resource+] # [:+resource+]
# The class to use for the model (string or constant). Alternatively pass a symbol # The class to use for the model (string or constant). Alternatively pass a symbol
# to represent a resource which does not have a class. # to represent a resource which does not have a class.

View File

@ -1,5 +1,7 @@
module CanCan module CanCan
# Used internally to load and authorize a given controller resource. # Used internally to load and authorize a given controller resource.
# This manages finding or building an instance of the resource. If a
# parent is given it will go through the association.
class ControllerResource # :nodoc: class ControllerResource # :nodoc:
def initialize(controller, name, parent = nil, options = {}) def initialize(controller, name, parent = nil, options = {})
raise ImplementationRemoved, "The :class option has been renamed to :resource for specifying the class in CanCan." if options.has_key? :class raise ImplementationRemoved, "The :class option has been renamed to :resource for specifying the class in CanCan." if options.has_key? :class
@ -9,6 +11,9 @@ module CanCan
@options = options @options = options
end end
# Returns the class used for this resource. This can be overriden by the :resource option.
# Sometimes one will use a symbol as the resource if a class does not exist for it. In that
# case "find" and "build" should not be called on it.
def model_class def model_class
resource_class = @options[:resource] resource_class = @options[:resource]
if resource_class.nil? if resource_class.nil?
@ -16,7 +21,7 @@ module CanCan
elsif resource_class.kind_of? String elsif resource_class.kind_of? String
resource_class.constantize resource_class.constantize
else else
resource_class # likely a symbol resource_class # could be a symbol
end end
end end
@ -24,12 +29,10 @@ module CanCan
self.model_instance ||= base.find(id) self.model_instance ||= base.find(id)
end end
# Build a new instance of this resource. If it is a class we just call "new" otherwise
# it's an associaiton and "build" is used.
def build(attributes) def build(attributes)
if base.kind_of? Class self.model_instance ||= (base.kind_of?(Class) ? base.new(attributes) : base.build(attributes))
self.model_instance ||= base.new(attributes)
else
self.model_instance ||= base.build(attributes)
end
end end
def model_instance def model_instance
@ -42,6 +45,8 @@ module CanCan
private private
# The object that methods (such as "find", "new" or "build") are called on.
# If there is a parent it will be the association, otherwise it will be the model's class.
def base def base
@parent ? @parent.model_instance.send(@name.to_s.pluralize) : model_class @parent ? @parent.model_instance.send(@name.to_s.pluralize) : model_class
end end

View File

@ -18,7 +18,7 @@ module CanCan
# exception.action # => :read # exception.action # => :read
# exception.subject # => Article # exception.subject # => Article
# #
# If the message is not specified (or is nil) it will default to "You are anot authorized # If the message is not specified (or is nil) it will default to "You are not authorized
# to access this page." This default can be overridden by setting default_message. # to access this page." This default can be overridden by setting default_message.
# #
# exception.default_message = "Default error message" # exception.default_message = "Default error message"

View File

@ -20,7 +20,9 @@ module CanCan
end end
def load_resource def load_resource
unless collection_actions.include? @params[:action].to_sym if collection_actions.include? @params[:action].to_sym
parent_resource
else
if new_actions.include? @params[:action].to_sym if new_actions.include? @params[:action].to_sym
resource.build(@params[model_name.to_sym]) resource.build(@params[model_name.to_sym])
elsif @params[:id] elsif @params[:id]
@ -54,7 +56,7 @@ module CanCan
end end
def model_name def model_name
@params[:controller].sub("Controller", "").underscore.split('/').last.singularize @options[:name] || @params[:controller].sub("Controller", "").underscore.split('/').last.singularize
end end
def collection_actions def collection_actions

View File

@ -226,7 +226,7 @@ describe CanCan::Ability do
it "should return an array with just behavior for conditions when there are no conditions" do it "should return an array with just behavior for conditions when there are no conditions" do
@ability.can :read, Array @ability.can :read, Array
@ability.conditions(:show, Array).should == [ [true, nil] ] @ability.conditions(:show, Array).should == [ [true, {}] ]
end end
it "should return false when performed on an action which isn't defined" do it "should return false when performed on an action which isn't defined" do

View File

@ -16,13 +16,13 @@ describe CanCan::ActiveRecordAdditions do
it "should call where with matching ability conditions" do it "should call where with matching ability conditions" do
@ability.can :read, @model_class, :foo => {:bar => 1} @ability.can :read, @model_class, :foo => {:bar => 1}
stub(@model_class).where(:foo => { :bar => 1 }).stub!.joins([:foo]) { :found_records } stub(@model_class).where(:foos => { :bar => 1 }).stub!.joins([:foo]) { :found_records }
@model_class.accessible_by(@ability, :read).should == :found_records @model_class.accessible_by(@ability, :read).should == :found_records
end end
it "should default to :read ability and use scoped when where isn't available" do it "should default to :read ability and use scoped when where isn't available" do
@ability.can :read, @model_class, :foo => {:bar => 1} @ability.can :read, @model_class, :foo => {:bar => 1}
stub(@model_class).scoped(:conditions => {:foo => {:bar => 1}}, :joins => [:foo]) { :found_records } stub(@model_class).scoped(:conditions => {:foos => {:bar => 1}}, :joins => [:foo]) { :found_records }
@model_class.accessible_by(@ability).should == :found_records @model_class.accessible_by(@ability).should == :found_records
end end
end end

View File

@ -30,4 +30,15 @@ describe CanCan::CanDefinition do
@conditions[:foo] = {:bar => {1 => 2}} @conditions[:foo] = {:bar => {1 => 2}}
@can.association_joins.should == [{:foo => [{:bar=>[]}]}] @can.association_joins.should == [{:foo => [{:bar=>[]}]}]
end end
it "should return table names in conditions for association joins" do
@conditions[:foo] = {:bar => 1}
@conditions[:test] = 1
@can.conditions(:tableize => true).should == { :foos => { :bar => 1}, :test => 1 }
end
it "should return no association joins if conditions is nil" do
can = CanCan::CanDefinition.new(true, :read, Integer, nil, nil)
can.association_joins.should be_nil
end
end end

View File

@ -86,12 +86,23 @@ describe CanCan::ResourceAuthorization do
end end
it "should load nested resource and fetch other resource through the association" do it "should load nested resource and fetch other resource through the association" do
stub(Person).find(456).stub!.abilities.stub!.find(123) { :some_ability } person = Object.new
stub(Person).find(456) { person }
stub(person).abilities.stub!.find(123) { :some_ability }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "show", :id => 123, :person_id => 456}, {:nested => :person}) authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "show", :id => 123, :person_id => 456}, {:nested => :person})
authorization.load_resource authorization.load_resource
@controller.instance_variable_get(:@person).should == person
@controller.instance_variable_get(:@ability).should == :some_ability @controller.instance_variable_get(:@ability).should == :some_ability
end end
it "should load nested resource for collection action" do
person = Object.new
stub(Person).find(456) { person }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "index", :person_id => 456}, {:nested => :person})
authorization.load_resource
@controller.instance_variable_get(:@person).should == person
end
it "should load nested resource and build resource through a deep association" do it "should load nested resource and build resource through a deep association" do
stub(Person).find(456).stub!.behaviors.stub!.find(789).stub!.abilities.stub!.build(nil) { :some_ability } stub(Person).find(456).stub!.behaviors.stub!.find(789).stub!.abilities.stub!.build(nil) { :some_ability }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "new", :person_id => 456, :behavior_id => 789}, {:nested => [:person, :behavior]}) authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "new", :person_id => 456, :behavior_id => 789}, {:nested => [:person, :behavior]})
@ -114,4 +125,11 @@ describe CanCan::ResourceAuthorization do
authorization.load_resource authorization.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource @controller.instance_variable_get(:@ability).should == :some_resource
end end
it "should use :name option to determine resource name" do
stub(Ability).find(123) { :some_resource }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "foo", :action => "show", :id => 123}, {:name => :ability})
authorization.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
end end