timcharper-declarative_authorization 0.4.1.2

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 +135 -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 +683 -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 +472 -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 +157 -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 +134 -0
  42. metadata +119 -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,683 @@
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_FILES = ["#{RAILS_ROOT}/config/authorization_rules.rb"] unless defined? AUTH_DSL_FILES
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, :omnipotent_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_FILES+. 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_FILES)
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
+ @omnipotent_roles = reader.auth_rules_reader.omnipotent_roles
83
+ @role_hierarchy = reader.auth_rules_reader.role_hierarchy
84
+
85
+ @role_titles = reader.auth_rules_reader.role_titles
86
+ @role_descriptions = reader.auth_rules_reader.role_descriptions
87
+ @reader = reader
88
+
89
+ # {[priv, ctx] => [priv, ...]}
90
+ @rev_priv_hierarchy = {}
91
+ @privilege_hierarchy.each do |key, value|
92
+ value.each do |val|
93
+ @rev_priv_hierarchy[val] ||= []
94
+ @rev_priv_hierarchy[val] << key
95
+ end
96
+ end
97
+ @rev_role_hierarchy = {}
98
+ @role_hierarchy.each do |higher_role, lower_roles|
99
+ lower_roles.each do |role|
100
+ (@rev_role_hierarchy[role] ||= []) << higher_role
101
+ end
102
+ end
103
+ end
104
+
105
+ def initialize_copy (from) # :nodoc:
106
+ [
107
+ :privileges, :privilege_hierarchy, :roles, :role_hierarchy, :role_titles,
108
+ :role_descriptions, :rev_priv_hierarchy, :rev_role_hierarchy
109
+ ].each {|attr| instance_variable_set(:"@#{attr}", from.send(attr).clone) }
110
+ @auth_rules = from.auth_rules.collect {|rule| rule.clone}
111
+ end
112
+
113
+ # Returns true if privilege is met by the current user. Raises
114
+ # AuthorizationError otherwise. +privilege+ may be given with or
115
+ # without context. In the latter case, the :+context+ option is
116
+ # required.
117
+ #
118
+ # Options:
119
+ # [:+context+]
120
+ # The context part of the privilege.
121
+ # Defaults either to the tableized +class_name+ of the given :+object+, if given.
122
+ # That is, :+users+ for :+object+ of type User.
123
+ # Raises AuthorizationUsageError if 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] && (
157
+ options[:object].class.respond_to?(:decl_auth_context) ?
158
+ options[:object].class.decl_auth_context :
159
+ options[:object].class.name.tableize.to_sym
160
+ ) rescue NoMethodError
161
+
162
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
163
+
164
+ return true unless (roles & @omnipotent_roles).empty?
165
+ # find a authorization rule that matches for at least one of the roles and
166
+ # at least one of the given privileges
167
+ attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
168
+ rules = matching_auth_rules(roles, privileges, options[:context])
169
+ if rules.empty?
170
+ raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
171
+ "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
172
+ "context #{options[:context].inspect})."
173
+ end
174
+
175
+ # Test each rule in turn to see whether any one of them is satisfied.
176
+ unless rules.any? {|rule| rule.validate?(attr_validator, options[:skip_attribute_test])}
177
+ raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{(options[:object] || options[:context]).inspect}."
178
+ end
179
+ true
180
+ end
181
+
182
+ # Calls permit! but rescues the AuthorizationException and returns false
183
+ # instead. If no exception is raised, permit? returns true and yields
184
+ # to the optional block.
185
+ def permit? (privilege, options = {}, &block) # :yields:
186
+ permit!(privilege, options)
187
+ yield if block_given?
188
+ true
189
+ rescue NotAuthorized
190
+ false
191
+ end
192
+
193
+ # Returns the obligations to be met by the current user for the given
194
+ # privilege as an array of obligation hashes in form of
195
+ # [{:object_attribute => obligation_value, ...}, ...]
196
+ # where +obligation_value+ is either (recursively) another obligation hash
197
+ # or a value spec, such as
198
+ # [operator, literal_value]
199
+ # The obligation hashes in the array should be OR'ed, conditions inside
200
+ # the hashes AND'ed.
201
+ #
202
+ # Example
203
+ # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
204
+ #
205
+ # Options
206
+ # [:+context+] See permit!
207
+ # [:+user+] See permit!
208
+ #
209
+ def obligations (privilege, options = {})
210
+ options = {:context => nil}.merge(options)
211
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
212
+
213
+ permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
214
+
215
+ attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
216
+ matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
217
+ rule.obligations(attr_validator)
218
+ end.flatten
219
+ end
220
+
221
+ # Returns the description for the given role. The description may be
222
+ # specified with the authorization rules. Returns +nil+ if none was
223
+ # given.
224
+ def description_for (role)
225
+ role_descriptions[role]
226
+ end
227
+
228
+ # Returns the title for the given role. The title may be
229
+ # specified with the authorization rules. Returns +nil+ if none was
230
+ # given.
231
+ def title_for (role)
232
+ role_titles[role]
233
+ end
234
+
235
+ # Returns the role symbols of the given user.
236
+ def roles_for (user)
237
+ user ||= Authorization.current_user
238
+ raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.inspect})" \
239
+ if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
240
+
241
+ RAILS_DEFAULT_LOGGER.info("The use of user.roles is deprecated. Please add a method " +
242
+ "role_symbols to your User model.") if defined?(RAILS_DEFAULT_LOGGER) and !user.respond_to?(:role_symbols)
243
+
244
+ roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
245
+
246
+ raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
247
+ "doesn't return an Array of Symbols (#{roles.inspect})" \
248
+ if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
249
+
250
+ (roles.empty? ? [:guest] : roles)
251
+ end
252
+
253
+ # Returns the role symbols and inherritted role symbols for the given user
254
+ def roles_with_hierarchy_for(user)
255
+ flatten_roles(roles_for(user))
256
+ end
257
+
258
+ # Returns an instance of Engine, which is created if there isn't one
259
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
260
+ # a new instance is always created.
261
+ def self.instance (dsl_file = nil)
262
+ if dsl_file or ENV['RAILS_ENV'] == 'development'
263
+ @@instance = new(dsl_file)
264
+ else
265
+ @@instance ||= new
266
+ end
267
+ end
268
+
269
+ class AttributeValidator # :nodoc:
270
+ attr_reader :user, :object, :engine, :context, :privilege
271
+ def initialize (engine, user, object = nil, privilege = nil, context = nil)
272
+ @engine = engine
273
+ @user = user
274
+ @object = object
275
+ @privilege = privilege
276
+ @context = context
277
+ end
278
+
279
+ def evaluate (value_block)
280
+ # TODO cache?
281
+ instance_eval(&value_block)
282
+ end
283
+ end
284
+
285
+ private
286
+ def user_roles_privleges_from_options(privilege, options)
287
+ options = {
288
+ :user => nil,
289
+ :context => nil,
290
+ :user_roles => nil
291
+ }.merge(options)
292
+ user = options[:user] || Authorization.current_user
293
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
294
+
295
+ raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
296
+ "set through Authorization.current_user" unless user
297
+
298
+ roles = options[:user_roles] || flatten_roles(roles_for(user))
299
+ privileges = flatten_privileges privileges, options[:context]
300
+ [user, roles, privileges]
301
+ end
302
+
303
+ def flatten_roles (roles)
304
+ # TODO caching?
305
+ flattened_roles = roles.clone.to_a
306
+ flattened_roles.each do |role|
307
+ flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
308
+ end
309
+ end
310
+
311
+ # Returns the privilege hierarchy flattened for given privileges in context.
312
+ def flatten_privileges (privileges, context = nil)
313
+ # TODO caching?
314
+ raise AuthorizationUsageError, "No context given or inferable from object" unless context
315
+ flattened_privileges = privileges.clone
316
+ flattened_privileges.each do |priv|
317
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
318
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
319
+ end
320
+ end
321
+
322
+ def matching_auth_rules (roles, privileges, context)
323
+ @auth_rules.select {|rule| rule.matches? roles, privileges, context}
324
+ end
325
+ end
326
+
327
+ class AuthorizationRule
328
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
329
+ :source_file, :source_line
330
+
331
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or,
332
+ options = {})
333
+ @role = role
334
+ @privileges = Set.new(privileges)
335
+ @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
336
+ @join_operator = join_operator
337
+ @attributes = []
338
+ @source_file = options[:source_file]
339
+ @source_line = options[:source_line]
340
+ end
341
+
342
+ def initialize_copy (from)
343
+ @privileges = @privileges.clone
344
+ @contexts = @contexts.clone
345
+ @attributes = @attributes.collect {|attribute| attribute.clone }
346
+ end
347
+
348
+ def append_privileges (privs)
349
+ @privileges.merge(privs)
350
+ end
351
+
352
+ def append_attribute (attribute)
353
+ @attributes << attribute
354
+ end
355
+
356
+ def matches? (roles, privs, context = nil)
357
+ roles = [roles] unless roles.is_a?(Array)
358
+ @contexts.include?(context) and roles.include?(@role) and
359
+ not (@privileges & privs).empty?
360
+ end
361
+
362
+ def validate? (attr_validator, skip_attribute = false)
363
+ skip_attribute or @attributes.empty? or
364
+ @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
365
+ begin
366
+ attr.validate?(attr_validator)
367
+ rescue NilAttributeValueError => e
368
+ nil # Bumping up against a nil attribute value flunks the rule.
369
+ end
370
+ end
371
+ end
372
+
373
+ def obligations (attr_validator)
374
+ exceptions = []
375
+ obligations = @attributes.collect do |attr|
376
+ begin
377
+ attr.obligation(attr_validator)
378
+ rescue NotAuthorized => e
379
+ exceptions << e
380
+ nil
381
+ end
382
+ end.flatten.compact
383
+
384
+ if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
385
+ raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
386
+ end
387
+
388
+ if @join_operator == :and and !obligations.empty?
389
+ merged_obligation = obligations.first
390
+ obligations[1..-1].each do |obligation|
391
+ merged_obligation = merged_obligation.deep_merge(obligation)
392
+ end
393
+ obligations = [merged_obligation]
394
+ end
395
+ obligations.empty? ? [{}] : obligations
396
+ end
397
+
398
+ def to_long_s
399
+ attributes.collect {|attr| attr.to_long_s } * "; "
400
+ end
401
+ end
402
+
403
+ class Attribute
404
+ # attr_conditions_hash of form
405
+ # { :object_attribute => [operator, value_block], ... }
406
+ # { :object_attribute => { :attr => ... } }
407
+ def initialize (conditions_hash)
408
+ @conditions_hash = conditions_hash
409
+ end
410
+
411
+ def initialize_copy (from)
412
+ @conditions_hash = deep_hash_clone(@conditions_hash)
413
+ end
414
+
415
+ def validate? (attr_validator, object = nil, hash = nil)
416
+ object ||= attr_validator.object
417
+ return false unless object
418
+
419
+ (hash || @conditions_hash).all? do |attr, value|
420
+ attr_value = object_attribute_value(object, attr)
421
+ if value.is_a?(Hash)
422
+ if attr_value.is_a?(Enumerable)
423
+ attr_value.any? do |inner_value|
424
+ validate?(attr_validator, inner_value, value)
425
+ end
426
+ elsif attr_value == nil
427
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
428
+ else
429
+ validate?(attr_validator, attr_value, value)
430
+ end
431
+ elsif value.is_a?(Array) and value.length == 2 and value.first.is_a?(Symbol)
432
+ evaluated = if value[1].is_a?(Proc)
433
+ attr_validator.evaluate(value[1])
434
+ else
435
+ value[1]
436
+ end
437
+ case value[0]
438
+ when :is
439
+ attr_value == evaluated
440
+ when :is_not
441
+ attr_value != evaluated
442
+ when :contains
443
+ begin
444
+ attr_value.include?(evaluated)
445
+ rescue NoMethodError => e
446
+ raise AuthorizationUsageError, "Operator contains requires a " +
447
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
448
+ "contains #{evaluated.inspect}: #{e}"
449
+ end
450
+ when :does_not_contain
451
+ begin
452
+ !attr_value.include?(evaluated)
453
+ rescue NoMethodError => e
454
+ raise AuthorizationUsageError, "Operator does_not_contain requires a " +
455
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
456
+ "does_not_contain #{evaluated.inspect}: #{e}"
457
+ end
458
+ when :intersects_with
459
+ begin
460
+ !(evaluated.to_set & attr_value.to_set).empty?
461
+ rescue NoMethodError => e
462
+ raise AuthorizationUsageError, "Operator intersects_with requires " +
463
+ "subclasses of Enumerable, got: #{attr_value.inspect} " +
464
+ "intersects_with #{evaluated.inspect}: #{e}"
465
+ end
466
+ when :is_in
467
+ begin
468
+ evaluated.include?(attr_value)
469
+ rescue NoMethodError => e
470
+ raise AuthorizationUsageError, "Operator is_in requires a " +
471
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
472
+ "is_in #{evaluated.inspect}: #{e}"
473
+ end
474
+ when :is_not_in
475
+ begin
476
+ !evaluated.include?(attr_value)
477
+ rescue NoMethodError => e
478
+ raise AuthorizationUsageError, "Operator is_not_in requires a " +
479
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
480
+ "is_not_in #{evaluated.inspect}: #{e}"
481
+ end
482
+ else
483
+ raise AuthorizationError, "Unknown operator #{value[0]}"
484
+ end
485
+ else
486
+ raise AuthorizationError, "Wrong conditions hash format"
487
+ end
488
+ end
489
+ end
490
+
491
+ # resolves all the values in condition_hash
492
+ def obligation (attr_validator, hash = nil)
493
+ hash = (hash || @conditions_hash).clone
494
+ hash.each do |attr, value|
495
+ if value.is_a?(Hash)
496
+ hash[attr] = obligation(attr_validator, value)
497
+ elsif value.is_a?(Array) and value.length == 2
498
+ hash[attr] = [value[0], attr_validator.evaluate(value[1])]
499
+ else
500
+ raise AuthorizationError, "Wrong conditions hash format"
501
+ end
502
+ end
503
+ hash
504
+ end
505
+
506
+ def to_long_s (hash = nil)
507
+ if hash
508
+ hash.inject({}) do |memo, key_val|
509
+ key, val = key_val
510
+ memo[key] = case val
511
+ when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
512
+ when Hash then to_long_s(val)
513
+ end
514
+ memo
515
+ end
516
+ else
517
+ "if_attribute #{to_long_s(@conditions_hash).inspect}"
518
+ end
519
+ end
520
+
521
+ protected
522
+ def object_attribute_value (object, attr)
523
+ begin
524
+ object.send(attr)
525
+ rescue ArgumentError, NoMethodError => e
526
+ raise AuthorizationUsageError, "Error when calling #{attr} on " +
527
+ "#{object.inspect} for validating attribute: #{e}"
528
+ end
529
+ end
530
+
531
+ def deep_hash_clone (hash)
532
+ hash.inject({}) do |memo, (key, val)|
533
+ memo[key] = case val
534
+ when Hash
535
+ deep_hash_clone(val)
536
+ when NilClass, Symbol
537
+ val
538
+ else
539
+ val.clone
540
+ end
541
+ memo
542
+ end
543
+ end
544
+ end
545
+
546
+ # An attribute condition that uses existing rules to decide validation
547
+ # and create obligations.
548
+ class AttributeWithPermission < Attribute
549
+ # E.g. privilege :read, attr_or_hash either :attribute or
550
+ # { :attribute => :deeper_attribute }
551
+ def initialize (privilege, attr_or_hash, context = nil)
552
+ @privilege = privilege
553
+ @context = context
554
+ @attr_hash = attr_or_hash
555
+ end
556
+
557
+ def initialize_copy (from)
558
+ @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
559
+ end
560
+
561
+ def validate? (attr_validator, object = nil, hash_or_attr = nil)
562
+ object ||= attr_validator.object
563
+ hash_or_attr ||= @attr_hash
564
+ return false unless object
565
+
566
+ case hash_or_attr
567
+ when Symbol
568
+ attr_value = object_attribute_value(object, hash_or_attr)
569
+ case attr_value
570
+ when nil
571
+ raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
572
+ when Enumerable
573
+ attr_value.any? do |inner_value|
574
+ attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
575
+ end
576
+ else
577
+ attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
578
+ end
579
+ when Hash
580
+ hash_or_attr.all? do |attr, sub_hash|
581
+ attr_value = object_attribute_value(object, attr)
582
+ if attr_value == nil
583
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
584
+ elsif attr_value.is_a?(Enumerable)
585
+ attr_value.any? do |inner_value|
586
+ validate?(attr_validator, inner_value, sub_hash)
587
+ end
588
+ else
589
+ validate?(attr_validator, attr_value, sub_hash)
590
+ end
591
+ end
592
+ when NilClass
593
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
594
+ else
595
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
596
+ end
597
+ end
598
+
599
+ # may return an array of obligations to be OR'ed
600
+ def obligation (attr_validator, hash_or_attr = nil, path = [])
601
+ hash_or_attr ||= @attr_hash
602
+ case hash_or_attr
603
+ when Symbol
604
+ @context ||= begin
605
+ rule_model = attr_validator.context.to_s.classify.constantize
606
+ context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
607
+ if context_reflection.klass.respond_to?(:decl_auth_context)
608
+ context_reflection.klass.decl_auth_context
609
+ else
610
+ context_reflection.klass.name.tableize.to_sym
611
+ end
612
+ rescue # missing model, reflections
613
+ hash_or_attr.to_s.pluralize.to_sym
614
+ end
615
+
616
+ obligations = attr_validator.engine.obligations(@privilege,
617
+ :context => @context,
618
+ :user => attr_validator.user)
619
+
620
+ obligations.collect {|obl| {hash_or_attr => obl} }
621
+ when Hash
622
+ obligations_array_attrs = []
623
+ obligations =
624
+ hash_or_attr.inject({}) do |all, pair|
625
+ attr, sub_hash = pair
626
+ all[attr] = obligation(attr_validator, sub_hash, path + [attr])
627
+ if all[attr].length > 1
628
+ obligations_array_attrs << attr
629
+ else
630
+ all[attr] = all[attr].first
631
+ end
632
+ all
633
+ end
634
+ obligations = [obligations]
635
+ obligations_array_attrs.each do |attr|
636
+ next_array_size = obligations.first[attr].length
637
+ obligations = obligations.collect do |obls|
638
+ (0...next_array_size).collect do |idx|
639
+ obls_wo_array = obls.clone
640
+ obls_wo_array[attr] = obls_wo_array[attr][idx]
641
+ obls_wo_array
642
+ end
643
+ end.flatten
644
+ end
645
+ obligations
646
+ when NilClass
647
+ attr_validator.engine.obligations(@privilege,
648
+ :context => attr_validator.context,
649
+ :user => attr_validator.user)
650
+ else
651
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
652
+ end
653
+ end
654
+
655
+ def to_long_s
656
+ "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
657
+ end
658
+
659
+ private
660
+ def self.reflection_for_path (parent_model, path)
661
+ reflection = path.empty? ? parent_model : begin
662
+ parent = reflection_for_path(parent_model, path[0..-2])
663
+ if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
664
+ parent.klass.reflect_on_association(path.last)
665
+ else
666
+ parent.reflect_on_association(path.last)
667
+ end
668
+ rescue
669
+ parent.reflect_on_association(path.last)
670
+ end
671
+ raise "invalid path #{path.inspect}" if reflection.nil?
672
+ reflection
673
+ end
674
+ end
675
+
676
+ # Represents a pseudo-user to facilitate guest users in applications
677
+ class GuestUser
678
+ attr_reader :role_symbols
679
+ def initialize (roles = [:guest])
680
+ @role_symbols = roles
681
+ end
682
+ end
683
+ end