ae_declarative_authorization 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Appraisals +31 -21
- data/CHANGELOG +189 -189
- data/Gemfile +7 -7
- data/Gemfile.lock +68 -60
- data/LICENSE.txt +20 -20
- data/README.md +620 -620
- data/README.rdoc +597 -597
- data/Rakefile +35 -33
- data/authorization_rules.dist.rb +20 -20
- data/declarative_authorization.gemspec +24 -24
- data/gemfiles/rails4252.gemfile +10 -10
- data/gemfiles/rails4252.gemfile.lock +126 -0
- data/gemfiles/rails4271.gemfile +10 -10
- data/gemfiles/rails4271.gemfile.lock +126 -0
- data/gemfiles/rails507.gemfile +11 -11
- data/gemfiles/rails507.gemfile.lock +136 -0
- data/gemfiles/rails516.gemfile +11 -0
- data/gemfiles/rails516.gemfile.lock +136 -0
- data/gemfiles/rails521.gemfile +11 -0
- data/gemfiles/rails521.gemfile.lock +144 -0
- data/init.rb +5 -5
- data/lib/declarative_authorization.rb +18 -18
- data/lib/declarative_authorization/authorization.rb +821 -821
- data/lib/declarative_authorization/helper.rb +78 -78
- data/lib/declarative_authorization/in_controller.rb +713 -713
- data/lib/declarative_authorization/in_model.rb +156 -156
- data/lib/declarative_authorization/maintenance.rb +215 -215
- data/lib/declarative_authorization/obligation_scope.rb +348 -345
- data/lib/declarative_authorization/railsengine.rb +5 -5
- data/lib/declarative_authorization/reader.rb +549 -549
- data/lib/declarative_authorization/test/helpers.rb +261 -261
- data/lib/declarative_authorization/version.rb +3 -3
- data/lib/generators/authorization/install/install_generator.rb +77 -77
- data/lib/generators/authorization/rules/rules_generator.rb +13 -13
- data/lib/generators/authorization/rules/templates/authorization_rules.rb +27 -27
- data/lib/tasks/authorization_tasks.rake +89 -89
- data/log/test.log +15246 -0
- data/pkg/ae_declarative_authorization-0.7.1.gem +0 -0
- data/pkg/ae_declarative_authorization-0.8.0.gem +0 -0
- data/test/authorization_test.rb +1121 -1121
- data/test/controller_filter_resource_access_test.rb +573 -573
- data/test/controller_test.rb +478 -478
- data/test/database.yml +3 -3
- data/test/dsl_reader_test.rb +178 -178
- data/test/functional/filter_access_to_with_id_in_scope_test.rb +88 -88
- data/test/functional/no_filter_access_to_test.rb +79 -79
- data/test/functional/params_block_arity_test.rb +39 -39
- data/test/helper_test.rb +248 -248
- data/test/maintenance_test.rb +46 -46
- data/test/model_test.rb +1840 -1840
- data/test/profiles/access_checking +20 -0
- data/test/schema.sql +60 -60
- data/test/test_helper.rb +174 -174
- data/test/test_support/minitest_compatibility.rb +26 -26
- metadata +17 -5
@@ -1,345 +1,348 @@
|
|
1
|
-
module Authorization
|
2
|
-
# The +ObligationScope+ class parses any number of obligations into joins and conditions.
|
3
|
-
#
|
4
|
-
# In +ObligationScope+ parlance, "association paths" are one-dimensional arrays in which each
|
5
|
-
# element represents an attribute or association (or "step"), and "leads" to the next step in the
|
6
|
-
# association path.
|
7
|
-
#
|
8
|
-
# Suppose we have this path defined in the context of model Foo:
|
9
|
-
# +{ :bar => { :baz => { :foo => { :attr => is { user } } } } }+
|
10
|
-
#
|
11
|
-
# To parse this path, +ObligationScope+ evaluates each step in the context of the preceding step.
|
12
|
-
# The first step is evaluated in the context of the parent scope, the second step is evaluated in
|
13
|
-
# the context of the first, and so forth. Every time we encounter a step representing an
|
14
|
-
# association, we make note of the fact by storing the path (up to that point), assigning it a
|
15
|
-
# table alias intended to match the one that will eventually be chosen by ActiveRecord when
|
16
|
-
# executing the +find+ method on the scope.
|
17
|
-
#
|
18
|
-
# +@table_aliases = {
|
19
|
-
# [] => 'foos',
|
20
|
-
# [:bar] => 'bars',
|
21
|
-
# [:bar, :baz] => 'bazzes',
|
22
|
-
# [:bar, :baz, :foo] => 'foos_bazzes' # Alias avoids collisions with 'foos' (already used)
|
23
|
-
# }+
|
24
|
-
#
|
25
|
-
# At the "end" of each path, we expect to find a comparison operation of some kind, generally
|
26
|
-
# comparing an attribute of the most recent association with some other value (such as an ID,
|
27
|
-
# constant, or array of values). When we encounter a step representing a comparison, we make
|
28
|
-
# note of the fact by storing the path (up to that point) and the comparison operation together.
|
29
|
-
# (Note that individual obligations' conditions are kept separate, to allow their conditions to
|
30
|
-
# be OR'ed together in the generated scope options.)
|
31
|
-
#
|
32
|
-
# +@obligation_conditions[<obligation>][[:bar, :baz, :foo]] = [
|
33
|
-
# [ :attr, :is, <user.id> ]
|
34
|
-
# ]+
|
35
|
-
#
|
36
|
-
# TODO update doc for Relations:
|
37
|
-
# After successfully parsing an obligation, all of the stored paths and conditions are converted
|
38
|
-
# into scope options (stored in +proxy_options+ as +:joins+ and +:conditions+). The resulting
|
39
|
-
# scope may then be used to find all scoped objects for which at least one of the parsed
|
40
|
-
# obligations is fully met.
|
41
|
-
#
|
42
|
-
# +@proxy_options[:joins] = { :bar => { :baz => :foo } }
|
43
|
-
# @proxy_options[:conditions] = [ 'foos_bazzes.attr = :foos_bazzes__id_0', { :foos_bazzes__id_0 => 1 } ]+
|
44
|
-
#
|
45
|
-
class ObligationScope < ActiveRecord::Relation
|
46
|
-
def initialize(model, options)
|
47
|
-
@finder_options = {}
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
#
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
#
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
when :
|
263
|
-
when :
|
264
|
-
when :
|
265
|
-
when :
|
266
|
-
when :
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
finder_options
|
319
|
-
finder_options
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
end
|
1
|
+
module Authorization
|
2
|
+
# The +ObligationScope+ class parses any number of obligations into joins and conditions.
|
3
|
+
#
|
4
|
+
# In +ObligationScope+ parlance, "association paths" are one-dimensional arrays in which each
|
5
|
+
# element represents an attribute or association (or "step"), and "leads" to the next step in the
|
6
|
+
# association path.
|
7
|
+
#
|
8
|
+
# Suppose we have this path defined in the context of model Foo:
|
9
|
+
# +{ :bar => { :baz => { :foo => { :attr => is { user } } } } }+
|
10
|
+
#
|
11
|
+
# To parse this path, +ObligationScope+ evaluates each step in the context of the preceding step.
|
12
|
+
# The first step is evaluated in the context of the parent scope, the second step is evaluated in
|
13
|
+
# the context of the first, and so forth. Every time we encounter a step representing an
|
14
|
+
# association, we make note of the fact by storing the path (up to that point), assigning it a
|
15
|
+
# table alias intended to match the one that will eventually be chosen by ActiveRecord when
|
16
|
+
# executing the +find+ method on the scope.
|
17
|
+
#
|
18
|
+
# +@table_aliases = {
|
19
|
+
# [] => 'foos',
|
20
|
+
# [:bar] => 'bars',
|
21
|
+
# [:bar, :baz] => 'bazzes',
|
22
|
+
# [:bar, :baz, :foo] => 'foos_bazzes' # Alias avoids collisions with 'foos' (already used)
|
23
|
+
# }+
|
24
|
+
#
|
25
|
+
# At the "end" of each path, we expect to find a comparison operation of some kind, generally
|
26
|
+
# comparing an attribute of the most recent association with some other value (such as an ID,
|
27
|
+
# constant, or array of values). When we encounter a step representing a comparison, we make
|
28
|
+
# note of the fact by storing the path (up to that point) and the comparison operation together.
|
29
|
+
# (Note that individual obligations' conditions are kept separate, to allow their conditions to
|
30
|
+
# be OR'ed together in the generated scope options.)
|
31
|
+
#
|
32
|
+
# +@obligation_conditions[<obligation>][[:bar, :baz, :foo]] = [
|
33
|
+
# [ :attr, :is, <user.id> ]
|
34
|
+
# ]+
|
35
|
+
#
|
36
|
+
# TODO update doc for Relations:
|
37
|
+
# After successfully parsing an obligation, all of the stored paths and conditions are converted
|
38
|
+
# into scope options (stored in +proxy_options+ as +:joins+ and +:conditions+). The resulting
|
39
|
+
# scope may then be used to find all scoped objects for which at least one of the parsed
|
40
|
+
# obligations is fully met.
|
41
|
+
#
|
42
|
+
# +@proxy_options[:joins] = { :bar => { :baz => :foo } }
|
43
|
+
# @proxy_options[:conditions] = [ 'foos_bazzes.attr = :foos_bazzes__id_0', { :foos_bazzes__id_0 => 1 } ]+
|
44
|
+
#
|
45
|
+
class ObligationScope < ActiveRecord::Relation
|
46
|
+
def initialize(model, options)
|
47
|
+
@finder_options = {}
|
48
|
+
|
49
|
+
if Rails.version >= "5.2"
|
50
|
+
super(model, table: model.table_name)
|
51
|
+
elsif Rails.version >= "5"
|
52
|
+
super(model, model.table_name, nil)
|
53
|
+
else
|
54
|
+
super(model, model.table_name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def scope
|
59
|
+
# TODO Refactor this. There is certainly a better way.
|
60
|
+
self.klass.joins(@finder_options[:joins]).includes(@finder_options[:include]).where(@finder_options[:conditions]).references(@finder_options[:include])
|
61
|
+
end
|
62
|
+
|
63
|
+
# Consumes the given obligation, converting it into scope join and condition options.
|
64
|
+
def parse!( obligation )
|
65
|
+
@current_obligation = obligation
|
66
|
+
@join_table_joins = Set.new
|
67
|
+
obligation_conditions[@current_obligation] ||= {}
|
68
|
+
follow_path( obligation )
|
69
|
+
|
70
|
+
rebuild_condition_options!
|
71
|
+
rebuild_join_options!
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
|
76
|
+
# Parses the next step in the association path. If it's an association, we advance down the
|
77
|
+
# path. Otherwise, it's an attribute, and we need to evaluate it as a comparison operation.
|
78
|
+
def follow_path( steps, past_steps = [] )
|
79
|
+
if steps.is_a?( Hash )
|
80
|
+
steps.each do |step, next_steps|
|
81
|
+
path_to_this_point = [past_steps, step].flatten
|
82
|
+
reflection = reflection_for( path_to_this_point ) rescue nil
|
83
|
+
if reflection
|
84
|
+
follow_path( next_steps, path_to_this_point )
|
85
|
+
else
|
86
|
+
follow_comparison( next_steps, past_steps, step )
|
87
|
+
end
|
88
|
+
end
|
89
|
+
elsif steps.is_a?( Array ) && steps.length == 2
|
90
|
+
if reflection_for( past_steps )
|
91
|
+
follow_comparison( steps, past_steps, :id )
|
92
|
+
else
|
93
|
+
follow_comparison( steps, past_steps[0..-2], past_steps[-1] )
|
94
|
+
end
|
95
|
+
else
|
96
|
+
raise "invalid obligation path #{[past_steps, steps].inspect}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def top_level_model
|
101
|
+
self.klass
|
102
|
+
end
|
103
|
+
|
104
|
+
def finder_options
|
105
|
+
@finder_options
|
106
|
+
end
|
107
|
+
|
108
|
+
# At the end of every association path, we expect to see a comparison of some kind; for
|
109
|
+
# example, +:attr => [ :is, :value ]+.
|
110
|
+
#
|
111
|
+
# This method parses the comparison and creates an obligation condition from it.
|
112
|
+
def follow_comparison( steps, past_steps, attribute )
|
113
|
+
operator = steps[0]
|
114
|
+
value = steps[1..-1]
|
115
|
+
value = value[0] if value.length == 1
|
116
|
+
|
117
|
+
add_obligation_condition_for( past_steps, [attribute, operator, value] )
|
118
|
+
end
|
119
|
+
|
120
|
+
# Adds the given expression to the current obligation's indicated path's conditions.
|
121
|
+
#
|
122
|
+
# Condition expressions must follow the format +[ <attribute>, <operator>, <value> ]+.
|
123
|
+
def add_obligation_condition_for( path, expression )
|
124
|
+
raise "invalid expression #{expression.inspect}" unless expression.is_a?( Array ) && expression.length == 3
|
125
|
+
add_obligation_join_for( path )
|
126
|
+
obligation_conditions[@current_obligation] ||= {}
|
127
|
+
( obligation_conditions[@current_obligation][path] ||= Set.new ) << expression
|
128
|
+
end
|
129
|
+
|
130
|
+
# Adds the given path to the list of obligation joins, if we haven't seen it before.
|
131
|
+
def add_obligation_join_for( path )
|
132
|
+
map_reflection_for( path ) if reflections[path].nil?
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns the model associated with the given path.
|
136
|
+
def model_for(path)
|
137
|
+
reflection = reflection_for(path)
|
138
|
+
|
139
|
+
if Authorization.is_a_association_proxy?(reflection)
|
140
|
+
reflection.proxy_association.reflection.klass
|
141
|
+
elsif reflection.respond_to?(:klass)
|
142
|
+
reflection.klass
|
143
|
+
else
|
144
|
+
reflection
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns the reflection corresponding to the given path.
|
149
|
+
def reflection_for(path, for_join_table_only = false)
|
150
|
+
@join_table_joins << path if for_join_table_only and !reflections[path]
|
151
|
+
reflections[path] ||= map_reflection_for( path )
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns a proper table alias for the given path. This alias may be used in SQL statements.
|
155
|
+
def table_alias_for( path )
|
156
|
+
table_aliases[path] ||= map_table_alias_for( path )
|
157
|
+
end
|
158
|
+
|
159
|
+
# Attempts to map a reflection for the given path. Raises if already defined.
|
160
|
+
def map_reflection_for( path )
|
161
|
+
raise "reflection for #{path.inspect} already exists" unless reflections[path].nil?
|
162
|
+
|
163
|
+
reflection = path.empty? ? top_level_model : begin
|
164
|
+
parent = reflection_for( path[0..-2] )
|
165
|
+
if !Authorization.is_a_association_proxy?(parent) and parent.respond_to?(:klass)
|
166
|
+
parent.klass.reflect_on_association( path.last )
|
167
|
+
else
|
168
|
+
parent.reflect_on_association( path.last )
|
169
|
+
end
|
170
|
+
rescue
|
171
|
+
parent.reflect_on_association( path.last )
|
172
|
+
end
|
173
|
+
raise "invalid path #{path.inspect}" if reflection.nil?
|
174
|
+
|
175
|
+
reflections[path] = reflection
|
176
|
+
map_table_alias_for( path ) # Claim a table alias for the path.
|
177
|
+
|
178
|
+
# Claim alias for join table
|
179
|
+
# TODO change how this is checked
|
180
|
+
if !Authorization.is_a_association_proxy?(reflection) and !reflection.respond_to?(:proxy_scope) and reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
181
|
+
join_table_path = path[0..-2] + [reflection.options[:through]]
|
182
|
+
reflection_for(join_table_path, true)
|
183
|
+
end
|
184
|
+
|
185
|
+
reflection
|
186
|
+
end
|
187
|
+
|
188
|
+
# Attempts to map a table alias for the given path. Raises if already defined.
|
189
|
+
def map_table_alias_for( path )
|
190
|
+
return "table alias for #{path.inspect} already exists" unless table_aliases[path].nil?
|
191
|
+
|
192
|
+
reflection = reflection_for( path )
|
193
|
+
table_alias = reflection.table_name
|
194
|
+
if table_aliases.values.include?( table_alias )
|
195
|
+
max_length = reflection.active_record.connection.table_alias_length
|
196
|
+
# Rails seems to pluralize reflection names
|
197
|
+
table_alias = "#{reflection.name.to_s.pluralize}_#{reflection.active_record.table_name}".to(max_length-1)
|
198
|
+
end
|
199
|
+
while table_aliases.values.include?( table_alias )
|
200
|
+
if table_alias =~ /\w(_\d+?)$/
|
201
|
+
table_index = $1.succ
|
202
|
+
table_alias = "#{table_alias[0..-(table_index.length+1)]}_#{table_index}"
|
203
|
+
else
|
204
|
+
table_alias = "#{table_alias[0..(max_length-3)]}_2"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
table_aliases[path] = table_alias
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns a hash mapping obligations to zero or more condition path sets.
|
211
|
+
def obligation_conditions
|
212
|
+
@obligation_conditions ||= {}
|
213
|
+
end
|
214
|
+
|
215
|
+
# Returns a hash mapping paths to reflections.
|
216
|
+
def reflections
|
217
|
+
# lets try to get the order of joins right
|
218
|
+
@reflections ||= ActiveSupport::OrderedHash.new
|
219
|
+
end
|
220
|
+
|
221
|
+
# Returns a hash mapping paths to proper table aliases to use in SQL statements.
|
222
|
+
def table_aliases
|
223
|
+
@table_aliases ||= {}
|
224
|
+
end
|
225
|
+
|
226
|
+
# Parses all of the defined obligation conditions and defines the scope's :conditions option.
|
227
|
+
def rebuild_condition_options!
|
228
|
+
conds = []
|
229
|
+
binds = {}
|
230
|
+
used_paths = Set.new
|
231
|
+
delete_paths = Set.new
|
232
|
+
obligation_conditions.each_with_index do |array, obligation_index|
|
233
|
+
obligation, conditions = array
|
234
|
+
obligation_conds = []
|
235
|
+
conditions.each do |path, expressions|
|
236
|
+
model = model_for( path )
|
237
|
+
table_alias = table_alias_for(path)
|
238
|
+
parent_model = (path.length > 1 ? model_for(path[0..-2]) : top_level_model)
|
239
|
+
expressions.each do |expression|
|
240
|
+
attribute, operator, value = expression
|
241
|
+
# prevent unnecessary joins:
|
242
|
+
if attribute == :id and operator == :is and parent_model.columns_hash["#{path.last}_id"]
|
243
|
+
attribute_name = :"#{path.last}_id"
|
244
|
+
attribute_table_alias = table_alias_for(path[0..-2])
|
245
|
+
used_paths << path[0..-2]
|
246
|
+
delete_paths << path
|
247
|
+
else
|
248
|
+
attribute_name = model.columns_hash["#{attribute}_id"] && :"#{attribute}_id" ||
|
249
|
+
model.columns_hash[attribute.to_s] && attribute ||
|
250
|
+
model.primary_key
|
251
|
+
attribute_table_alias = table_alias
|
252
|
+
used_paths << path
|
253
|
+
end
|
254
|
+
bindvar = "#{attribute_table_alias}__#{attribute_name}_#{obligation_index}".to_sym
|
255
|
+
|
256
|
+
sql_attribute = "#{parent_model.connection.quote_table_name(attribute_table_alias)}." +
|
257
|
+
"#{parent_model.connection.quote_table_name(attribute_name)}"
|
258
|
+
if value.nil? and [:is, :is_not].include?(operator)
|
259
|
+
obligation_conds << "#{sql_attribute} IS #{[:contains, :is].include?(operator) ? '' : 'NOT '}NULL"
|
260
|
+
else
|
261
|
+
attribute_operator = case operator
|
262
|
+
when :contains, :is then "= :#{bindvar}"
|
263
|
+
when :does_not_contain, :is_not then "<> :#{bindvar}"
|
264
|
+
when :is_in, :intersects_with then "IN (:#{bindvar})"
|
265
|
+
when :is_not_in then "NOT IN (:#{bindvar})"
|
266
|
+
when :lt then "< :#{bindvar}"
|
267
|
+
when :lte then "<= :#{bindvar}"
|
268
|
+
when :gt then "> :#{bindvar}"
|
269
|
+
when :gte then ">= :#{bindvar}"
|
270
|
+
else raise AuthorizationUsageError, "Unknown operator: #{operator}"
|
271
|
+
end
|
272
|
+
obligation_conds << "#{sql_attribute} #{attribute_operator}"
|
273
|
+
binds[bindvar] = attribute_value(value)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
obligation_conds << "1=1" if obligation_conds.empty?
|
278
|
+
conds << "(#{obligation_conds.join(' AND ')})"
|
279
|
+
end
|
280
|
+
(delete_paths - used_paths).each {|path| reflections.delete(path)}
|
281
|
+
|
282
|
+
finder_options[:conditions] = [ conds.join( " OR " ), binds ]
|
283
|
+
end
|
284
|
+
|
285
|
+
def attribute_value(value)
|
286
|
+
value.class.respond_to?(:descends_from_active_record?) && value.class.descends_from_active_record? && value.id ||
|
287
|
+
value.is_a?(Array) && value[0].class.respond_to?(:descends_from_active_record?) && value[0].class.descends_from_active_record? && value.map( &:id ) ||
|
288
|
+
value
|
289
|
+
end
|
290
|
+
|
291
|
+
# Parses all of the defined obligation joins and defines the scope's :joins or :includes option.
|
292
|
+
# TODO: Support non-linear association paths. Right now, we just break down the longest path parsed.
|
293
|
+
def rebuild_join_options!
|
294
|
+
joins = (finder_options[:joins] || []) + (finder_options[:includes] || [])
|
295
|
+
|
296
|
+
reflections.keys.each do |path|
|
297
|
+
next if path.empty? or @join_table_joins.include?(path)
|
298
|
+
|
299
|
+
existing_join = joins.find do |join|
|
300
|
+
existing_path = join_to_path(join)
|
301
|
+
min_length = [existing_path.length, path.length].min
|
302
|
+
existing_path.first(min_length) == path.first(min_length)
|
303
|
+
end
|
304
|
+
|
305
|
+
if existing_join
|
306
|
+
if join_to_path(existing_join).length < path.length
|
307
|
+
joins[joins.index(existing_join)] = path_to_join(path)
|
308
|
+
end
|
309
|
+
else
|
310
|
+
joins << path_to_join(path)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
case obligation_conditions.length
|
315
|
+
when 0 then
|
316
|
+
# No obligation conditions means we don't have to mess with joins or includes at all.
|
317
|
+
when 1 then
|
318
|
+
finder_options[:joins] = joins
|
319
|
+
finder_options.delete( :include )
|
320
|
+
else
|
321
|
+
finder_options.delete( :joins )
|
322
|
+
finder_options[:include] = joins
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def path_to_join(path)
|
327
|
+
case path.length
|
328
|
+
when 0 then nil
|
329
|
+
when 1 then path[0]
|
330
|
+
else
|
331
|
+
hash = { path[-2] => path[-1] }
|
332
|
+
path[0..-3].reverse.each do |elem|
|
333
|
+
hash = { elem => hash }
|
334
|
+
end
|
335
|
+
hash
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def join_to_path(join)
|
340
|
+
case join
|
341
|
+
when Symbol
|
342
|
+
[join]
|
343
|
+
when Hash
|
344
|
+
[join.keys.first] + join_to_path(join[join.keys.first])
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|