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