diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 6e1738c..3a3c89f 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -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 * Adding joins clause to accessible_by when conditions are across associations diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 7e4d6f0..11e9b4b 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -1,8 +1,8 @@ module CanCan - + # This module is designed to be included into an Ability class. This will # provide the "can" methods for defining and checking abilities. - # + # # class Ability # include CanCan::Ability # @@ -14,39 +14,40 @@ module CanCan # end # end # end - # + # 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 - # + # # You can also pass the class instead of an instance (if you don't have one handy). - # + # # can? :create, Project - # + # # Any additional arguments will be passed into the "can" block definition. This # can be used to pass more information about the user's request for example. - # + # # can? :create, Project, request.remote_ip - # + # # can :create Project do |project, remote_ip| # # ... # end - # - # Not only can you use the can? method in the controller and view (see ControllerAdditions), + # + # Not only can you use the can? method in the controller and view (see ControllerAdditions), # but you can also call it directly on an ability instance. - # + # # ability.can? :destroy, @project - # + # # This makes testing a user's abilities very easy. - # + # # def test "user can only destroy projects which he owns" # user = User.new # ability = Ability.new(user) # assert ability.can?(:destroy, Project.new(:user => user)) # assert ability.cannot?(:destroy, Project.new) # end - # + # + # 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 matching_can_definition(action, subject) do |can_definition| @@ -56,133 +57,133 @@ module CanCan end false end - + # Convenience method which works the same as "can?" but returns the opposite value. - # + # # cannot? :destroy, @project - # + # def cannot?(*args) !can?(*args) end - + # Defines which abilities are allowed using two arguments. The first one is the action # you're setting the permission for, the second one is the class of object you're setting it on. - # + # # can :update, Article - # + # # You can pass an array for either of these parameters to match any one. # # can [:update, :destroy], [Article, Comment] # # 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. # # 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. - # + # + # Here the user can only see active projects which he owns. See ActiveRecordAdditions#accessible_by + # 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 # write any Ruby code you want. # # can :update, Project do |project| # project && project.groups.include?(user.group) # end - # + # # 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). - # + # # can :read, :all do |object_class, object| # object_class != Order # end - # + # # Here the user has permission to read all objects except orders. - # - # You can also pass :manage as the action which will match any action. In this case the action is + # + # You can also pass :manage as the action which will match any action. In this case the action is # passed to the block. - # + # # can :manage, Comment do |action, comment| # action != :destroy # end - # + # # You can pass custom objects into this "can" method, this is usually done through a symbol # and is useful if a class isn't available to define permissions on. - # + # # can :read, :stats # can? :read, :stats # => true - # + # def can(action, subject, conditions = nil, &block) can_definitions << CanDefinition.new(true, action, subject, conditions, block) 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 # cannot :read, Comment - # + # # A block can be passed just like "can", however if the logic is complex it is recommended # to use the "can" method. - # + # # cannot :read, Product do |product| # product.invisible? # end - # + # def cannot(action, subject, conditions = nil, &block) can_definitions << CanDefinition.new(false, action, subject, conditions, block) end - + # Alias one or more actions into another one. - # + # # alias_action :update, :destroy, :to => :modify # can :modify, Comment - # + # # Then :modify permission will apply to both :update and :destroy requests. - # + # # can? :update, Comment # => true # can? :destroy, Comment # => true - # + # # This only works in one direction. Passing the aliased action into the "can?" call # will not work because aliases are meant to generate more generic actions. - # + # # alias_action :update, :destroy, :to => :modify # can :update, Comment # 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. - # + # # alias_action :index, :show, :to => :read # alias_action :new, :to => :create # alias_action :edit, :to => :update - # + # # This way one can use params[:action] in the controller to determine the permission. def alias_action(*args) target = args.pop[:to] aliased_actions[target] ||= [] aliased_actions[target] += args end - + # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key. def aliased_actions @aliased_actions ||= default_alias_actions end - + # Removes previously aliased actions including the defaults. def clear_aliased_actions @aliased_actions = {} end - + # Returns an array of arrays composing from desired action and hash of conditions which match the given ability. # This is useful if you need to generate a database query based on the current ability. # @@ -194,18 +195,21 @@ module CanCan # conditions :read, Article # returns [ [ false, { :blocked => true } ], [ true, { :visible => true } ] ] # # Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method. - # + # # 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, subject) + def conditions(action, subject, options = {}) matched = matching_can_definition(action, subject) 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}" end matched.map{|can_definition| - [can_definition.base_behavior, can_definition.conditions] + [can_definition.base_behavior, can_definition.conditions(options)] } else false @@ -226,8 +230,8 @@ module CanCan # 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 # determined from that. - def sql_conditions(action, subject) - conds = conditions(action, subject) + def sql_conditions(action, subject, options = {}) + conds = conditions(action, subject, options) return false if conds == false 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]) conds.reverse.inject(nil) do |sql, action| behavior, condition = action - if condition + if condition && !condition.empty? condition = "#{subject.send(:sanitize_sql, condition)}" condition = "not (#{condition})" if behavior == false else @@ -268,11 +272,11 @@ module CanCan end private - + def can_definitions @can_definitions ||= [] end - + def matching_can_definition(action, subject) if block_given? can_definitions.reverse.each do |can_definition| @@ -288,7 +292,7 @@ module CanCan matched end end - + def default_alias_actions { :read => [:index, :show], diff --git a/lib/cancan/active_record_additions.rb b/lib/cancan/active_record_additions.rb index eb0729e..31545f2 100644 --- a/lib/cancan/active_record_additions.rb +++ b/lib/cancan/active_record_additions.rb @@ -1,5 +1,5 @@ module CanCan - # This module is automatically included into all Active Record. + # This module is automatically included into all Active Record models. module ActiveRecordAdditions module ClassMethods # Returns a scope which fetches only the records that the passed ability @@ -7,20 +7,20 @@ module CanCan # is usually called from a controller and passed the +current_ability+. # # @articles = Article.accessible_by(current_ability) - # + # # Here only the articles which the user is able to read will be returned. # If the user does not have permission to read any articles then an empty # result is returned. Since this is a scope it can be combined with any # other scopes or pagination. - # + # # An alternative action can optionally be passed as a second argument. - # + # # @articles = Article.accessible_by(current_ability, :update) - # + # # Here only the articles which the user can update are returned. This # internally uses Ability#conditions method, see that for more information. 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) if respond_to? :where where(conditions).joins(joins) @@ -29,7 +29,7 @@ module CanCan end end end - + def self.included(base) base.extend ClassMethods end diff --git a/lib/cancan/can_definition.rb b/lib/cancan/can_definition.rb index 0124c6a..e044ac8 100644 --- a/lib/cancan/can_definition.rb +++ b/lib/cancan/can_definition.rb @@ -1,35 +1,59 @@ module CanCan # 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: 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) @base_behavior = base_behavior @actions = [action].flatten @subjects = [subject].flatten - @conditions = conditions + @conditions = conditions || {} @block = block 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) @expanded_actions = @actions.map do |action| aliased_actions[action] ? [action, *aliased_actions[action]] : action end.flatten end - + def matches?(action, subject) matches_action?(action) && matches_subject?(subject) end - + def can?(action, subject, extra_args) result = can_without_base_behavior?(action, subject, extra_args) return :_NOT_MATCHED if result == :_NOT_MATCHED || !result @base_behavior ? result : !result 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) joins = [] - (conditions || []).each do |name, value| + conditions.each do |name, value| if value.kind_of? Hash nested = association_joins(value) if nested @@ -41,17 +65,17 @@ module CanCan end joins unless joins.empty? end - + private - + def matches_action?(action) @expanded_actions.include?(:manage) || @expanded_actions.include?(action) end - + def matches_subject?(subject) @subjects.include?(:all) || @subjects.include?(subject) || @subjects.any? { |sub| sub.kind_of?(Class) && subject.kind_of?(sub) } end - + def can_without_base_behavior?(action, subject, extra_args) if @block call_block(action, subject, extra_args) @@ -63,7 +87,7 @@ module CanCan true end end - + def matches_conditions?(subject, conditions = @conditions) conditions.all? do |name, value| attribute = subject.send(name) @@ -80,7 +104,7 @@ module CanCan end end end - + def call_block(action, subject, extra_args) block_args = [] block_args << action if @expanded_actions.include?(:manage) diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index 863780d..28f3bb4 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -1,35 +1,35 @@ module CanCan - + # This module is automatically included into all controllers. # It also makes the "can?" and "cannot?" methods available to all views. module ControllerAdditions module ClassMethods # Sets up a before filter which loads and authorizes the current resource. This performs both # load_resource and authorize_resource and accepts the same arguments. See those methods for details. - # + # # class BooksController < ApplicationController # load_and_authorize_resource # end - # + # def load_and_authorize_resource(options = {}) ResourceAuthorization.add_before_filter(self, :load_and_authorize_resource, options) end - + # Sets up a before filter which loads the appropriate model resource into an instance variable. # For example, given an ArticlesController it will load the current article into the @article # instance variable. It does this by either calling Article.find(params[:id]) or # Article.new(params[:article]) depending upon the action. It does nothing for the "index" # action. - # + # # Call this method directly on the controller class. - # + # # class BooksController < ApplicationController # load_resource # end - # + # # A resource is not loaded if the instance variable is already set. This makes it easy to override # the behavior through a before_filter on certain actions. - # + # # class BooksController < ApplicationController # before_filter :find_book_by_permalink, :only => :show # load_resource @@ -40,107 +40,117 @@ module CanCan # @book = Book.find_by_permalink!(params[:id) # end # end - # + # # See load_and_authorize_resource to automatically authorize the resource too. - # + # # Options: # [:+only+] # Only applies before filter to given actions. - # + # # [:+except+] # Does not apply before filter to given actions. - # + # # [:+nested+] # Specify which resource this is nested under. - # + # # load_resource :nested => :author - # + # # Deep nesting can be defined in an array. - # + # # 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+] # The class to use for the model (string or constant). - # + # # [:+collection+] # Specify which actions are resource collection actions in addition to :+index+. This # is usually not necessary because it will try to guess depending on if an :+id+ # is present in +params+. - # + # # load_resource :collection => [:sort, :list] - # + # # [:+new+] # Specify which actions are new resource actions in addition to :+new+ and :+create+. # Pass an action name into here if you would like to build a new resource instead of # fetch one. - # + # # load_resource :new => :build - # + # def load_resource(options = {}) ResourceAuthorization.add_before_filter(self, :load_resource, options) end - + # Sets up a before filter which authorizes the current resource using the instance variable. # For example, if you have an ArticlesController it will check the @article instance variable # and ensure the user can perform the current action on it. Under the hood it is doing # something like the following. - # + # # authorize!(params[:action].to_sym, @article || Article) - # + # # Call this method directly on the controller class. - # + # # class BooksController < ApplicationController # authorize_resource # end - # + # # See load_and_authorize_resource to automatically load the resource too. - # + # # Options: # [:+only+] # Only applies before filter to given actions. - # + # # [:+except+] # 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+] # The class to use for the model (string or constant). Alternatively pass a symbol # to represent a resource which does not have a class. - # + # def authorize_resource(options = {}) ResourceAuthorization.add_before_filter(self, :authorize_resource, options) end end - + def self.included(base) base.extend ClassMethods base.helper_method :can?, :cannot? end - + # Raises a CanCan::AccessDenied exception if the current_ability cannot # perform the given action. This is usually called in a controller action or # before filter to perform the authorization. - # + # # def show # @article = Article.find(params[:id]) # authorize! :read, @article # end - # + # # A :message option can be passed to specify a different message. - # + # # authorize! :read, @article, :message => "Not authorized to read #{@article.name}" - # + # # You can rescue from the exception in the controller to customize how unauthorized # access is displayed to the user. - # + # # class ApplicationController < ActionController::Base # rescue_from CanCan::AccessDenied do |exception| # flash[:error] = exception.message # redirect_to root_url # end # end - # + # # See the CanCan::AccessDenied exception for more details on working with the exception. - # + # # See the load_and_authorize_resource method to automatically add the authorize! behavior # to the default RESTful actions. def authorize!(action, subject, *args) @@ -150,46 +160,46 @@ module CanCan end raise AccessDenied.new(message, action, subject) if cannot?(action, subject, *args) end - + def unauthorized!(message = nil) raise ImplementationRemoved, "The unauthorized! method has been removed from CanCan, use authorize! instead." end - + # Creates and returns the current user's ability and caches it. If you # want to override how the Ability is defined then this is the place. # Just define the method in the controller to change behavior. - # + # # def current_ability # # instead of Ability.new(current_user) # @current_ability ||= UserAbility.new(current_account) # end - # + # # Notice it is important to cache the ability object so it is not # recreated every time. def current_ability @current_ability ||= ::Ability.new(current_user) end - + # Use in the controller or view to check the user's permission for a given action # and object. - # + # # can? :destroy, @project - # + # # You can also pass the class instead of an instance (if you don't have one handy). - # + # # <% if can? :create, Project %> # <%= link_to "New Project", new_project_path %> # <% end %> - # + # # This simply calls "can?" on the current_ability. See Ability#can?. def can?(*args) current_ability.can?(*args) end - + # Convenience method which works the same as "can?" but returns the opposite value. - # + # # cannot? :destroy, @project - # + # def cannot?(*args) current_ability.cannot?(*args) end diff --git a/lib/cancan/controller_resource.rb b/lib/cancan/controller_resource.rb index d2f18cb..36d77bc 100644 --- a/lib/cancan/controller_resource.rb +++ b/lib/cancan/controller_resource.rb @@ -1,5 +1,7 @@ module CanCan # 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: 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 @@ -9,6 +11,9 @@ module CanCan @options = options 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 resource_class = @options[:resource] if resource_class.nil? @@ -16,7 +21,7 @@ module CanCan elsif resource_class.kind_of? String resource_class.constantize else - resource_class # likely a symbol + resource_class # could be a symbol end end @@ -24,12 +29,10 @@ module CanCan self.model_instance ||= base.find(id) 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) - if base.kind_of? Class - self.model_instance ||= base.new(attributes) - else - self.model_instance ||= base.build(attributes) - end + self.model_instance ||= (base.kind_of?(Class) ? base.new(attributes) : base.build(attributes)) end def model_instance @@ -42,6 +45,8 @@ module CanCan 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 @parent ? @parent.model_instance.send(@name.to_s.pluralize) : model_class end diff --git a/lib/cancan/exceptions.rb b/lib/cancan/exceptions.rb index 33e5bee..177584f 100644 --- a/lib/cancan/exceptions.rb +++ b/lib/cancan/exceptions.rb @@ -1,41 +1,41 @@ module CanCan # A general CanCan exception class Error < StandardError; end - + # Raised when removed code is called, an alternative solution is provided in message. class ImplementationRemoved < Error; end - + # This error is raised when a user isn't allowed to access a given controller action. # This usually happens within a call to ControllerAdditions#authorize! but can be # raised manually. - # + # # raise CanCan::AccessDenied.new("Not authorized!", :read, Article) - # + # # The passed message, action, and subject are optional and can later be retrieved when # rescuing from the exception. - # + # # exception.message # => "Not authorized!" # exception.action # => :read # 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. - # + # # exception.default_message = "Default error message" # exception.message # => "Default error message" - # + # # See ControllerAdditions#authorized! for more information on rescuing from this exception. class AccessDenied < Error attr_reader :action, :subject attr_writer :default_message - + def initialize(message = nil, action = nil, subject = nil) @message = message @action = action @subject = subject @default_message = "You are not authorized to access this page." end - + def to_s @message || @default_message end diff --git a/lib/cancan/resource_authorization.rb b/lib/cancan/resource_authorization.rb index 21429df..aa8f710 100644 --- a/lib/cancan/resource_authorization.rb +++ b/lib/cancan/resource_authorization.rb @@ -7,20 +7,22 @@ module CanCan ResourceAuthorization.new(controller, controller.params, options.except(:only, :except)).send(method) end end - + def initialize(controller, params, options = {}) @controller = controller @params = params @options = options end - + def load_and_authorize_resource load_resource authorize_resource end - + 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 resource.build(@params[model_name.to_sym]) elsif @params[:id] @@ -28,17 +30,17 @@ module CanCan end end end - + def authorize_resource @controller.authorize!(@params[:action].to_sym, resource.model_instance || resource.model_class) end - + private - + def resource @resource ||= ControllerResource.new(@controller, model_name, parent_resource, @options) end - + def parent_resource parent = nil [@options[:nested]].flatten.compact.each do |name| @@ -52,15 +54,15 @@ module CanCan end parent end - + def model_name - @params[:controller].sub("Controller", "").underscore.split('/').last.singularize + @options[:name] || @params[:controller].sub("Controller", "").underscore.split('/').last.singularize end - + def collection_actions [:index] + [@options[:collection]].flatten end - + def new_actions [:new, :create] + [@options[:new]].flatten end diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index 26c2b76..af8e484 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -5,13 +5,13 @@ describe CanCan::Ability do @ability = Object.new @ability.extend(CanCan::Ability) end - + it "should be able to :read anything" do @ability.can :read, :all @ability.can?(:read, String).should be_true @ability.can?(:read, 123).should be_true end - + it "should not have permission to do something it doesn't know about" do @ability.can?(:foodfight, String).should be_false end @@ -38,21 +38,21 @@ describe CanCan::Ability do @ability.can?(:read, 1).should be_true @ability.can?(:read, -1).should be_true end - + it "should pass class with object if :all objects are accepted" do @ability.can :preview, :all do |object_class, object| [object_class, object] end @ability.can?(:preview, 123).should == [Fixnum, 123] end - + it "should pass class with no object if :all objects are accepted and class is passed directly" do @ability.can :preview, :all do |object_class, object| [object_class, object] end @ability.can?(:preview, Hash).should == [Hash, nil] end - + it "should pass action and object for global manage actions" do @ability.can :manage, Array do |action, object| [action, object] @@ -60,14 +60,14 @@ describe CanCan::Ability do @ability.can?(:stuff, [1, 2]).should == [:stuff, [1, 2]] @ability.can?(:stuff, Array).should == [:stuff, nil] end - + it "should alias update or destroy actions to modify action" do @ability.alias_action :update, :destroy, :to => :modify @ability.can(:modify, :all) { :modify_called } @ability.can?(:update, 123).should == :modify_called @ability.can?(:destroy, 123).should == :modify_called end - + it "should return block result for action, object_class, and object for any action" do @ability.can :manage, :all do |action, object_class, object| [action, object_class, object] @@ -75,56 +75,56 @@ describe CanCan::Ability do @ability.can?(:foo, 123).should == [:foo, Fixnum, 123] @ability.can?(:bar, Fixnum).should == [:bar, Fixnum, nil] 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) { :create_called } @ability.can(:update, :all) { :update_called } @ability.can?(:new, 123).should == :create_called @ability.can?(:edit, 123).should == :update_called 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, Array] @ability.can?(:update, "foo").should be_true @ability.can?(:update, []).should be_true @ability.can?(:update, 123).should be_false end - + it "should support custom objects in the can definition" 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 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 support block on 'cannot' method" do @ability.can :read, :all @ability.cannot :read, Integer do |int| @@ -164,13 +164,13 @@ describe CanCan::Ability do @ability.alias_action :destroy, :to => :modify @ability.aliased_actions[:modify].should == [:update, :destroy] end - + it "should clear aliased actions" do @ability.alias_action :update, :to => :modify @ability.clear_aliased_actions @ability.aliased_actions[:modify].should be_nil end - + it "should pass additional arguments to block from can?" do @ability.can :read, Integer do |int, x| int > x @@ -178,34 +178,34 @@ describe CanCan::Ability do @ability.can?(:read, 2, 1).should be_true @ability.can?(:read, 2, 3).should be_false 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_true end - + it "should allow an array of options in conditions hash" do @ability.can :read, Array, :first => [1, 3, 5] @ability.can?(:read, [1, 2, 3]).should be_true @ability.can?(:read, [2, 3]).should be_false @ability.can?(:read, [3, 4]).should be_true end - + it "should allow a range of options in conditions hash" do @ability.can :read, Array, :first => 1..3 @ability.can?(:read, [1, 2, 3]).should be_true @ability.can?(:read, [3, 4]).should be_true @ability.can?(:read, [4, 5]).should be_false end - + it "should allow nested hashes in conditions hash" do @ability.can :read, Array, :first => { :length => 5 } @ability.can?(:read, ["foo", "bar"]).should be_false @ability.can?(:read, ["test1", "foo"]).should be_true end - + it "should allow nested hash of arrays and match any element" do @ability.can :read, Array, :first => { :to_i => 3 } @ability.can?(:read, [[1, 2, 3]]).should be_true @@ -226,9 +226,9 @@ describe CanCan::Ability do it "should return an array with just behavior for conditions when there are no conditions" do @ability.can :read, Array - @ability.conditions(:show, Array).should == [ [true, nil] ] + @ability.conditions(:show, Array).should == [ [true, {}] ] end - + it "should return false when performed on an action which isn't defined" do @ability.conditions(:foo, Array).should == false end diff --git a/spec/cancan/active_record_additions_spec.rb b/spec/cancan/active_record_additions_spec.rb index 640f44a..7166393 100644 --- a/spec/cancan/active_record_additions_spec.rb +++ b/spec/cancan/active_record_additions_spec.rb @@ -8,21 +8,21 @@ describe CanCan::ActiveRecordAdditions do @ability = Object.new @ability.extend(CanCan::Ability) end - + it "should call where(:id => nil) when no ability is defined so no records are found" do stub(@model_class).where(:id => nil).stub!.joins(nil) { :no_where } @model_class.accessible_by(@ability, :read).should == :no_where end - + it "should call where with matching ability conditions" do @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 end - + it "should default to :read ability and use scoped when where isn't available" do @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 end end diff --git a/spec/cancan/can_definition_spec.rb b/spec/cancan/can_definition_spec.rb index 3a4cb24..00f605b 100644 --- a/spec/cancan/can_definition_spec.rb +++ b/spec/cancan/can_definition_spec.rb @@ -5,7 +5,7 @@ describe CanCan::CanDefinition do @conditions = {} @can = CanCan::CanDefinition.new(true, :read, Integer, @conditions, nil) end - + it "should return no association joins if none exist" do @can.association_joins.should be_nil end @@ -30,4 +30,15 @@ describe CanCan::CanDefinition do @conditions[:foo] = {:bar => {1 => 2}} @can.association_joins.should == [{:foo => [{:bar=>[]}]}] 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 diff --git a/spec/cancan/controller_additions_spec.rb b/spec/cancan/controller_additions_spec.rb index 9239557..f886c35 100644 --- a/spec/cancan/controller_additions_spec.rb +++ b/spec/cancan/controller_additions_spec.rb @@ -9,11 +9,11 @@ describe CanCan::ControllerAdditions do mock(@controller_class).helper_method(:can?, :cannot?) @controller_class.send(:include, CanCan::ControllerAdditions) end - + it "should raise ImplementationRemoved when attempting to call 'unauthorized!' on a controller" do lambda { @controller.unauthorized! }.should raise_error(CanCan::ImplementationRemoved) end - + it "should raise access denied exception if ability us unauthorized to perform a certain action" do begin @controller.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!" @@ -25,12 +25,12 @@ describe CanCan::ControllerAdditions do fail "Expected CanCan::AccessDenied exception to be raised" end end - + it "should not raise access denied exception if ability is authorized to perform an action" do @controller.current_ability.can :read, :foo lambda { @controller.authorize!(:read, :foo) }.should_not raise_error end - + it "should raise access denied exception with default message if not specified" do begin @controller.authorize! :read, :foo @@ -41,29 +41,29 @@ describe CanCan::ControllerAdditions do fail "Expected CanCan::AccessDenied exception to be raised" end end - + it "should have a current_ability method which generates an ability for the current user" do @controller.current_ability.should be_kind_of(Ability) end - + it "should provide a can? and cannot? methods which go through the current ability" do @controller.current_ability.should be_kind_of(Ability) @controller.can?(:foo, :bar).should be_false @controller.cannot?(:foo, :bar).should be_true end - + it "load_and_authorize_resource should setup a before filter which passes call to ResourceAuthorization" do stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.load_and_authorize_resource mock(@controller_class).before_filter({}) { |options, block| block.call(@controller) } @controller_class.load_and_authorize_resource :foo => :bar end - + it "authorize_resource should setup a before filter which passes call to ResourceAuthorization" do stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.authorize_resource mock(@controller_class).before_filter(:except => :show) { |options, block| block.call(@controller) } @controller_class.authorize_resource :foo => :bar, :except => :show end - + it "load_resource should setup a before filter which passes call to ResourceAuthorization" do stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.load_resource mock(@controller_class).before_filter(:only => [:show, :index]) { |options, block| block.call(@controller) } diff --git a/spec/cancan/controller_resource_spec.rb b/spec/cancan/controller_resource_spec.rb index c93b055..ee61e44 100644 --- a/spec/cancan/controller_resource_spec.rb +++ b/spec/cancan/controller_resource_spec.rb @@ -4,53 +4,53 @@ describe CanCan::ControllerResource do before(:each) do @controller = Object.new end - + it "should determine model class by constantizing give name" do CanCan::ControllerResource.new(@controller, :ability).model_class.should == Ability end - + it "should fetch model through model class and assign it to the instance" do stub(Ability).find(123) { :some_ability } CanCan::ControllerResource.new(@controller, :ability).find(123) @controller.instance_variable_get(:@ability).should == :some_ability end - + it "should fetch model through parent and assign it to the instance" do parent = Object.new stub(parent).model_instance.stub!.abilities.stub!.find(123) { :some_ability } CanCan::ControllerResource.new(@controller, :ability, parent).find(123) @controller.instance_variable_get(:@ability).should == :some_ability end - + it "should build model through model class and assign it to the instance" do stub(Ability).new(123) { :some_ability } CanCan::ControllerResource.new(@controller, :ability).build(123) @controller.instance_variable_get(:@ability).should == :some_ability end - + it "should build model through parent and assign it to the instance" do parent = Object.new stub(parent).model_instance.stub!.abilities.stub!.build(123) { :some_ability } CanCan::ControllerResource.new(@controller, :ability, parent).build(123) @controller.instance_variable_get(:@ability).should == :some_ability end - + it "should not load resource if instance variable is already provided" do @controller.instance_variable_set(:@ability, :some_ability) CanCan::ControllerResource.new(@controller, :ability).find(123) @controller.instance_variable_get(:@ability).should == :some_ability end - + it "should use the model class option if provided" do stub(Person).find(123) { :some_resource } CanCan::ControllerResource.new(@controller, :ability, nil, :resource => Person).find(123) @controller.instance_variable_get(:@ability).should == :some_resource end - + it "should convert string to constant for resource" do CanCan::ControllerResource.new(@controller, :ability, nil, :resource => "Person").model_class.should == Person end - + it "should raise an exception when specifying :class option since it is no longer used" do lambda { CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person) diff --git a/spec/cancan/exceptions_spec.rb b/spec/cancan/exceptions_spec.rb index 6ea6b43..1c98832 100644 --- a/spec/cancan/exceptions_spec.rb +++ b/spec/cancan/exceptions_spec.rb @@ -5,29 +5,29 @@ describe CanCan::AccessDenied do before(:each) do @exception = CanCan::AccessDenied.new(nil, :some_action, :some_subject) end - + it "should have action and subject accessors" do @exception.action.should == :some_action @exception.subject.should == :some_subject end - + it "should have a changable default message" do @exception.message.should == "You are not authorized to access this page." @exception.default_message = "Unauthorized!" @exception.message.should == "Unauthorized!" end end - + describe "with only a message" do before(:each) do @exception = CanCan::AccessDenied.new("Access denied!") end - + it "should have nil action and subject" do @exception.action.should be_nil @exception.subject.should be_nil end - + it "should have passed message" do @exception.message.should == "Access denied!" end diff --git a/spec/cancan/resource_authorization_spec.rb b/spec/cancan/resource_authorization_spec.rb index 29f2048..6f50b3a 100644 --- a/spec/cancan/resource_authorization_spec.rb +++ b/spec/cancan/resource_authorization_spec.rb @@ -4,101 +4,112 @@ describe CanCan::ResourceAuthorization do before(:each) do @controller = Object.new # simple stub for now end - + it "should load the resource into an instance variable if params[:id] is specified" do stub(Ability).find(123) { :some_resource } authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show", :id => 123) authorization.load_resource @controller.instance_variable_get(:@ability).should == :some_resource end - + it "should properly load resource for namespaced controller" do stub(Ability).find(123) { :some_resource } authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "admin/abilities", :action => "show", :id => 123) authorization.load_resource @controller.instance_variable_get(:@ability).should == :some_resource end - + it "should properly load resource for namespaced controller when using '::' for namespace" do stub(Ability).find(123) { :some_resource } authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "Admin::AbilitiesController", :action => "show", :id => 123) authorization.load_resource @controller.instance_variable_get(:@ability).should == :some_resource end - + it "should build a new resource with hash if params[:id] is not specified" do stub(Ability).new(:foo => "bar") { :some_resource } authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "create", :ability => {:foo => "bar"}) authorization.load_resource @controller.instance_variable_get(:@ability).should == :some_resource end - + it "should build a new resource even if attribute hash isn't specified" do stub(Ability).new(nil) { :some_resource } authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "new") authorization.load_resource @controller.instance_variable_get(:@ability).should == :some_resource end - + it "should not build a resource when on index action" do authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "index") authorization.load_resource @controller.instance_variable_get(:@ability).should be_nil end - + it "should perform authorization using controller action and loaded model" do @controller.instance_variable_set(:@ability, :some_resource) stub(@controller).authorize!(:show, :some_resource) { raise CanCan::AccessDenied } authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show") lambda { authorization.authorize_resource }.should raise_error(CanCan::AccessDenied) end - + it "should perform authorization using controller action and non loaded model" do stub(@controller).authorize!(:show, Ability) { raise CanCan::AccessDenied } authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show") lambda { authorization.authorize_resource }.should raise_error(CanCan::AccessDenied) end - + it "should call load_resource and authorize_resource for load_and_authorize_resource" do authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show") mock(authorization).load_resource mock(authorization).authorize_resource authorization.load_and_authorize_resource end - + it "should not build a resource when on custom collection action" do authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "sort"}, {:collection => [:sort, :list]}) authorization.load_resource @controller.instance_variable_get(:@ability).should be_nil end - + it "should build a resource when on custom new action even when params[:id] exists" do stub(Ability).new(nil) { :some_resource } authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "build", :id => 123}, {:new => :build}) authorization.load_resource @controller.instance_variable_get(:@ability).should == :some_resource end - + it "should not try to load resource for other action if params[:id] is undefined" do authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "list") authorization.load_resource @controller.instance_variable_get(:@ability).should be_nil end - + 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.load_resource + @controller.instance_variable_get(:@person).should == person @controller.instance_variable_get(:@ability).should == :some_ability 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 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.load_resource @controller.instance_variable_get(:@ability).should == :some_ability end - + it "should not load nested resource and build through this if *_id param isn't specified" do stub(Person).find(456) { :some_person } stub(Ability).new(nil) { :some_ability } @@ -107,11 +118,18 @@ describe CanCan::ResourceAuthorization do @controller.instance_variable_get(:@person).should == :some_person @controller.instance_variable_get(:@ability).should == :some_ability end - + it "should load the model using a custom class" do stub(Person).find(123) { :some_resource } authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "show", :id => 123}, {:resource => Person}) authorization.load_resource @controller.instance_variable_get(:@ability).should == :some_resource 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4e670be..20c75f6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,7 +13,7 @@ end class Ability include CanCan::Ability - + def initialize(user) end end