uhees-declarative_authorization 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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