cancan 1.2.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +38 -0
- data/README.rdoc +14 -9
- data/Rakefile +12 -12
- data/lib/cancan.rb +1 -1
- data/lib/cancan/ability.rb +32 -35
- data/lib/cancan/active_record_additions.rb +3 -4
- data/lib/cancan/can_definition.rb +37 -47
- data/lib/cancan/controller_additions.rb +58 -26
- data/lib/cancan/controller_resource.rb +115 -40
- data/lib/cancan/query.rb +97 -0
- data/spec/cancan/ability_spec.rb +94 -43
- data/spec/cancan/active_record_additions_spec.rb +27 -4
- data/spec/cancan/can_definition_spec.rb +7 -7
- data/spec/cancan/controller_additions_spec.rb +12 -6
- data/spec/cancan/controller_resource_spec.rb +218 -26
- data/spec/cancan/query_spec.rb +107 -0
- data/spec/matchers.rb +13 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +25 -1
- metadata +10 -17
- data/lib/cancan/resource_authorization.rb +0 -70
- data/spec/cancan/resource_authorization_spec.rb +0 -135
@@ -1,54 +1,129 @@
|
|
1
1
|
module CanCan
|
2
|
-
#
|
3
|
-
# This
|
4
|
-
# parent is given it will go through the association.
|
2
|
+
# Handle the load and authorization controller logic so we don't clutter up all controllers with non-interface methods.
|
3
|
+
# This class is used internally, so you do not need to call methods directly on it.
|
5
4
|
class ControllerResource # :nodoc:
|
6
|
-
def
|
7
|
-
|
5
|
+
def self.add_before_filter(controller_class, method, *args)
|
6
|
+
options = args.extract_options!
|
7
|
+
resource_name = args.first
|
8
|
+
controller_class.before_filter(options.slice(:only, :except)) do |controller|
|
9
|
+
ControllerResource.new(controller, resource_name, options.except(:only, :except)).send(method)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(controller, *args)
|
8
14
|
@controller = controller
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
15
|
+
@params = controller.params
|
16
|
+
@options = args.extract_options!
|
17
|
+
@name = args.first
|
18
|
+
raise CanCan::ImplementationRemoved, "The :nested option is no longer supported, instead use :through with separate load/authorize call." if @options[:nested]
|
19
|
+
raise CanCan::ImplementationRemoved, "The :name option is no longer supported, instead pass the name as the first argument." if @options[:name]
|
20
|
+
raise CanCan::ImplementationRemoved, "The :resource option has been renamed back to :class, use false if no class." if @options[:resource]
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_and_authorize_resource
|
24
|
+
load_resource
|
25
|
+
authorize_resource
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_resource
|
29
|
+
if !resource_instance && (parent? || member_action?)
|
30
|
+
@controller.instance_variable_set("@#{instance_name}", load_resource_instance)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def authorize_resource
|
35
|
+
@controller.authorize!(authorization_action, resource_instance || resource_class)
|
36
|
+
end
|
37
|
+
|
38
|
+
def parent?
|
39
|
+
@options.has_key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def load_resource_instance
|
45
|
+
if !parent? && new_actions.include?(@params[:action].to_sym)
|
46
|
+
build_resource
|
47
|
+
elsif id_param || @options[:singleton]
|
48
|
+
find_resource
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_resource
|
53
|
+
method_name = @options[:singleton] ? "build_#{name}" : "new"
|
54
|
+
resource_base.send(*[method_name, @params[name]].compact)
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_resource
|
58
|
+
if @options[:singleton]
|
59
|
+
resource_base.send(name)
|
23
60
|
else
|
24
|
-
|
61
|
+
@options[:find_by] ? resource_base.send("find_by_#{@options[:find_by]}!", id_param) : resource_base.find(id_param)
|
25
62
|
end
|
26
63
|
end
|
27
|
-
|
28
|
-
def
|
29
|
-
|
64
|
+
|
65
|
+
def authorization_action
|
66
|
+
parent? ? :read : @params[:action].to_sym
|
30
67
|
end
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
def build(attributes)
|
35
|
-
self.model_instance ||= (base.kind_of?(Class) ? base.new(attributes) : base.build(attributes))
|
68
|
+
|
69
|
+
def id_param
|
70
|
+
@params[parent? ? :"#{name}_id" : :id]
|
36
71
|
end
|
37
|
-
|
38
|
-
def
|
39
|
-
@
|
72
|
+
|
73
|
+
def member_action?
|
74
|
+
!collection_actions.include? @params[:action].to_sym
|
40
75
|
end
|
41
|
-
|
42
|
-
|
43
|
-
|
76
|
+
|
77
|
+
# Returns the class used for this resource. This can be overriden by the :class option.
|
78
|
+
# If +false+ is passed in it will use the resource name as a symbol in which case it should
|
79
|
+
# only be used for authorization, not loading since there's no class to load through.
|
80
|
+
def resource_class
|
81
|
+
case @options[:class]
|
82
|
+
when false then name.to_sym
|
83
|
+
when nil then name.to_s.camelize.constantize
|
84
|
+
when String then @options[:class].constantize
|
85
|
+
else @options[:class]
|
86
|
+
end
|
44
87
|
end
|
45
|
-
|
46
|
-
|
47
|
-
|
88
|
+
|
89
|
+
def resource_instance
|
90
|
+
@controller.instance_variable_get("@#{instance_name}")
|
91
|
+
end
|
92
|
+
|
48
93
|
# The object that methods (such as "find", "new" or "build") are called on.
|
49
|
-
# If
|
50
|
-
|
51
|
-
|
94
|
+
# If the :through option is passed it will go through an association on that instance.
|
95
|
+
# If the :singleton option is passed it won't use the association because it needs to be handled later.
|
96
|
+
def resource_base
|
97
|
+
if through_resource
|
98
|
+
@options[:singleton] ? through_resource : through_resource.send(name.to_s.pluralize)
|
99
|
+
else
|
100
|
+
resource_class
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# The object to load this resource through.
|
105
|
+
def through_resource
|
106
|
+
@options[:through] && [@options[:through]].flatten.map { |i| @controller.instance_variable_get("@#{i}") }.compact.first
|
107
|
+
end
|
108
|
+
|
109
|
+
def name
|
110
|
+
@name || name_from_controller
|
111
|
+
end
|
112
|
+
|
113
|
+
def name_from_controller
|
114
|
+
@params[:controller].sub("Controller", "").underscore.split('/').last.singularize
|
115
|
+
end
|
116
|
+
|
117
|
+
def instance_name
|
118
|
+
@options[:instance_name] || name
|
119
|
+
end
|
120
|
+
|
121
|
+
def collection_actions
|
122
|
+
[:index] + [@options[:collection]].flatten
|
123
|
+
end
|
124
|
+
|
125
|
+
def new_actions
|
126
|
+
[:new, :create] + [@options[:new]].flatten
|
52
127
|
end
|
53
128
|
end
|
54
129
|
end
|
data/lib/cancan/query.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
module CanCan
|
2
|
+
|
3
|
+
# Generates the sql conditions and association joins for use in ActiveRecord queries.
|
4
|
+
# Normally you will not use this class directly, but instead through ActiveRecordAdditions#accessible_by.
|
5
|
+
class Query
|
6
|
+
def initialize(sanitizer, can_definitions)
|
7
|
+
@sanitizer = sanitizer
|
8
|
+
@can_definitions = can_definitions
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns conditions intended to be used inside a database query. Normally you will not call this
|
12
|
+
# method directly, but instead go through ActiveRecordAdditions#accessible_by.
|
13
|
+
#
|
14
|
+
# If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
|
15
|
+
#
|
16
|
+
# can :manage, User, :id => 1
|
17
|
+
# query(:manage, User).conditions # => { :id => 1 }
|
18
|
+
#
|
19
|
+
# If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
|
20
|
+
#
|
21
|
+
# can :manage, User, :id => 1
|
22
|
+
# can :manage, User, :manager_id => 1
|
23
|
+
# cannot :manage, User, :self_managed => true
|
24
|
+
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
|
25
|
+
#
|
26
|
+
def conditions
|
27
|
+
if @can_definitions.size == 1 && @can_definitions.first.base_behavior
|
28
|
+
# Return the conditions directly if there's just one definition
|
29
|
+
@can_definitions.first.tableized_conditions
|
30
|
+
else
|
31
|
+
@can_definitions.reverse.inject(false_sql) do |sql, can_definition|
|
32
|
+
merge_conditions(sql, can_definition.tableized_conditions, can_definition.base_behavior)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the associations used in conditions for the :joins option of a search.
|
38
|
+
# See ActiveRecordAdditions#accessible_by for use in Active Record.
|
39
|
+
def joins
|
40
|
+
joins_hash = {}
|
41
|
+
@can_definitions.each do |can_definition|
|
42
|
+
merge_joins(joins_hash, can_definition.associations_hash)
|
43
|
+
end
|
44
|
+
clean_joins(joins_hash) unless joins_hash.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def merge_conditions(sql, conditions_hash, behavior)
|
50
|
+
if conditions_hash.blank?
|
51
|
+
behavior ? true_sql : false_sql
|
52
|
+
else
|
53
|
+
conditions = sanitize_sql(conditions_hash)
|
54
|
+
case sql
|
55
|
+
when true_sql
|
56
|
+
behavior ? true_sql : "not (#{conditions})"
|
57
|
+
when false_sql
|
58
|
+
behavior ? conditions : false_sql
|
59
|
+
else
|
60
|
+
behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def false_sql
|
66
|
+
sanitize_sql(['?=?', true, false])
|
67
|
+
end
|
68
|
+
|
69
|
+
def true_sql
|
70
|
+
sanitize_sql(['?=?', true, true])
|
71
|
+
end
|
72
|
+
|
73
|
+
def sanitize_sql(conditions)
|
74
|
+
@sanitizer.send(:sanitize_sql, conditions)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Takes two hashes and does a deep merge.
|
78
|
+
def merge_joins(base, add)
|
79
|
+
add.each do |name, nested|
|
80
|
+
if base[name].is_a?(Hash) && !nested.empty?
|
81
|
+
merge_joins(base[name], nested)
|
82
|
+
else
|
83
|
+
base[name] = nested
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Removes empty hashes and moves everything into arrays.
|
89
|
+
def clean_joins(joins_hash)
|
90
|
+
joins = []
|
91
|
+
joins_hash.each do |name, nested|
|
92
|
+
joins << (nested.empty? ? name : {name => clean_joins(nested)})
|
93
|
+
end
|
94
|
+
joins
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/spec/cancan/ability_spec.rb
CHANGED
@@ -16,50 +16,81 @@ describe CanCan::Ability do
|
|
16
16
|
@ability.can?(:foodfight, String).should be_false
|
17
17
|
end
|
18
18
|
|
19
|
-
it "should
|
19
|
+
it "should pass true to `can?` when non false/nil is returned in block" do
|
20
20
|
@ability.can :read, :all
|
21
21
|
@ability.can :read, Symbol do |sym|
|
22
|
-
sym
|
22
|
+
"foo" # TODO test that sym is nil when no instance is passed
|
23
23
|
end
|
24
|
-
@ability.can?(:read,
|
25
|
-
|
24
|
+
@ability.can?(:read, :some_symbol).should == true
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should pass to previous can definition, if block returns false or nil" do
|
28
|
+
@ability.can :read, Symbol
|
29
|
+
@ability.can :read, Integer do |i|
|
30
|
+
i < 5
|
31
|
+
end
|
32
|
+
@ability.can :read, Integer do |i|
|
33
|
+
i > 10
|
34
|
+
end
|
35
|
+
@ability.can?(:read, Symbol).should be_true
|
36
|
+
@ability.can?(:read, 11).should be_true
|
37
|
+
@ability.can?(:read, 1).should be_true
|
38
|
+
@ability.can?(:read, 6).should be_false
|
26
39
|
end
|
27
40
|
|
28
41
|
it "should pass class with object if :all objects are accepted" do
|
29
42
|
@ability.can :preview, :all do |object_class, object|
|
30
|
-
|
43
|
+
object_class.should == Fixnum
|
44
|
+
object.should == 123
|
45
|
+
@block_called = true
|
31
46
|
end
|
32
|
-
@ability.can?(:preview, 123)
|
47
|
+
@ability.can?(:preview, 123)
|
48
|
+
@block_called.should be_true
|
33
49
|
end
|
34
50
|
|
35
51
|
it "should pass class with no object if :all objects are accepted and class is passed directly" do
|
36
52
|
@ability.can :preview, :all do |object_class, object|
|
37
|
-
|
53
|
+
object_class.should == Hash
|
54
|
+
object.should be_nil
|
55
|
+
@block_called = true
|
38
56
|
end
|
39
|
-
@ability.can?(:preview, Hash)
|
57
|
+
@ability.can?(:preview, Hash)
|
58
|
+
@block_called.should be_true
|
40
59
|
end
|
41
60
|
|
42
61
|
it "should pass action and object for global manage actions" do
|
43
62
|
@ability.can :manage, Array do |action, object|
|
44
|
-
|
63
|
+
action.should == :stuff
|
64
|
+
object.should == [1, 2]
|
65
|
+
@block_called = true
|
45
66
|
end
|
46
|
-
@ability.can?(:stuff, [1, 2]).should
|
47
|
-
@
|
67
|
+
@ability.can?(:stuff, [1, 2]).should
|
68
|
+
@block_called.should be_true
|
48
69
|
end
|
49
70
|
|
50
71
|
it "should alias update or destroy actions to modify action" do
|
51
72
|
@ability.alias_action :update, :destroy, :to => :modify
|
52
|
-
@ability.can
|
53
|
-
@ability.can?(:update, 123).should
|
54
|
-
@ability.can?(:destroy, 123).should
|
73
|
+
@ability.can :modify, :all
|
74
|
+
@ability.can?(:update, 123).should be_true
|
75
|
+
@ability.can?(:destroy, 123).should be_true
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should allow deeply nested aliased actions" do
|
79
|
+
@ability.alias_action :increment, :to => :sort
|
80
|
+
@ability.alias_action :sort, :to => :modify
|
81
|
+
@ability.can :modify, :all
|
82
|
+
@ability.can?(:increment, 123).should be_true
|
55
83
|
end
|
56
84
|
|
57
85
|
it "should return block result for action, object_class, and object for any action" do
|
58
86
|
@ability.can :manage, :all do |action, object_class, object|
|
59
|
-
|
87
|
+
action.should == :foo
|
88
|
+
object_class.should == Fixnum
|
89
|
+
object.should == 123
|
90
|
+
@block_called = true
|
60
91
|
end
|
61
|
-
@ability.can?(:foo, 123)
|
62
|
-
@
|
92
|
+
@ability.can?(:foo, 123)
|
93
|
+
@block_called.should be_true
|
63
94
|
end
|
64
95
|
|
65
96
|
it "should automatically alias index and show into read calls" do
|
@@ -69,10 +100,10 @@ describe CanCan::Ability do
|
|
69
100
|
end
|
70
101
|
|
71
102
|
it "should automatically alias new and edit into create and update respectively" do
|
72
|
-
@ability.can
|
73
|
-
@ability.can
|
74
|
-
@ability.can?(:new, 123).should
|
75
|
-
@ability.can?(:edit, 123).should
|
103
|
+
@ability.can :create, :all
|
104
|
+
@ability.can :update, :all
|
105
|
+
@ability.can?(:new, 123).should be_true
|
106
|
+
@ability.can?(:edit, 123).should be_true
|
76
107
|
end
|
77
108
|
|
78
109
|
it "should not respond to prepare (now using initialize)" do
|
@@ -104,6 +135,13 @@ describe CanCan::Ability do
|
|
104
135
|
@ability.can?(:read, :nonstats).should be_false
|
105
136
|
end
|
106
137
|
|
138
|
+
it "should check ancestors of class" do
|
139
|
+
@ability.can :read, Numeric
|
140
|
+
@ability.can?(:read, Integer).should be_true
|
141
|
+
@ability.can?(:read, 1.23).should be_true
|
142
|
+
@ability.can?(:read, "foo").should be_false
|
143
|
+
end
|
144
|
+
|
107
145
|
it "should support 'cannot' method to define what user cannot do" do
|
108
146
|
@ability.can :read, :all
|
109
147
|
@ability.cannot :read, Integer
|
@@ -121,6 +159,40 @@ describe CanCan::Ability do
|
|
121
159
|
@ability.can?(:read, 123).should be_false
|
122
160
|
end
|
123
161
|
|
162
|
+
it "should pass to previous can definition, if block returns false or nil" do
|
163
|
+
#same as previous
|
164
|
+
@ability.can :read, :all
|
165
|
+
@ability.cannot :read, Integer do |int|
|
166
|
+
int > 10 ? nil : ( int > 5 )
|
167
|
+
end
|
168
|
+
@ability.can?(:read, "foo").should be_true
|
169
|
+
@ability.can?(:read, 3).should be_true
|
170
|
+
@ability.can?(:read, 8).should be_false
|
171
|
+
@ability.can?(:read, 123).should be_true
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should always return `false` for single cannot definition" do
|
176
|
+
@ability.cannot :read, Integer do |int|
|
177
|
+
int > 10 ? nil : ( int > 5 )
|
178
|
+
end
|
179
|
+
@ability.can?(:read, "foo").should be_false
|
180
|
+
@ability.can?(:read, 3).should be_false
|
181
|
+
@ability.can?(:read, 8).should be_false
|
182
|
+
@ability.can?(:read, 123).should be_false
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should pass to previous cannot definition, if block returns false or nil" do
|
186
|
+
@ability.cannot :read, :all
|
187
|
+
@ability.can :read, Integer do |int|
|
188
|
+
int > 10 ? nil : ( int > 5 )
|
189
|
+
end
|
190
|
+
@ability.can?(:read, "foo").should be_false
|
191
|
+
@ability.can?(:read, 3).should be_false
|
192
|
+
@ability.can?(:read, 10).should be_true
|
193
|
+
@ability.can?(:read, 123).should be_false
|
194
|
+
end
|
195
|
+
|
124
196
|
it "should append aliased actions" do
|
125
197
|
@ability.alias_action :update, :to => :modify
|
126
198
|
@ability.alias_action :destroy, :to => :modify
|
@@ -174,30 +246,9 @@ describe CanCan::Ability do
|
|
174
246
|
@ability.can?(:read, [[4, 5, 6]]).should be_false
|
175
247
|
end
|
176
248
|
|
177
|
-
it "should return conditions for a given ability" do
|
178
|
-
@ability.can :read, Array, :first => 1, :last => 3
|
179
|
-
@ability.conditions(:show, Array).should == {:first => 1, :last => 3}
|
180
|
-
end
|
181
|
-
|
182
|
-
it "should raise an exception when a block is used on condition" do
|
183
|
-
@ability.can :read, Array do |a|
|
184
|
-
true
|
185
|
-
end
|
186
|
-
lambda { @ability.conditions(:show, Array) }.should raise_error(CanCan::Error, "Cannot determine ability conditions from block for :show Array")
|
187
|
-
end
|
188
|
-
|
189
|
-
it "should return an empty hash for conditions when there are no conditions" do
|
190
|
-
@ability.can :read, Array
|
191
|
-
@ability.conditions(:show, Array).should == {}
|
192
|
-
end
|
193
|
-
|
194
|
-
it "should return false when performed on an action which isn't defined" do
|
195
|
-
@ability.conditions(:foo, Array).should == false
|
196
|
-
end
|
197
|
-
|
198
249
|
it "should has eated cheezburger" do
|
199
250
|
lambda {
|
200
251
|
@ability.can? :has, :cheezburger
|
201
|
-
}.should
|
252
|
+
}.should raise_error(CanCan::Error, "Nom nom nom. I eated it.")
|
202
253
|
end
|
203
254
|
end
|