modifying Ability to use symbol for subject instead of class, also adding subject aliases

This commit is contained in:
Ryan Bates 2011-03-23 17:00:33 -07:00
parent 5d97cfb236
commit 98ed39264e
3 changed files with 313 additions and 347 deletions

View File

@ -8,7 +8,7 @@ module CanCan
# #
# def initialize(user) # def initialize(user)
# if user.admin? # if user.admin?
# can :manage, :all # can :access, :all
# else # else
# can :read, :all # can :read, :all
# end # end
@ -78,11 +78,11 @@ module CanCan
# #
# can [:update, :destroy], [Article, Comment] # can [:update, :destroy], [Article, Comment]
# #
# You can pass :all to match any object and :manage to match any action. Here are some examples. # You can pass :all to match any object and :access to match any action. Here are some examples.
# #
# can :manage, :all # can :access, :all
# can :update, :all # can :update, :all
# can :manage, Project # can :access, Project
# #
# You can pass a hash of conditions as the third argument. Here the user can only see active projects which he owns. # You can pass a hash of conditions as the third argument. Here the user can only see active projects which he owns.
# #
@ -158,11 +158,6 @@ module CanCan
# can :update, Comment # can :update, Comment
# can? :modify, Comment # => false # can? :modify, Comment # => false
# #
# Unless that exact alias is used.
#
# can :modify, Comment
# can? :modify, Comment # => true
#
# The following aliases are added by default for conveniently mapping common controller actions. # The following aliases are added by default for conveniently mapping common controller actions.
# #
# alias_action :index, :show, :to => :read # alias_action :index, :show, :to => :read
@ -172,18 +167,42 @@ module CanCan
# This way one can use params[:action] in the controller to determine the permission. # This way one can use params[:action] in the controller to determine the permission.
def alias_action(*args) def alias_action(*args)
target = args.pop[:to] target = args.pop[:to]
aliased_actions[target] ||= [] aliases[:actions][target] ||= []
aliased_actions[target] += args aliases[:actions][target] += args
end end
# Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key. # Alias one or more subjects into another one.
def aliased_actions #
@aliased_actions ||= default_alias_actions # alias_subject :admins, :moderators, :to => :users
# can :update, :users
#
# Then :modify permission will apply to both :update and :destroy requests.
#
# can? :update, :admins # => true
# can? :update, :moderators # => true
#
# This only works in one direction. Passing the aliased subject into the "can?" call
# will not work because aliases are meant to generate more generic subjects.
#
# alias_subject :admins, :moderators, :to => :users
# can :update, :admins
# can? :update, :users # => false
#
def alias_subject(*args)
target = args.pop[:to]
aliases[:subjects][target] ||= []
aliases[:subjects][target] += args
end end
# Removes previously aliased actions including the defaults. # Returns a hash of action and subject aliases.
def clear_aliased_actions def aliases
@aliased_actions = {} @aliases ||= default_aliases
end
# Removes previously aliased actions or subjects including the defaults.
def clear_aliases
aliases[:actions] = {}
aliases[:subjects] = {}
end end
def model_adapter(model_class, action) def model_adapter(model_class, action)
@ -206,7 +225,7 @@ module CanCan
def unauthorized_message(action, subject) def unauthorized_message(action, subject)
keys = unauthorized_message_keys(action, subject) keys = unauthorized_message_keys(action, subject)
variables = {:action => action.to_s} variables = {:action => action.to_s}
variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase variables[:subject] = (subject.kind_of?(Symbol) ? subject.to_s : subject.class.to_s.underscore.humanize.downcase.pluralize)
message = I18n.translate(nil, variables.merge(:scope => :unauthorized, :default => keys + [""])) message = I18n.translate(nil, variables.merge(:scope => :unauthorized, :default => keys + [""]))
message.blank? ? nil : message message.blank? ? nil : message
end end
@ -230,9 +249,9 @@ module CanCan
private private
def unauthorized_message_keys(action, subject) def unauthorized_message_keys(action, subject)
subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.kind_of? Symbol subject = (subject.kind_of?(Symbol) ? subject.to_s : subject.class.to_s.underscore.humanize.downcase.pluralize)
[subject, :all].map do |try_subject| [aliases_for(:subjects, subject.to_sym), :all].flatten.map do |try_subject|
[aliases_for_action(action), :manage].flatten.map do |try_action| [aliases_for(:actions, action.to_sym), :access].flatten.map do |try_action|
:"#{try_action}.#{try_subject}" :"#{try_action}.#{try_subject}"
end end
end.flatten end.flatten
@ -241,18 +260,18 @@ module CanCan
# Accepts an array of actions and returns an array of actions which match. # Accepts an array of actions and returns an array of actions which match.
# This should be called before "matches?" and other checking methods since they # This should be called before "matches?" and other checking methods since they
# rely on the actions to be expanded. # rely on the actions to be expanded.
def expand_actions(actions) def expand_aliases(type, items)
actions.map do |action| items.map do |item|
aliased_actions[action] ? [action, *expand_actions(aliased_actions[action])] : action aliases[type][item] ? [item, *expand_aliases(type, aliases[type][item])] : item
end.flatten end.flatten
end end
# Given an action, it will try to find all of the actions which are aliased to it. # Given an action, it will try to find all of the actions which are aliased to it.
# This does the opposite kind of lookup as expand_actions. # This does the opposite kind of lookup as expand_aliases.
def aliases_for_action(action) def aliases_for(type, action)
results = [action] results = [action]
aliased_actions.each do |aliased_action, actions| aliases[type].each do |aliased_action, actions|
results += aliases_for_action(aliased_action) if actions.include? action results += aliases_for(type, aliased_action) if actions.include? action
end end
results results
end end
@ -265,7 +284,8 @@ module CanCan
# This does not take into consideration any hash conditions or block statements # This does not take into consideration any hash conditions or block statements
def relevant_rules(action, subject) def relevant_rules(action, subject)
rules.reverse.select do |rule| rules.reverse.select do |rule|
rule.expanded_actions = expand_actions(rule.actions) rule.expanded_actions = expand_aliases(:actions, rule.actions)
rule.expanded_subjects = expand_aliases(:subjects, rule.subjects)
rule.relevant? action, subject rule.relevant? action, subject
end end
end end
@ -286,11 +306,15 @@ module CanCan
end end
end end
def default_alias_actions def default_aliases
{ {
:read => [:index, :show], :subjects => {},
:create => [:new], :actions => {
:update => [:edit], :read => [:index, :show],
:create => [:new],
:update => [:edit],
:destroy => [:delete],
}
} }
end end
end end

View File

@ -4,7 +4,7 @@ module CanCan
# helpful methods to determine permission checking and conditions hash generation. # helpful methods to determine permission checking and conditions hash generation.
class Rule # :nodoc: class Rule # :nodoc:
attr_reader :base_behavior, :subjects, :actions, :conditions attr_reader :base_behavior, :subjects, :actions, :conditions
attr_writer :expanded_actions attr_writer :expanded_actions, :expanded_subjects
# The first argument when initializing is the base_behavior which is a true/false # 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 # value. True for "can" and false for "cannot". The next two arguments are the action
@ -30,11 +30,11 @@ module CanCan
def matches_conditions?(action, subject, extra_args) def matches_conditions?(action, subject, extra_args)
if @match_all if @match_all
call_block_with_all(action, subject, extra_args) call_block_with_all(action, subject, extra_args)
elsif @block && !subject_class?(subject) elsif @block && subject_object?(subject)
@block.call(subject, *extra_args) @block.call(subject, *extra_args)
elsif @conditions.kind_of?(Hash) && subject.class == Hash elsif @conditions.kind_of?(Hash) && subject.class == Hash
nested_subject_matches_conditions?(subject) nested_subject_matches_conditions?(subject)
elsif @conditions.kind_of?(Hash) && !subject_class?(subject) elsif @conditions.kind_of?(Hash) && subject_object?(subject)
matches_conditions_hash?(subject) matches_conditions_hash?(subject)
else else
# Don't stop at "cannot" definitions when there are conditions. # Don't stop at "cannot" definitions when there are conditions.
@ -72,21 +72,24 @@ module CanCan
private private
def subject_class?(subject) def subject_object?(subject)
klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class # klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
klass == Class || klass == Module # klass == Class || klass == Module
!subject.kind_of?(Symbol)
end end
def matches_action?(action) def matches_action?(action)
@expanded_actions.include?(:manage) || @expanded_actions.include?(action) @expanded_actions.include?(:access) || @expanded_actions.include?(action)
end end
def matches_subject?(subject) def matches_subject?(subject)
@subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject) subject = subject_name(subject) if subject_object? subject
@expanded_subjects.include?(:all) || @expanded_subjects.include?(subject) # || matches_subject_class?(subject)
end end
# TODO deperecate this
def matches_subject_class?(subject) def matches_subject_class?(subject)
@subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) } @expanded_subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) }
end end
# Checks if the given subject matches the given conditions hash. # Checks if the given subject matches the given conditions hash.
@ -128,15 +131,19 @@ module CanCan
end end
def call_block_with_all(action, subject, extra_args) def call_block_with_all(action, subject, extra_args)
if subject.class == Class if subject_object? subject
@block.call(action, subject, nil, *extra_args) @block.call(action, subject_name(subject), subject, *extra_args)
else else
@block.call(action, subject.class, subject, *extra_args) @block.call(action, subject, nil, *extra_args)
end end
end end
def subject_name(subject)
subject.class.to_s.underscore.humanize.downcase.pluralize.to_sym
end
def model_adapter(subject) def model_adapter(subject)
ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class) ModelAdapters::AbstractAdapter.adapter_class(subject_object?(subject) ? subject.class : subject)
end end
end end
end end

View File

@ -6,341 +6,255 @@ describe CanCan::Ability do
@ability.extend(CanCan::Ability) @ability.extend(CanCan::Ability)
end end
it "should be able to :read anything" do
@ability.can :read, :all # Basic Action & Subject
@ability.can?(:read, String).should be_true
@ability.can?(:read, 123).should be_true it "allows access to only what is defined" do
@ability.can?(:paint, :fences).should be_false
@ability.can :paint, :fences
@ability.can?(:paint, :fences).should be_true
@ability.can?(:wax, :fences).should be_false
@ability.can?(:paint, :cars).should be_false
end end
it "should not have permission to do something it doesn't know about" do it "allows access to everything when :access, :all is used" do
@ability.can?(:foodfight, String).should be_false @ability.can?(:paint, :fences).should be_false
@ability.can :access, :all
@ability.can?(:paint, :fences).should be_true
@ability.can?(:wax, :fences).should be_true
@ability.can?(:paint, :cars).should be_true
end end
it "should pass true to `can?` when non false/nil is returned in block" do it "allows access to multiple actions and subjects" do
@ability.can :read, :all @ability.can [:paint, :sand], [:fences, :decks]
@ability.can :read, Symbol do |sym| @ability.can?(:paint, :fences).should be_true
"foo" # TODO test that sym is nil when no instance is passed @ability.can?(:sand, :fences).should be_true
end @ability.can?(:paint, :decks).should be_true
@ability.can?(:read, :some_symbol).should == true @ability.can?(:sand, :decks).should be_true
@ability.can?(:wax, :fences).should be_false
@ability.can?(:paint, :cars).should be_false
end end
it "should pass nil to a block when no instance is passed" do
@ability.can :read, Symbol do |sym| # Aliases
sym.should be_nil
true it "has default index, show, new, update, delete aliases" do
end @ability.can :read, :projects
@ability.can?(:read, Symbol).should be_true @ability.can?(:index, :projects).should be_true
@ability.can?(:show, :projects).should be_true
@ability.can :create, :projects
@ability.can?(:new, :projects).should be_true
@ability.can :update, :projects
@ability.can?(:edit, :projects).should be_true
@ability.can :destroy, :projects
@ability.can?(:delete, :projects).should be_true
end end
it "should pass to previous rule, if block returns false or nil" do it "follows deep action aliases" do
@ability.can :read, Symbol
@ability.can :read, Integer do |i|
i < 5
end
@ability.can :read, Integer do |i|
i > 10
end
@ability.can?(:read, Symbol).should be_true
@ability.can?(:read, 11).should be_true
@ability.can?(:read, 1).should be_true
@ability.can?(:read, 6).should be_false
end
it "should not pass class with object if :all objects are accepted" do
@ability.can :preview, :all do |object|
object.should == 123
@block_called = true
end
@ability.can?(:preview, 123)
@block_called.should be_true
end
it "should not call block when only class is passed, only return true" do
@block_called = false
@ability.can :preview, :all do |object|
@block_called = true
end
@ability.can?(:preview, Hash).should be_true
@block_called.should be_false
end
it "should pass only object for global manage actions" do
@ability.can :manage, String do |object|
object.should == "foo"
@block_called = true
end
@ability.can?(:stuff, "foo").should
@block_called.should be_true
end
it "should alias update or destroy actions to modify action" do
@ability.alias_action :update, :destroy, :to => :modify @ability.alias_action :update, :destroy, :to => :modify
@ability.can :modify, :all @ability.can :modify, :projects
@ability.can?(:update, 123).should be_true @ability.can?(:update, :projects).should be_true
@ability.can?(:destroy, 123).should be_true @ability.can?(:destroy, :projects).should be_true
@ability.can?(:edit, :projects).should be_true
end end
it "should allow deeply nested aliased actions" do it "adds up action aliases" do
@ability.alias_action :increment, :to => :sort
@ability.alias_action :sort, :to => :modify
@ability.can :modify, :all
@ability.can?(:increment, 123).should be_true
end
it "should always call block with arguments when passing no arguments to can" do
@ability.can do |action, object_class, object|
action.should == :foo
object_class.should == 123.class
object.should == 123
@block_called = true
end
@ability.can?(:foo, 123)
@block_called.should be_true
end
it "should pass nil to object when comparing class with can check" do
@ability.can do |action, object_class, object|
action.should == :foo
object_class.should == Hash
object.should be_nil
@block_called = true
end
@ability.can?(:foo, Hash)
@block_called.should be_true
end
it "should automatically alias index and show into read calls" do
@ability.can :read, :all
@ability.can?(:index, 123).should be_true
@ability.can?(:show, 123).should be_true
end
it "should automatically alias new and edit into create and update respectively" do
@ability.can :create, :all
@ability.can :update, :all
@ability.can?(:new, 123).should be_true
@ability.can?(:edit, 123).should be_true
end
it "should not respond to prepare (now using initialize)" do
@ability.should_not respond_to(:prepare)
end
it "should offer cannot? method which is simply invert of can?" do
@ability.cannot?(:tie, String).should be_true
end
it "should be able to specify multiple actions and match any" do
@ability.can [:read, :update], :all
@ability.can?(:read, 123).should be_true
@ability.can?(:update, 123).should be_true
@ability.can?(:count, 123).should be_false
end
it "should be able to specify multiple classes and match any" do
@ability.can :update, [String, Range]
@ability.can?(:update, "foo").should be_true
@ability.can?(:update, 1..3).should be_true
@ability.can?(:update, 123).should be_false
end
it "should support custom objects in the rule" do
@ability.can :read, :stats
@ability.can?(:read, :stats).should be_true
@ability.can?(:update, :stats).should be_false
@ability.can?(:read, :nonstats).should be_false
end
it "should check ancestors of class" do
@ability.can :read, Numeric
@ability.can?(:read, Integer).should be_true
@ability.can?(:read, 1.23).should be_true
@ability.can?(:read, "foo").should be_false
end
it "should support 'cannot' method to define what user cannot do" do
@ability.can :read, :all
@ability.cannot :read, Integer
@ability.can?(:read, "foo").should be_true
@ability.can?(:read, 123).should be_false
end
it "should pass to previous rule, if block returns false or nil" do
@ability.can :read, :all
@ability.cannot :read, Integer do |int|
int > 10 ? nil : ( int > 5 )
end
@ability.can?(:read, "foo").should be_true
@ability.can?(:read, 3).should be_true
@ability.can?(:read, 8).should be_false
@ability.can?(:read, 123).should be_true
end
it "should always return `false` for single cannot definition" do
@ability.cannot :read, Integer do |int|
int > 10 ? nil : ( int > 5 )
end
@ability.can?(:read, "foo").should be_false
@ability.can?(:read, 3).should be_false
@ability.can?(:read, 8).should be_false
@ability.can?(:read, 123).should be_false
end
it "should pass to previous cannot definition, if block returns false or nil" do
@ability.cannot :read, :all
@ability.can :read, Integer do |int|
int > 10 ? nil : ( int > 5 )
end
@ability.can?(:read, "foo").should be_false
@ability.can?(:read, 3).should be_false
@ability.can?(:read, 10).should be_true
@ability.can?(:read, 123).should be_false
end
it "should append aliased actions" do
@ability.alias_action :update, :to => :modify @ability.alias_action :update, :to => :modify
@ability.alias_action :destroy, :to => :modify @ability.alias_action :destroy, :to => :modify
@ability.aliased_actions[:modify].should == [:update, :destroy] @ability.can :modify, :projects
@ability.can?(:update, :projects).should be_true
@ability.can?(:destroy, :projects).should be_true
end end
it "should clear aliased actions" do it "follows deep subject aliases" do
@ability.alias_action :update, :to => :modify @ability.alias_subject :mammals, :to => :animals
@ability.clear_aliased_actions @ability.alias_subject :cats, :to => :mammals
@ability.aliased_actions[:modify].should be_nil @ability.can :pet, :animals
@ability.can?(:pet, :mammals).should be_true
end end
it "should pass additional arguments to block from can?" do it "clears current and default aliases" do
@ability.can :read, Integer do |int, x| @ability.alias_action :update, :destroy, :to => :modify
int > x @ability.clear_aliases
end @ability.can :modify, :projects
@ability.can?(:read, 2, 1).should be_true @ability.can?(:update, :projects).should be_false
@ability.can?(:read, 2, 3).should be_false @ability.can :read, :projects
@ability.can?(:show, :projects).should be_false
end end
it "should use conditions as third parameter and determine abilities from it" do
@ability.can :read, Range, :begin => 1, :end => 3 # Hash Conditions
it "maps object to pluralized subject name" do
@ability.can :read, :ranges
@ability.can?(:read, :ranges).should be_true
@ability.can?(:read, 1..3).should be_true @ability.can?(:read, 1..3).should be_true
@ability.can?(:read, 1..4).should be_false @ability.can?(:read, 123).should be_false
@ability.can?(:read, Range).should be_true
end end
it "should allow an array of options in conditions hash" do it "checks conditions hash on instances only" do
@ability.can :read, Range, :begin => [1, 3, 5] @ability.can :read, :ranges, :begin => 1
@ability.can?(:read, :ranges).should be_true
@ability.can?(:read, 1..3).should be_true
@ability.can?(:read, 2..4).should be_false
end
it "checks conditions on both rules and matches either one" do
@ability.can :read, :ranges, :begin => 1
@ability.can :read, :ranges, :begin => 2
@ability.can?(:read, 1..3).should be_true
@ability.can?(:read, 2..4).should be_true
@ability.can?(:read, 3..5).should be_false
end
it "checks an array of options in conditions hash" do
@ability.can :read, :ranges, :begin => [1, 3, 5]
@ability.can?(:read, 1..3).should be_true @ability.can?(:read, 1..3).should be_true
@ability.can?(:read, 2..4).should be_false @ability.can?(:read, 2..4).should be_false
@ability.can?(:read, 3..5).should be_true @ability.can?(:read, 3..5).should be_true
end end
it "should allow a range of options in conditions hash" do it "checks a range of options in conditions hash" do
@ability.can :read, Range, :begin => 1..3 @ability.can :read, :ranges, :begin => 1..3
@ability.can?(:read, 1..10).should be_true @ability.can?(:read, 1..10).should be_true
@ability.can?(:read, 3..30).should be_true @ability.can?(:read, 3..30).should be_true
@ability.can?(:read, 4..40).should be_false @ability.can?(:read, 4..40).should be_false
end end
it "should allow nested hashes in conditions hash" do it "checks nested conditions hash" do
@ability.can :read, Range, :begin => { :to_i => 5 } @ability.can :read, :ranges, :begin => { :to_i => 5 }
@ability.can?(:read, 5..7).should be_true @ability.can?(:read, 5..7).should be_true
@ability.can?(:read, 6..8).should be_false @ability.can?(:read, 6..8).should be_false
end end
it "should match any element passed in to nesting if it's an array (for has_many associations)" do it "matches any element passed in to nesting if it's an array (for has_many associations)" do
@ability.can :read, Range, :to_a => { :to_i => 3 } @ability.can :read, :ranges, :to_a => { :to_i => 3 }
@ability.can?(:read, 1..5).should be_true @ability.can?(:read, 1..5).should be_true
@ability.can?(:read, 4..6).should be_false @ability.can?(:read, 4..6).should be_false
end end
it "should not stop at cannot definition when comparing class" do
@ability.can :read, Range
@ability.cannot :read, Range, :begin => 1
@ability.can?(:read, 2..5).should be_true
@ability.can?(:read, 1..5).should be_false
@ability.can?(:read, Range).should be_true
end
it "should stop at cannot definition when no hash is present" do # Block Conditions
@ability.can :read, :all
@ability.cannot :read, Range
@ability.can?(:read, 1..5).should be_false
@ability.can?(:read, Range).should be_false
end
it "should allow to check ability for Module" do it "executes block passing object only when instance is used" do
module B; end @ability.can :read, :ranges do |range|
class A; include B; end range.begin == 5
@ability.can :read, B
@ability.can?(:read, A).should be_true
@ability.can?(:read, A.new).should be_true
end
it "should pass nil to a block for ability on Module when no instance is passed" do
module B; end
class A; include B; end
@ability.can :read, B do |sym|
sym.should be_nil
true
end end
@ability.can?(:read, B).should be_true @ability.can?(:read, :ranges).should be_true
@ability.can?(:read, A).should be_true @ability.can?(:read, 5..7).should be_true
@ability.can?(:read, 6..8).should be_false
end end
it "passing a hash of subjects should check permissions through association" do it "returns true when other object is returned in block" do
@ability.can :read, Range, :string => {:length => 3} @ability.can :read, :ranges do |range|
@ability.can?(:read, "foo" => Range).should be_true "foo"
@ability.can?(:read, "foobar" => Range).should be_false end
@ability.can?(:read, 123 => Range).should be_true @ability.can?(:read, 5..7).should be_true
end end
it "should allow to check ability on Hash-like object" do it "passes to previous rule when block returns false" do
@ability.can :read, :fixnums do |i|
i < 5
end
@ability.can :read, :fixnums do |i|
i > 10
end
@ability.can?(:read, 11).should be_true
@ability.can?(:read, 1).should be_true
@ability.can?(:read, 6).should be_false
end
it "calls block passing arguments when no arguments are given to can" do
@ability.can do |action, subject, object|
action.should == :read
subject.should == :ranges
object.should == (2..4)
@block_called = true
end
@ability.can?(:read, 2..4)
@block_called.should be_true
end
it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do
lambda {
@ability.can :read, :ranges, :published => true do
false
end
}.should raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read ranges ability. Use either one.")
end
# Cannot
it "offers cannot? method which inverts can?" do
@ability.cannot?(:wax, :cars).should be_true
end
it "supports 'cannot' method to define what user cannot do" do
@ability.can :read, :all
@ability.cannot :read, :ranges
@ability.can?(:read, :books).should be_true
@ability.can?(:read, 1..3).should be_false
@ability.can?(:read, :ranges).should be_false
end
it "passes to previous rule if cannot check returns false" do
@ability.can :read, :all
@ability.cannot :read, :ranges, :begin => 3
@ability.cannot :read, :ranges do |range|
range.begin == 5
end
@ability.can?(:read, :books).should be_true
@ability.can?(:read, 2..4).should be_true
@ability.can?(:read, 3..7).should be_false
@ability.can?(:read, 5..9).should be_false
end
# Hash Association
it "checks permission through association when hash is passed as subject" do
@ability.can :read, :books, :range => {:begin => 3}
@ability.can?(:read, (1..4) => :books).should be_false
@ability.can?(:read, (3..5) => :books).should be_true
@ability.can?(:read, 123 => :books).should be_true
end
it "checks ability on hash subclass" do
class Container < Hash; end class Container < Hash; end
@ability.can :read, Container @ability.can :read, :containers
@ability.can?(:read, Container.new).should be_true @ability.can?(:read, Container.new).should be_true
end end
it "should have initial attributes based on hash conditions of 'new' action" do
@ability.can :manage, Range, :foo => "foo", :hash => {:skip => "hashes"} # Initial Attributes
@ability.can :create, Range, :bar => 123, :array => %w[skip arrays]
@ability.can :new, Range, :baz => "baz", :range => 1..3 it "has initial attributes based on hash conditions for a given action" do
@ability.cannot :new, Range, :ignore => "me" @ability.can :access, :ranges, :foo => "foo", :hash => {:skip => "hashes"}
@ability.attributes_for(:new, Range).should == {:foo => "foo", :bar => 123, :baz => "baz"} @ability.can :create, :ranges, :bar => 123, :array => %w[skip arrays]
@ability.can :new, :ranges, :baz => "baz", :range => 1..3
@ability.cannot :new, :ranges, :ignore => "me"
@ability.attributes_for(:new, :ranges).should == {:foo => "foo", :bar => 123, :baz => "baz"}
end end
it "should raise access denied exception if ability us unauthorized to perform a certain action" do
# Unauthorized Exception
it "raises CanCan::AccessDenied when calling authorize! on unauthorized action" do
begin begin
@ability.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!" @ability.authorize! :read, :books, :message => "Access denied!"
rescue CanCan::AccessDenied => e rescue CanCan::AccessDenied => e
e.message.should == "Access denied!" e.message.should == "Access denied!"
e.action.should == :read e.action.should == :read
e.subject.should == :foo e.subject.should == :books
else else
fail "Expected CanCan::AccessDenied exception to be raised" fail "Expected CanCan::AccessDenied exception to be raised"
end end
end end
it "should not raise access denied exception if ability is authorized to perform an action" do
@ability.can :read, :foo
lambda { @ability.authorize!(:read, :foo) }.should_not raise_error
end
it "should know when block is used in conditions" do
@ability.can :read, :foo
@ability.should_not have_block(:read, :foo)
@ability.can :read, :foo do |foo|
false
end
@ability.should have_block(:read, :foo)
end
it "should know when raw sql is used in conditions" do
@ability.can :read, :foo
@ability.should_not have_raw_sql(:read, :foo)
@ability.can :read, :foo, 'false'
@ability.should have_raw_sql(:read, :foo)
end
it "should raise access denied exception with default message if not specified" do it "should raise access denied exception with default message if not specified" do
begin begin
@ability.authorize! :read, :foo @ability.authorize! :read, :books
rescue CanCan::AccessDenied => e rescue CanCan::AccessDenied => e
e.default_message = "Access denied!" e.default_message = "Access denied!"
e.message.should == "Access denied!" e.message.should == "Access denied!"
@ -349,7 +263,32 @@ describe CanCan::Ability do
end end
end end
it "should determine model adapter class by asking AbstractAdapter" do it "does not raise access denied exception if ability is authorized to perform an action" do
@ability.can :read, :books
lambda { @ability.authorize!(:read, :books) }.should_not raise_error
end
# Determining Conditions
it "knows when a block is used for conditions" do
@ability.can :read, :books
@ability.should_not have_block(:read, :books)
@ability.can :read, :books do |foo|
false
end
@ability.should have_block(:read, :books)
end
it "knows when raw sql is used for conditions" do
@ability.can :read, :books
@ability.should_not have_raw_sql(:read, :books)
@ability.can :read, :books, 'false'
@ability.should have_raw_sql(:read, :books)
end
it "determines model adapter class by asking AbstractAdapter" do
pending
model_class = Object.new model_class = Object.new
adapter_class = Object.new adapter_class = Object.new
stub(CanCan::ModelAdapters::AbstractAdapter).adapter_class(model_class) { adapter_class } stub(CanCan::ModelAdapters::AbstractAdapter).adapter_class(model_class) { adapter_class }
@ -357,54 +296,50 @@ describe CanCan::Ability do
@ability.model_adapter(model_class, :read).should == :adapter_instance @ability.model_adapter(model_class, :read).should == :adapter_instance
end end
it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do
lambda { # Unauthorized I18n Message
@ability.can :read, Array, :published => true do
false
end
}.should raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read Array ability. Use either one.")
end
describe "unauthorized message" do describe "unauthorized message" do
after(:each) do after(:each) do
I18n.backend = nil I18n.backend = nil
end end
it "should use action/subject in i18n" do it "uses action/subject in i18n" do
I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}} I18n.backend.store_translations :en, :unauthorized => {:update => {:ranges => "update ranges"}}
@ability.unauthorized_message(:update, Array).should == "foo" @ability.unauthorized_message(:update, :ranges).should == "update ranges"
@ability.unauthorized_message(:update, [1, 2, 3]).should == "foo" @ability.unauthorized_message(:update, 2..4).should == "update ranges"
@ability.unauthorized_message(:update, :missing).should be_nil @ability.unauthorized_message(:update, :missing).should be_nil
end end
it "should use symbol as subject directly" do it "uses symbol as subject directly" do
I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}} I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
@ability.unauthorized_message(:has, :cheezburger).should == "Nom nom nom. I eated it." @ability.unauthorized_message(:has, :cheezburger).should == "Nom nom nom. I eated it."
end end
it "should fall back to 'manage' and 'all'" do it "falls back to 'access' and 'all'" do
I18n.backend.store_translations :en, :unauthorized => { I18n.backend.store_translations :en, :unauthorized => {
:manage => {:all => "manage all", :array => "manage array"}, :access => {:all => "access all", :ranges => "access ranges"},
:update => {:all => "update all", :array => "update array"} :update => {:all => "update all", :ranges => "update ranges"}
} }
@ability.unauthorized_message(:update, Array).should == "update array" @ability.unauthorized_message(:update, :ranges).should == "update ranges"
@ability.unauthorized_message(:update, Hash).should == "update all" @ability.unauthorized_message(:update, :hashes).should == "update all"
@ability.unauthorized_message(:foo, Array).should == "manage array" @ability.unauthorized_message(:create, :ranges).should == "access ranges"
@ability.unauthorized_message(:foo, Hash).should == "manage all" @ability.unauthorized_message(:create, :hashes).should == "access all"
end end
it "should follow aliased actions" do it "follows aliases" do
I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}} I18n.backend.store_translations :en, :unauthorized => {:modify => {:ranges => "modify ranges"}}
@ability.alias_action :update, :to => :modify @ability.alias_action :update, :to => :modify
@ability.unauthorized_message(:update, Array).should == "modify array" @ability.alias_subject :areas, :to => :ranges
@ability.unauthorized_message(:edit, Array).should == "modify array" @ability.unauthorized_message(:update, :areas).should == "modify ranges"
@ability.unauthorized_message(:edit, :ranges).should == "modify ranges"
end end
it "should have variables for action and subject" do it "has variables for action and subject" do
I18n.backend.store_translations :en, :unauthorized => {:manage => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n I18n.backend.store_translations :en, :unauthorized => {:access => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
@ability.unauthorized_message(:update, Array).should == "update array" @ability.unauthorized_message(:update, :ranges).should == "update ranges"
@ability.unauthorized_message(:update, ArgumentError).should == "update argument error" @ability.unauthorized_message(:edit, 1..3).should == "edit ranges"
@ability.unauthorized_message(:edit, 1..3).should == "edit range" # @ability.unauthorized_message(:update, ArgumentError).should == "update argument error"
end end
end end
end end