ae_declarative_authorization 0.7.0 → 0.7.1

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