zeiv-declarative_authorization 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +189 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +632 -0
  5. data/Rakefile +53 -0
  6. data/app/controllers/authorization_rules_controller.rb +258 -0
  7. data/app/controllers/authorization_usages_controller.rb +22 -0
  8. data/app/helpers/authorization_rules_helper.rb +218 -0
  9. data/app/views/authorization_rules/_change.erb +58 -0
  10. data/app/views/authorization_rules/_show_graph.erb +44 -0
  11. data/app/views/authorization_rules/_suggestions.erb +48 -0
  12. data/app/views/authorization_rules/change.html.erb +169 -0
  13. data/app/views/authorization_rules/graph.dot.erb +68 -0
  14. data/app/views/authorization_rules/graph.html.erb +47 -0
  15. data/app/views/authorization_rules/index.html.erb +17 -0
  16. data/app/views/authorization_usages/index.html.erb +36 -0
  17. data/authorization_rules.dist.rb +20 -0
  18. data/config/routes.rb +20 -0
  19. data/garlic_example.rb +20 -0
  20. data/init.rb +5 -0
  21. data/lib/declarative_authorization.rb +19 -0
  22. data/lib/declarative_authorization/adapters/active_record.rb +13 -0
  23. data/lib/declarative_authorization/adapters/active_record/base_extensions.rb +0 -0
  24. data/lib/declarative_authorization/adapters/active_record/obligation_scope_builder.rb +0 -0
  25. data/lib/declarative_authorization/authorization.rb +798 -0
  26. data/lib/declarative_authorization/development_support/analyzer.rb +261 -0
  27. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  28. data/lib/declarative_authorization/development_support/change_supporter.rb +620 -0
  29. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  30. data/lib/declarative_authorization/helper.rb +68 -0
  31. data/lib/declarative_authorization/in_controller.rb +703 -0
  32. data/lib/declarative_authorization/in_model.rb +188 -0
  33. data/lib/declarative_authorization/maintenance.rb +210 -0
  34. data/lib/declarative_authorization/obligation_scope.rb +361 -0
  35. data/lib/declarative_authorization/rails_legacy.rb +22 -0
  36. data/lib/declarative_authorization/railsengine.rb +6 -0
  37. data/lib/declarative_authorization/reader.rb +546 -0
  38. data/lib/generators/authorization/install/install_generator.rb +77 -0
  39. data/lib/generators/authorization/rules/rules_generator.rb +14 -0
  40. data/lib/generators/authorization/rules/templates/authorization_rules.rb +27 -0
  41. data/lib/tasks/authorization_tasks.rake +89 -0
  42. data/test/authorization_test.rb +1124 -0
  43. data/test/controller_filter_resource_access_test.rb +575 -0
  44. data/test/controller_test.rb +480 -0
  45. data/test/database.yml +3 -0
  46. data/test/dsl_reader_test.rb +178 -0
  47. data/test/helper_test.rb +247 -0
  48. data/test/maintenance_test.rb +46 -0
  49. data/test/model_test.rb +2008 -0
  50. data/test/schema.sql +56 -0
  51. data/test/test_helper.rb +255 -0
  52. metadata +95 -0
@@ -0,0 +1,22 @@
1
+ # Groups methods and additions that were used but are not readily available in
2
+ # all supported Rails versions.
3
+ class Hash
4
+ unless {}.respond_to?(:deep_merge)
5
+ # Imported from Rails 2.2
6
+ def deep_merge(other_hash)
7
+ self.merge(other_hash) do |key, oldval, newval|
8
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
9
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
10
+ oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ class String
17
+ unless "".respond_to?(:html_safe)
18
+ def html_safe
19
+ self
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ require 'rails'
2
+
3
+ module Authorization
4
+ class RailsEngine < Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,546 @@
1
+ # Authorization::Reader
2
+
3
+ require File.dirname(__FILE__) + '/authorization.rb'
4
+
5
+ module Authorization
6
+ # Parses an authorization configuration file in the authorization DSL and
7
+ # constructs a data model of its contents.
8
+ #
9
+ # For examples and the modeled data model, see the
10
+ # README[link:files/README_rdoc.html].
11
+ #
12
+ # Also, see role definition methods
13
+ # * AuthorizationRulesReader#role,
14
+ # * AuthorizationRulesReader#includes,
15
+ # * AuthorizationRulesReader#title,
16
+ # * AuthorizationRulesReader#description
17
+ #
18
+ # Methods for rule definition in roles
19
+ # * AuthorizationRulesReader#has_permission_on,
20
+ # * AuthorizationRulesReader#to,
21
+ # * AuthorizationRulesReader#if_attribute,
22
+ # * AuthorizationRulesReader#if_permitted_to
23
+ #
24
+ # Methods to be used in if_attribute statements
25
+ # * AuthorizationRulesReader#contains,
26
+ # * AuthorizationRulesReader#does_not_contain,
27
+ # * AuthorizationRulesReader#intersects_with,
28
+ # * AuthorizationRulesReader#is,
29
+ # * AuthorizationRulesReader#is_not,
30
+ # * AuthorizationRulesReader#is_in,
31
+ # * AuthorizationRulesReader#is_not_in,
32
+ # * AuthorizationRulesReader#lt,
33
+ # * AuthorizationRulesReader#lte,
34
+ # * AuthorizationRulesReader#gt,
35
+ # * AuthorizationRulesReader#gte
36
+ #
37
+ # And privilege definition methods
38
+ # * PrivilegesReader#privilege,
39
+ # * PrivilegesReader#includes
40
+ #
41
+ module Reader
42
+ # Signals that the specified file to load was not found.
43
+ class DSLFileNotFoundError < Exception; end
44
+ # Signals errors that occur while reading and parsing an authorization DSL
45
+ class DSLError < Exception; end
46
+ # Signals errors in the syntax of an authorization DSL.
47
+ class DSLSyntaxError < DSLError; end
48
+
49
+ # Top-level reader, parses the methods +privileges+ and +authorization+.
50
+ # +authorization+ takes a block with authorization rules as described in
51
+ # AuthorizationRulesReader. The block to +privileges+ defines privilege
52
+ # hierarchies, as described in PrivilegesReader.
53
+ #
54
+ class DSLReader
55
+ attr_reader :privileges_reader, :auth_rules_reader # :nodoc:
56
+
57
+ def initialize ()
58
+ @privileges_reader = PrivilegesReader.new
59
+ @auth_rules_reader = AuthorizationRulesReader.new
60
+ end
61
+
62
+ def initialize_copy (from) # :nodoc:
63
+ @privileges_reader = from.privileges_reader.clone
64
+ @auth_rules_reader = from.auth_rules_reader.clone
65
+ end
66
+
67
+ # ensures you get back a DSLReader
68
+ # if you provide a:
69
+ # DSLReader - you will get it back.
70
+ # String or Array - it will treat it as if you have passed a path or an array of paths and attempt to load those.
71
+ def self.factory(obj)
72
+ case obj
73
+ when Reader::DSLReader
74
+ obj
75
+ when String, Array
76
+ load(obj)
77
+ end
78
+ end
79
+
80
+ # Parses a authorization DSL specification from the string given
81
+ # in +dsl_data+. Raises DSLSyntaxError if errors occur on parsing.
82
+ def parse (dsl_data, file_name = nil)
83
+ if file_name
84
+ DSLMethods.new(self).instance_eval(dsl_data, file_name)
85
+ else
86
+ DSLMethods.new(self).instance_eval(dsl_data)
87
+ end
88
+ rescue SyntaxError, NoMethodError, NameError => e
89
+ raise DSLSyntaxError, "Illegal DSL syntax: #{e}"
90
+ end
91
+
92
+ # Load and parse a DSL from the given file name.
93
+ def load (dsl_file)
94
+ parse(File.read(dsl_file), dsl_file) if File.exist?(dsl_file)
95
+ end
96
+
97
+ # Load and parse a DSL from the given file name. Raises Authorization::Reader::DSLFileNotFoundError
98
+ # if the file cannot be found.
99
+ def load! (dsl_file)
100
+ raise ::Authorization::Reader::DSLFileNotFoundError, "Error reading authorization rules file with path '#{dsl_file}'! Please ensure it exists and that it is accessible." unless File.exist?(dsl_file)
101
+ load(dsl_file)
102
+ end
103
+
104
+ # Loads and parses DSL files and returns a new reader
105
+ def self.load (dsl_files)
106
+ # TODO cache reader in production mode?
107
+ reader = new
108
+ dsl_files = [dsl_files].flatten
109
+ dsl_files.each do |file|
110
+ reader.load(file)
111
+ end
112
+ reader
113
+ end
114
+
115
+ # DSL methods
116
+ class DSLMethods # :nodoc:
117
+ def initialize (parent)
118
+ @parent = parent
119
+ end
120
+
121
+ def privileges (&block)
122
+ @parent.privileges_reader.instance_eval(&block)
123
+ end
124
+
125
+ def contexts (&block)
126
+ # Not implemented
127
+ end
128
+
129
+ def authorization (&block)
130
+ @parent.auth_rules_reader.instance_eval(&block)
131
+ end
132
+ end
133
+ end
134
+
135
+ # The PrivilegeReader handles the part of the authorization DSL in
136
+ # a +privileges+ block. Here, privilege hierarchies are defined.
137
+ class PrivilegesReader
138
+ # TODO handle privileges with separated context
139
+ attr_reader :privileges, :privilege_hierarchy # :nodoc:
140
+
141
+ def initialize # :nodoc:
142
+ @current_priv = nil
143
+ @current_context = nil
144
+ @privileges = []
145
+ # {priv => [[priv,ctx], ...]}
146
+ @privilege_hierarchy = {}
147
+ end
148
+
149
+ def initialize_copy (from) # :nodoc:
150
+ @privileges = from.privileges.clone
151
+ @privilege_hierarchy = from.privilege_hierarchy.clone
152
+ end
153
+
154
+ def append_privilege (priv) # :nodoc:
155
+ @privileges << priv unless @privileges.include?(priv)
156
+ end
157
+
158
+ # Defines part of a privilege hierarchy. For the given +privilege+,
159
+ # included privileges may be defined in the block (through includes)
160
+ # or as option :+includes+. If the optional context is given,
161
+ # the privilege hierarchy is limited to that context.
162
+ #
163
+ def privilege (privilege, context = nil, options = {}, &block)
164
+ if context.is_a?(Hash)
165
+ options = context
166
+ context = nil
167
+ end
168
+ @current_priv = privilege
169
+ @current_context = context
170
+ append_privilege privilege
171
+ instance_eval(&block) if block
172
+ includes(*options[:includes]) if options[:includes]
173
+ ensure
174
+ @current_priv = nil
175
+ @current_context = nil
176
+ end
177
+
178
+ # Specifies +privileges+ that are to be assigned as lower ones. Only to
179
+ # be used inside a privilege block.
180
+ def includes (*privileges)
181
+ raise DSLError, "includes only in privilege block" if @current_priv.nil?
182
+ privileges.each do |priv|
183
+ append_privilege priv
184
+ @privilege_hierarchy[@current_priv] ||= []
185
+ @privilege_hierarchy[@current_priv] << [priv, @current_context]
186
+ end
187
+ end
188
+ end
189
+
190
+ class AuthorizationRulesReader
191
+ attr_reader :roles, :role_hierarchy, :auth_rules,
192
+ :role_descriptions, :role_titles, :omnipotent_roles # :nodoc:
193
+
194
+ def initialize # :nodoc:
195
+ @current_role = nil
196
+ @current_rule = nil
197
+ @roles = []
198
+ @omnipotent_roles = []
199
+ # higher_role => [lower_roles]
200
+ @role_hierarchy = {}
201
+ @role_titles = {}
202
+ @role_descriptions = {}
203
+ @auth_rules = AuthorizationRuleSet.new
204
+ end
205
+
206
+ def initialize_copy (from) # :nodoc:
207
+ [:roles, :role_hierarchy, :auth_rules,
208
+ :role_descriptions, :role_titles, :omnipotent_roles].each do |attribute|
209
+ instance_variable_set(:"@#{attribute}", from.send(attribute).clone)
210
+ end
211
+ end
212
+
213
+ def append_role (role, options = {}) # :nodoc:
214
+ @roles << role unless @roles.include? role
215
+ @role_titles[role] = options[:title] if options[:title]
216
+ @role_descriptions[role] = options[:description] if options[:description]
217
+ end
218
+
219
+ # Defines the authorization rules for the given +role+ in the
220
+ # following block.
221
+ # role :admin do
222
+ # has_permissions_on ...
223
+ # end
224
+ #
225
+ def role (role, options = {}, &block)
226
+ append_role role, options
227
+ @current_role = role
228
+ yield
229
+ ensure
230
+ @current_role = nil
231
+ end
232
+
233
+ # Roles may inherit all the rights from subroles. The given +roles+
234
+ # become subroles of the current block's role.
235
+ # role :admin do
236
+ # includes :user
237
+ # has_permission_on :employees, :to => [:update, :create]
238
+ # end
239
+ # role :user do
240
+ # has_permission_on :employees, :to => :read
241
+ # end
242
+ #
243
+ def includes (*roles)
244
+ raise DSLError, "includes only in role blocks" if @current_role.nil?
245
+ @role_hierarchy[@current_role] ||= []
246
+ @role_hierarchy[@current_role] += roles.flatten
247
+ end
248
+
249
+ # Allows the definition of privileges to be allowed for the current role,
250
+ # either in a has_permission_on block or directly in one call.
251
+ # role :admin
252
+ # has_permission_on :employees, :to => :read
253
+ # has_permission_on [:employees, :orders], :to => :read
254
+ # has_permission_on :employees do
255
+ # to :create
256
+ # if_attribute ...
257
+ # end
258
+ # has_permission_on :employees, :to => :delete do
259
+ # if_attribute ...
260
+ # end
261
+ # end
262
+ # The block form allows to describe restrictions on the permissions
263
+ # using if_attribute. Multiple has_permission_on statements are
264
+ # OR'ed when evaluating the permissions. Also, multiple if_attribute
265
+ # statements in one block are OR'ed if no :+join_by+ option is given
266
+ # (see below). To AND conditions, either set :+join_by+ to :and or place
267
+ # them in one if_attribute statement.
268
+ #
269
+ # Available options
270
+ # [:+to+]
271
+ # A symbol or an array of symbols representing the privileges that
272
+ # should be granted in this statement.
273
+ # [:+join_by+]
274
+ # Join operator to logically connect the constraint statements inside
275
+ # of the has_permission_on block. May be :+and+ or :+or+. Defaults to :+or+.
276
+ #
277
+ def has_permission_on (*args, &block)
278
+ options = args.extract_options!
279
+ context = args.flatten
280
+
281
+ raise DSLError, "has_permission_on only allowed in role blocks" if @current_role.nil?
282
+ options = {:to => [], :join_by => :or}.merge(options)
283
+
284
+ privs = options[:to]
285
+ privs = [privs] unless privs.is_a?(Array)
286
+ raise DSLError, "has_permission_on either needs a block or :to option" if !block_given? and privs.empty?
287
+
288
+ file, line = file_and_line_number_from_call_stack
289
+ rule = AuthorizationRule.new(@current_role, privs, context, options[:join_by],
290
+ :source_file => file, :source_line => line)
291
+ @auth_rules << rule
292
+ if block_given?
293
+ @current_rule = rule
294
+ yield
295
+ raise DSLError, "has_permission_on block content specifies no privileges" if rule.privileges.empty?
296
+ # TODO ensure?
297
+ @current_rule = nil
298
+ end
299
+ end
300
+
301
+ # Removes any permission checks for the current role.
302
+ # role :admin
303
+ # has_omnipotence
304
+ # end
305
+ def has_omnipotence
306
+ raise DSLError, "has_omnipotence only allowed in role blocks" if @current_role.nil?
307
+ @omnipotent_roles << @current_role
308
+ end
309
+
310
+ # Sets a description for the current role. E.g.
311
+ # role :admin
312
+ # description "To be assigned to administrative personnel"
313
+ # has_permission_on ...
314
+ # end
315
+ def description (text)
316
+ raise DSLError, "description only allowed in role blocks" if @current_role.nil?
317
+ role_descriptions[@current_role] = text
318
+ end
319
+
320
+ # Sets a human-readable title for the current role. E.g.
321
+ # role :admin
322
+ # title "Administrator"
323
+ # has_permission_on ...
324
+ # end
325
+ def title (text)
326
+ raise DSLError, "title only allowed in role blocks" if @current_role.nil?
327
+ role_titles[@current_role] = text
328
+ end
329
+
330
+ # Used in a has_permission_on block, to may be used to specify privileges
331
+ # to be assigned to the current role under the conditions specified in
332
+ # the current block.
333
+ # role :admin
334
+ # has_permission_on :employees do
335
+ # to :create, :read, :update, :delete
336
+ # end
337
+ # end
338
+ def to (*privs)
339
+ raise DSLError, "to only allowed in has_permission_on blocks" if @current_rule.nil?
340
+ @current_rule.append_privileges(privs.flatten)
341
+ end
342
+
343
+ # In a has_permission_on block, if_attribute specifies conditions
344
+ # of dynamic parameters that have to be met for the user to meet the
345
+ # privileges in this block. Conditions are evaluated on the context
346
+ # object. Thus, the following allows CRUD for branch admins only on
347
+ # employees that belong to the same branch as the current user.
348
+ # role :branch_admin
349
+ # has_permission_on :employees do
350
+ # to :create, :read, :update, :delete
351
+ # if_attribute :branch => is { user.branch }
352
+ # end
353
+ # end
354
+ # In this case, is is the operator for evaluating the condition. Another
355
+ # operator is contains for collections. In the block supplied to the
356
+ # operator, +user+ specifies the current user for whom the condition
357
+ # is evaluated.
358
+ #
359
+ # Conditions may be nested:
360
+ # role :company_admin
361
+ # has_permission_on :employees do
362
+ # to :create, :read, :update, :delete
363
+ # if_attribute :branch => { :company => is {user.branch.company} }
364
+ # end
365
+ # end
366
+ #
367
+ # has_many and has_many through associations may also be nested.
368
+ # Then, at least one item in the association needs to fulfill the
369
+ # subsequent condition:
370
+ # if_attribute :company => { :branches => { :manager => { :last_name => is { user.last_name } } }
371
+ # Beware of possible performance issues when using has_many associations in
372
+ # permitted_to? checks. For
373
+ # permitted_to? :read, object
374
+ # a check like
375
+ # object.company.branches.any? { |branch| branch.manager ... }
376
+ # will be executed. with_permission_to scopes construct efficient SQL
377
+ # joins, though.
378
+ #
379
+ # Multiple attributes in one :if_attribute statement are AND'ed.
380
+ # Multiple if_attribute statements are OR'ed if the join operator for the
381
+ # has_permission_on block isn't explicitly set. Thus, the following would
382
+ # require the current user either to be of the same branch AND the employee
383
+ # to be "changeable_by_coworker". OR the current user has to be the
384
+ # employee in question.
385
+ # has_permission_on :employees, :to => :manage do
386
+ # if_attribute :branch => is {user.branch}, :changeable_by_coworker => true
387
+ # if_attribute :id => is {user.id}
388
+ # end
389
+ # The join operator for if_attribute rules can explicitly set to AND, though.
390
+ # See has_permission_on for details.
391
+ #
392
+ # Arrays and fixed values may be used directly as hash values:
393
+ # if_attribute :id => 1
394
+ # if_attribute :type => "special"
395
+ # if_attribute :id => [1,2]
396
+ #
397
+ def if_attribute (attr_conditions_hash)
398
+ raise DSLError, "if_attribute only in has_permission blocks" if @current_rule.nil?
399
+ parse_attribute_conditions_hash!(attr_conditions_hash)
400
+ @current_rule.append_attribute Attribute.new(attr_conditions_hash)
401
+ end
402
+
403
+ # if_permitted_to allows the has_permission_on block to depend on
404
+ # permissions on associated objects. By using it, the authorization
405
+ # rules may be a lot DRYer. E.g.:
406
+ #
407
+ # role :branch_manager
408
+ # has_permission_on :branches, :to => :manage do
409
+ # if_attribute :employees => contains { user }
410
+ # end
411
+ # has_permission_on :employees, :to => :read do
412
+ # if_permitted_to :read, :branch
413
+ # # instead of
414
+ # # if_attribute :branch => { :employees => contains { user } }
415
+ # end
416
+ # end
417
+ #
418
+ # if_permitted_to associations may be nested as well:
419
+ # if_permitted_to :read, :branch => :company
420
+ #
421
+ # You can even use has_many associations as target. Then, it is checked
422
+ # if the current user has the required privilege on *any* of the target objects.
423
+ # if_permitted_to :read, :branch => :employees
424
+ # Beware of performance issues with permission checks. In the current implementation,
425
+ # all employees are checked until the first permitted is found.
426
+ # with_permissions_to, on the other hand, constructs more efficient SQL
427
+ # instead.
428
+ #
429
+ # To check permissions based on the current object, the attribute has to
430
+ # be left out:
431
+ # has_permission_on :branches, :to => :manage do
432
+ # if_attribute :employees => contains { user }
433
+ # end
434
+ # has_permission_on :branches, :to => :paint_green do
435
+ # if_permitted_to :update
436
+ # end
437
+ # Normally, one would merge those rules into one. Dividing makes sense
438
+ # if additional if_attribute are used in the second rule or those rules
439
+ # are applied to different roles.
440
+ #
441
+ # Options:
442
+ # [:+context+]
443
+ # When using with_permissions_to, the target context of the if_permitted_to
444
+ # statement is inferred from the last reflections target class. Still,
445
+ # you may override this algorithm by setting the context explicitly.
446
+ # if_permitted_to :read, :home_branch, :context => :branches
447
+ # if_permitted_to :read, :branch => :main_company, :context => :companies
448
+ #
449
+ def if_permitted_to (privilege, attr_or_hash = nil, options = {})
450
+ raise DSLError, "if_permitted_to only in has_permission blocks" if @current_rule.nil?
451
+ options[:context] ||= attr_or_hash.delete(:context) if attr_or_hash.is_a?(Hash)
452
+ # only :context option in attr_or_hash:
453
+ attr_or_hash = nil if attr_or_hash.is_a?(Hash) and attr_or_hash.empty?
454
+ @current_rule.append_attribute AttributeWithPermission.new(privilege,
455
+ attr_or_hash, options[:context])
456
+ end
457
+
458
+ # In an if_attribute statement, is says that the value has to be
459
+ # met exactly by the if_attribute attribute. For information on the block
460
+ # argument, see if_attribute.
461
+ def is (&block)
462
+ [:is, block]
463
+ end
464
+
465
+ # The negation of is.
466
+ def is_not (&block)
467
+ [:is_not, block]
468
+ end
469
+
470
+ # In an if_attribute statement, contains says that the value has to be
471
+ # part of the collection specified by the if_attribute attribute.
472
+ # For information on the block argument, see if_attribute.
473
+ def contains (&block)
474
+ [:contains, block]
475
+ end
476
+
477
+ # The negation of contains. Currently, query rewriting is disabled
478
+ # for does_not_contain.
479
+ def does_not_contain (&block)
480
+ [:does_not_contain, block]
481
+ end
482
+
483
+ # In an if_attribute statement, intersects_with requires that at least
484
+ # one of the values has to be part of the collection specified by the
485
+ # if_attribute attribute. The value block needs to evaluate to an
486
+ # Enumerable. For information on the block argument, see if_attribute.
487
+ def intersects_with (&block)
488
+ [:intersects_with, block]
489
+ end
490
+
491
+ # In an if_attribute statement, is_in says that the value has to
492
+ # contain the attribute value.
493
+ # For information on the block argument, see if_attribute.
494
+ def is_in (&block)
495
+ [:is_in, block]
496
+ end
497
+
498
+ # The negation of is_in.
499
+ def is_not_in (&block)
500
+ [:is_not_in, block]
501
+ end
502
+
503
+ # Less than
504
+ def lt (&block)
505
+ [:lt, block]
506
+ end
507
+
508
+ # Less than or equal to
509
+ def lte (&block)
510
+ [:lte, block]
511
+ end
512
+
513
+ # Greater than
514
+ def gt (&block)
515
+ [:gt, block]
516
+ end
517
+
518
+ # Greater than or equal to
519
+ def gte (&block)
520
+ [:gte, block]
521
+ end
522
+
523
+ private
524
+ def parse_attribute_conditions_hash! (hash)
525
+ merge_hash = {}
526
+ hash.each do |key, value|
527
+ if value.is_a?(Hash)
528
+ parse_attribute_conditions_hash!(value)
529
+ elsif !value.is_a?(Array)
530
+ merge_hash[key] = [:is, proc { value }]
531
+ elsif value.is_a?(Array) and !value[0].is_a?(Symbol)
532
+ merge_hash[key] = [:is_in, proc { value }]
533
+ end
534
+ end
535
+ hash.merge!(merge_hash)
536
+ end
537
+
538
+ def file_and_line_number_from_call_stack
539
+ caller_parts = caller(2).first.split(':')
540
+ [caller_parts[0] == "(eval)" ? nil : caller_parts[0],
541
+ caller_parts[1] && caller_parts[1].to_i]
542
+ end
543
+ end
544
+ end
545
+ end
546
+