viva-declarative_authorization 0.3.2.2.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 +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