ae_declarative_authorization 0.7.1 → 0.8.0

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 (56) hide show
  1. checksums.yaml +5 -5
  2. data/Appraisals +31 -21
  3. data/CHANGELOG +189 -189
  4. data/Gemfile +7 -7
  5. data/Gemfile.lock +68 -60
  6. data/LICENSE.txt +20 -20
  7. data/README.md +620 -620
  8. data/README.rdoc +597 -597
  9. data/Rakefile +35 -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/rails4252.gemfile.lock +126 -0
  14. data/gemfiles/rails4271.gemfile +10 -10
  15. data/gemfiles/rails4271.gemfile.lock +126 -0
  16. data/gemfiles/rails507.gemfile +11 -11
  17. data/gemfiles/rails507.gemfile.lock +136 -0
  18. data/gemfiles/rails516.gemfile +11 -0
  19. data/gemfiles/rails516.gemfile.lock +136 -0
  20. data/gemfiles/rails521.gemfile +11 -0
  21. data/gemfiles/rails521.gemfile.lock +144 -0
  22. data/init.rb +5 -5
  23. data/lib/declarative_authorization.rb +18 -18
  24. data/lib/declarative_authorization/authorization.rb +821 -821
  25. data/lib/declarative_authorization/helper.rb +78 -78
  26. data/lib/declarative_authorization/in_controller.rb +713 -713
  27. data/lib/declarative_authorization/in_model.rb +156 -156
  28. data/lib/declarative_authorization/maintenance.rb +215 -215
  29. data/lib/declarative_authorization/obligation_scope.rb +348 -345
  30. data/lib/declarative_authorization/railsengine.rb +5 -5
  31. data/lib/declarative_authorization/reader.rb +549 -549
  32. data/lib/declarative_authorization/test/helpers.rb +261 -261
  33. data/lib/declarative_authorization/version.rb +3 -3
  34. data/lib/generators/authorization/install/install_generator.rb +77 -77
  35. data/lib/generators/authorization/rules/rules_generator.rb +13 -13
  36. data/lib/generators/authorization/rules/templates/authorization_rules.rb +27 -27
  37. data/lib/tasks/authorization_tasks.rake +89 -89
  38. data/log/test.log +15246 -0
  39. data/pkg/ae_declarative_authorization-0.7.1.gem +0 -0
  40. data/pkg/ae_declarative_authorization-0.8.0.gem +0 -0
  41. data/test/authorization_test.rb +1121 -1121
  42. data/test/controller_filter_resource_access_test.rb +573 -573
  43. data/test/controller_test.rb +478 -478
  44. data/test/database.yml +3 -3
  45. data/test/dsl_reader_test.rb +178 -178
  46. data/test/functional/filter_access_to_with_id_in_scope_test.rb +88 -88
  47. data/test/functional/no_filter_access_to_test.rb +79 -79
  48. data/test/functional/params_block_arity_test.rb +39 -39
  49. data/test/helper_test.rb +248 -248
  50. data/test/maintenance_test.rb +46 -46
  51. data/test/model_test.rb +1840 -1840
  52. data/test/profiles/access_checking +20 -0
  53. data/test/schema.sql +60 -60
  54. data/test/test_helper.rb +174 -174
  55. data/test/test_support/minitest_compatibility.rb +26 -26
  56. metadata +17 -5
data/init.rb CHANGED
@@ -1,5 +1,5 @@
1
- begin
2
- require File.join(File.dirname(__FILE__), 'lib', 'declarative_authorization') # From here
3
- rescue LoadError
4
- require 'declarative_authorization' # From gem
5
- end
1
+ begin
2
+ require File.join(File.dirname(__FILE__), 'lib', 'declarative_authorization') # From here
3
+ rescue LoadError
4
+ require 'declarative_authorization' # From gem
5
+ end
@@ -1,18 +1,18 @@
1
- require File.join(%w{declarative_authorization helper})
2
- require File.join(%w{declarative_authorization in_controller})
3
- if defined?(ActiveRecord)
4
- require File.join(%w{declarative_authorization in_model})
5
- require File.join(%w{declarative_authorization obligation_scope})
6
- end
7
-
8
- min_rails_version = '4.2.5.2'
9
- if Rails::VERSION::STRING < min_rails_version
10
- raise "ae_declarative_authorization requires Rails #{min_rails_version}. You are using #{Rails::VERSION::STRING}."
11
- end
12
-
13
- require File.join(%w{declarative_authorization railsengine}) if defined?(::Rails::Engine)
14
-
15
- ActionController::Base.send :include, Authorization::AuthorizationInController
16
- ActionController::Base.helper Authorization::AuthorizationHelper
17
-
18
- ActiveRecord::Base.send :include, Authorization::AuthorizationInModel if defined?(ActiveRecord)
1
+ require File.join(%w{declarative_authorization helper})
2
+ require File.join(%w{declarative_authorization in_controller})
3
+ if defined?(ActiveRecord)
4
+ require File.join(%w{declarative_authorization in_model})
5
+ require File.join(%w{declarative_authorization obligation_scope})
6
+ end
7
+
8
+ min_rails_version = '4.2.5.2'
9
+ if Rails::VERSION::STRING < min_rails_version
10
+ raise "ae_declarative_authorization requires Rails #{min_rails_version}. You are using #{Rails::VERSION::STRING}."
11
+ end
12
+
13
+ require File.join(%w{declarative_authorization railsengine}) if defined?(::Rails::Engine)
14
+
15
+ ActionController::Base.send :include, Authorization::AuthorizationInController
16
+ ActionController::Base.helper Authorization::AuthorizationHelper
17
+
18
+ ActiveRecord::Base.send :include, Authorization::AuthorizationInModel if defined?(ActiveRecord)
@@ -1,821 +1,821 @@
1
- # Authorization
2
- require File.dirname(__FILE__) + '/reader.rb'
3
- require "set"
4
- require "forwardable"
5
-
6
- module Authorization
7
- # An exception raised if anything goes wrong in the Authorization realm
8
- class AuthorizationError < StandardError ; end
9
- # NotAuthorized is raised if the current user is not allowed to perform
10
- # the given operation possibly on a specific object.
11
- class NotAuthorized < AuthorizationError ; end
12
- # AttributeAuthorizationError is more specific than NotAuthorized, signaling
13
- # that the access was denied on the grounds of attribute conditions.
14
- class AttributeAuthorizationError < NotAuthorized ; end
15
- # AuthorizationUsageError is used whenever a situation is encountered
16
- # in which the application misused the plugin. That is, if, e.g.,
17
- # authorization rules may not be evaluated.
18
- class AuthorizationUsageError < AuthorizationError ; end
19
- # NilAttributeValueError is raised by Attribute#validate? when it hits a nil attribute value.
20
- # The exception is raised to ensure that the entire rule is invalidated.
21
- class NilAttributeValueError < AuthorizationError; end
22
-
23
- AUTH_DSL_FILES = [Pathname.new(Rails.root || '').join("config", "authorization_rules.rb").to_s] unless defined? AUTH_DSL_FILES
24
-
25
- # Controller-independent method for retrieving the current user.
26
- # Needed for model security where the current controller is not available.
27
- def self.current_user
28
- Thread.current["current_user"] || guest_user
29
- end
30
-
31
- # Controller-independent method for setting the current user.
32
- def self.current_user=(user)
33
- Thread.current["current_user"] = user
34
- end
35
-
36
- def self.guest_user
37
- @@guest_user ||= AnonymousUser.new
38
- end
39
-
40
- def self.non_guest_current_user
41
- current_user unless current_user.is_a?(AnonymousUser)
42
- end
43
-
44
- # For use in test cases only
45
- def self.ignore_access_control(state = nil) # :nodoc:
46
- Thread.current["ignore_access_control"] = state unless state.nil?
47
- Thread.current["ignore_access_control"] || false
48
- end
49
-
50
- @@dot_path = "dot"
51
- def self.dot_path
52
- @@dot_path
53
- end
54
-
55
- def self.dot_path=(path)
56
- @@dot_path = path
57
- end
58
-
59
- @@default_role = :guest
60
- def self.default_role
61
- @@default_role
62
- end
63
-
64
- def self.default_role=(role)
65
- @@default_role = role.to_sym
66
- end
67
-
68
- def self.is_a_association_proxy?(object)
69
- object.respond_to?(:proxy_association)
70
- end
71
-
72
- # Authorization::Engine implements the reference monitor. It may be used
73
- # for querying the permission and retrieving obligations under which
74
- # a certain privilege is granted for the current user.
75
- #
76
- class Engine
77
- extend Forwardable
78
- attr_reader :reader
79
-
80
- def_delegators :@reader, :auth_rules_reader, :privileges_reader, :load, :load!
81
- def_delegators :auth_rules_reader, :auth_rules, :roles, :omnipotent_roles, :role_hierarchy, :role_titles, :role_descriptions
82
- def_delegators :privileges_reader, :privileges, :privilege_hierarchy
83
-
84
- # If +reader+ is not given, a new one is created with the default
85
- # authorization configuration of +AUTH_DSL_FILES+. If given, may be either
86
- # a Reader object or a path to a configuration file.
87
- def initialize(reader = nil)
88
- #@auth_rules = AuthorizationRuleSet.new reader.auth_rules_reader.auth_rules
89
- @reader = Reader::DSLReader.factory(reader || AUTH_DSL_FILES)
90
- end
91
-
92
- def initialize_copy(from) # :nodoc:
93
- @reader = from.reader.clone
94
- end
95
-
96
- # {[priv, ctx] => [priv, ...]}
97
- def rev_priv_hierarchy
98
- if @rev_priv_hierarchy.nil?
99
- @rev_priv_hierarchy = {}
100
- privilege_hierarchy.each do |key, value|
101
- value.each do |val|
102
- @rev_priv_hierarchy[val] ||= []
103
- @rev_priv_hierarchy[val] << key
104
- end
105
- end
106
- end
107
- @rev_priv_hierarchy
108
- end
109
-
110
- # {[priv, ctx] => [priv, ...]}
111
- def rev_role_hierarchy
112
- if @rev_role_hierarchy.nil?
113
- @rev_role_hierarchy = {}
114
- role_hierarchy.each do |higher_role, lower_roles|
115
- lower_roles.each do |role|
116
- (@rev_role_hierarchy[role] ||= []) << higher_role
117
- end
118
- end
119
- end
120
- @rev_role_hierarchy
121
- end
122
-
123
- # Returns true if privilege is met by the current user. Raises
124
- # AuthorizationError otherwise. +privilege+ may be given with or
125
- # without context. In the latter case, the :+context+ option is
126
- # required.
127
- #
128
- # Options:
129
- # [:+context+]
130
- # The context part of the privilege.
131
- # Defaults either to the tableized +class_name+ of the given :+object+, if given.
132
- # That is, :+users+ for :+object+ of type User.
133
- # Raises AuthorizationUsageError if context is missing and not to be inferred.
134
- # [:+object+] An context object to test attribute checks against.
135
- # [:+skip_attribute_test+]
136
- # Skips those attribute checks in the
137
- # authorization rules. Defaults to false.
138
- # [:+user+]
139
- # The user to check the authorization for.
140
- # Defaults to Authorization#current_user.
141
- # [:+bang+]
142
- # Should NotAuthorized exceptions be raised
143
- # Defaults to true.
144
- #
145
- def permit!(privilege, options = {})
146
- return true if Authorization.ignore_access_control
147
- options = {
148
- :object => nil,
149
- :skip_attribute_test => false,
150
- :context => nil,
151
- :bang => true
152
- }.merge(options)
153
-
154
- # Make sure we're handling all privileges as symbols.
155
- privilege = privilege.is_a?( Array ) ?
156
- privilege.flatten.collect { |priv| priv.to_sym } :
157
- privilege.to_sym
158
-
159
- #
160
- # If the object responds to :proxy_reflection, we're probably working with
161
- # an association proxy. Use 'new' to leverage ActiveRecord's builder
162
- # functionality to obtain an object against which we can check permissions.
163
- #
164
- # Example: permit!( :edit, :object => user.posts )
165
- #
166
- if Authorization.is_a_association_proxy?(options[:object]) && options[:object].respond_to?(:new)
167
- options[:object] = options[:object].where(nil).new
168
- end
169
-
170
- options[:context] ||= options[:object] && (
171
- options[:object].class.respond_to?(:decl_auth_context) ?
172
- options[:object].class.decl_auth_context :
173
- options[:object].class.name.tableize.to_sym
174
- ) rescue NoMethodError
175
-
176
- user, roles, privileges = user_roles_privleges_from_options(privilege, options)
177
-
178
- return true if roles.is_a?(Hash) && !(roles.keys & omnipotent_roles).empty?
179
-
180
- # find a authorization rule that matches for at least one of the roles and
181
- # at least one of the given privileges
182
- attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
183
- rules = matching_auth_rules(roles, privileges, options[:context])
184
-
185
- # Test each rule in turn to see whether any one of them is satisfied.
186
- rules.each do |rule|
187
- return true if rule.validate?(attr_validator, options[:skip_attribute_test])
188
- end
189
-
190
- if options[:bang]
191
- if rules.empty?
192
- raise NotAuthorized, "No matching rules found for #{privilege} for User with id #{user.try(:id)} " +
193
- "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
194
- "context #{options[:context].inspect})."
195
- else
196
- raise AttributeAuthorizationError, "#{privilege} not allowed for User with id #{user.try(:id)} on #{(options[:object] || options[:context]).inspect}."
197
- end
198
- else
199
- false
200
- end
201
- end
202
-
203
- # Calls permit! but doesn't raise authorization errors. If no exception is
204
- # raised, permit? returns true and yields to the optional block.
205
- def permit?(privilege, options = {}) # :yields:
206
- if permit!(privilege, options.merge(:bang=> false))
207
- yield if block_given?
208
- true
209
- else
210
- false
211
- end
212
- end
213
-
214
- # Returns the obligations to be met by the current user for the given
215
- # privilege as an array of obligation hashes in form of
216
- # [{:object_attribute => obligation_value, ...}, ...]
217
- # where +obligation_value+ is either (recursively) another obligation hash
218
- # or a value spec, such as
219
- # [operator, literal_value]
220
- # The obligation hashes in the array should be OR'ed, conditions inside
221
- # the hashes AND'ed.
222
- #
223
- # Example
224
- # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
225
- #
226
- # Options
227
- # [:+context+] See permit!
228
- # [:+user+] See permit!
229
- #
230
- def obligations(privilege, options = {})
231
- options = {:context => nil}.merge(options)
232
- user, roles, privileges = user_roles_privleges_from_options(privilege, options)
233
-
234
- permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
235
-
236
- return [] if roles.is_a?(Hash) && !(roles.keys & omnipotent_roles).empty?
237
-
238
- attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
239
- matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
240
- rule.obligations(attr_validator)
241
- end.flatten
242
- end
243
-
244
- # Returns the description for the given role. The description may be
245
- # specified with the authorization rules. Returns +nil+ if none was
246
- # given.
247
- def description_for(role)
248
- role_descriptions[role]
249
- end
250
-
251
- # Returns the title for the given role. The title may be
252
- # specified with the authorization rules. Returns +nil+ if none was
253
- # given.
254
- def title_for(role)
255
- role_titles[role]
256
- end
257
-
258
- # Returns the role symbols of the given user.
259
- def roles_for(user)
260
- user ||= Authorization.current_user
261
- raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.try(:id)})" \
262
- if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
263
-
264
- Rails.logger.info("The use of user.roles is deprecated. Please add a method " +
265
- "role_symbols to your User model.") if defined?(Rails) and Rails.respond_to?(:logger) and !user.respond_to?(:role_symbols)
266
-
267
- roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
268
-
269
- raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
270
- "doesn't return an Array of Symbols (#{roles.inspect})" \
271
- if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
272
-
273
- (roles.empty? ? [Authorization.default_role] : roles)
274
- end
275
-
276
- # Returns the role symbols and inherritted role symbols for the given user
277
- def roles_with_hierarchy_for(user)
278
- flatten_roles(roles_for(user))
279
- end
280
-
281
- def self.development_reload?
282
- if Rails.env.development?
283
- mod_time = AUTH_DSL_FILES.map do |m|
284
- begin
285
- File.mtime(m)
286
- rescue
287
- Time.at(0)
288
- end
289
- end.flatten.max
290
- @@auth_dsl_last_modified ||= mod_time
291
- if mod_time > @@auth_dsl_last_modified
292
- @@auth_dsl_last_modified = mod_time
293
- return true
294
- end
295
- end
296
- end
297
-
298
- # Returns an instance of Engine, which is created if there isn't one
299
- # yet. If +dsl_file+ is given, it is passed on to Engine.new and
300
- # a new instance is always created.
301
- def self.instance(dsl_file = nil)
302
- if dsl_file or development_reload?
303
- @@instance = new(dsl_file)
304
- else
305
- @@instance ||= new
306
- end
307
- end
308
-
309
- class AttributeValidator # :nodoc:
310
- attr_reader :user, :object, :engine, :context, :privilege
311
- def initialize(engine, user, object = nil, privilege = nil, context = nil)
312
- @engine = engine
313
- @user = user
314
- @object = object
315
- @privilege = privilege
316
- @context = context
317
- end
318
-
319
- def evaluate(value_block)
320
- # TODO cache?
321
- if value_block.is_a? Proc
322
- instance_eval(&value_block)
323
- else
324
- value_block
325
- end
326
- end
327
- end
328
-
329
- private
330
-
331
- def user_roles_privleges_from_options(privilege, options)
332
- options = {
333
- :user => nil,
334
- :context => nil,
335
- :user_roles => nil
336
- }.merge(options)
337
- user = options[:user] || Authorization.current_user
338
- privileges = privilege.is_a?(Array) ? privilege : [privilege]
339
-
340
- raise AuthorizationUsageError, "No user object given for user id (#{user.try(:id)}) or " +
341
- "set through Authorization.current_user" unless user
342
-
343
- roles = options[:user_roles] || flatten_roles(roles_for(user))
344
- privileges = flatten_privileges privileges, options[:context]
345
- [user, roles, privileges]
346
- end
347
-
348
- def flatten_roles(roles)
349
- # TODO: caching?
350
- hierarchy = role_hierarchy
351
- flattened_roles = {}
352
- roles.each do |role|
353
- flattened_roles[role] = true
354
- if (hierarchy_for_role = hierarchy[role])
355
- hierarchy_for_role.each do |r|
356
- flattened_roles[r] = true
357
- end
358
- end
359
- end
360
- flattened_roles
361
- end
362
-
363
- # Returns the privilege hierarchy flattened for given privileges in context.
364
- def flatten_privileges(privileges, context = nil)
365
- # TODO: caching?
366
- raise AuthorizationUsageError, 'No context given or inferable from object' unless context
367
- hierarchy = rev_priv_hierarchy
368
-
369
- flattened_privileges = privileges.clone
370
- flattened_privileges.each do |priv|
371
- flattened_privileges.concat(hierarchy[[priv, nil]]) if hierarchy[[priv, nil]]
372
- flattened_privileges.concat(hierarchy[[priv, context]]) if hierarchy[[priv, context]]
373
- end
374
- flattened_privileges.uniq
375
- end
376
-
377
- def matching_auth_rules(roles, privileges, context)
378
- auth_rules.matching(roles, privileges, context)
379
- end
380
- end
381
-
382
-
383
- class AuthorizationRuleSet
384
- include Enumerable
385
- extend Forwardable
386
- def_delegators :@rules, :each, :length, :[]
387
-
388
- def initialize(rules = [])
389
- @rules = rules.clone
390
- reset!
391
- end
392
-
393
- def initialize_copy(source)
394
- @rules = @rules.collect {|rule| rule.clone}
395
- reset!
396
- end
397
-
398
- def matching(roles, privileges, context)
399
- rules = cached_auth_rules[context] || []
400
- rules.select do |rule|
401
- rule.matches? roles, privileges, context
402
- end
403
- end
404
-
405
- def delete(rule)
406
- @rules.delete rule
407
- reset!
408
- end
409
-
410
- def <<(rule)
411
- @rules << rule
412
- reset!
413
- end
414
-
415
- def each(&block)
416
- @rules.each &block
417
- end
418
-
419
- private
420
- def reset!
421
- @cached_auth_rules =nil
422
- end
423
-
424
- def cached_auth_rules
425
- return @cached_auth_rules if @cached_auth_rules
426
- @cached_auth_rules = {}
427
- @rules.each do |rule|
428
- rule.contexts.each do |context|
429
- @cached_auth_rules[context] ||= []
430
- @cached_auth_rules[context] << rule
431
- end
432
- end
433
- @cached_auth_rules
434
- end
435
- end
436
-
437
- class AuthorizationRule
438
- attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
439
- :source_file, :source_line
440
-
441
- def initialize(role, privileges = [], contexts = nil, join_operator = :or,
442
- options = {})
443
- @role = role
444
- @privileges = Set.new(privileges)
445
- @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
446
- @join_operator = join_operator
447
- @attributes = []
448
- @source_file = options[:source_file]
449
- @source_line = options[:source_line]
450
- end
451
-
452
- def initialize_copy(from)
453
- @privileges = @privileges.clone
454
- @contexts = @contexts.clone
455
- @attributes = @attributes.collect {|attribute| attribute.clone }
456
- end
457
-
458
- def append_privileges(privs)
459
- @privileges.merge(privs)
460
- end
461
-
462
- def append_attribute(attribute)
463
- @attributes << attribute
464
- end
465
-
466
- def matches?(roles, privs, context = nil)
467
- roles = Hash[[*roles].map { |r| [r, true] }] unless roles.is_a?(Hash)
468
- @contexts.include?(context) && roles.include?(@role) && privs.any? { |priv| @privileges.include?(priv) }
469
- end
470
-
471
- def validate?(attr_validator, skip_attribute = false)
472
- skip_attribute or @attributes.empty? or
473
- @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
474
- begin
475
- attr.validate?(attr_validator)
476
- rescue NilAttributeValueError => e
477
- nil # Bumping up against a nil attribute value flunks the rule.
478
- end
479
- end
480
- end
481
-
482
- def obligations(attr_validator)
483
- exceptions = []
484
- obligations = @attributes.collect do |attr|
485
- begin
486
- attr.obligation(attr_validator)
487
- rescue NotAuthorized => e
488
- exceptions << e
489
- nil
490
- end
491
- end
492
-
493
- if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
494
- raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
495
- end
496
-
497
- if @join_operator == :and and !obligations.empty?
498
- # cross product of OR'ed obligations in arrays
499
- arrayed_obligations = obligations.map {|obligation| obligation.is_a?(Hash) ? [obligation] : obligation}
500
- merged_obligations = arrayed_obligations.first
501
- arrayed_obligations[1..-1].each do |inner_obligations|
502
- previous_merged_obligations = merged_obligations
503
- merged_obligations = inner_obligations.collect do |inner_obligation|
504
- previous_merged_obligations.collect do |merged_obligation|
505
- merged_obligation.deep_merge(inner_obligation)
506
- end
507
- end.flatten
508
- end
509
- obligations = merged_obligations
510
- else
511
- obligations = obligations.flatten.compact
512
- end
513
- obligations.empty? ? [{}] : obligations
514
- end
515
-
516
- def to_long_s
517
- attributes.collect {|attr| attr.to_long_s } * "; "
518
- end
519
- end
520
-
521
- class Attribute
522
- # attr_conditions_hash of form
523
- # { :object_attribute => [operator, value_block], ... }
524
- # { :object_attribute => { :attr => ... } }
525
- def initialize(conditions_hash)
526
- @conditions_hash = conditions_hash
527
- end
528
-
529
- def initialize_copy(from)
530
- @conditions_hash = deep_hash_clone(@conditions_hash)
531
- end
532
-
533
- def validate?(attr_validator, object = nil, hash = nil)
534
- object ||= attr_validator.object
535
- return false unless object
536
-
537
- if Authorization.is_a_association_proxy?(object) && object.respond_to?(:empty?)
538
- return false if object.empty?
539
- object.each do |member|
540
- return true if validate?(attr_validator, member, hash)
541
- end
542
- return false
543
- end
544
-
545
- (hash || @conditions_hash).all? do |attr, value|
546
- attr_value = object_attribute_value(object, attr)
547
- if value.is_a?(Hash)
548
- if attr_value.is_a?(Enumerable)
549
- attr_value.any? do |inner_value|
550
- validate?(attr_validator, inner_value, value)
551
- end
552
- elsif attr_value == nil
553
- raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
554
- else
555
- validate?(attr_validator, attr_value, value)
556
- end
557
- elsif value.is_a?(Array) and value.length == 2 and value.first.is_a?(Symbol)
558
- evaluated = if value[1].is_a?(Proc)
559
- attr_validator.evaluate(value[1])
560
- else
561
- value[1]
562
- end
563
- case value[0]
564
- when :is
565
- attr_value == evaluated
566
- when :is_not
567
- attr_value != evaluated
568
- when :contains
569
- begin
570
- attr_value.include?(evaluated)
571
- rescue NoMethodError => e
572
- raise AuthorizationUsageError, "Operator contains requires a " +
573
- "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
574
- "contains #{evaluated.inspect}: #{e}"
575
- end
576
- when :does_not_contain
577
- begin
578
- !attr_value.include?(evaluated)
579
- rescue NoMethodError => e
580
- raise AuthorizationUsageError, "Operator does_not_contain requires a " +
581
- "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
582
- "does_not_contain #{evaluated.inspect}: #{e}"
583
- end
584
- when :intersects_with
585
- begin
586
- !(evaluated.to_set & attr_value.to_set).empty?
587
- rescue NoMethodError => e
588
- raise AuthorizationUsageError, "Operator intersects_with requires " +
589
- "subclasses of Enumerable, got: #{attr_value.inspect} " +
590
- "intersects_with #{evaluated.inspect}: #{e}"
591
- end
592
- when :is_in
593
- begin
594
- evaluated.include?(attr_value)
595
- rescue NoMethodError => e
596
- raise AuthorizationUsageError, "Operator is_in requires a " +
597
- "subclass of Enumerable as value, got: #{attr_value.inspect} " +
598
- "is_in #{evaluated.inspect}: #{e}"
599
- end
600
- when :is_not_in
601
- begin
602
- !evaluated.include?(attr_value)
603
- rescue NoMethodError => e
604
- raise AuthorizationUsageError, "Operator is_not_in requires a " +
605
- "subclass of Enumerable as value, got: #{attr_value.inspect} " +
606
- "is_not_in #{evaluated.inspect}: #{e}"
607
- end
608
- when :lt
609
- attr_value && attr_value < evaluated
610
- when :lte
611
- attr_value && attr_value <= evaluated
612
- when :gt
613
- attr_value && attr_value > evaluated
614
- when :gte
615
- attr_value && attr_value >= evaluated
616
- when :id_in_scope
617
- evaluated.exists?(attr_value)
618
- else
619
- raise AuthorizationError, "Unknown operator #{value[0]}"
620
- end
621
- else
622
- raise AuthorizationError, "Wrong conditions hash format"
623
- end
624
- end
625
- end
626
-
627
- # resolves all the values in condition_hash
628
- def obligation(attr_validator, hash = nil)
629
- hash = (hash || @conditions_hash).clone
630
- hash.each do |attr, value|
631
- if value.is_a?(Hash)
632
- hash[attr] = obligation(attr_validator, value)
633
- elsif value.is_a?(Array) and value.length == 2
634
- hash[attr] = [value[0], attr_validator.evaluate(value[1])]
635
- else
636
- raise AuthorizationError, "Wrong conditions hash format"
637
- end
638
- end
639
- hash
640
- end
641
-
642
- def to_long_s(hash = nil)
643
- if hash
644
- hash.inject({}) do |memo, key_val|
645
- key, val = key_val
646
- memo[key] = case val
647
- when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
648
- when Hash then to_long_s(val)
649
- end
650
- memo
651
- end
652
- else
653
- "if_attribute #{to_long_s(@conditions_hash).inspect}"
654
- end
655
- end
656
-
657
- protected
658
- def object_attribute_value(object, attr)
659
- begin
660
- object.send(attr)
661
- rescue ArgumentError, NoMethodError => e
662
- raise AuthorizationUsageError, "Error occurred while validating attribute ##{attr} on #{object.inspect}: #{e}.\n" +
663
- "Please check your authorization rules and ensure the attribute is correctly spelled and \n" +
664
- "corresponds to a method on the model you are authorizing for."
665
- end
666
- end
667
-
668
- def deep_hash_clone(hash)
669
- hash.inject({}) do |memo, (key, val)|
670
- memo[key] = case val
671
- when Hash
672
- deep_hash_clone(val)
673
- when NilClass, Symbol
674
- val
675
- else
676
- val.clone
677
- end
678
- memo
679
- end
680
- end
681
- end
682
-
683
- # An attribute condition that uses existing rules to decide validation
684
- # and create obligations.
685
- class AttributeWithPermission < Attribute
686
- # E.g. privilege :read, attr_or_hash either :attribute or
687
- # { :attribute => :deeper_attribute }
688
- def initialize(privilege, attr_or_hash, context = nil)
689
- @privilege = privilege
690
- @context = context
691
- @attr_hash = attr_or_hash
692
- end
693
-
694
- def initialize_copy(from)
695
- @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
696
- end
697
-
698
- def validate?(attr_validator, object = nil, hash_or_attr = nil)
699
- object ||= attr_validator.object
700
- hash_or_attr ||= @attr_hash
701
- return false unless object
702
-
703
- case hash_or_attr
704
- when Symbol
705
- attr_value = object_attribute_value(object, hash_or_attr)
706
- case attr_value
707
- when nil
708
- raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
709
- when Enumerable
710
- attr_value.any? do |inner_value|
711
- attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
712
- end
713
- else
714
- attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
715
- end
716
- when Hash
717
- hash_or_attr.all? do |attr, sub_hash|
718
- attr_value = object_attribute_value(object, attr)
719
- if attr_value == nil
720
- raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
721
- elsif attr_value.is_a?(Enumerable)
722
- attr_value.any? do |inner_value|
723
- validate?(attr_validator, inner_value, sub_hash)
724
- end
725
- else
726
- validate?(attr_validator, attr_value, sub_hash)
727
- end
728
- end
729
- when NilClass
730
- attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
731
- else
732
- raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
733
- end
734
- end
735
-
736
- # may return an array of obligations to be OR'ed
737
- def obligation(attr_validator, hash_or_attr = nil, path = [])
738
- hash_or_attr ||= @attr_hash
739
- case hash_or_attr
740
- when Symbol
741
- @context ||= begin
742
- rule_model = attr_validator.context.to_s.classify.constantize
743
- context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
744
- if context_reflection.klass.respond_to?(:decl_auth_context)
745
- context_reflection.klass.decl_auth_context
746
- else
747
- context_reflection.klass.name.tableize.to_sym
748
- end
749
- rescue # missing model, reflections
750
- hash_or_attr.to_s.pluralize.to_sym
751
- end
752
-
753
- obligations = attr_validator.engine.obligations(@privilege,
754
- :context => @context,
755
- :user => attr_validator.user)
756
-
757
- obligations.collect {|obl| {hash_or_attr => obl} }
758
- when Hash
759
- obligations_array_attrs = []
760
- obligations =
761
- hash_or_attr.inject({}) do |all, pair|
762
- attr, sub_hash = pair
763
- all[attr] = obligation(attr_validator, sub_hash, path + [attr])
764
- if all[attr].length > 1
765
- obligations_array_attrs << attr
766
- else
767
- all[attr] = all[attr].first
768
- end
769
- all
770
- end
771
- obligations = [obligations]
772
- obligations_array_attrs.each do |attr|
773
- next_array_size = obligations.first[attr].length
774
- obligations = obligations.collect do |obls|
775
- (0...next_array_size).collect do |idx|
776
- obls_wo_array = obls.clone
777
- obls_wo_array[attr] = obls_wo_array[attr][idx]
778
- obls_wo_array
779
- end
780
- end.flatten
781
- end
782
- obligations
783
- when NilClass
784
- attr_validator.engine.obligations(@privilege,
785
- :context => attr_validator.context,
786
- :user => attr_validator.user)
787
- else
788
- raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
789
- end
790
- end
791
-
792
- def to_long_s
793
- "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
794
- end
795
-
796
- def self.reflection_for_path(parent_model, path)
797
- reflection = path.empty? ? parent_model : begin
798
- parent = reflection_for_path(parent_model, path[0..-2])
799
- if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
800
- parent.klass.reflect_on_association(path.last)
801
- else
802
- parent.reflect_on_association(path.last)
803
- end
804
- rescue
805
- parent.reflect_on_association(path.last)
806
- end
807
- raise "invalid path #{path.inspect}" if reflection.nil?
808
- reflection
809
- end
810
- end
811
-
812
- # Represents a pseudo-user to facilitate anonymous users in applications
813
- class AnonymousUser
814
- attr_reader :role_symbols
815
- def initialize(roles = [Authorization.default_role])
816
- @role_symbols = roles
817
- end
818
-
819
- def id; end
820
- end
821
- end
1
+ # Authorization
2
+ require File.dirname(__FILE__) + '/reader.rb'
3
+ require "set"
4
+ require "forwardable"
5
+
6
+ module Authorization
7
+ # An exception raised if anything goes wrong in the Authorization realm
8
+ class AuthorizationError < StandardError ; end
9
+ # NotAuthorized is raised if the current user is not allowed to perform
10
+ # the given operation possibly on a specific object.
11
+ class NotAuthorized < AuthorizationError ; end
12
+ # AttributeAuthorizationError is more specific than NotAuthorized, signaling
13
+ # that the access was denied on the grounds of attribute conditions.
14
+ class AttributeAuthorizationError < NotAuthorized ; end
15
+ # AuthorizationUsageError is used whenever a situation is encountered
16
+ # in which the application misused the plugin. That is, if, e.g.,
17
+ # authorization rules may not be evaluated.
18
+ class AuthorizationUsageError < AuthorizationError ; end
19
+ # NilAttributeValueError is raised by Attribute#validate? when it hits a nil attribute value.
20
+ # The exception is raised to ensure that the entire rule is invalidated.
21
+ class NilAttributeValueError < AuthorizationError; end
22
+
23
+ AUTH_DSL_FILES = [Pathname.new(Rails.root || '').join("config", "authorization_rules.rb").to_s] unless defined? AUTH_DSL_FILES
24
+
25
+ # Controller-independent method for retrieving the current user.
26
+ # Needed for model security where the current controller is not available.
27
+ def self.current_user
28
+ Thread.current["current_user"] || guest_user
29
+ end
30
+
31
+ # Controller-independent method for setting the current user.
32
+ def self.current_user=(user)
33
+ Thread.current["current_user"] = user
34
+ end
35
+
36
+ def self.guest_user
37
+ @@guest_user ||= AnonymousUser.new
38
+ end
39
+
40
+ def self.non_guest_current_user
41
+ current_user unless current_user.is_a?(AnonymousUser)
42
+ end
43
+
44
+ # For use in test cases only
45
+ def self.ignore_access_control(state = nil) # :nodoc:
46
+ Thread.current["ignore_access_control"] = state unless state.nil?
47
+ Thread.current["ignore_access_control"] || false
48
+ end
49
+
50
+ @@dot_path = "dot"
51
+ def self.dot_path
52
+ @@dot_path
53
+ end
54
+
55
+ def self.dot_path=(path)
56
+ @@dot_path = path
57
+ end
58
+
59
+ @@default_role = :guest
60
+ def self.default_role
61
+ @@default_role
62
+ end
63
+
64
+ def self.default_role=(role)
65
+ @@default_role = role.to_sym
66
+ end
67
+
68
+ def self.is_a_association_proxy?(object)
69
+ object.respond_to?(:proxy_association)
70
+ end
71
+
72
+ # Authorization::Engine implements the reference monitor. It may be used
73
+ # for querying the permission and retrieving obligations under which
74
+ # a certain privilege is granted for the current user.
75
+ #
76
+ class Engine
77
+ extend Forwardable
78
+ attr_reader :reader
79
+
80
+ def_delegators :@reader, :auth_rules_reader, :privileges_reader, :load, :load!
81
+ def_delegators :auth_rules_reader, :auth_rules, :roles, :omnipotent_roles, :role_hierarchy, :role_titles, :role_descriptions
82
+ def_delegators :privileges_reader, :privileges, :privilege_hierarchy
83
+
84
+ # If +reader+ is not given, a new one is created with the default
85
+ # authorization configuration of +AUTH_DSL_FILES+. If given, may be either
86
+ # a Reader object or a path to a configuration file.
87
+ def initialize(reader = nil)
88
+ #@auth_rules = AuthorizationRuleSet.new reader.auth_rules_reader.auth_rules
89
+ @reader = Reader::DSLReader.factory(reader || AUTH_DSL_FILES)
90
+ end
91
+
92
+ def initialize_copy(from) # :nodoc:
93
+ @reader = from.reader.clone
94
+ end
95
+
96
+ # {[priv, ctx] => [priv, ...]}
97
+ def rev_priv_hierarchy
98
+ if @rev_priv_hierarchy.nil?
99
+ @rev_priv_hierarchy = {}
100
+ privilege_hierarchy.each do |key, value|
101
+ value.each do |val|
102
+ @rev_priv_hierarchy[val] ||= []
103
+ @rev_priv_hierarchy[val] << key
104
+ end
105
+ end
106
+ end
107
+ @rev_priv_hierarchy
108
+ end
109
+
110
+ # {[priv, ctx] => [priv, ...]}
111
+ def rev_role_hierarchy
112
+ if @rev_role_hierarchy.nil?
113
+ @rev_role_hierarchy = {}
114
+ role_hierarchy.each do |higher_role, lower_roles|
115
+ lower_roles.each do |role|
116
+ (@rev_role_hierarchy[role] ||= []) << higher_role
117
+ end
118
+ end
119
+ end
120
+ @rev_role_hierarchy
121
+ end
122
+
123
+ # Returns true if privilege is met by the current user. Raises
124
+ # AuthorizationError otherwise. +privilege+ may be given with or
125
+ # without context. In the latter case, the :+context+ option is
126
+ # required.
127
+ #
128
+ # Options:
129
+ # [:+context+]
130
+ # The context part of the privilege.
131
+ # Defaults either to the tableized +class_name+ of the given :+object+, if given.
132
+ # That is, :+users+ for :+object+ of type User.
133
+ # Raises AuthorizationUsageError if context is missing and not to be inferred.
134
+ # [:+object+] An context object to test attribute checks against.
135
+ # [:+skip_attribute_test+]
136
+ # Skips those attribute checks in the
137
+ # authorization rules. Defaults to false.
138
+ # [:+user+]
139
+ # The user to check the authorization for.
140
+ # Defaults to Authorization#current_user.
141
+ # [:+bang+]
142
+ # Should NotAuthorized exceptions be raised
143
+ # Defaults to true.
144
+ #
145
+ def permit!(privilege, options = {})
146
+ return true if Authorization.ignore_access_control
147
+ options = {
148
+ :object => nil,
149
+ :skip_attribute_test => false,
150
+ :context => nil,
151
+ :bang => true
152
+ }.merge(options)
153
+
154
+ # Make sure we're handling all privileges as symbols.
155
+ privilege = privilege.is_a?( Array ) ?
156
+ privilege.flatten.collect { |priv| priv.to_sym } :
157
+ privilege.to_sym
158
+
159
+ #
160
+ # If the object responds to :proxy_reflection, we're probably working with
161
+ # an association proxy. Use 'new' to leverage ActiveRecord's builder
162
+ # functionality to obtain an object against which we can check permissions.
163
+ #
164
+ # Example: permit!( :edit, :object => user.posts )
165
+ #
166
+ if Authorization.is_a_association_proxy?(options[:object]) && options[:object].respond_to?(:new)
167
+ options[:object] = options[:object].where(nil).new
168
+ end
169
+
170
+ options[:context] ||= options[:object] && (
171
+ options[:object].class.respond_to?(:decl_auth_context) ?
172
+ options[:object].class.decl_auth_context :
173
+ options[:object].class.name.tableize.to_sym
174
+ ) rescue NoMethodError
175
+
176
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
177
+
178
+ return true if roles.is_a?(Hash) && !(roles.keys & omnipotent_roles).empty?
179
+
180
+ # find a authorization rule that matches for at least one of the roles and
181
+ # at least one of the given privileges
182
+ attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
183
+ rules = matching_auth_rules(roles, privileges, options[:context])
184
+
185
+ # Test each rule in turn to see whether any one of them is satisfied.
186
+ rules.each do |rule|
187
+ return true if rule.validate?(attr_validator, options[:skip_attribute_test])
188
+ end
189
+
190
+ if options[:bang]
191
+ if rules.empty?
192
+ raise NotAuthorized, "No matching rules found for #{privilege} for User with id #{user.try(:id)} " +
193
+ "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
194
+ "context #{options[:context].inspect})."
195
+ else
196
+ raise AttributeAuthorizationError, "#{privilege} not allowed for User with id #{user.try(:id)} on #{(options[:object] || options[:context]).inspect}."
197
+ end
198
+ else
199
+ false
200
+ end
201
+ end
202
+
203
+ # Calls permit! but doesn't raise authorization errors. If no exception is
204
+ # raised, permit? returns true and yields to the optional block.
205
+ def permit?(privilege, options = {}) # :yields:
206
+ if permit!(privilege, options.merge(:bang=> false))
207
+ yield if block_given?
208
+ true
209
+ else
210
+ false
211
+ end
212
+ end
213
+
214
+ # Returns the obligations to be met by the current user for the given
215
+ # privilege as an array of obligation hashes in form of
216
+ # [{:object_attribute => obligation_value, ...}, ...]
217
+ # where +obligation_value+ is either (recursively) another obligation hash
218
+ # or a value spec, such as
219
+ # [operator, literal_value]
220
+ # The obligation hashes in the array should be OR'ed, conditions inside
221
+ # the hashes AND'ed.
222
+ #
223
+ # Example
224
+ # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
225
+ #
226
+ # Options
227
+ # [:+context+] See permit!
228
+ # [:+user+] See permit!
229
+ #
230
+ def obligations(privilege, options = {})
231
+ options = {:context => nil}.merge(options)
232
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
233
+
234
+ permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
235
+
236
+ return [] if roles.is_a?(Hash) && !(roles.keys & omnipotent_roles).empty?
237
+
238
+ attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
239
+ matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
240
+ rule.obligations(attr_validator)
241
+ end.flatten
242
+ end
243
+
244
+ # Returns the description for the given role. The description may be
245
+ # specified with the authorization rules. Returns +nil+ if none was
246
+ # given.
247
+ def description_for(role)
248
+ role_descriptions[role]
249
+ end
250
+
251
+ # Returns the title for the given role. The title may be
252
+ # specified with the authorization rules. Returns +nil+ if none was
253
+ # given.
254
+ def title_for(role)
255
+ role_titles[role]
256
+ end
257
+
258
+ # Returns the role symbols of the given user.
259
+ def roles_for(user)
260
+ user ||= Authorization.current_user
261
+ raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.try(:id)})" \
262
+ if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
263
+
264
+ Rails.logger.info("The use of user.roles is deprecated. Please add a method " +
265
+ "role_symbols to your User model.") if defined?(Rails) and Rails.respond_to?(:logger) and !user.respond_to?(:role_symbols)
266
+
267
+ roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
268
+
269
+ raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
270
+ "doesn't return an Array of Symbols (#{roles.inspect})" \
271
+ if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
272
+
273
+ (roles.empty? ? [Authorization.default_role] : roles)
274
+ end
275
+
276
+ # Returns the role symbols and inherritted role symbols for the given user
277
+ def roles_with_hierarchy_for(user)
278
+ flatten_roles(roles_for(user))
279
+ end
280
+
281
+ def self.development_reload?
282
+ if Rails.env.development?
283
+ mod_time = AUTH_DSL_FILES.map do |m|
284
+ begin
285
+ File.mtime(m)
286
+ rescue
287
+ Time.at(0)
288
+ end
289
+ end.flatten.max
290
+ @@auth_dsl_last_modified ||= mod_time
291
+ if mod_time > @@auth_dsl_last_modified
292
+ @@auth_dsl_last_modified = mod_time
293
+ return true
294
+ end
295
+ end
296
+ end
297
+
298
+ # Returns an instance of Engine, which is created if there isn't one
299
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
300
+ # a new instance is always created.
301
+ def self.instance(dsl_file = nil)
302
+ if dsl_file or development_reload?
303
+ @@instance = new(dsl_file)
304
+ else
305
+ @@instance ||= new
306
+ end
307
+ end
308
+
309
+ class AttributeValidator # :nodoc:
310
+ attr_reader :user, :object, :engine, :context, :privilege
311
+ def initialize(engine, user, object = nil, privilege = nil, context = nil)
312
+ @engine = engine
313
+ @user = user
314
+ @object = object
315
+ @privilege = privilege
316
+ @context = context
317
+ end
318
+
319
+ def evaluate(value_block)
320
+ # TODO cache?
321
+ if value_block.is_a? Proc
322
+ instance_eval(&value_block)
323
+ else
324
+ value_block
325
+ end
326
+ end
327
+ end
328
+
329
+ private
330
+
331
+ def user_roles_privleges_from_options(privilege, options)
332
+ options = {
333
+ :user => nil,
334
+ :context => nil,
335
+ :user_roles => nil
336
+ }.merge(options)
337
+ user = options[:user] || Authorization.current_user
338
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
339
+
340
+ raise AuthorizationUsageError, "No user object given for user id (#{user.try(:id)}) or " +
341
+ "set through Authorization.current_user" unless user
342
+
343
+ roles = options[:user_roles] || flatten_roles(roles_for(user))
344
+ privileges = flatten_privileges privileges, options[:context]
345
+ [user, roles, privileges]
346
+ end
347
+
348
+ def flatten_roles(roles)
349
+ # TODO: caching?
350
+ hierarchy = role_hierarchy
351
+ flattened_roles = {}
352
+ roles.each do |role|
353
+ flattened_roles[role] = true
354
+ if (hierarchy_for_role = hierarchy[role])
355
+ hierarchy_for_role.each do |r|
356
+ flattened_roles[r] = true
357
+ end
358
+ end
359
+ end
360
+ flattened_roles
361
+ end
362
+
363
+ # Returns the privilege hierarchy flattened for given privileges in context.
364
+ def flatten_privileges(privileges, context = nil)
365
+ # TODO: caching?
366
+ raise AuthorizationUsageError, 'No context given or inferable from object' unless context
367
+ hierarchy = rev_priv_hierarchy
368
+
369
+ flattened_privileges = privileges.clone
370
+ flattened_privileges.each do |priv|
371
+ flattened_privileges.concat(hierarchy[[priv, nil]]) if hierarchy[[priv, nil]]
372
+ flattened_privileges.concat(hierarchy[[priv, context]]) if hierarchy[[priv, context]]
373
+ end
374
+ flattened_privileges.uniq
375
+ end
376
+
377
+ def matching_auth_rules(roles, privileges, context)
378
+ auth_rules.matching(roles, privileges, context)
379
+ end
380
+ end
381
+
382
+
383
+ class AuthorizationRuleSet
384
+ include Enumerable
385
+ extend Forwardable
386
+ def_delegators :@rules, :each, :length, :[]
387
+
388
+ def initialize(rules = [])
389
+ @rules = rules.clone
390
+ reset!
391
+ end
392
+
393
+ def initialize_copy(source)
394
+ @rules = @rules.collect {|rule| rule.clone}
395
+ reset!
396
+ end
397
+
398
+ def matching(roles, privileges, context)
399
+ rules = cached_auth_rules[context] || []
400
+ rules.select do |rule|
401
+ rule.matches? roles, privileges, context
402
+ end
403
+ end
404
+
405
+ def delete(rule)
406
+ @rules.delete rule
407
+ reset!
408
+ end
409
+
410
+ def <<(rule)
411
+ @rules << rule
412
+ reset!
413
+ end
414
+
415
+ def each(&block)
416
+ @rules.each &block
417
+ end
418
+
419
+ private
420
+ def reset!
421
+ @cached_auth_rules =nil
422
+ end
423
+
424
+ def cached_auth_rules
425
+ return @cached_auth_rules if @cached_auth_rules
426
+ @cached_auth_rules = {}
427
+ @rules.each do |rule|
428
+ rule.contexts.each do |context|
429
+ @cached_auth_rules[context] ||= []
430
+ @cached_auth_rules[context] << rule
431
+ end
432
+ end
433
+ @cached_auth_rules
434
+ end
435
+ end
436
+
437
+ class AuthorizationRule
438
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
439
+ :source_file, :source_line
440
+
441
+ def initialize(role, privileges = [], contexts = nil, join_operator = :or,
442
+ options = {})
443
+ @role = role
444
+ @privileges = Set.new(privileges)
445
+ @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
446
+ @join_operator = join_operator
447
+ @attributes = []
448
+ @source_file = options[:source_file]
449
+ @source_line = options[:source_line]
450
+ end
451
+
452
+ def initialize_copy(from)
453
+ @privileges = @privileges.clone
454
+ @contexts = @contexts.clone
455
+ @attributes = @attributes.collect {|attribute| attribute.clone }
456
+ end
457
+
458
+ def append_privileges(privs)
459
+ @privileges.merge(privs)
460
+ end
461
+
462
+ def append_attribute(attribute)
463
+ @attributes << attribute
464
+ end
465
+
466
+ def matches?(roles, privs, context = nil)
467
+ roles = Hash[[*roles].map { |r| [r, true] }] unless roles.is_a?(Hash)
468
+ @contexts.include?(context) && roles.include?(@role) && privs.any? { |priv| @privileges.include?(priv) }
469
+ end
470
+
471
+ def validate?(attr_validator, skip_attribute = false)
472
+ skip_attribute or @attributes.empty? or
473
+ @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
474
+ begin
475
+ attr.validate?(attr_validator)
476
+ rescue NilAttributeValueError => e
477
+ nil # Bumping up against a nil attribute value flunks the rule.
478
+ end
479
+ end
480
+ end
481
+
482
+ def obligations(attr_validator)
483
+ exceptions = []
484
+ obligations = @attributes.collect do |attr|
485
+ begin
486
+ attr.obligation(attr_validator)
487
+ rescue NotAuthorized => e
488
+ exceptions << e
489
+ nil
490
+ end
491
+ end
492
+
493
+ if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
494
+ raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
495
+ end
496
+
497
+ if @join_operator == :and and !obligations.empty?
498
+ # cross product of OR'ed obligations in arrays
499
+ arrayed_obligations = obligations.map {|obligation| obligation.is_a?(Hash) ? [obligation] : obligation}
500
+ merged_obligations = arrayed_obligations.first
501
+ arrayed_obligations[1..-1].each do |inner_obligations|
502
+ previous_merged_obligations = merged_obligations
503
+ merged_obligations = inner_obligations.collect do |inner_obligation|
504
+ previous_merged_obligations.collect do |merged_obligation|
505
+ merged_obligation.deep_merge(inner_obligation)
506
+ end
507
+ end.flatten
508
+ end
509
+ obligations = merged_obligations
510
+ else
511
+ obligations = obligations.flatten.compact
512
+ end
513
+ obligations.empty? ? [{}] : obligations
514
+ end
515
+
516
+ def to_long_s
517
+ attributes.collect {|attr| attr.to_long_s } * "; "
518
+ end
519
+ end
520
+
521
+ class Attribute
522
+ # attr_conditions_hash of form
523
+ # { :object_attribute => [operator, value_block], ... }
524
+ # { :object_attribute => { :attr => ... } }
525
+ def initialize(conditions_hash)
526
+ @conditions_hash = conditions_hash
527
+ end
528
+
529
+ def initialize_copy(from)
530
+ @conditions_hash = deep_hash_clone(@conditions_hash)
531
+ end
532
+
533
+ def validate?(attr_validator, object = nil, hash = nil)
534
+ object ||= attr_validator.object
535
+ return false unless object
536
+
537
+ if Authorization.is_a_association_proxy?(object) && object.respond_to?(:empty?)
538
+ return false if object.empty?
539
+ object.each do |member|
540
+ return true if validate?(attr_validator, member, hash)
541
+ end
542
+ return false
543
+ end
544
+
545
+ (hash || @conditions_hash).all? do |attr, value|
546
+ attr_value = object_attribute_value(object, attr)
547
+ if value.is_a?(Hash)
548
+ if attr_value.is_a?(Enumerable)
549
+ attr_value.any? do |inner_value|
550
+ validate?(attr_validator, inner_value, value)
551
+ end
552
+ elsif attr_value == nil
553
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
554
+ else
555
+ validate?(attr_validator, attr_value, value)
556
+ end
557
+ elsif value.is_a?(Array) and value.length == 2 and value.first.is_a?(Symbol)
558
+ evaluated = if value[1].is_a?(Proc)
559
+ attr_validator.evaluate(value[1])
560
+ else
561
+ value[1]
562
+ end
563
+ case value[0]
564
+ when :is
565
+ attr_value == evaluated
566
+ when :is_not
567
+ attr_value != evaluated
568
+ when :contains
569
+ begin
570
+ attr_value.include?(evaluated)
571
+ rescue NoMethodError => e
572
+ raise AuthorizationUsageError, "Operator contains requires a " +
573
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
574
+ "contains #{evaluated.inspect}: #{e}"
575
+ end
576
+ when :does_not_contain
577
+ begin
578
+ !attr_value.include?(evaluated)
579
+ rescue NoMethodError => e
580
+ raise AuthorizationUsageError, "Operator does_not_contain requires a " +
581
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
582
+ "does_not_contain #{evaluated.inspect}: #{e}"
583
+ end
584
+ when :intersects_with
585
+ begin
586
+ !(evaluated.to_set & attr_value.to_set).empty?
587
+ rescue NoMethodError => e
588
+ raise AuthorizationUsageError, "Operator intersects_with requires " +
589
+ "subclasses of Enumerable, got: #{attr_value.inspect} " +
590
+ "intersects_with #{evaluated.inspect}: #{e}"
591
+ end
592
+ when :is_in
593
+ begin
594
+ evaluated.include?(attr_value)
595
+ rescue NoMethodError => e
596
+ raise AuthorizationUsageError, "Operator is_in requires a " +
597
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
598
+ "is_in #{evaluated.inspect}: #{e}"
599
+ end
600
+ when :is_not_in
601
+ begin
602
+ !evaluated.include?(attr_value)
603
+ rescue NoMethodError => e
604
+ raise AuthorizationUsageError, "Operator is_not_in requires a " +
605
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
606
+ "is_not_in #{evaluated.inspect}: #{e}"
607
+ end
608
+ when :lt
609
+ attr_value && attr_value < evaluated
610
+ when :lte
611
+ attr_value && attr_value <= evaluated
612
+ when :gt
613
+ attr_value && attr_value > evaluated
614
+ when :gte
615
+ attr_value && attr_value >= evaluated
616
+ when :id_in_scope
617
+ evaluated.exists?(attr_value)
618
+ else
619
+ raise AuthorizationError, "Unknown operator #{value[0]}"
620
+ end
621
+ else
622
+ raise AuthorizationError, "Wrong conditions hash format"
623
+ end
624
+ end
625
+ end
626
+
627
+ # resolves all the values in condition_hash
628
+ def obligation(attr_validator, hash = nil)
629
+ hash = (hash || @conditions_hash).clone
630
+ hash.each do |attr, value|
631
+ if value.is_a?(Hash)
632
+ hash[attr] = obligation(attr_validator, value)
633
+ elsif value.is_a?(Array) and value.length == 2
634
+ hash[attr] = [value[0], attr_validator.evaluate(value[1])]
635
+ else
636
+ raise AuthorizationError, "Wrong conditions hash format"
637
+ end
638
+ end
639
+ hash
640
+ end
641
+
642
+ def to_long_s(hash = nil)
643
+ if hash
644
+ hash.inject({}) do |memo, key_val|
645
+ key, val = key_val
646
+ memo[key] = case val
647
+ when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
648
+ when Hash then to_long_s(val)
649
+ end
650
+ memo
651
+ end
652
+ else
653
+ "if_attribute #{to_long_s(@conditions_hash).inspect}"
654
+ end
655
+ end
656
+
657
+ protected
658
+ def object_attribute_value(object, attr)
659
+ begin
660
+ object.send(attr)
661
+ rescue ArgumentError, NoMethodError => e
662
+ raise AuthorizationUsageError, "Error occurred while validating attribute ##{attr} on #{object.inspect}: #{e}.\n" +
663
+ "Please check your authorization rules and ensure the attribute is correctly spelled and \n" +
664
+ "corresponds to a method on the model you are authorizing for."
665
+ end
666
+ end
667
+
668
+ def deep_hash_clone(hash)
669
+ hash.inject({}) do |memo, (key, val)|
670
+ memo[key] = case val
671
+ when Hash
672
+ deep_hash_clone(val)
673
+ when NilClass, Symbol
674
+ val
675
+ else
676
+ val.clone
677
+ end
678
+ memo
679
+ end
680
+ end
681
+ end
682
+
683
+ # An attribute condition that uses existing rules to decide validation
684
+ # and create obligations.
685
+ class AttributeWithPermission < Attribute
686
+ # E.g. privilege :read, attr_or_hash either :attribute or
687
+ # { :attribute => :deeper_attribute }
688
+ def initialize(privilege, attr_or_hash, context = nil)
689
+ @privilege = privilege
690
+ @context = context
691
+ @attr_hash = attr_or_hash
692
+ end
693
+
694
+ def initialize_copy(from)
695
+ @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
696
+ end
697
+
698
+ def validate?(attr_validator, object = nil, hash_or_attr = nil)
699
+ object ||= attr_validator.object
700
+ hash_or_attr ||= @attr_hash
701
+ return false unless object
702
+
703
+ case hash_or_attr
704
+ when Symbol
705
+ attr_value = object_attribute_value(object, hash_or_attr)
706
+ case attr_value
707
+ when nil
708
+ raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
709
+ when Enumerable
710
+ attr_value.any? do |inner_value|
711
+ attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
712
+ end
713
+ else
714
+ attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
715
+ end
716
+ when Hash
717
+ hash_or_attr.all? do |attr, sub_hash|
718
+ attr_value = object_attribute_value(object, attr)
719
+ if attr_value == nil
720
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
721
+ elsif attr_value.is_a?(Enumerable)
722
+ attr_value.any? do |inner_value|
723
+ validate?(attr_validator, inner_value, sub_hash)
724
+ end
725
+ else
726
+ validate?(attr_validator, attr_value, sub_hash)
727
+ end
728
+ end
729
+ when NilClass
730
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
731
+ else
732
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
733
+ end
734
+ end
735
+
736
+ # may return an array of obligations to be OR'ed
737
+ def obligation(attr_validator, hash_or_attr = nil, path = [])
738
+ hash_or_attr ||= @attr_hash
739
+ case hash_or_attr
740
+ when Symbol
741
+ @context ||= begin
742
+ rule_model = attr_validator.context.to_s.classify.constantize
743
+ context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
744
+ if context_reflection.klass.respond_to?(:decl_auth_context)
745
+ context_reflection.klass.decl_auth_context
746
+ else
747
+ context_reflection.klass.name.tableize.to_sym
748
+ end
749
+ rescue # missing model, reflections
750
+ hash_or_attr.to_s.pluralize.to_sym
751
+ end
752
+
753
+ obligations = attr_validator.engine.obligations(@privilege,
754
+ :context => @context,
755
+ :user => attr_validator.user)
756
+
757
+ obligations.collect {|obl| {hash_or_attr => obl} }
758
+ when Hash
759
+ obligations_array_attrs = []
760
+ obligations =
761
+ hash_or_attr.inject({}) do |all, pair|
762
+ attr, sub_hash = pair
763
+ all[attr] = obligation(attr_validator, sub_hash, path + [attr])
764
+ if all[attr].length > 1
765
+ obligations_array_attrs << attr
766
+ else
767
+ all[attr] = all[attr].first
768
+ end
769
+ all
770
+ end
771
+ obligations = [obligations]
772
+ obligations_array_attrs.each do |attr|
773
+ next_array_size = obligations.first[attr].length
774
+ obligations = obligations.collect do |obls|
775
+ (0...next_array_size).collect do |idx|
776
+ obls_wo_array = obls.clone
777
+ obls_wo_array[attr] = obls_wo_array[attr][idx]
778
+ obls_wo_array
779
+ end
780
+ end.flatten
781
+ end
782
+ obligations
783
+ when NilClass
784
+ attr_validator.engine.obligations(@privilege,
785
+ :context => attr_validator.context,
786
+ :user => attr_validator.user)
787
+ else
788
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
789
+ end
790
+ end
791
+
792
+ def to_long_s
793
+ "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
794
+ end
795
+
796
+ def self.reflection_for_path(parent_model, path)
797
+ reflection = path.empty? ? parent_model : begin
798
+ parent = reflection_for_path(parent_model, path[0..-2])
799
+ if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
800
+ parent.klass.reflect_on_association(path.last)
801
+ else
802
+ parent.reflect_on_association(path.last)
803
+ end
804
+ rescue
805
+ parent.reflect_on_association(path.last)
806
+ end
807
+ raise "invalid path #{path.inspect}" if reflection.nil?
808
+ reflection
809
+ end
810
+ end
811
+
812
+ # Represents a pseudo-user to facilitate anonymous users in applications
813
+ class AnonymousUser
814
+ attr_reader :role_symbols
815
+ def initialize(roles = [Authorization.default_role])
816
+ @role_symbols = roles
817
+ end
818
+
819
+ def id; end
820
+ end
821
+ end