From a5f838a964f8717c3243eb6e2d5cd3a949a8986a Mon Sep 17 00:00:00 2001 From: Ryan Bates Date: Thu, 2 Sep 2010 17:01:10 -0700 Subject: [PATCH] use I18n for unauthorization messages - closes #103 --- lib/cancan/ability.rb | 40 ++++++++++++++++++- lib/cancan/can_definition.rb | 2 +- lib/cancan/controller_additions.rb | 8 +--- spec/cancan/ability_spec.rb | 50 ++++++++++++++++-------- spec/cancan/controller_additions_spec.rb | 1 + 5 files changed, 76 insertions(+), 25 deletions(-) diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 1d1df1c..1287938 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -49,7 +49,6 @@ module CanCan # # Also see the RSpec Matchers to aid in testing. def can?(action, subject, *extra_args) - raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger match = relevant_can_definitions(action, subject).detect do |can_definition| can_definition.matches_conditions?(action, subject, extra_args) end @@ -189,9 +188,36 @@ module CanCan Query.new(subject, relevant_can_definitions_for_query(action, subject)) end + # See ControllerAdditions#authorize! for documentation. + def authorize!(action, subject, *args) + message = nil + if args.last.kind_of?(Hash) && args.last.has_key?(:message) + message = args.pop[:message] + end + if cannot?(action, subject, *args) + message ||= unauthorized_message(action, subject) + raise AccessDenied.new(message, action, subject) + end + end + + def unauthorized_message(action, subject) + keys = unauthorized_message_keys(action, subject) + message = I18n.translate(nil, :scope => :unauthorized, :default => keys + [""]) + message.blank? ? nil : message + end + private - # Accepts a hash of aliased actions and returns an array of actions which match. + def unauthorized_message_keys(action, subject) + subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.kind_of? Symbol + [subject, :all].map do |try_subject| + [aliases_for_action(action), :manage].flatten.map do |try_action| + :"#{try_action}.#{try_subject}" + end + end.flatten + end + + # 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 # rely on the actions to be expanded. def expand_actions(actions) @@ -200,6 +226,16 @@ module CanCan end.flatten end + # 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. + def aliases_for_action(action) + results = [action] + aliased_actions.each do |aliased_action, actions| + results += aliases_for_action(aliased_action) if actions.include? action + end + results + end + def can_definitions @can_definitions ||= [] end diff --git a/lib/cancan/can_definition.rb b/lib/cancan/can_definition.rb index dea8432..6f7a3ef 100644 --- a/lib/cancan/can_definition.rb +++ b/lib/cancan/can_definition.rb @@ -94,7 +94,7 @@ module CanCan end end end - + def call_block_with_all(action, subject, extra_args) if subject.class == Class @block.call(action, subject, nil, *extra_args) diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index 8670e5f..b4e9054 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -185,12 +185,8 @@ module CanCan # # See the load_and_authorize_resource method to automatically add the authorize! behavior # to the default RESTful actions. - def authorize!(action, subject, *args) - message = nil - if args.last.kind_of?(Hash) && args.last.has_key?(:message) - message = args.pop[:message] - end - raise AccessDenied.new(message, action, subject) if cannot?(action, subject, *args) + def authorize!(*args) + current_ability.authorize!(*args) end def unauthorized!(message = nil) diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index 9be365a..224f724 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -157,18 +157,7 @@ describe CanCan::Ability do @ability.can?(:read, 123).should be_false end - it "should support block on 'cannot' method" do - @ability.can :read, :all - @ability.cannot :read, Integer do |int| - int > 5 - end - @ability.can?(:read, "foo").should be_true - @ability.can?(:read, 3).should be_true - @ability.can?(:read, 123).should be_false - end - it "should pass to previous can definition, if block returns false or nil" do - #same as previous @ability.can :read, :all @ability.cannot :read, Integer do |int| int > 10 ? nil : ( int > 5 ) @@ -177,7 +166,6 @@ describe CanCan::Ability do @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 @@ -262,9 +250,39 @@ describe CanCan::Ability do @ability.can?(:read, Array).should be_true end - it "should has eated cheezburger" do - lambda { - @ability.can? :has, :cheezburger - }.should raise_error(CanCan::Error, "Nom nom nom. I eated it.") + describe "unauthorized message" do + after(:each) do + I18n.backend = nil + end + + it "should use action/subject in i18n" do + I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}} + @ability.unauthorized_message(:update, Array).should == "foo" + @ability.unauthorized_message(:update, [1, 2, 3]).should == "foo" + @ability.unauthorized_message(:update, :missing).should be_nil + end + + it "should use symbol as subject directly" do + 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." + end + + it "should fall back to 'manage' and 'all'" do + I18n.backend.store_translations :en, :unauthorized => { + :manage => {:all => "manage all", :array => "manage array"}, + :update => {:all => "update all", :array => "update array"} + } + @ability.unauthorized_message(:update, Array).should == "update array" + @ability.unauthorized_message(:update, Hash).should == "update all" + @ability.unauthorized_message(:foo, Array).should == "manage array" + @ability.unauthorized_message(:foo, Hash).should == "manage all" + end + + it "should follow aliased actions" do + I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}} + @ability.alias_action :update, :to => :modify + @ability.unauthorized_message(:update, Array).should == "modify array" + @ability.unauthorized_message(:edit, Array).should == "modify array" + end end end diff --git a/spec/cancan/controller_additions_spec.rb b/spec/cancan/controller_additions_spec.rb index 1c654cc..4385e12 100644 --- a/spec/cancan/controller_additions_spec.rb +++ b/spec/cancan/controller_additions_spec.rb @@ -15,6 +15,7 @@ describe CanCan::ControllerAdditions do end it "should raise access denied exception if ability us unauthorized to perform a certain action" do + # TODO this should probably be moved into Ability spec begin @controller.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!" rescue CanCan::AccessDenied => e