47 Commits
1.2.0 ... 1.3.3

Author SHA1 Message Date
Ryan Bates
a10a38c82f releasing version 1.3.3 2010-08-20 16:27:25 -07:00
Ryan Bates
caed4fcee5 use RSpec namespace for matcher - closes #119 2010-08-18 16:22:43 -07:00
Ryan Bates
e893e12260 fixing broken spec and minor improvements to tableized_conditions method 2010-08-18 16:04:08 -07:00
McClain Looney
3d7742ea43 fix for bug 123 2010-08-17 09:33:11 -05:00
Ryan Bates
a566ea0f4f releasing version 1.3.2 which fixes slice error when passing custom resource name 2010-08-07 08:38:35 -07:00
Ryan Bates
333ddf1970 properly pass along resource name without slice error - closes #112 2010-08-07 08:33:31 -07:00
Ryan Bates
cd74267364 releasing version 1.3.1 with sanitize_sql fix 2010-08-06 23:28:51 -07:00
Ryan Bates
f8631dcc93 fixing error on protected sanitize_sql - closes #111 2010-08-06 23:24:04 -07:00
Ryan Bates
7a17586eb3 releasing version 1.3.0 2010-08-06 16:00:52 -07:00
Ryan Bates
13427e18d5 updating readme 2010-08-06 15:54:25 -07:00
Ryan Bates
1dccc0252a removing :resource option from rdocs since it's not longer used 2010-08-06 15:05:24 -07:00
Ryan Bates
75ce2bdefa allow :parent => false option to work in load/authorize resource 2010-08-06 14:26:57 -07:00
Ryan Bates
c9e0f4e3ef renaming :singular resource option to :singleton 2010-08-06 13:06:18 -07:00
Ryan Bates
7d9e710f05 updating changelog 2010-08-06 11:42:30 -07:00
Ryan Bates
236cece3b3 adding :find_by option to load_resource - closes #19 2010-08-06 11:18:54 -07:00
Ryan Bates
84f4c904b7 adding :singular option to support has_one associations in load/authorize resource - closes #93 2010-08-06 11:03:10 -07:00
Ryan Bates
6998e8bdd1 support multiple resources in :through option of load_resource, this makes polymorphic associations possible - closes #73 2010-08-06 10:35:42 -07:00
Ryan Bates
961b8c2477 consider ancestors when matching classes in Ability#can, this way it works with STI - closes #55 2010-08-06 10:06:37 -07:00
Ryan Bates
a157b65fbf adding :instance_name option to load/authorize_resource - closes #44 2010-08-06 09:35:07 -07:00
Ryan Bates
47f0aa597e change how params are passed to ControllerResource and use HashWithIndifferentAccess in tests 2010-08-06 09:24:01 -07:00
Ryan Bates
67b069579e don't pass nil to 'new' call when no params are specified - closes #63 2010-08-05 16:52:37 -07:00
Ryan Bates
156839b73e only use the :read action when authorizing parent resources 2010-08-05 16:24:08 -07:00
Ryan Bates
25a1c553bf adding :through option to replace :nesting option and moving ResourceAuthorization class code into ControllerResource 2010-08-05 16:12:30 -07:00
Ryan Bates
8dee01195d improving inline documentation for Query 2010-07-21 12:16:08 -07:00
Ryan Bates
5eae169d7b mentioning CanCan contributors in README 2010-07-21 12:06:28 -07:00
Ryan Bates
66ff1f2ea3 removing metrics link in readme, farewell Caliper :( 2010-07-21 12:02:27 -07:00
Ryan Bates
18dcf2a121 mention Rails 3 installation in README - closes #56 2010-07-21 11:59:24 -07:00
Ryan Bates
9b26f4d767 fixing specs for older versions of ruby and rspec 2010-07-21 11:56:19 -07:00
Ryan Bates
25637bb33a removing extra white space at end of lines 2010-07-21 11:45:26 -07:00
Ryan Bates
c5737f6d28 adding thanks to changelog 2010-07-20 17:11:05 -07:00
Ryan Bates
1659f21bb3 updating changelong with support for multiple can calls in accessible_by - closes #71 2010-07-20 17:09:46 -07:00
Ryan Bates
ba8cb3cf6d refactoring query.joins 2010-07-20 17:05:17 -07:00
Ryan Bates
e098ddaacd refactoring query.conditions 2010-07-20 16:00:22 -07:00
Ryan Bates
964a4765b1 removing need to pass tableize option around for query conditions 2010-07-20 13:43:43 -07:00
Ryan Bates
a42e067f3b extracting out Query class for generating sql conditions and association joins 2010-07-20 13:20:01 -07:00
Ryan Bates
60848143b7 refactoring can definition matching behavior 2010-07-20 11:04:03 -07:00
Ryan Bates
5d8f04363d merging with master and resolving a couple conflicts 2010-07-19 16:36:01 -07:00
Ryan Bates
cad425989e supporting deeply nested aliases - closes #98 2010-07-19 16:03:09 -07:00
Yura Sokolov
75eb1917f9 add test for single cannot definition 2010-05-25 14:28:29 +04:00
Yura Sokolov
5fd793090a fix logic error for single cannot condition - it should return no records 2010-05-25 14:09:01 +04:00
Yura Sokolov
ac19422a90 add tests for merging conditions and joins 2010-05-25 14:02:26 +04:00
Yura Sokolov
9c0346b90b can accept array for sql sanitizing in conditions 2010-05-25 14:01:53 +04:00
Yura Sokolov
b473d8827f CanDefinition#only_block? 2010-05-25 12:23:39 +04:00
Yura Sokolov
bcab8d6369 fix error with single cannot condition 2010-05-25 12:14:01 +04:00
Yura Sokolov
dbc1538054 small refactoring: CanDefinition #definitive? #conditions_empty? 2010-05-24 18:15:20 +04:00
Yura Sokolov
46f03013f3 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
2010-05-24 15:17:10 +04:00
Sokolov Yura
7d7d249182 passing throw matching rules with not matching conditions
Main goal is to allow:

cannot :manage, :all
can :read, :all
can :manage, User, :id=>user.id
can :manage, User, :manager_id=>user.id

Signed-off-by: Sokolov Yura <funny.falcon@gmail.com>
2010-05-16 22:13:02 +04:00
23 changed files with 959 additions and 495 deletions

View File

@@ -1,3 +1,47 @@
1.3.3 (August 20, 2010)
* Switching to Rspec namespace to remove deprecation warning in Rspec 2 - see issue #119
* Pluralize nested associations for conditions in accessible_by (thanks mlooney) - see issue #123
1.3.2 (August 7, 2010)
* Fixing slice error when passing in custom resource name - see issue #112
1.3.1 (August 6, 2010)
* Fixing protected sanitize_sql error - see issue #111
1.3.0 (August 6, 2010)
* Adding :find_by option to load_resource - see issue #19
* Adding :singleton option to load_resource - see issue #93
* Supporting multiple resources in :through option for polymorphic associations - see issue #73
* Supporting Single Table Inheritance for "can" comparisons - see issue #55
* Adding :instance_name option to load/authorize_resource - see issue #44
* Don't pass nil to "new" to keep MongoMapper happy - see issue #63
* Parent resources are now authorized with :read action.
* Changing :resource option in load/authorize_resource back to :class with ability to pass false
* Removing :nested option in favor of :through option with separate load/authorize call
* Moving internal logic from ResourceAuthorization to ControllerResource class
* Supporting multiple "can" and "cannot" calls with accessible_by (thanks funny-falcon) - see issue #71
* Supporting deeply nested aliases - see issue #98
1.2.0 (July 16, 2010) 1.2.0 (July 16, 2010)
* Load nested parent resources on collection actions such as "index" (thanks dohzya) * Load nested parent resources on collection actions such as "index" (thanks dohzya)

View File

@@ -1,18 +1,22 @@
= CanCan = CanCan
Wiki[http://wiki.github.com/ryanb/cancan] | RDocs[http://rdoc.info/projects/ryanb/cancan] | Screencast[http://railscasts.com/episodes/192-authorization-with-cancan] | Metrics[http://getcaliper.com/caliper/project?repo=git%3A%2F%2Fgithub.com%2Fryanb%2Fcancan.git] Wiki[http://wiki.github.com/ryanb/cancan] | RDocs[http://rdoc.info/projects/ryanb/cancan] | Screencast[http://railscasts.com/episodes/192-authorization-with-cancan]
CanCan is an authorization solution for Ruby on Rails. This restricts what a given user is allowed to access throughout the application. It is completely decoupled from any role based implementation and focusses on keeping permission logic in a single location (the +Ability+ class) so it is not duplicated across controllers, views, and database queries. CanCan is an authorization solution for Ruby on Rails for restricting what a given user is allowed to access throughout the application. It does not care how your user roles are defined, it simply focusses on keeping permission logic in a single location (the +Ability+ class) so it is not duplicated across controllers, views, and database queries.
This assumes you already have authentication (such as Authlogic[http://github.com/binarylogic/authlogic] or Devise[http://github.com/plataformatec/devise]) that provides a +current_user+ method which CanCan relies on. See {Changing Defaults}[http://wiki.github.com/ryanb/cancan/changing-defaults] if you need different behavior. By default, the +current_user+ method is required, so if you have not already, set up some authentication (such as Authlogic[http://github.com/binarylogic/authlogic] or Devise[http://github.com/plataformatec/devise]). See {Changing Defaults}[http://wiki.github.com/ryanb/cancan/changing-defaults] if you need different behavior.
== Installation == Installation
CanCan is provided as a gem. Simply include it in your environment.rb or Gemfile. To install CanCan, include the gem in the environment.rb in Rails 2.3.
config.gem "cancan" config.gem "cancan"
Or the Gemfile in Rails 3.
gem "cancan"
Alternatively it can be installed as a plugin. Alternatively it can be installed as a plugin.
script/plugin install git://github.com/ryanb/cancan.git script/plugin install git://github.com/ryanb/cancan.git
@@ -20,7 +24,7 @@ Alternatively it can be installed as a plugin.
== Getting Started == Getting Started
First, define a class called +Ability+ in "models/ability.rb". It should look something like this. First, define a class called +Ability+ in "models/ability.rb" or anywhere else in the load path. It should look something like this.
class Ability class Ability
include CanCan::Ability include CanCan::Ability
@@ -136,7 +140,8 @@ See {Fetching Records}[http://wiki.github.com/ryanb/cancan/fetching-records] for
== Additional Docs == Additional Docs
* {Upgrading to 1.1}[http://wiki.github.com/ryanb/cancan/upgrading-to-11] * {Upgrading to 1.3}[http://wiki.github.com/ryanb/cancan/upgrading-to-13]
* {Nested Resources}[http://wiki.github.com/ryanb/cancan/nested-resources]
* {Testing Abilities}[http://wiki.github.com/ryanb/cancan/testing-abilities] * {Testing Abilities}[http://wiki.github.com/ryanb/cancan/testing-abilities]
* {Accessing Request Data}[http://wiki.github.com/ryanb/cancan/accessing-request-data] * {Accessing Request Data}[http://wiki.github.com/ryanb/cancan/accessing-request-data]
* {Admin Namespace}[http://wiki.github.com/ryanb/cancan/admin-namespace] * {Admin Namespace}[http://wiki.github.com/ryanb/cancan/admin-namespace]
@@ -144,4 +149,4 @@ See {Fetching Records}[http://wiki.github.com/ryanb/cancan/fetching-records] for
== Special Thanks == Special Thanks
CanCan was inspired by declarative_authorization[http://github.com/stffn/declarative_authorization/] and aegis[http://github.com/makandra/aegis]. Many thanks to the authors and contributors. CanCan was inspired by declarative_authorization[http://github.com/stffn/declarative_authorization/] and aegis[http://github.com/makandra/aegis]. Also many thanks to the CanCan contributors[http://github.com/ryanb/cancan/contributors]. See the CHANGELOG[http://github.com/ryanb/cancan/blob/master/CHANGELOG.rdoc] for the full list.

View File

@@ -1,11 +1,11 @@
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "cancan" s.name = "cancan"
s.version = "1.2.0" s.version = "1.3.3"
s.author = "Ryan Bates" s.author = "Ryan Bates"
s.email = "ryan@railscasts.com" s.email = "ryan@railscasts.com"
s.homepage = "http://github.com/ryanb/cancan" s.homepage = "http://github.com/ryanb/cancan"
s.summary = "Simple authorization solution for Rails." s.summary = "Simple authorization solution for Rails."
s.description = "Simple authorization solution for Rails which is completely decoupled from the user's roles. All permissions are stored in a single location for convenience." s.description = "Simple authorization solution for Rails which is decoupled from user roles. All permissions are stored in a single location."
s.files = Dir["{lib,spec}/**/*", "[A-Z]*", "init.rb"] s.files = Dir["{lib,spec}/**/*", "[A-Z]*", "init.rb"]
s.require_path = "lib" s.require_path = "lib"

View File

@@ -1,7 +1,7 @@
require 'cancan/ability' require 'cancan/ability'
require 'cancan/can_definition' require 'cancan/can_definition'
require 'cancan/controller_resource' require 'cancan/controller_resource'
require 'cancan/resource_authorization'
require 'cancan/controller_additions' require 'cancan/controller_additions'
require 'cancan/active_record_additions' require 'cancan/active_record_additions'
require 'cancan/exceptions' require 'cancan/exceptions'
require 'cancan/query'

View File

@@ -50,8 +50,10 @@ module CanCan
# Also see the RSpec Matchers to aid in testing. # 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
can_definition = matching_can_definition(action, subject) match = relevant_can_definitions(action, subject).detect do |can_definition|
can_definition && can_definition.can?(action, subject, extra_args) can_definition.matches_conditions?(action, subject, extra_args)
end
match ? match.base_behavior : false
end end
# Convenience method which works the same as "can?" but returns the opposite value. # Convenience method which works the same as "can?" but returns the opposite value.
@@ -180,47 +182,42 @@ module CanCan
@aliased_actions = {} @aliased_actions = {}
end end
# Returns a hash of conditions which match the given ability. This is useful if you need to generate a database # Returns a CanCan::Query instance to help generate database queries based on the ability.
# query based on the current ability. # If any relevant can definitions use a block then an exception will be raised because an
# # SQL query cannot be generated from blocks of code.
# can :read, Article, :visible => true def query(action, subject)
# conditions :read, Article # returns { :visible => true } Query.new(subject, relevant_can_definitions_for_query(action, subject))
#
# 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, options = {})
can_definition = matching_can_definition(action, subject)
if can_definition
raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" if can_definition.block
can_definition.conditions(options) || {}
else
false
end
end
# Returns the associations used in conditions. This is usually used in the :joins option for a search.
# See ActiveRecordAdditions#accessible_by for use in Active Record.
def association_joins(action, subject)
can_definition = matching_can_definition(action, subject)
if can_definition
raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}" if can_definition.block
can_definition.association_joins
end
end end
private private
# 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(actions)
actions.map do |action|
aliased_actions[action] ? [action, *expand_actions(aliased_actions[action])] : action
end.flatten
end
def can_definitions def can_definitions
@can_definitions ||= [] @can_definitions ||= []
end end
def matching_can_definition(action, subject) # Returns an array of CanDefinition instances which match the action and subject
can_definitions.reverse.detect do |can_definition| # This does not take into consideration any hash conditions or block statements
can_definition.expand_actions(aliased_actions) def relevant_can_definitions(action, subject)
can_definition.matches? action, subject can_definitions.reverse.select do |can_definition|
can_definition.expanded_actions = expand_actions(can_definition.actions)
can_definition.relevant? action, subject
end
end
def relevant_can_definitions_for_query(action, subject)
relevant_can_definitions(action, subject).each do |can_definition|
if can_definition.only_block?
raise Error, "Cannot determine SQL conditions or joins from block for #{action.inspect} #{subject.inspect}"
end
end end
end end

View File

@@ -20,12 +20,11 @@ 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.conditions(action, self, :tableize => true) || {:id => nil} query = ability.query(action, self)
joins = ability.association_joins(action, self)
if respond_to? :where if respond_to? :where
where(conditions).joins(joins) where(query.conditions).joins(query.joins)
else else
scoped(:conditions => conditions, :joins => joins) scoped(:conditions => query.conditions, :joins => query.joins)
end end
end end
end end

View File

@@ -3,8 +3,8 @@ module CanCan
# it holds the information about a "can" call made on Ability and provides # it holds the information about a "can" call made on Ability and provides
# helpful methods to determine permission checking and conditions hash generation. # helpful methods to determine permission checking and conditions hash generation.
class CanDefinition # :nodoc: class CanDefinition # :nodoc:
include ActiveSupport::Inflector attr_reader :base_behavior, :actions
attr_reader :block attr_writer :expanded_actions
# 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
@@ -18,51 +18,47 @@ module CanCan
@block = block @block = block
end end
# Accepts a hash of aliased actions and returns an array of actions which match. # Matches both the subject and action, not necessarily the conditions
# This should be called before "matches?" and other checking methods since they def relevant?(action, subject)
# 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) matches_action?(action) && matches_subject?(subject)
end end
def can?(action, subject, extra_args) # Matches the block or conditions hash
result = can_without_base_behavior?(action, subject, extra_args) def matches_conditions?(action, subject, extra_args)
@base_behavior ? result : !result if @block
end call_block(action, subject, extra_args)
elsif @conditions.kind_of?(Hash) && subject.class != Class
# Returns a hash of conditions. If the ":tableize => true" option is passed matches_conditions_hash?(subject)
# 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 else
@conditions true
end end
end end
def association_joins(conditions = @conditions) def tableized_conditions(conditions = @conditions)
joins = [] conditions.inject({}) do |result_hash, (name, value)|
conditions.each do |name, value|
if value.kind_of? Hash if value.kind_of? Hash
nested = association_joins(value) name = name.to_s.tableize.to_sym
if nested value = tableized_conditions(value)
joins << {name => nested} end
else result_hash[name] = value
joins << name result_hash
end end
end end
def only_block?
conditions_empty? && !@block.nil?
end end
joins unless joins.empty?
def conditions_empty?
@conditions == {} || @conditions.nil?
end
def associations_hash(conditions = @conditions)
hash = {}
conditions.map do |name, value|
hash[name] = associations_hash(value) if value.kind_of? Hash
end
hash
end end
private private
@@ -72,27 +68,21 @@ module CanCan
end end
def matches_subject?(subject) def matches_subject?(subject)
@subjects.include?(:all) || @subjects.include?(subject) || @subjects.any? { |sub| sub.kind_of?(Class) && subject.kind_of?(sub) } @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
end end
def can_without_base_behavior?(action, subject, extra_args) def matches_subject_class?(subject)
if @block @subjects.any? { |sub| sub.kind_of?(Class) && (subject.kind_of?(sub) || subject.kind_of?(Class) && subject.ancestors.include?(sub)) }
call_block(action, subject, extra_args)
elsif @conditions && subject.class != Class
matches_conditions? subject
else
true
end
end end
def matches_conditions?(subject, conditions = @conditions) def matches_conditions_hash?(subject, conditions = @conditions)
conditions.all? do |name, value| conditions.all? do |name, value|
attribute = subject.send(name) attribute = subject.send(name)
if value.kind_of?(Hash) if value.kind_of?(Hash)
if attribute.kind_of? Array if attribute.kind_of? Array
attribute.any? { |element| matches_conditions? element, value } attribute.any? { |element| matches_conditions_hash? element, value }
else else
matches_conditions? attribute, value matches_conditions_hash? attribute, value
end end
elsif value.kind_of?(Array) || value.kind_of?(Range) elsif value.kind_of?(Array) || value.kind_of?(Range)
value.include? attribute value.include? attribute

View File

@@ -11,11 +11,11 @@ module CanCan
# load_and_authorize_resource # load_and_authorize_resource
# end # end
# #
def load_and_authorize_resource(options = {}) def load_and_authorize_resource(*args)
ResourceAuthorization.add_before_filter(self, :load_and_authorize_resource, options) ControllerResource.add_before_filter(self, :load_and_authorize_resource, *args)
end end
# Sets up a before filter which loads the appropriate model resource into an instance variable. # Sets up a before filter which loads the model resource into an instance variable.
# For example, given an ArticlesController it will load the current article into the @article # 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 # 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" # Article.new(params[:article]) depending upon the action. It does nothing for the "index"
@@ -41,6 +41,20 @@ module CanCan
# end # end
# end # end
# #
# If a name is provided which does not match the controller it assumes it is a parent resource. Child
# resources can then be loaded through it.
#
# class BooksController < ApplicationController
# load_resource :author
# load_resource :book, :through => :author
# end
#
# Here the author resource will be loaded before each action using params[:author_id]. The book resource
# will then be loaded through the @author instance variable.
#
# That first argument is optional and will default to the singular name of the controller.
# A hash of options (see below) can also be passed to this method to further customize it.
#
# See load_and_authorize_resource to automatically authorize the resource too. # See load_and_authorize_resource to automatically authorize the resource too.
# #
# Options: # Options:
@@ -50,27 +64,30 @@ module CanCan
# [:+except+] # [:+except+]
# Does not apply before filter to given actions. # Does not apply before filter to given actions.
# #
# [:+nested+] # [:+through+]
# Specify which resource this is nested under. # Load this resource through another one. This should match the name of the parent instance variable.
# #
# load_resource :nested => :author # [:+singleton+]
# Pass +true+ if this is a singleton resource through a +has_one+ association.
# #
# Deep nesting can be defined in an array. # [:+parent+]
# True or false depending on if the resource is considered a parent resource. This defaults to +true+ if a resource
# name is given which does not match the controller.
# #
# load_resource :nested => [:publisher, :author] # [:+class+]
#
# [:+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). # The class to use for the model (string or constant).
# #
# [:+instance_name+]
# The name of the instance variable to load the resource into.
#
# [:+find_by+]
# Find using a different attribute other than id. For example.
#
# load_resource :find_by => :permalink # will use find_by_permlink!(params[:id])
#
# [:+collection+] # [:+collection+]
# Specify which actions are resource collection actions in addition to :+index+. This # 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 usually not necessary because it will try to guess depending on if the id param is present.
# is present in +params+.
# #
# load_resource :collection => [:sort, :list] # load_resource :collection => [:sort, :list]
# #
@@ -81,11 +98,11 @@ module CanCan
# #
# load_resource :new => :build # load_resource :new => :build
# #
def load_resource(options = {}) def load_resource(*args)
ResourceAuthorization.add_before_filter(self, :load_resource, options) ControllerResource.add_before_filter(self, :load_resource, *args)
end end
# Sets up a before filter which authorizes the current resource using the instance variable. # Sets up a before filter which authorizes the resource using the instance variable.
# For example, if you have an ArticlesController it will check the @article 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 # and ensure the user can perform the current action on it. Under the hood it is doing
# something like the following. # something like the following.
@@ -98,6 +115,19 @@ module CanCan
# authorize_resource # authorize_resource
# end # end
# #
# If you pass in the name of a resource which does not match the controller it will assume
# it is a parent resource.
#
# class BooksController < ApplicationController
# authorize_resource :author
# authorize_resource :book
# end
#
# Here it will authorize :+show+, @+author+ on every action before authorizing the book.
#
# That first argument is optional and will default to the singular name of the controller.
# A hash of options (see below) can also be passed to this method to further customize it.
#
# See load_and_authorize_resource to automatically load the resource too. # See load_and_authorize_resource to automatically load the resource too.
# #
# Options: # Options:
@@ -107,17 +137,19 @@ module CanCan
# [:+except+] # [:+except+]
# Does not apply before filter to given actions. # Does not apply before filter to given actions.
# #
# [:+name+] # [:+parent+]
# The name of the resource if it cannot be determined from controller (string or symbol). # True or false depending on if the resource is considered a parent resource. This defaults to +true+ if a resource
# name is given which does not match the controller.
# #
# load_resource :name => :article # [:+class+]
# The class to use for the model (string or constant). This passed in when the instance variable is not set.
# Pass +false+ if there is no associated class for this resource and it will use a symbol of the resource name.
# #
# [:+resource+] # [:+instance_name+]
# The class to use for the model (string or constant). Alternatively pass a symbol # The name of the instance variable for this resource.
# to represent a resource which does not have a class.
# #
def authorize_resource(options = {}) def authorize_resource(*args)
ResourceAuthorization.add_before_filter(self, :authorize_resource, options) ControllerResource.add_before_filter(self, :authorize_resource, *args)
end end
end end

View File

@@ -1,54 +1,129 @@
module CanCan module CanCan
# Used internally to load and authorize a given controller resource. # Handle the load and authorization controller logic so we don't clutter up all controllers with non-interface methods.
# This manages finding or building an instance of the resource. If a # This class is used internally, so you do not need to call methods directly on it.
# parent is given it will go through the association.
class ControllerResource # :nodoc: class ControllerResource # :nodoc:
def initialize(controller, name, parent = nil, options = {}) def self.add_before_filter(controller_class, method, *args)
raise ImplementationRemoved, "The :class option has been renamed to :resource for specifying the class in CanCan." if options.has_key? :class options = args.extract_options!
resource_name = args.first
controller_class.before_filter(options.slice(:only, :except)) do |controller|
ControllerResource.new(controller, resource_name, options.except(:only, :except)).send(method)
end
end
def initialize(controller, *args)
@controller = controller @controller = controller
@name = name @params = controller.params
@parent = parent @options = args.extract_options!
@options = options @name = args.first
raise CanCan::ImplementationRemoved, "The :nested option is no longer supported, instead use :through with separate load/authorize call." if @options[:nested]
raise CanCan::ImplementationRemoved, "The :name option is no longer supported, instead pass the name as the first argument." if @options[:name]
raise CanCan::ImplementationRemoved, "The :resource option has been renamed back to :class, use false if no class." if @options[:resource]
end end
# Returns the class used for this resource. This can be overriden by the :resource option. def load_and_authorize_resource
# Sometimes one will use a symbol as the resource if a class does not exist for it. In that load_resource
# case "find" and "build" should not be called on it. authorize_resource
def model_class end
resource_class = @options[:resource]
if resource_class.nil? def load_resource
@name.to_s.camelize.constantize if !resource_instance && (parent? || member_action?)
elsif resource_class.kind_of? String @controller.instance_variable_set("@#{instance_name}", load_resource_instance)
resource_class.constantize
else
resource_class # could be a symbol
end end
end end
def find(id) def authorize_resource
self.model_instance ||= base.find(id) @controller.authorize!(authorization_action, resource_instance || resource_class)
end end
# Build a new instance of this resource. If it is a class we just call "new" otherwise def parent?
# it's an associaiton and "build" is used. @options.has_key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
def build(attributes)
self.model_instance ||= (base.kind_of?(Class) ? base.new(attributes) : base.build(attributes))
end
def model_instance
@controller.instance_variable_get("@#{@name}")
end
def model_instance=(instance)
@controller.instance_variable_set("@#{@name}", instance)
end end
private private
def load_resource_instance
if !parent? && new_actions.include?(@params[:action].to_sym)
build_resource
elsif id_param || @options[:singleton]
find_resource
end
end
def build_resource
method_name = @options[:singleton] ? "build_#{name}" : "new"
resource_base.send(*[method_name, @params[name]].compact)
end
def find_resource
if @options[:singleton]
resource_base.send(name)
else
@options[:find_by] ? resource_base.send("find_by_#{@options[:find_by]}!", id_param) : resource_base.find(id_param)
end
end
def authorization_action
parent? ? :read : @params[:action].to_sym
end
def id_param
@params[parent? ? :"#{name}_id" : :id]
end
def member_action?
!collection_actions.include? @params[:action].to_sym
end
# Returns the class used for this resource. This can be overriden by the :class option.
# If +false+ is passed in it will use the resource name as a symbol in which case it should
# only be used for authorization, not loading since there's no class to load through.
def resource_class
case @options[:class]
when false then name.to_sym
when nil then name.to_s.camelize.constantize
when String then @options[:class].constantize
else @options[:class]
end
end
def resource_instance
@controller.instance_variable_get("@#{instance_name}")
end
# The object that methods (such as "find", "new" or "build") are called on. # 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. # If the :through option is passed it will go through an association on that instance.
def base # If the :singleton option is passed it won't use the association because it needs to be handled later.
@parent ? @parent.model_instance.send(@name.to_s.pluralize) : model_class def resource_base
if through_resource
@options[:singleton] ? through_resource : through_resource.send(name.to_s.pluralize)
else
resource_class
end
end
# The object to load this resource through.
def through_resource
@options[:through] && [@options[:through]].flatten.map { |i| @controller.instance_variable_get("@#{i}") }.compact.first
end
def name
@name || name_from_controller
end
def name_from_controller
@params[:controller].sub("Controller", "").underscore.split('/').last.singularize
end
def instance_name
@options[:instance_name] || name
end
def collection_actions
[:index] + [@options[:collection]].flatten
end
def new_actions
[:new, :create] + [@options[:new]].flatten
end end
end end
end end

View File

@@ -1,4 +1,5 @@
Spec::Matchers.define :be_able_to do |*args| RSpec = Spec unless defined? RSpec # for RSpec 1 compatability
RSpec::Matchers.define :be_able_to do |*args|
match do |ability| match do |ability|
ability.can?(*args) ability.can?(*args)
end end

97
lib/cancan/query.rb Normal file
View File

@@ -0,0 +1,97 @@
module CanCan
# Generates the sql conditions and association joins for use in ActiveRecord queries.
# Normally you will not use this class directly, but instead through ActiveRecordAdditions#accessible_by.
class Query
def initialize(sanitizer, can_definitions)
@sanitizer = sanitizer
@can_definitions = can_definitions
end
# Returns conditions intended to be used inside a database query. Normally you will not call this
# method directly, but instead go through ActiveRecordAdditions#accessible_by.
#
# If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
#
# can :manage, User, :id => 1
# query(:manage, User).conditions # => { :id => 1 }
#
# If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
#
# can :manage, User, :id => 1
# can :manage, User, :manager_id => 1
# cannot :manage, User, :self_managed => true
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
#
def conditions
if @can_definitions.size == 1 && @can_definitions.first.base_behavior
# Return the conditions directly if there's just one definition
@can_definitions.first.tableized_conditions
else
@can_definitions.reverse.inject(false_sql) do |sql, can_definition|
merge_conditions(sql, can_definition.tableized_conditions, can_definition.base_behavior)
end
end
end
# Returns the associations used in conditions for the :joins option of a search.
# See ActiveRecordAdditions#accessible_by for use in Active Record.
def joins
joins_hash = {}
@can_definitions.each do |can_definition|
merge_joins(joins_hash, can_definition.associations_hash)
end
clean_joins(joins_hash) unless joins_hash.empty?
end
private
def merge_conditions(sql, conditions_hash, behavior)
if conditions_hash.blank?
behavior ? true_sql : false_sql
else
conditions = sanitize_sql(conditions_hash)
case sql
when true_sql
behavior ? true_sql : "not (#{conditions})"
when false_sql
behavior ? conditions : false_sql
else
behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
end
end
end
def false_sql
sanitize_sql(['?=?', true, false])
end
def true_sql
sanitize_sql(['?=?', true, true])
end
def sanitize_sql(conditions)
@sanitizer.send(:sanitize_sql, conditions)
end
# Takes two hashes and does a deep merge.
def merge_joins(base, add)
add.each do |name, nested|
if base[name].is_a?(Hash) && !nested.empty?
merge_joins(base[name], nested)
else
base[name] = nested
end
end
end
# Removes empty hashes and moves everything into arrays.
def clean_joins(joins_hash)
joins = []
joins_hash.each do |name, nested|
joins << (nested.empty? ? name : {name => clean_joins(nested)})
end
joins
end
end
end

View File

@@ -1,70 +0,0 @@
module CanCan
# Handle the load and authorization controller logic so we don't clutter up all controllers with non-interface methods.
# This class is used internally, so you do not need to call methods directly on it.
class ResourceAuthorization # :nodoc:
def self.add_before_filter(controller_class, method, options = {})
controller_class.before_filter(options.slice(:only, :except)) do |controller|
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
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]
resource.find(@params[:id])
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|
id = @params["#{name}_id".to_sym]
if id
parent = ControllerResource.new(@controller, name, parent)
parent.find(id)
else
parent = nil
end
end
parent
end
def model_name
@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
end
end

View File

@@ -16,50 +16,81 @@ describe CanCan::Ability do
@ability.can?(:foodfight, String).should be_false @ability.can?(:foodfight, String).should be_false
end end
it "should return what block returns on a can call" do it "should pass true to `can?` when non false/nil is returned in block" do
@ability.can :read, :all @ability.can :read, :all
@ability.can :read, Symbol do |sym| @ability.can :read, Symbol do |sym|
sym "foo" # TODO test that sym is nil when no instance is passed
end end
@ability.can?(:read, Symbol).should be_nil @ability.can?(:read, :some_symbol).should == true
@ability.can?(:read, :some_symbol).should == :some_symbol end
it "should pass to previous can definition, if block returns false or nil" 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 end
it "should pass class with object if :all objects are accepted" do it "should pass class with object if :all objects are accepted" do
@ability.can :preview, :all do |object_class, object| @ability.can :preview, :all do |object_class, object|
[object_class, object] object_class.should == Fixnum
object.should == 123
@block_called = true
end end
@ability.can?(:preview, 123).should == [Fixnum, 123] @ability.can?(:preview, 123)
@block_called.should be_true
end end
it "should pass class with no object if :all objects are accepted and class is passed directly" do 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| @ability.can :preview, :all do |object_class, object|
[object_class, object] object_class.should == Hash
object.should be_nil
@block_called = true
end end
@ability.can?(:preview, Hash).should == [Hash, nil] @ability.can?(:preview, Hash)
@block_called.should be_true
end end
it "should pass action and object for global manage actions" do it "should pass action and object for global manage actions" do
@ability.can :manage, Array do |action, object| @ability.can :manage, Array do |action, object|
[action, object] action.should == :stuff
object.should == [1, 2]
@block_called = true
end end
@ability.can?(:stuff, [1, 2]).should == [:stuff, [1, 2]] @ability.can?(:stuff, [1, 2]).should
@ability.can?(:stuff, Array).should == [:stuff, nil] @block_called.should be_true
end end
it "should alias update or destroy actions to modify action" do 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) { :modify_called } @ability.can :modify, :all
@ability.can?(:update, 123).should == :modify_called @ability.can?(:update, 123).should be_true
@ability.can?(:destroy, 123).should == :modify_called @ability.can?(:destroy, 123).should be_true
end
it "should allow deeply nested aliased actions" 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 end
it "should return block result for action, object_class, and object for any action" do it "should return block result for action, object_class, and object for any action" do
@ability.can :manage, :all do |action, object_class, object| @ability.can :manage, :all do |action, object_class, object|
[action, object_class, object] action.should == :foo
object_class.should == Fixnum
object.should == 123
@block_called = true
end end
@ability.can?(:foo, 123).should == [:foo, Fixnum, 123] @ability.can?(:foo, 123)
@ability.can?(:bar, Fixnum).should == [:bar, Fixnum, nil] @block_called.should be_true
end end
it "should automatically alias index and show into read calls" do it "should automatically alias index and show into read calls" do
@@ -69,10 +100,10 @@ describe CanCan::Ability do
end end
it "should automatically alias new and edit into create and update respectively" do it "should automatically alias new and edit into create and update respectively" do
@ability.can(:create, :all) { :create_called } @ability.can :create, :all
@ability.can(:update, :all) { :update_called } @ability.can :update, :all
@ability.can?(:new, 123).should == :create_called @ability.can?(:new, 123).should be_true
@ability.can?(:edit, 123).should == :update_called @ability.can?(:edit, 123).should be_true
end end
it "should not respond to prepare (now using initialize)" do it "should not respond to prepare (now using initialize)" do
@@ -104,6 +135,13 @@ describe CanCan::Ability do
@ability.can?(:read, :nonstats).should be_false @ability.can?(:read, :nonstats).should be_false
end 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 it "should support 'cannot' method to define what user cannot do" do
@ability.can :read, :all @ability.can :read, :all
@ability.cannot :read, Integer @ability.cannot :read, Integer
@@ -121,6 +159,40 @@ describe CanCan::Ability do
@ability.can?(:read, 123).should be_false @ability.can?(:read, 123).should be_false
end 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 )
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 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
@@ -174,30 +246,9 @@ describe CanCan::Ability do
@ability.can?(:read, [[4, 5, 6]]).should be_false @ability.can?(:read, [[4, 5, 6]]).should be_false
end end
it "should return conditions for a given ability" do
@ability.can :read, Array, :first => 1, :last => 3
@ability.conditions(:show, Array).should == {:first => 1, :last => 3}
end
it "should raise an exception when a block is used on condition" do
@ability.can :read, Array do |a|
true
end
lambda { @ability.conditions(:show, Array) }.should raise_error(CanCan::Error, "Cannot determine ability conditions from block for :show Array")
end
it "should return an empty hash for conditions when there are no conditions" do
@ability.can :read, Array
@ability.conditions(:show, Array).should == {}
end
it "should return false when performed on an action which isn't defined" do
@ability.conditions(:foo, Array).should == false
end
it "should has eated cheezburger" do it "should has eated cheezburger" do
lambda { lambda {
@ability.can? :has, :cheezburger @ability.can? :has, :cheezburger
}.should raise_exception(CanCan::Error, "Nom nom nom. I eated it.") }.should raise_error(CanCan::Error, "Nom nom nom. I eated it.")
end end
end end

View File

@@ -2,16 +2,16 @@ require "spec_helper"
describe CanCan::ActiveRecordAdditions do describe CanCan::ActiveRecordAdditions do
before(:each) do before(:each) do
@model_class = Class.new @model_class = Class.new(Person)
stub(@model_class).scoped { :scoped_stub } stub(@model_class).scoped { :scoped_stub }
@model_class.send(:include, CanCan::ActiveRecordAdditions) @model_class.send(:include, CanCan::ActiveRecordAdditions)
@ability = Object.new @ability = Object.new
@ability.extend(CanCan::Ability) @ability.extend(CanCan::Ability)
end end
it "should call where(:id => nil) when no ability is defined so no records are found" do it "should call where('true=false') when no ability is defined so no records are found" do
stub(@model_class).where(:id => nil).stub!.joins(nil) { :no_where } stub(@model_class).where('true=false').stub!.joins(nil) { :no_match }
@model_class.accessible_by(@ability, :read).should == :no_where @model_class.accessible_by(@ability, :read).should == :no_match
end end
it "should call where with matching ability conditions" do it "should call where with matching ability conditions" do
@@ -25,4 +25,27 @@ describe CanCan::ActiveRecordAdditions do
stub(@model_class).scoped(:conditions => {:foos => {: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
it "should merge association joins and sanitize conditions" do
@ability.can :read, @model_class, :foo => {:bar => 1}
@ability.can :read, @model_class, :too => {:car => 1, :far => {:bar => 1}}
condition_variants = [
'(toos.fars.bar=1 AND toos.car=1) OR (foos.bar=1)', # faked sql sanitizer is stupid ;-)
'(toos.car=1 AND toos.fars.bar=1) OR (foos.bar=1)'
]
joins_variants = [
[:foo, {:too => [:far]}],
[{:too => [:far]}, :foo]
]
condition_variants.each do |condition|
joins_variants.each do |joins|
stub(@model_class).scoped( :conditions => condition, :joins => joins ) { :found_records }
end
end
# @ability.conditions(:read, @model_class).should == '(too.car=1 AND too.far.bar=1) OR (foo.bar=1)'
# @ability.associations_hash(:read, @model_class).should == [{:too => [:far]}, :foo]
@model_class.accessible_by(@ability).should == :found_records
end
end end

View File

@@ -7,38 +7,50 @@ describe CanCan::CanDefinition do
end end
it "should return no association joins if none exist" do it "should return no association joins if none exist" do
@can.association_joins.should be_nil @can.associations_hash.should == {}
end end
it "should return no association for joins if just attributes" do it "should return no association for joins if just attributes" do
@conditions[:foo] = :bar @conditions[:foo] = :bar
@can.association_joins.should be_nil @can.associations_hash.should == {}
end end
it "should return single association for joins" do it "should return single association for joins" do
@conditions[:foo] = {:bar => 1} @conditions[:foo] = {:bar => 1}
@can.association_joins.should == [:foo] @can.associations_hash.should == {:foo => {}}
end end
it "should return multiple associations for joins" do it "should return multiple associations for joins" do
@conditions[:foo] = {:bar => 1} @conditions[:foo] = {:bar => 1}
@conditions[:test] = {1 => 2} @conditions[:test] = {1 => 2}
@can.association_joins.map(&:to_s).sort.should == [:foo, :test].map(&:to_s).sort @can.associations_hash.should == {:foo => {}, :test => {}}
end end
it "should return nested associations for joins" do it "should return nested associations for joins" do
@conditions[:foo] = {:bar => {1 => 2}} @conditions[:foo] = {:bar => {1 => 2}}
@can.association_joins.should == [{:foo => [:bar]}] @can.associations_hash.should == {:foo => {:bar => {}}}
end
it "should tableize correctly for absurdly complex permissions" do
@conditions[:unit] = {:property=>{:landlord=>{:weasle_id=>560}}}
@conditions[:test] = 1
@can.tableized_conditions.should == {:units => {:properties => {:landlords=>{:weasle_id=>560}}}, :test => 1}
end
it "should tableize correctly for complex permissions" do
@conditions[:unit] = {:property=>{:landlord_id=>560}}
@conditions[:test] = 1
@can.tableized_conditions.should == {:units => {:properties => {:landlord_id=>560}}, :test => 1}
end end
it "should return table names in conditions for association joins" do it "should return table names in conditions for association joins" do
@conditions[:foo] = {:bar => 1} @conditions[:foo] = {:bar => 1}
@conditions[:test] = 1 @conditions[:test] = 1
@can.conditions(:tableize => true).should == { :foos => { :bar => 1}, :test => 1 } @can.tableized_conditions.should == {:foos => {:bar => 1}, :test => 1}
end end
it "should return no association joins if conditions is nil" do it "should return no association joins if conditions is nil" do
can = CanCan::CanDefinition.new(true, :read, Integer, nil, nil) can = CanCan::CanDefinition.new(true, :read, Integer, nil, nil)
can.association_joins.should be_nil can.associations_hash.should == {}
end end
end end

View File

@@ -52,20 +52,26 @@ describe CanCan::ControllerAdditions do
@controller.cannot?(:foo, :bar).should be_true @controller.cannot?(:foo, :bar).should be_true
end end
it "load_and_authorize_resource should setup a before filter which passes call to ResourceAuthorization" do it "load_and_authorize_resource should setup a before filter which passes call to ControllerResource" do
stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.load_and_authorize_resource stub(CanCan::ControllerResource).new(@controller, nil, :foo => :bar).mock!.load_and_authorize_resource
mock(@controller_class).before_filter({}) { |options, block| block.call(@controller) } mock(@controller_class).before_filter({}) { |options, block| block.call(@controller) }
@controller_class.load_and_authorize_resource :foo => :bar @controller_class.load_and_authorize_resource :foo => :bar
end end
it "authorize_resource should setup a before filter which passes call to ResourceAuthorization" do it "load_and_authorize_resource should properly pass first argument as the resource name" do
stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.authorize_resource stub(CanCan::ControllerResource).new(@controller, :project, :foo => :bar).mock!.load_and_authorize_resource
mock(@controller_class).before_filter({}) { |options, block| block.call(@controller) }
@controller_class.load_and_authorize_resource :project, :foo => :bar
end
it "authorize_resource should setup a before filter which passes call to ControllerResource" do
stub(CanCan::ControllerResource).new(@controller, nil, :foo => :bar).mock!.authorize_resource
mock(@controller_class).before_filter(:except => :show) { |options, block| block.call(@controller) } mock(@controller_class).before_filter(:except => :show) { |options, block| block.call(@controller) }
@controller_class.authorize_resource :foo => :bar, :except => :show @controller_class.authorize_resource :foo => :bar, :except => :show
end end
it "load_resource should setup a before filter which passes call to ResourceAuthorization" do it "load_resource should setup a before filter which passes call to ControllerResource" do
stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.load_resource stub(CanCan::ControllerResource).new(@controller, nil, :foo => :bar).mock!.load_resource
mock(@controller_class).before_filter(:only => [:show, :index]) { |options, block| block.call(@controller) } mock(@controller_class).before_filter(:only => [:show, :index]) { |options, block| block.call(@controller) }
@controller_class.load_resource :foo => :bar, :only => [:show, :index] @controller_class.load_resource :foo => :bar, :only => [:show, :index]
end end

View File

@@ -2,58 +2,250 @@ require "spec_helper"
describe CanCan::ControllerResource do describe CanCan::ControllerResource do
before(:each) do before(:each) do
@controller = Object.new @params = HashWithIndifferentAccess.new(:controller => "abilities")
@controller = Object.new # simple stub for now
stub(@controller).params { @params }
end end
it "should determine model class by constantizing give name" do it "should load the resource into an instance variable if params[:id] is specified" do
CanCan::ControllerResource.new(@controller, :ability).model_class.should == Ability @params.merge!(:action => "show", :id => 123)
end stub(Ability).find(123) { :some_resource }
resource = CanCan::ControllerResource.new(@controller)
it "should fetch model through model class and assign it to the instance" do resource.load_resource
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 @controller.instance_variable_get(:@ability).should == :some_resource
end end
it "should convert string to constant for resource" do it "should not load resource into an instance variable if already set" do
CanCan::ControllerResource.new(@controller, :ability, nil, :resource => "Person").model_class.should == Person @params.merge!(:action => "show", :id => 123)
@controller.instance_variable_set(:@ability, :some_ability)
resource = CanCan::ControllerResource.new(@controller)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_ability
end end
it "should raise an exception when specifying :class option since it is no longer used" do it "should properly load resource for namespaced controller" do
@params.merge!(:controller => "admin/abilities", :action => "show", :id => 123)
stub(Ability).find(123) { :some_resource }
resource = CanCan::ControllerResource.new(@controller)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
it "should properly load resource for namespaced controller when using '::' for namespace" do
@params.merge!(:controller => "Admin::AbilitiesController", :action => "show", :id => 123)
stub(Ability).find(123) { :some_resource }
resource = CanCan::ControllerResource.new(@controller)
resource.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
@params.merge!(:action => "create", :ability => {:foo => "bar"})
stub(Ability).new("foo" => "bar") { :some_resource }
resource = CanCan::ControllerResource.new(@controller)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
it "should build a new resource with no arguments if attribute hash isn't specified" do
@params[:action] = "new"
mock(Ability).new { :some_resource }
resource = CanCan::ControllerResource.new(@controller)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
it "should not build a resource when on index action" do
@params[:action] = "index"
resource = CanCan::ControllerResource.new(@controller)
resource.load_resource
@controller.instance_variable_get(:@ability).should be_nil
end
it "should perform authorization using controller action and loaded model" do
@params[:action] = "show"
@controller.instance_variable_set(:@ability, :some_resource)
stub(@controller).authorize!(:show, :some_resource) { raise CanCan::AccessDenied }
resource = CanCan::ControllerResource.new(@controller)
lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
end
it "should perform authorization using controller action and non loaded model" do
@params[:action] = "show"
stub(@controller).authorize!(:show, Ability) { raise CanCan::AccessDenied }
resource = CanCan::ControllerResource.new(@controller)
lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
end
it "should call load_resource and authorize_resource for load_and_authorize_resource" do
@params[:action] = "show"
resource = CanCan::ControllerResource.new(@controller)
mock(resource).load_resource
mock(resource).authorize_resource
resource.load_and_authorize_resource
end
it "should not build a resource when on custom collection action" do
@params[:action] = "sort"
resource = CanCan::ControllerResource.new(@controller, :collection => [:sort, :list])
resource.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
@params.merge!(:action => "build", :id => 123)
stub(Ability).new { :some_resource }
resource = CanCan::ControllerResource.new(@controller, :new => :build)
resource.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
@params[:action] = "list"
resource = CanCan::ControllerResource.new(@controller)
resource.load_resource
@controller.instance_variable_get(:@ability).should be_nil
end
it "should be a parent resource when name is provided which doesn't match controller" do
resource = CanCan::ControllerResource.new(@controller, :person)
resource.should be_parent
end
it "should not be a parent resource when name is provided which matches controller" do
resource = CanCan::ControllerResource.new(@controller, :ability)
resource.should_not be_parent
end
it "should be parent if specified in options" do
resource = CanCan::ControllerResource.new(@controller, :ability, {:parent => true})
resource.should be_parent
end
it "should not be parent if specified in options" do
resource = CanCan::ControllerResource.new(@controller, :person, {:parent => false})
resource.should_not be_parent
end
it "should load parent resource through proper id parameter when supplying a resource with a different name" do
@params.merge!(:action => "index", :person_id => 123)
stub(Person).find(123) { :some_person }
resource = CanCan::ControllerResource.new(@controller, :person)
resource.load_resource
@controller.instance_variable_get(:@person).should == :some_person
end
it "should load parent resource for collection action" do
@params.merge!(:action => "index", :person_id => 456)
stub(Person).find(456) { :some_person }
resource = CanCan::ControllerResource.new(@controller, :person)
resource.load_resource
@controller.instance_variable_get(:@person).should == :some_person
end
it "should load resource through the association of another parent resource" do
@params.merge!(:action => "show", :id => 123)
person = Object.new
@controller.instance_variable_set(:@person, person)
stub(person).abilities.stub!.find(123) { :some_ability }
resource = CanCan::ControllerResource.new(@controller, :through => :person)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should not load through parent resource if instance isn't loaded" do
@params.merge!(:action => "show", :id => 123)
stub(Ability).find(123) { :some_ability }
resource = CanCan::ControllerResource.new(@controller, :through => :person)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should load through first matching if multiple are given" do
@params.merge!(:action => "show", :id => 123)
person = Object.new
@controller.instance_variable_set(:@person, person)
stub(person).abilities.stub!.find(123) { :some_ability }
resource = CanCan::ControllerResource.new(@controller, :through => [:thing, :person])
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should find record through has_one association with :singleton option" do
@params.merge!(:action => "show")
person = Object.new
@controller.instance_variable_set(:@person, person)
stub(person).ability { :some_ability }
resource = CanCan::ControllerResource.new(@controller, :through => :person, :singleton => true)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should build record through has_one association with :singleton option" do
@params.merge!(:action => "create", :ability => :ability_attributes)
person = Object.new
@controller.instance_variable_set(:@person, person)
stub(person).build_ability(:ability_attributes) { :new_ability }
resource = CanCan::ControllerResource.new(@controller, :through => :person, :singleton => true)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :new_ability
end
it "should only authorize :read action on parent resource" do
@params.merge!(:action => "new", :person_id => 123)
stub(Person).find(123) { :some_person }
stub(@controller).authorize!(:read, :some_person) { raise CanCan::AccessDenied }
resource = CanCan::ControllerResource.new(@controller, :person)
lambda { resource.load_and_authorize_resource }.should raise_error(CanCan::AccessDenied)
end
it "should load the model using a custom class" do
@params.merge!(:action => "show", :id => 123)
stub(Person).find(123) { :some_resource }
resource = CanCan::ControllerResource.new(@controller, :class => Person)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
it "should authorize based on resource name if class is false" do
@params.merge!(:action => "show", :id => 123)
stub(@controller).authorize!(:show, :ability) { raise CanCan::AccessDenied }
resource = CanCan::ControllerResource.new(@controller, :class => false)
lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
end
it "should load and authorize using custom instance name" do
@params.merge!(:action => "show", :id => 123)
stub(Ability).find(123) { :some_ability }
stub(@controller).authorize!(:show, :some_ability) { raise CanCan::AccessDenied }
resource = CanCan::ControllerResource.new(@controller, :instance_name => :custom_ability)
lambda { resource.load_and_authorize_resource }.should raise_error(CanCan::AccessDenied)
@controller.instance_variable_get(:@custom_ability).should == :some_ability
end
it "should load resource using custom find_by attribute" do
@params.merge!(:action => "show", :id => 123)
stub(Ability).find_by_name!(123) { :some_resource }
resource = CanCan::ControllerResource.new(@controller, :find_by => :name)
resource.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
it "should raise ImplementationRemoved when adding :name option" do
lambda { lambda {
CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person) CanCan::ControllerResource.new(@controller, :name => :foo)
}.should raise_error(CanCan::ImplementationRemoved)
end
it "should raise ImplementationRemoved exception when specifying :resource option since it is no longer used" do
lambda {
CanCan::ControllerResource.new(@controller, :resource => Person)
}.should raise_error(CanCan::ImplementationRemoved)
end
it "should raise ImplementationRemoved exception when passing :nested option" do
lambda {
CanCan::ControllerResource.new(@controller, :nested => :person)
}.should raise_error(CanCan::ImplementationRemoved) }.should raise_error(CanCan::ImplementationRemoved)
end end
end end

107
spec/cancan/query_spec.rb Normal file
View File

@@ -0,0 +1,107 @@
require "spec_helper"
describe CanCan::Query do
before(:each) do
@ability = Object.new
@ability.extend(CanCan::Ability)
end
it "should have false conditions if no abilities match" do
@ability.query(:destroy, Person).conditions.should == "true=false"
end
it "should return hash for single `can` definition" do
@ability.can :read, Person, :blocked => false, :user_id => 1
@ability.query(:read, Person).conditions.should == { :blocked => false, :user_id => 1 }
end
it "should merge multiple can definitions into single SQL string joining with OR" do
@ability.can :read, Person, :blocked => false
@ability.can :read, Person, :admin => true
@ability.query(:read, Person).conditions.should == "(admin=true) OR (blocked=false)"
end
it "should merge multiple can definitions into single SQL string joining with OR and AND" do
@ability.can :read, Person, :blocked => false, :active => true
@ability.can :read, Person, :admin => true
@ability.query(:read, Person).conditions.should orderlessly_match("(blocked=false AND active=true) OR (admin=true)")
end
it "should merge multiple can definitions into single SQL string joining with OR and AND" do
@ability.can :read, Person, :blocked => false, :active => true
@ability.can :read, Person, :admin => true
@ability.query(:read, Person).conditions.should orderlessly_match("(blocked=false AND active=true) OR (admin=true)")
end
it "should return false conditions for cannot clause" do
@ability.cannot :read, Person
@ability.query(:read, Person).conditions.should == "true=false"
end
it "should return SQL for single `can` definition in front of default `cannot` condition" do
@ability.cannot :read, Person
@ability.can :read, Person, :blocked => false, :user_id => 1
@ability.query(:read, Person).conditions.should orderlessly_match("blocked=false AND user_id=1")
end
it "should return true condition for single `can` definition in front of default `can` condition" do
@ability.can :read, Person
@ability.can :read, Person, :blocked => false, :user_id => 1
@ability.query(:read, Person).conditions.should == 'true=true'
end
it "should return false condition for single `cannot` definition" do
@ability.cannot :read, Person, :blocked => true, :user_id => 1
@ability.query(:read, Person).conditions.should == 'true=false'
end
it "should return `false condition` for single `cannot` definition in front of default `cannot` condition" do
@ability.cannot :read, Person
@ability.cannot :read, Person, :blocked => true, :user_id => 1
@ability.query(:read, Person).conditions.should == 'true=false'
end
it "should return `not (sql)` for single `cannot` definition in front of default `can` condition" do
@ability.can :read, Person
@ability.cannot :read, Person, :blocked => true, :user_id => 1
@ability.query(:read, Person).conditions.should orderlessly_match("not (blocked=true AND user_id=1)")
end
it "should return appropriate sql conditions in complex case" do
@ability.can :read, Person
@ability.can :manage, Person, :id => 1
@ability.can :update, Person, :manager_id => 1
@ability.cannot :update, Person, :self_managed => true
@ability.query(:update, Person).conditions.should == 'not (self_managed=true) AND ((manager_id=1) OR (id=1))'
@ability.query(:manage, Person).conditions.should == {:id=>1}
@ability.query(:read, Person).conditions.should == 'true=true'
end
it "should have nil joins if no can definitions" do
@ability.query(:read, Person).joins.should be_nil
end
it "should have nil joins if no nested hashes specified in conditions" do
@ability.can :read, Person, :blocked => false
@ability.can :read, Person, :admin => true
@ability.query(:read, Person).joins.should be_nil
end
it "should merge separate joins into a single array" do
@ability.can :read, Person, :project => { :blocked => false }
@ability.can :read, Person, :company => { :admin => true }
@ability.query(:read, Person).joins.inspect.should orderlessly_match([:company, :project].inspect)
end
it "should merge same joins into a single array" do
@ability.can :read, Person, :project => { :blocked => false }
@ability.can :read, Person, :project => { :admin => true }
@ability.query(:read, Person).joins.should == [:project]
end
it "should merge complex, nested joins" do
@ability.can :read, Person, :project => { :bar => {:test => true} }, :company => { :bar => {:test => true} }
@ability.can :read, Person, :project => { :foo => {:bar => true}, :bar => {:zip => :zap} }
@ability.query(:read, Person).joins.inspect.should orderlessly_match([{:project => [:bar, :foo]}, {:company => [:bar]}].inspect)
end
end

View File

@@ -1,135 +0,0 @@
require "spec_helper"
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
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 }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "new", :person_id => 456}, {:nested => [:person, :behavior]})
authorization.load_resource
@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

13
spec/matchers.rb Normal file
View File

@@ -0,0 +1,13 @@
Spec::Matchers.define :orderlessly_match do |original_string|
match do |given_string|
original_string.split('').sort == given_string.split('').sort
end
failure_message_for_should do |given_string|
"expected \"#{given_string}\" to have the same characters as \"#{original_string}\""
end
failure_message_for_should_not do |given_string|
"expected \"#{given_string}\" not to have the same characters as \"#{original_string}\""
end
end

1
spec/spec.opts Normal file
View File

@@ -0,0 +1 @@
--color

View File

@@ -4,6 +4,7 @@ require 'active_support'
require 'active_record' require 'active_record'
require 'action_controller' require 'action_controller'
require 'action_view' require 'action_view'
require 'matchers'
require 'cancan' require 'cancan'
require 'cancan/matchers' require 'cancan/matchers'
@@ -18,6 +19,29 @@ class Ability
end end
end end
# this class helps out in testing nesting # this class helps out in testing SQL conditions
class Person class Person
class << self
protected
def sanitize_sql(hash_cond)
case hash_cond
when Hash
sanitize_hash(hash_cond).join(' AND ')
when Array
hash_cond.shift.gsub('?'){"#{hash_cond.shift.inspect}"}
when String then hash_cond
end
end
def sanitize_hash(hash)
hash.map do |name, value|
if Hash === value
sanitize_hash(value).map{|cond| "#{name}.#{cond}"}
else
"#{name}=#{value}"
end
end.flatten
end
end
end end