ae_declarative_authorization 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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