timcharper-declarative_authorization 0.4.1.2

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 +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