tarsolya-declarative_authorization 0.4.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 +139 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +503 -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 +218 -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 +169 -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 +681 -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 +623 -0
  28. data/lib/declarative_authorization/in_model.rb +162 -0
  29. data/lib/declarative_authorization/maintenance.rb +198 -0
  30. data/lib/declarative_authorization/obligation_scope.rb +345 -0
  31. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  32. data/lib/declarative_authorization/reader.rb +496 -0
  33. data/test/authorization_test.rb +971 -0
  34. data/test/controller_filter_resource_access_test.rb +511 -0
  35. data/test/controller_test.rb +465 -0
  36. data/test/dsl_reader_test.rb +173 -0
  37. data/test/helper_test.rb +171 -0
  38. data/test/maintenance_test.rb +46 -0
  39. data/test/model_test.rb +1694 -0
  40. data/test/schema.sql +54 -0
  41. data/test/test_helper.rb +137 -0
  42. metadata +118 -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
data/config/routes.rb ADDED
@@ -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
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,681 @@
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
+ unless defined?(Rails::Engine).nil?
23
+ class Engine < Rails::Engine
24
+ engine_name :declarative_authorization
25
+ end
26
+ end
27
+
28
+ AUTH_DSL_FILES = ["#{Rails.root}/config/authorization_rules.rb"] unless defined? AUTH_DSL_FILES
29
+
30
+ # Controller-independent method for retrieving the current user.
31
+ # Needed for model security where the current controller is not available.
32
+ def self.current_user
33
+ Thread.current["current_user"] || GuestUser.new
34
+ end
35
+
36
+ # Controller-independent method for setting the current user.
37
+ def self.current_user=(user)
38
+ Thread.current["current_user"] = user
39
+ end
40
+
41
+ # For use in test cases only
42
+ def self.ignore_access_control (state = nil) # :nodoc:
43
+ Thread.current["ignore_access_control"] = state unless state.nil?
44
+ Thread.current["ignore_access_control"] || false
45
+ end
46
+
47
+ def self.activate_authorization_rules_browser? # :nodoc:
48
+ ::Rails.env.development?
49
+ end
50
+
51
+ @@dot_path = "dot"
52
+ def self.dot_path
53
+ @@dot_path
54
+ end
55
+
56
+ def self.dot_path= (path)
57
+ @@dot_path = path
58
+ end
59
+
60
+ # Authorization::Engine implements the reference monitor. It may be used
61
+ # for querying the permission and retrieving obligations under which
62
+ # a certain privilege is granted for the current user.
63
+ #
64
+ class Engine
65
+ attr_reader :roles, :omnipotent_roles, :role_titles, :role_descriptions, :privileges,
66
+ :privilege_hierarchy, :auth_rules, :role_hierarchy, :rev_priv_hierarchy,
67
+ :rev_role_hierarchy
68
+
69
+ # If +reader+ is not given, a new one is created with the default
70
+ # authorization configuration of +AUTH_DSL_FILES+. If given, may be either
71
+ # a Reader object or a path to a configuration file.
72
+ def initialize (reader = nil)
73
+ reader = Reader::DSLReader.factory(reader || AUTH_DSL_FILES)
74
+
75
+ @privileges = reader.privileges_reader.privileges
76
+ # {priv => [[priv, ctx],...]}
77
+ @privilege_hierarchy = reader.privileges_reader.privilege_hierarchy
78
+ @auth_rules = reader.auth_rules_reader.auth_rules
79
+ @roles = reader.auth_rules_reader.roles
80
+ @omnipotent_roles = reader.auth_rules_reader.omnipotent_roles
81
+ @role_hierarchy = reader.auth_rules_reader.role_hierarchy
82
+
83
+ @role_titles = reader.auth_rules_reader.role_titles
84
+ @role_descriptions = reader.auth_rules_reader.role_descriptions
85
+ @reader = reader
86
+
87
+ # {[priv, ctx] => [priv, ...]}
88
+ @rev_priv_hierarchy = {}
89
+ @privilege_hierarchy.each do |key, value|
90
+ value.each do |val|
91
+ @rev_priv_hierarchy[val] ||= []
92
+ @rev_priv_hierarchy[val] << key
93
+ end
94
+ end
95
+ @rev_role_hierarchy = {}
96
+ @role_hierarchy.each do |higher_role, lower_roles|
97
+ lower_roles.each do |role|
98
+ (@rev_role_hierarchy[role] ||= []) << higher_role
99
+ end
100
+ end
101
+ end
102
+
103
+ def initialize_copy (from) # :nodoc:
104
+ [
105
+ :privileges, :privilege_hierarchy, :roles, :role_hierarchy, :role_titles,
106
+ :role_descriptions, :rev_priv_hierarchy, :rev_role_hierarchy
107
+ ].each {|attr| instance_variable_set(:"@#{attr}", from.send(attr).clone) }
108
+ @auth_rules = from.auth_rules.collect {|rule| rule.clone}
109
+ end
110
+
111
+ # Returns true if privilege is met by the current user. Raises
112
+ # AuthorizationError otherwise. +privilege+ may be given with or
113
+ # without context. In the latter case, the :+context+ option is
114
+ # required.
115
+ #
116
+ # Options:
117
+ # [:+context+]
118
+ # The context part of the privilege.
119
+ # Defaults either to the tableized +class_name+ of the given :+object+, if given.
120
+ # That is, :+users+ for :+object+ of type User.
121
+ # Raises AuthorizationUsageError if context is missing and not to be infered.
122
+ # [:+object+] An context object to test attribute checks against.
123
+ # [:+skip_attribute_test+]
124
+ # Skips those attribute checks in the
125
+ # authorization rules. Defaults to false.
126
+ # [:+user+]
127
+ # The user to check the authorization for.
128
+ # Defaults to Authorization#current_user.
129
+ #
130
+ def permit! (privilege, options = {})
131
+ return true if Authorization.ignore_access_control
132
+ options = {
133
+ :object => nil,
134
+ :skip_attribute_test => false,
135
+ :context => nil
136
+ }.merge(options)
137
+
138
+ # Make sure we're handling all privileges as symbols.
139
+ privilege = privilege.is_a?( Array ) ?
140
+ privilege.flatten.collect { |priv| priv.to_sym } :
141
+ privilege.to_sym
142
+
143
+ #
144
+ # If the object responds to :proxy_reflection, we're probably working with
145
+ # an association proxy. Use 'new' to leverage ActiveRecord's builder
146
+ # functionality to obtain an object against which we can check permissions.
147
+ #
148
+ # Example: permit!( :edit, :object => user.posts )
149
+ #
150
+ if options[:object].respond_to?( :proxy_reflection ) && options[:object].respond_to?( :new )
151
+ options[:object] = options[:object].new
152
+ end
153
+
154
+ options[:context] ||= options[:object] && (
155
+ options[:object].class.respond_to?(:decl_auth_context) ?
156
+ options[:object].class.decl_auth_context :
157
+ options[:object].class.name.tableize.to_sym
158
+ ) rescue NoMethodError
159
+
160
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
161
+
162
+ return true unless (roles & @omnipotent_roles).empty?
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
+
211
+ permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
212
+
213
+ attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
214
+ matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
215
+ rule.obligations(attr_validator)
216
+ end.flatten
217
+ end
218
+
219
+ # Returns the description for the given role. The description may be
220
+ # specified with the authorization rules. Returns +nil+ if none was
221
+ # given.
222
+ def description_for (role)
223
+ role_descriptions[role]
224
+ end
225
+
226
+ # Returns the title for the given role. The title may be
227
+ # specified with the authorization rules. Returns +nil+ if none was
228
+ # given.
229
+ def title_for (role)
230
+ role_titles[role]
231
+ end
232
+
233
+ # Returns the role symbols of the given user.
234
+ def roles_for (user)
235
+ user ||= Authorization.current_user
236
+ raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.inspect})" \
237
+ if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
238
+
239
+ RAILS_DEFAULT_LOGGER.info("The use of user.roles is deprecated. Please add a method " +
240
+ "role_symbols to your User model.") if defined?(RAILS_DEFAULT_LOGGER) and !user.respond_to?(:role_symbols)
241
+
242
+ roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
243
+
244
+ raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
245
+ "doesn't return an Array of Symbols (#{roles.inspect})" \
246
+ if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
247
+
248
+ (roles.empty? ? [:guest] : roles)
249
+ end
250
+
251
+ # Returns the role symbols and inherritted role symbols for the given user
252
+ def roles_with_hierarchy_for(user)
253
+ flatten_roles(roles_for(user))
254
+ end
255
+
256
+ # Returns an instance of Engine, which is created if there isn't one
257
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
258
+ # a new instance is always created.
259
+ def self.instance (dsl_file = nil)
260
+ if dsl_file or ENV['RAILS_ENV'] == 'development'
261
+ @@instance = new(dsl_file)
262
+ else
263
+ @@instance ||= new
264
+ end
265
+ end
266
+
267
+ class AttributeValidator # :nodoc:
268
+ attr_reader :user, :object, :engine, :context, :privilege
269
+ def initialize (engine, user, object = nil, privilege = nil, context = nil)
270
+ @engine = engine
271
+ @user = user
272
+ @object = object
273
+ @privilege = privilege
274
+ @context = context
275
+ end
276
+
277
+ def evaluate (value_block)
278
+ # TODO cache?
279
+ instance_eval(&value_block)
280
+ end
281
+ end
282
+
283
+ private
284
+ def user_roles_privleges_from_options(privilege, options)
285
+ options = {
286
+ :user => nil,
287
+ :context => nil,
288
+ :user_roles => nil
289
+ }.merge(options)
290
+ user = options[:user] || Authorization.current_user
291
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
292
+
293
+ raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
294
+ "set through Authorization.current_user" unless user
295
+
296
+ roles = options[:user_roles] || flatten_roles(roles_for(user))
297
+ privileges = flatten_privileges privileges, options[:context]
298
+ [user, roles, privileges]
299
+ end
300
+
301
+ def flatten_roles (roles)
302
+ # TODO caching?
303
+ flattened_roles = roles.clone.to_a
304
+ flattened_roles.each do |role|
305
+ flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
306
+ end
307
+ end
308
+
309
+ # Returns the privilege hierarchy flattened for given privileges in context.
310
+ def flatten_privileges (privileges, context = nil)
311
+ # TODO caching?
312
+ raise AuthorizationUsageError, "No context given or inferable from object" unless context
313
+ flattened_privileges = privileges.clone
314
+ flattened_privileges.each do |priv|
315
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
316
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
317
+ end
318
+ end
319
+
320
+ def matching_auth_rules (roles, privileges, context)
321
+ @auth_rules.select {|rule| rule.matches? roles, privileges, context}
322
+ end
323
+ end
324
+
325
+ class AuthorizationRule
326
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
327
+ :source_file, :source_line
328
+
329
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or,
330
+ options = {})
331
+ @role = role
332
+ @privileges = Set.new(privileges)
333
+ @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
334
+ @join_operator = join_operator
335
+ @attributes = []
336
+ @source_file = options[:source_file]
337
+ @source_line = options[:source_line]
338
+ end
339
+
340
+ def initialize_copy (from)
341
+ @privileges = @privileges.clone
342
+ @contexts = @contexts.clone
343
+ @attributes = @attributes.collect {|attribute| attribute.clone }
344
+ end
345
+
346
+ def append_privileges (privs)
347
+ @privileges.merge(privs)
348
+ end
349
+
350
+ def append_attribute (attribute)
351
+ @attributes << attribute
352
+ end
353
+
354
+ def matches? (roles, privs, context = nil)
355
+ roles = [roles] unless roles.is_a?(Array)
356
+ @contexts.include?(context) and roles.include?(@role) and
357
+ not (@privileges & privs).empty?
358
+ end
359
+
360
+ def validate? (attr_validator, skip_attribute = false)
361
+ skip_attribute or @attributes.empty? or
362
+ @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
363
+ begin
364
+ attr.validate?(attr_validator)
365
+ rescue NilAttributeValueError => e
366
+ nil # Bumping up against a nil attribute value flunks the rule.
367
+ end
368
+ end
369
+ end
370
+
371
+ def obligations (attr_validator)
372
+ exceptions = []
373
+ obligations = @attributes.collect do |attr|
374
+ begin
375
+ attr.obligation(attr_validator)
376
+ rescue NotAuthorized => e
377
+ exceptions << e
378
+ nil
379
+ end
380
+ end.flatten.compact
381
+
382
+ if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
383
+ raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
384
+ end
385
+
386
+ if @join_operator == :and and !obligations.empty?
387
+ merged_obligation = obligations.first
388
+ obligations[1..-1].each do |obligation|
389
+ merged_obligation = merged_obligation.deep_merge(obligation)
390
+ end
391
+ obligations = [merged_obligation]
392
+ end
393
+ obligations.empty? ? [{}] : obligations
394
+ end
395
+
396
+ def to_long_s
397
+ attributes.collect {|attr| attr.to_long_s } * "; "
398
+ end
399
+ end
400
+
401
+ class Attribute
402
+ # attr_conditions_hash of form
403
+ # { :object_attribute => [operator, value_block], ... }
404
+ # { :object_attribute => { :attr => ... } }
405
+ def initialize (conditions_hash)
406
+ @conditions_hash = conditions_hash
407
+ end
408
+
409
+ def initialize_copy (from)
410
+ @conditions_hash = deep_hash_clone(@conditions_hash)
411
+ end
412
+
413
+ def validate? (attr_validator, object = nil, hash = nil)
414
+ object ||= attr_validator.object
415
+ return false unless object
416
+
417
+ (hash || @conditions_hash).all? do |attr, value|
418
+ attr_value = object_attribute_value(object, attr)
419
+ if value.is_a?(Hash)
420
+ if attr_value.is_a?(Enumerable)
421
+ attr_value.any? do |inner_value|
422
+ validate?(attr_validator, inner_value, value)
423
+ end
424
+ elsif attr_value == nil
425
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
426
+ else
427
+ validate?(attr_validator, attr_value, value)
428
+ end
429
+ elsif value.is_a?(Array) and value.length == 2 and value.first.is_a?(Symbol)
430
+ evaluated = if value[1].is_a?(Proc)
431
+ attr_validator.evaluate(value[1])
432
+ else
433
+ value[1]
434
+ end
435
+ case value[0]
436
+ when :is
437
+ attr_value == evaluated
438
+ when :is_not
439
+ attr_value != evaluated
440
+ when :contains
441
+ begin
442
+ attr_value.include?(evaluated)
443
+ rescue NoMethodError => e
444
+ raise AuthorizationUsageError, "Operator contains requires a " +
445
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
446
+ "contains #{evaluated.inspect}: #{e}"
447
+ end
448
+ when :does_not_contain
449
+ begin
450
+ !attr_value.include?(evaluated)
451
+ rescue NoMethodError => e
452
+ raise AuthorizationUsageError, "Operator does_not_contain requires a " +
453
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
454
+ "does_not_contain #{evaluated.inspect}: #{e}"
455
+ end
456
+ when :intersects_with
457
+ begin
458
+ !(evaluated.to_set & attr_value.to_set).empty?
459
+ rescue NoMethodError => e
460
+ raise AuthorizationUsageError, "Operator intersects_with requires " +
461
+ "subclasses of Enumerable, got: #{attr_value.inspect} " +
462
+ "intersects_with #{evaluated.inspect}: #{e}"
463
+ end
464
+ when :is_in
465
+ begin
466
+ evaluated.include?(attr_value)
467
+ rescue NoMethodError => e
468
+ raise AuthorizationUsageError, "Operator is_in requires a " +
469
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
470
+ "is_in #{evaluated.inspect}: #{e}"
471
+ end
472
+ when :is_not_in
473
+ begin
474
+ !evaluated.include?(attr_value)
475
+ rescue NoMethodError => e
476
+ raise AuthorizationUsageError, "Operator is_not_in requires a " +
477
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
478
+ "is_not_in #{evaluated.inspect}: #{e}"
479
+ end
480
+ else
481
+ raise AuthorizationError, "Unknown operator #{value[0]}"
482
+ end
483
+ else
484
+ raise AuthorizationError, "Wrong conditions hash format"
485
+ end
486
+ end
487
+ end
488
+
489
+ # resolves all the values in condition_hash
490
+ def obligation (attr_validator, hash = nil)
491
+ hash = (hash || @conditions_hash).clone
492
+ hash.each do |attr, value|
493
+ if value.is_a?(Hash)
494
+ hash[attr] = obligation(attr_validator, value)
495
+ elsif value.is_a?(Array) and value.length == 2
496
+ hash[attr] = [value[0], attr_validator.evaluate(value[1])]
497
+ else
498
+ raise AuthorizationError, "Wrong conditions hash format"
499
+ end
500
+ end
501
+ hash
502
+ end
503
+
504
+ def to_long_s (hash = nil)
505
+ if hash
506
+ hash.inject({}) do |memo, key_val|
507
+ key, val = key_val
508
+ memo[key] = case val
509
+ when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
510
+ when Hash then to_long_s(val)
511
+ end
512
+ memo
513
+ end
514
+ else
515
+ "if_attribute #{to_long_s(@conditions_hash).inspect}"
516
+ end
517
+ end
518
+
519
+ protected
520
+ def object_attribute_value (object, attr)
521
+ begin
522
+ object.send(attr)
523
+ rescue ArgumentError, NoMethodError => e
524
+ raise AuthorizationUsageError, "Error when calling #{attr} on " +
525
+ "#{object.inspect} for validating attribute: #{e}"
526
+ end
527
+ end
528
+
529
+ def deep_hash_clone (hash)
530
+ hash.inject({}) do |memo, (key, val)|
531
+ memo[key] = case val
532
+ when Hash
533
+ deep_hash_clone(val)
534
+ when NilClass, Symbol
535
+ val
536
+ else
537
+ val.clone
538
+ end
539
+ memo
540
+ end
541
+ end
542
+ end
543
+
544
+ # An attribute condition that uses existing rules to decide validation
545
+ # and create obligations.
546
+ class AttributeWithPermission < Attribute
547
+ # E.g. privilege :read, attr_or_hash either :attribute or
548
+ # { :attribute => :deeper_attribute }
549
+ def initialize (privilege, attr_or_hash, context = nil)
550
+ @privilege = privilege
551
+ @context = context
552
+ @attr_hash = attr_or_hash
553
+ end
554
+
555
+ def initialize_copy (from)
556
+ @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
557
+ end
558
+
559
+ def validate? (attr_validator, object = nil, hash_or_attr = nil)
560
+ object ||= attr_validator.object
561
+ hash_or_attr ||= @attr_hash
562
+ return false unless object
563
+
564
+ case hash_or_attr
565
+ when Symbol
566
+ attr_value = object_attribute_value(object, hash_or_attr)
567
+ case attr_value
568
+ when nil
569
+ raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
570
+ when Enumerable
571
+ attr_value.any? do |inner_value|
572
+ attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
573
+ end
574
+ else
575
+ attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
576
+ end
577
+ when Hash
578
+ hash_or_attr.all? do |attr, sub_hash|
579
+ attr_value = object_attribute_value(object, attr)
580
+ if attr_value == nil
581
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
582
+ elsif attr_value.is_a?(Enumerable)
583
+ attr_value.any? do |inner_value|
584
+ validate?(attr_validator, inner_value, sub_hash)
585
+ end
586
+ else
587
+ validate?(attr_validator, attr_value, sub_hash)
588
+ end
589
+ end
590
+ when NilClass
591
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
592
+ else
593
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
594
+ end
595
+ end
596
+
597
+ # may return an array of obligations to be OR'ed
598
+ def obligation (attr_validator, hash_or_attr = nil, path = [])
599
+ hash_or_attr ||= @attr_hash
600
+ case hash_or_attr
601
+ when Symbol
602
+ @context ||= begin
603
+ rule_model = attr_validator.context.to_s.classify.constantize
604
+ context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
605
+ if context_reflection.klass.respond_to?(:decl_auth_context)
606
+ context_reflection.klass.decl_auth_context
607
+ else
608
+ context_reflection.klass.name.tableize.to_sym
609
+ end
610
+ rescue # missing model, reflections
611
+ hash_or_attr.to_s.pluralize.to_sym
612
+ end
613
+
614
+ obligations = attr_validator.engine.obligations(@privilege,
615
+ :context => @context,
616
+ :user => attr_validator.user)
617
+
618
+ obligations.collect {|obl| {hash_or_attr => obl} }
619
+ when Hash
620
+ obligations_array_attrs = []
621
+ obligations =
622
+ hash_or_attr.inject({}) do |all, pair|
623
+ attr, sub_hash = pair
624
+ all[attr] = obligation(attr_validator, sub_hash, path + [attr])
625
+ if all[attr].length > 1
626
+ obligations_array_attrs << attr
627
+ else
628
+ all[attr] = all[attr].first
629
+ end
630
+ all
631
+ end
632
+ obligations = [obligations]
633
+ obligations_array_attrs.each do |attr|
634
+ next_array_size = obligations.first[attr].length
635
+ obligations = obligations.collect do |obls|
636
+ (0...next_array_size).collect do |idx|
637
+ obls_wo_array = obls.clone
638
+ obls_wo_array[attr] = obls_wo_array[attr][idx]
639
+ obls_wo_array
640
+ end
641
+ end.flatten
642
+ end
643
+ obligations
644
+ when NilClass
645
+ attr_validator.engine.obligations(@privilege,
646
+ :context => attr_validator.context,
647
+ :user => attr_validator.user)
648
+ else
649
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
650
+ end
651
+ end
652
+
653
+ def to_long_s
654
+ "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
655
+ end
656
+
657
+ private
658
+ def self.reflection_for_path (parent_model, path)
659
+ reflection = path.empty? ? parent_model : begin
660
+ parent = reflection_for_path(parent_model, path[0..-2])
661
+ if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
662
+ parent.klass.reflect_on_association(path.last)
663
+ else
664
+ parent.reflect_on_association(path.last)
665
+ end
666
+ rescue
667
+ parent.reflect_on_association(path.last)
668
+ end
669
+ raise "invalid path #{path.inspect}" if reflection.nil?
670
+ reflection
671
+ end
672
+ end
673
+
674
+ # Represents a pseudo-user to facilitate guest users in applications
675
+ class GuestUser
676
+ attr_reader :role_symbols
677
+ def initialize (roles = [:guest])
678
+ @role_symbols = roles
679
+ end
680
+ end
681
+ end