uhees-declarative_authorization 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG +77 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +490 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +235 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +183 -0
  8. data/app/views/authorization_rules/_change.erb +49 -0
  9. data/app/views/authorization_rules/_show_graph.erb +37 -0
  10. data/app/views/authorization_rules/_suggestion.erb +9 -0
  11. data/app/views/authorization_rules/_suggestions.erb +24 -0
  12. data/app/views/authorization_rules/change.html.erb +124 -0
  13. data/app/views/authorization_rules/graph.dot.erb +68 -0
  14. data/app/views/authorization_rules/graph.html.erb +40 -0
  15. data/app/views/authorization_rules/index.html.erb +17 -0
  16. data/app/views/authorization_usages/index.html.erb +36 -0
  17. data/authorization_rules.dist.rb +20 -0
  18. data/config/routes.rb +7 -0
  19. data/garlic_example.rb +20 -0
  20. data/init.rb +5 -0
  21. data/lib/declarative_authorization.rb +15 -0
  22. data/lib/declarative_authorization/authorization.rb +630 -0
  23. data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
  24. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  25. data/lib/declarative_authorization/development_support/change_supporter.rb +578 -0
  26. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  27. data/lib/declarative_authorization/helper.rb +60 -0
  28. data/lib/declarative_authorization/in_controller.rb +367 -0
  29. data/lib/declarative_authorization/in_model.rb +150 -0
  30. data/lib/declarative_authorization/maintenance.rb +188 -0
  31. data/lib/declarative_authorization/obligation_scope.rb +297 -0
  32. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  33. data/lib/declarative_authorization/reader.rb +438 -0
  34. data/test/authorization_test.rb +823 -0
  35. data/test/controller_test.rb +418 -0
  36. data/test/dsl_reader_test.rb +157 -0
  37. data/test/helper_test.rb +154 -0
  38. data/test/maintenance_test.rb +41 -0
  39. data/test/model_test.rb +1171 -0
  40. data/test/schema.sql +53 -0
  41. data/test/test_helper.rb +103 -0
  42. metadata +104 -0
data/garlic_example.rb ADDED
@@ -0,0 +1,20 @@
1
+ garlic do
2
+ repo 'rails', :url => 'git://github.com/rails/rails'#, :local => "~/dev/vendor/rails"
3
+ repo 'declarative_authorization', :path => '.'
4
+
5
+ target 'edge'
6
+ target '2.1-stable', :branch => 'origin/2-1-stable'
7
+ target '2.2.0-RC1', :tag => 'v2.2.0'
8
+
9
+ all_targets do
10
+ prepare do
11
+ plugin 'declarative_authorization', :clone => true
12
+ end
13
+
14
+ run do
15
+ cd "vendor/plugins/declarative_authorization" do
16
+ sh "rake"
17
+ end
18
+ end
19
+ end
20
+ end
data/init.rb ADDED
@@ -0,0 +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
@@ -0,0 +1,15 @@
1
+ require File.join(%w{declarative_authorization rails_legacy})
2
+ require File.join(%w{declarative_authorization helper})
3
+ require File.join(%w{declarative_authorization in_controller})
4
+ require File.join(%w{declarative_authorization in_model})
5
+ require File.join(%w{declarative_authorization obligation_scope})
6
+
7
+ min_rails_version = "2.1.0"
8
+ if Rails::VERSION::STRING < min_rails_version
9
+ raise "declarative_authorization requires Rails #{min_rails_version}. You are using #{Rails::VERSION::STRING}."
10
+ end
11
+
12
+ ActionController::Base.send :include, Authorization::AuthorizationInController
13
+ ActionController::Base.helper Authorization::AuthorizationHelper
14
+
15
+ ActiveRecord::Base.send :include, Authorization::AuthorizationInModel
@@ -0,0 +1,630 @@
1
+ # Authorization
2
+ require File.dirname(__FILE__) + '/reader.rb'
3
+ require "set"
4
+
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, signalling
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_FILE = "#{RAILS_ROOT}/config/authorization_rules.rb"
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"] || GuestUser.new
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
+ @@ignore_access_control = false
37
+ # For use in test cases only
38
+ def self.ignore_access_control (state = nil) # :nodoc:
39
+ false
40
+ end
41
+
42
+ def self.activate_authorization_rules_browser? # :nodoc:
43
+ ::RAILS_ENV == 'development'
44
+ end
45
+
46
+ @@dot_path = "dot"
47
+ def self.dot_path
48
+ @@dot_path
49
+ end
50
+
51
+ def self.dot_path= (path)
52
+ @@dot_path = path
53
+ end
54
+
55
+ # Authorization::Engine implements the reference monitor. It may be used
56
+ # for querying the permission and retrieving obligations under which
57
+ # a certain privilege is granted for the current user.
58
+ #
59
+ class Engine
60
+ attr_reader :roles, :role_titles, :role_descriptions, :privileges,
61
+ :privilege_hierarchy, :auth_rules, :role_hierarchy, :rev_priv_hierarchy,
62
+ :rev_role_hierarchy
63
+
64
+ # If +reader+ is not given, a new one is created with the default
65
+ # authorization configuration of +AUTH_DSL_FILE+. If given, may be either
66
+ # a Reader object or a path to a configuration file.
67
+ def initialize (reader = nil)
68
+ if reader.nil?
69
+ begin
70
+ reader = Reader::DSLReader.load(AUTH_DSL_FILE)
71
+ rescue SystemCallError
72
+ reader = Reader::DSLReader.new
73
+ end
74
+ elsif reader.is_a?(String)
75
+ reader = Reader::DSLReader.load(reader)
76
+ end
77
+ @privileges = reader.privileges_reader.privileges
78
+ # {priv => [[priv, ctx],...]}
79
+ @privilege_hierarchy = reader.privileges_reader.privilege_hierarchy
80
+ @auth_rules = reader.auth_rules_reader.auth_rules
81
+ @roles = reader.auth_rules_reader.roles
82
+ @role_hierarchy = reader.auth_rules_reader.role_hierarchy
83
+
84
+ @role_titles = reader.auth_rules_reader.role_titles
85
+ @role_descriptions = reader.auth_rules_reader.role_descriptions
86
+ @reader = reader
87
+
88
+ # {[priv, ctx] => [priv, ...]}
89
+ @rev_priv_hierarchy = {}
90
+ @privilege_hierarchy.each do |key, value|
91
+ value.each do |val|
92
+ @rev_priv_hierarchy[val] ||= []
93
+ @rev_priv_hierarchy[val] << key
94
+ end
95
+ end
96
+ @rev_role_hierarchy = {}
97
+ @role_hierarchy.each do |higher_role, lower_roles|
98
+ lower_roles.each do |role|
99
+ (@rev_role_hierarchy[role] ||= []) << higher_role
100
+ end
101
+ end
102
+ end
103
+
104
+ def initialize_copy (from) # :nodoc:
105
+ [
106
+ :privileges, :privilege_hierarchy, :roles, :role_hierarchy, :role_titles,
107
+ :role_descriptions, :rev_priv_hierarchy, :rev_role_hierarchy
108
+ ].each {|attr| instance_variable_set(:"@#{attr}", from.send(attr).clone) }
109
+ @auth_rules = from.auth_rules.collect {|rule| rule.clone}
110
+ end
111
+
112
+ # Returns true if privilege is met by the current user. Raises
113
+ # AuthorizationError otherwise. +privilege+ may be given with or
114
+ # without context. In the latter case, the :+context+ option is
115
+ # required.
116
+ #
117
+ # Options:
118
+ # [:+context+]
119
+ # The context part of the privilege.
120
+ # Defaults either to the +table_name+ of the given :+object+, if given.
121
+ # That is, either :+users+ for :+object+ of type User.
122
+ # Raises AuthorizationUsageError if
123
+ # context is missing and not to be infered.
124
+ # [:+object+] An context object to test attribute checks against.
125
+ # [:+skip_attribute_test+]
126
+ # Skips those attribute checks in the
127
+ # authorization rules. Defaults to false.
128
+ # [:+user+]
129
+ # The user to check the authorization for.
130
+ # Defaults to Authorization#current_user.
131
+ #
132
+ def permit! (privilege, options = {})
133
+ return true if Authorization.ignore_access_control
134
+ options = {
135
+ :object => nil,
136
+ :skip_attribute_test => false,
137
+ :context => nil
138
+ }.merge(options)
139
+
140
+ # Make sure we're handling all privileges as symbols.
141
+ privilege = privilege.is_a?( Array ) ?
142
+ privilege.flatten.collect { |priv| priv.to_sym } :
143
+ privilege.to_sym
144
+
145
+ #
146
+ # If the object responds to :proxy_reflection, we're probably working with
147
+ # an association proxy. Use 'new' to leverage ActiveRecord's builder
148
+ # functionality to obtain an object against which we can check permissions.
149
+ #
150
+ # Example: permit!( :edit, :object => user.posts )
151
+ #
152
+ if options[:object].respond_to?( :proxy_reflection ) && options[:object].respond_to?( :new )
153
+ options[:object] = options[:object].new
154
+ end
155
+
156
+ options[:context] ||= options[:object] && options[:object].class.table_name.to_sym rescue NoMethodError
157
+
158
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
159
+
160
+ # find a authorization rule that matches for at least one of the roles and
161
+ # at least one of the given privileges
162
+ attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
163
+ rules = matching_auth_rules(roles, privileges, options[:context])
164
+ if rules.empty?
165
+ raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
166
+ "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
167
+ "context #{options[:context].inspect})."
168
+ end
169
+
170
+ # Test each rule in turn to see whether any one of them is satisfied.
171
+ unless rules.any? {|rule| rule.validate?(attr_validator, options[:skip_attribute_test])}
172
+ raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{(options[:object] || options[:context]).inspect}."
173
+ end
174
+ true
175
+ end
176
+
177
+ # Calls permit! but rescues the AuthorizationException and returns false
178
+ # instead. If no exception is raised, permit? returns true and yields
179
+ # to the optional block.
180
+ def permit? (privilege, options = {}, &block) # :yields:
181
+ permit!(privilege, options)
182
+ yield if block_given?
183
+ true
184
+ rescue NotAuthorized
185
+ false
186
+ end
187
+
188
+ # Returns the obligations to be met by the current user for the given
189
+ # privilege as an array of obligation hashes in form of
190
+ # [{:object_attribute => obligation_value, ...}, ...]
191
+ # where +obligation_value+ is either (recursively) another obligation hash
192
+ # or a value spec, such as
193
+ # [operator, literal_value]
194
+ # The obligation hashes in the array should be OR'ed, conditions inside
195
+ # the hashes AND'ed.
196
+ #
197
+ # Example
198
+ # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
199
+ #
200
+ # Options
201
+ # [:+context+] See permit!
202
+ # [:+user+] See permit!
203
+ #
204
+ def obligations (privilege, options = {})
205
+ options = {:context => nil}.merge(options)
206
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
207
+ attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
208
+ matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
209
+ rule.obligations(attr_validator)
210
+ end.flatten
211
+ end
212
+
213
+ # Returns the description for the given role. The description may be
214
+ # specified with the authorization rules. Returns +nil+ if none was
215
+ # given.
216
+ def description_for (role)
217
+ role_descriptions[role]
218
+ end
219
+
220
+ # Returns the title for the given role. The title may be
221
+ # specified with the authorization rules. Returns +nil+ if none was
222
+ # given.
223
+ def title_for (role)
224
+ role_titles[role]
225
+ end
226
+
227
+ # Returns the role symbols of the given user.
228
+ def roles_for (user)
229
+ raise AuthorizationUsageError, "User object doesn't respond to roles" \
230
+ if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
231
+
232
+ RAILS_DEFAULT_LOGGER.info("The use of user.roles is deprecated. Please add a method " +
233
+ "role_symbols to your User model.") if defined?(RAILS_DEFAULT_LOGGER) and !user.respond_to?(:role_symbols)
234
+
235
+ roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
236
+
237
+ raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
238
+ "doesn't return an Array of Symbols (#{roles.inspect})" \
239
+ if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
240
+
241
+ (roles.empty? ? [:guest] : roles)
242
+ end
243
+
244
+ # Returns the role symbols and inherritted role symbols for the given user
245
+ def roles_with_hierarchy_for(user)
246
+ flatten_roles(roles_for(user))
247
+ end
248
+
249
+ # Returns an instance of Engine, which is created if there isn't one
250
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
251
+ # a new instance is always created.
252
+ def self.instance (dsl_file = nil)
253
+ if dsl_file or ENV['RAILS_ENV'] == 'development'
254
+ @@instance = new(dsl_file)
255
+ else
256
+ @@instance ||= new
257
+ end
258
+ end
259
+
260
+ class AttributeValidator # :nodoc:
261
+ attr_reader :user, :object, :engine, :context, :privilege
262
+ def initialize (engine, user, object = nil, privilege = nil, context = nil)
263
+ @engine = engine
264
+ @user = user
265
+ @object = object
266
+ @privilege = privilege
267
+ @context = context
268
+ end
269
+
270
+ def evaluate (value_block)
271
+ # TODO cache?
272
+ instance_eval(&value_block)
273
+ end
274
+ end
275
+
276
+ private
277
+ def user_roles_privleges_from_options(privilege, options)
278
+ options = {
279
+ :user => nil,
280
+ :context => nil,
281
+ :user_roles => nil
282
+ }.merge(options)
283
+ user = options[:user] || Authorization.current_user
284
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
285
+
286
+ raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
287
+ "set through Authorization.current_user" unless user
288
+
289
+ roles = options[:user_roles] || flatten_roles(roles_for(user))
290
+ privileges = flatten_privileges privileges, options[:context]
291
+ [user, roles, privileges]
292
+ end
293
+
294
+ def flatten_roles (roles)
295
+ # TODO caching?
296
+ flattened_roles = roles.clone.to_a
297
+ flattened_roles.each do |role|
298
+ flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
299
+ end
300
+ end
301
+
302
+ # Returns the privilege hierarchy flattened for given privileges in context.
303
+ def flatten_privileges (privileges, context = nil)
304
+ # TODO caching?
305
+ #if context.nil?
306
+ # context = privileges.collect { |p| p.to_s.split('_') }.
307
+ # reject { |p_p| p_p.length < 2 }.
308
+ # collect { |p_p| (p_p[1..-1] * '_').to_sym }.first
309
+ # raise AuthorizationUsageError, "No context given or inferable from privileges #{privileges.inspect}" unless context
310
+ #end
311
+ raise AuthorizationUsageError, "No context given or inferable from object" unless context
312
+ #context_regex = Regexp.new "_#{context}$"
313
+ # TODO work with contextless privileges
314
+ #flattened_privileges = privileges.collect {|p| p.to_s.sub(context_regex, '')}
315
+ flattened_privileges = privileges.clone #collect {|p| p.to_s.end_with?(context.to_s) ?
316
+ # p : [p, "#{p}_#{context}".to_sym] }.flatten
317
+ flattened_privileges.each do |priv|
318
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
319
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
320
+ end
321
+ end
322
+
323
+ def matching_auth_rules (roles, privileges, context)
324
+ @auth_rules.select {|rule| rule.matches? roles, privileges, context}
325
+ end
326
+ end
327
+
328
+ class AuthorizationRule
329
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
330
+ :source_file, :source_line
331
+
332
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or,
333
+ options = {})
334
+ @role = role
335
+ @privileges = Set.new(privileges)
336
+ @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
337
+ @join_operator = join_operator
338
+ @attributes = []
339
+ @source_file = options[:source_file]
340
+ @source_line = options[:source_line]
341
+ end
342
+
343
+ def initialize_copy (from)
344
+ @privileges = @privileges.clone
345
+ @contexts = @contexts.clone
346
+ @attributes = @attributes.collect {|attribute| attribute.clone }
347
+ end
348
+
349
+ def append_privileges (privs)
350
+ @privileges.merge(privs)
351
+ end
352
+
353
+ def append_attribute (attribute)
354
+ @attributes << attribute
355
+ end
356
+
357
+ def matches? (roles, privs, context = nil)
358
+ roles = [roles] unless roles.is_a?(Array)
359
+ @contexts.include?(context) and roles.include?(@role) and
360
+ not (@privileges & privs).empty?
361
+ end
362
+
363
+ def validate? (attr_validator, skip_attribute = false)
364
+ skip_attribute or @attributes.empty? or
365
+ @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
366
+ begin
367
+ attr.validate?(attr_validator)
368
+ rescue NilAttributeValueError => e
369
+ nil # Bumping up against a nil attribute value flunks the rule.
370
+ end
371
+ end
372
+ end
373
+
374
+ def obligations (attr_validator)
375
+ obligations = @attributes.collect {|attr| attr.obligation(attr_validator) }.flatten
376
+ if @join_operator == :and and !obligations.empty?
377
+ merged_obligation = obligations.first
378
+ obligations[1..-1].each do |obligation|
379
+ merged_obligation = merged_obligation.deep_merge(obligation)
380
+ end
381
+ obligations = [merged_obligation]
382
+ end
383
+ obligations.empty? ? [{}] : obligations
384
+ end
385
+
386
+ def to_long_s
387
+ attributes.collect {|attr| attr.to_long_s } * "; "
388
+ end
389
+ end
390
+
391
+ class Attribute
392
+ # attr_conditions_hash of form
393
+ # { :object_attribute => [operator, value_block], ... }
394
+ # { :object_attribute => { :attr => ... } }
395
+ def initialize (conditions_hash)
396
+ @conditions_hash = conditions_hash
397
+ end
398
+
399
+ def initialize_copy (from)
400
+ @conditions_hash = deep_hash_clone(@conditions_hash)
401
+ end
402
+
403
+ def validate? (attr_validator, object = nil, hash = nil)
404
+ object ||= attr_validator.object
405
+ return false unless object
406
+
407
+ (hash || @conditions_hash).all? do |attr, value|
408
+ attr_value = object_attribute_value(object, attr)
409
+ if value.is_a?(Hash)
410
+ if attr_value.is_a?(Array)
411
+ raise AuthorizationUsageError, "Unable evaluate multiple attributes " +
412
+ "on a collection. Cannot use '=>' operator on #{attr.inspect} " +
413
+ "(#{attr_value.inspect}) for attributes #{value.inspect}."
414
+ elsif attr_value.nil?
415
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
416
+ end
417
+ validate?(attr_validator, attr_value, value)
418
+ elsif value.is_a?(Array) and value.length == 2
419
+ evaluated = if value[1].is_a?(Proc)
420
+ attr_validator.evaluate(value[1])
421
+ else
422
+ value[1]
423
+ end
424
+ case value[0]
425
+ when :is
426
+ attr_value == evaluated
427
+ when :is_not
428
+ attr_value != evaluated
429
+ when :contains
430
+ begin
431
+ attr_value.include?(evaluated)
432
+ rescue NoMethodError => e
433
+ raise AuthorizationUsageError, "Operator contains requires a " +
434
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
435
+ "contains #{evaluated.inspect}: #{e}"
436
+ end
437
+ when :does_not_contain
438
+ begin
439
+ !attr_value.include?(evaluated)
440
+ rescue NoMethodError => e
441
+ raise AuthorizationUsageError, "Operator does_not_contain requires a " +
442
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
443
+ "does_not_contain #{evaluated.inspect}: #{e}"
444
+ end
445
+ when :intersects_with
446
+ begin
447
+ !(evaluated.to_set & attr_value.to_set).empty?
448
+ rescue NoMethodError => e
449
+ raise AuthorizationUsageError, "Operator intersects_with requires " +
450
+ "subclasses of Enumerable, got: #{attr_value.inspect} " +
451
+ "intersects_with #{evaluated.inspect}: #{e}"
452
+ end
453
+ when :is_in
454
+ begin
455
+ evaluated.include?(attr_value)
456
+ rescue NoMethodError => e
457
+ raise AuthorizationUsageError, "Operator is_in requires a " +
458
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
459
+ "is_in #{evaluated.inspect}: #{e}"
460
+ end
461
+ when :is_not_in
462
+ begin
463
+ !evaluated.include?(attr_value)
464
+ rescue NoMethodError => e
465
+ raise AuthorizationUsageError, "Operator is_not_in requires a " +
466
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
467
+ "is_not_in #{evaluated.inspect}: #{e}"
468
+ end
469
+ else
470
+ raise AuthorizationError, "Unknown operator #{value[0]}"
471
+ end
472
+ else
473
+ raise AuthorizationError, "Wrong conditions hash format"
474
+ end
475
+ end
476
+ end
477
+
478
+ # resolves all the values in condition_hash
479
+ def obligation (attr_validator, hash = nil)
480
+ hash = (hash || @conditions_hash).clone
481
+ hash.each do |attr, value|
482
+ if value.is_a?(Hash)
483
+ hash[attr] = obligation(attr_validator, value)
484
+ elsif value.is_a?(Array) and value.length == 2
485
+ hash[attr] = [value[0], attr_validator.evaluate(value[1])]
486
+ else
487
+ raise AuthorizationError, "Wrong conditions hash format"
488
+ end
489
+ end
490
+ hash
491
+ end
492
+
493
+ def to_long_s (hash = nil)
494
+ if hash
495
+ hash.inject({}) do |memo, key_val|
496
+ key, val = key_val
497
+ memo[key] = case val
498
+ when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
499
+ when Hash then to_long_s(val)
500
+ end
501
+ memo
502
+ end
503
+ else
504
+ "if_attribute #{to_long_s(@conditions_hash).inspect}"
505
+ end
506
+ end
507
+
508
+ protected
509
+ def object_attribute_value (object, attr)
510
+ begin
511
+ object.send(attr)
512
+ rescue ArgumentError, NoMethodError => e
513
+ raise AuthorizationUsageError, "Error when calling #{attr} on " +
514
+ "#{object.inspect} for validating attribute: #{e}"
515
+ end
516
+ end
517
+
518
+ def deep_hash_clone (hash)
519
+ hash.inject({}) do |memo, (key, val)|
520
+ memo[key] = case val
521
+ when Hash
522
+ deep_hash_clone(val)
523
+ when NilClass, Symbol
524
+ val
525
+ else
526
+ val.clone
527
+ end
528
+ memo
529
+ end
530
+ end
531
+ end
532
+
533
+ # An attribute condition that uses existing rules to decide validation
534
+ # and create obligations.
535
+ class AttributeWithPermission < Attribute
536
+ # E.g. privilege :read, attr_or_hash either :attribute or
537
+ # { :attribute => :deeper_attribute }
538
+ def initialize (privilege, attr_or_hash, context = nil)
539
+ @privilege = privilege
540
+ @context = context
541
+ @attr_hash = attr_or_hash
542
+ end
543
+
544
+ def initialize_copy (from)
545
+ @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
546
+ end
547
+
548
+ def validate? (attr_validator, object = nil, hash_or_attr = nil)
549
+ object ||= attr_validator.object
550
+ hash_or_attr ||= @attr_hash
551
+ return false unless object
552
+
553
+ case hash_or_attr
554
+ when Symbol
555
+ attr_value = object_attribute_value(object, hash_or_attr)
556
+ if attr_value.nil?
557
+ raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
558
+ end
559
+ attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
560
+ when Hash
561
+ hash_or_attr.all? do |attr, sub_hash|
562
+ attr_value = object_attribute_value(object, attr)
563
+ if attr_value.nil?
564
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
565
+ end
566
+ validate?(attr_validator, attr_value, sub_hash)
567
+ end
568
+ when NilClass
569
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
570
+ else
571
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
572
+ end
573
+ end
574
+
575
+ # may return an array of obligations to be OR'ed
576
+ def obligation (attr_validator, hash_or_attr = nil)
577
+ hash_or_attr ||= @attr_hash
578
+ case hash_or_attr
579
+ when Symbol
580
+ obligations = attr_validator.engine.obligations(@privilege,
581
+ :context => @context || hash_or_attr.to_s.pluralize.to_sym,
582
+ :user => attr_validator.user)
583
+ obligations.collect {|obl| {hash_or_attr => obl} }
584
+ when Hash
585
+ obligations_array_attrs = []
586
+ obligations =
587
+ hash_or_attr.inject({}) do |all, pair|
588
+ attr, sub_hash = pair
589
+ all[attr] = obligation(attr_validator, sub_hash)
590
+ if all[attr].length > 1
591
+ obligations_array_attrs << attr
592
+ else
593
+ all[attr] = all[attr].first
594
+ end
595
+ all
596
+ end
597
+ obligations = [obligations]
598
+ obligations_array_attrs.each do |attr|
599
+ next_array_size = obligations.first[attr].length
600
+ obligations = obligations.collect do |obls|
601
+ (0...next_array_size).collect do |idx|
602
+ obls_wo_array = obls.clone
603
+ obls_wo_array[attr] = obls_wo_array[attr][idx]
604
+ obls_wo_array
605
+ end
606
+ end.flatten
607
+ end
608
+ obligations
609
+ when NilClass
610
+ attr_validator.engine.obligations(@privilege,
611
+ :context => attr_validator.context,
612
+ :user => attr_validator.user)
613
+ else
614
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
615
+ end
616
+ end
617
+
618
+ def to_long_s
619
+ "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
620
+ end
621
+ end
622
+
623
+ # Represents a pseudo-user to facilitate guest users in applications
624
+ class GuestUser
625
+ attr_reader :role_symbols
626
+ def initialize (roles = [:guest])
627
+ @role_symbols = roles
628
+ end
629
+ end
630
+ end