ae_declarative_authorization 0.7.0 → 0.7.1

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.
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
+