declarative_authorization 0.3.2.3

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 +510 -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 +634 -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 +597 -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 +386 -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 +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
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,634 @@
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
+ user ||= Authorization.current_user
233
+ raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.inspect})" \
234
+ if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
235
+
236
+ RAILS_DEFAULT_LOGGER.info("The use of user.roles is deprecated. Please add a method " +
237
+ "role_symbols to your User model.") if defined?(RAILS_DEFAULT_LOGGER) and !user.respond_to?(:role_symbols)
238
+
239
+ roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
240
+
241
+ raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
242
+ "doesn't return an Array of Symbols (#{roles.inspect})" \
243
+ if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
244
+
245
+ (roles.empty? ? [:guest] : roles)
246
+ end
247
+
248
+ # Returns the role symbols and inherritted role symbols for the given user
249
+ def roles_with_hierarchy_for(user)
250
+ flatten_roles(roles_for(user))
251
+ end
252
+
253
+ # Returns an instance of Engine, which is created if there isn't one
254
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
255
+ # a new instance is always created.
256
+ def self.instance (dsl_file = nil)
257
+ if dsl_file or ENV['RAILS_ENV'] == 'development'
258
+ @@instance = new(dsl_file)
259
+ else
260
+ @@instance ||= new
261
+ end
262
+ end
263
+
264
+ class AttributeValidator # :nodoc:
265
+ attr_reader :user, :object, :engine, :context, :privilege
266
+ def initialize (engine, user, object = nil, privilege = nil, context = nil)
267
+ @engine = engine
268
+ @user = user
269
+ @object = object
270
+ @privilege = privilege
271
+ @context = context
272
+ end
273
+
274
+ def evaluate (value_block)
275
+ # TODO cache?
276
+ instance_eval(&value_block)
277
+ end
278
+ end
279
+
280
+ private
281
+ def user_roles_privleges_from_options(privilege, options)
282
+ options = {
283
+ :user => nil,
284
+ :context => nil,
285
+ :user_roles => nil
286
+ }.merge(options)
287
+ user = options[:user] || Authorization.current_user
288
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
289
+
290
+ raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
291
+ "set through Authorization.current_user" unless user
292
+
293
+ roles = options[:user_roles] || flatten_roles(roles_for(user))
294
+ privileges = flatten_privileges privileges, options[:context]
295
+ [user, roles, privileges]
296
+ end
297
+
298
+ def flatten_roles (roles)
299
+ # TODO caching?
300
+ flattened_roles = roles.clone.to_a
301
+ flattened_roles.each do |role|
302
+ flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
303
+ end
304
+ end
305
+
306
+ # Returns the privilege hierarchy flattened for given privileges in context.
307
+ def flatten_privileges (privileges, context = nil)
308
+ # TODO caching?
309
+ #if context.nil?
310
+ # context = privileges.collect { |p| p.to_s.split('_') }.
311
+ # reject { |p_p| p_p.length < 2 }.
312
+ # collect { |p_p| (p_p[1..-1] * '_').to_sym }.first
313
+ # raise AuthorizationUsageError, "No context given or inferable from privileges #{privileges.inspect}" unless context
314
+ #end
315
+ raise AuthorizationUsageError, "No context given or inferable from object" unless context
316
+ #context_regex = Regexp.new "_#{context}$"
317
+ # TODO work with contextless privileges
318
+ #flattened_privileges = privileges.collect {|p| p.to_s.sub(context_regex, '')}
319
+ flattened_privileges = privileges.clone #collect {|p| p.to_s.end_with?(context.to_s) ?
320
+ # p : [p, "#{p}_#{context}".to_sym] }.flatten
321
+ flattened_privileges.each do |priv|
322
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
323
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
324
+ end
325
+ end
326
+
327
+ def matching_auth_rules (roles, privileges, context)
328
+ @auth_rules.select {|rule| rule.matches? roles, privileges, context}
329
+ end
330
+ end
331
+
332
+ class AuthorizationRule
333
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
334
+ :source_file, :source_line
335
+
336
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or,
337
+ options = {})
338
+ @role = role
339
+ @privileges = Set.new(privileges)
340
+ @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
341
+ @join_operator = join_operator
342
+ @attributes = []
343
+ @source_file = options[:source_file]
344
+ @source_line = options[:source_line]
345
+ end
346
+
347
+ def initialize_copy (from)
348
+ @privileges = @privileges.clone
349
+ @contexts = @contexts.clone
350
+ @attributes = @attributes.collect {|attribute| attribute.clone }
351
+ end
352
+
353
+ def append_privileges (privs)
354
+ @privileges.merge(privs)
355
+ end
356
+
357
+ def append_attribute (attribute)
358
+ @attributes << attribute
359
+ end
360
+
361
+ def matches? (roles, privs, context = nil)
362
+ roles = [roles] unless roles.is_a?(Array)
363
+ @contexts.include?(context) and roles.include?(@role) and
364
+ not (@privileges & privs).empty?
365
+ end
366
+
367
+ def validate? (attr_validator, skip_attribute = false)
368
+ skip_attribute or @attributes.empty? or
369
+ @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
370
+ begin
371
+ attr.validate?(attr_validator)
372
+ rescue NilAttributeValueError => e
373
+ nil # Bumping up against a nil attribute value flunks the rule.
374
+ end
375
+ end
376
+ end
377
+
378
+ def obligations (attr_validator)
379
+ obligations = @attributes.collect {|attr| attr.obligation(attr_validator) }.flatten
380
+ if @join_operator == :and and !obligations.empty?
381
+ merged_obligation = obligations.first
382
+ obligations[1..-1].each do |obligation|
383
+ merged_obligation = merged_obligation.deep_merge(obligation)
384
+ end
385
+ obligations = [merged_obligation]
386
+ end
387
+ obligations.empty? ? [{}] : obligations
388
+ end
389
+
390
+ def to_long_s
391
+ attributes.collect {|attr| attr.to_long_s } * "; "
392
+ end
393
+ end
394
+
395
+ class Attribute
396
+ # attr_conditions_hash of form
397
+ # { :object_attribute => [operator, value_block], ... }
398
+ # { :object_attribute => { :attr => ... } }
399
+ def initialize (conditions_hash)
400
+ @conditions_hash = conditions_hash
401
+ end
402
+
403
+ def initialize_copy (from)
404
+ @conditions_hash = deep_hash_clone(@conditions_hash)
405
+ end
406
+
407
+ def validate? (attr_validator, object = nil, hash = nil)
408
+ object ||= attr_validator.object
409
+ return false unless object
410
+
411
+ (hash || @conditions_hash).all? do |attr, value|
412
+ attr_value = object_attribute_value(object, attr)
413
+ if value.is_a?(Hash)
414
+ if attr_value.is_a?(Array)
415
+ raise AuthorizationUsageError, "Unable evaluate multiple attributes " +
416
+ "on a collection. Cannot use '=>' operator on #{attr.inspect} " +
417
+ "(#{attr_value.inspect}) for attributes #{value.inspect}."
418
+ elsif attr_value.nil?
419
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
420
+ end
421
+ validate?(attr_validator, attr_value, value)
422
+ elsif value.is_a?(Array) and value.length == 2
423
+ evaluated = if value[1].is_a?(Proc)
424
+ attr_validator.evaluate(value[1])
425
+ else
426
+ value[1]
427
+ end
428
+ case value[0]
429
+ when :is
430
+ attr_value == evaluated
431
+ when :is_not
432
+ attr_value != evaluated
433
+ when :contains
434
+ begin
435
+ attr_value.include?(evaluated)
436
+ rescue NoMethodError => e
437
+ raise AuthorizationUsageError, "Operator contains requires a " +
438
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
439
+ "contains #{evaluated.inspect}: #{e}"
440
+ end
441
+ when :does_not_contain
442
+ begin
443
+ !attr_value.include?(evaluated)
444
+ rescue NoMethodError => e
445
+ raise AuthorizationUsageError, "Operator does_not_contain requires a " +
446
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
447
+ "does_not_contain #{evaluated.inspect}: #{e}"
448
+ end
449
+ when :intersects_with
450
+ begin
451
+ !(evaluated.to_set & attr_value.to_set).empty?
452
+ rescue NoMethodError => e
453
+ raise AuthorizationUsageError, "Operator intersects_with requires " +
454
+ "subclasses of Enumerable, got: #{attr_value.inspect} " +
455
+ "intersects_with #{evaluated.inspect}: #{e}"
456
+ end
457
+ when :is_in
458
+ begin
459
+ evaluated.include?(attr_value)
460
+ rescue NoMethodError => e
461
+ raise AuthorizationUsageError, "Operator is_in requires a " +
462
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
463
+ "is_in #{evaluated.inspect}: #{e}"
464
+ end
465
+ when :is_not_in
466
+ begin
467
+ !evaluated.include?(attr_value)
468
+ rescue NoMethodError => e
469
+ raise AuthorizationUsageError, "Operator is_not_in requires a " +
470
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
471
+ "is_not_in #{evaluated.inspect}: #{e}"
472
+ end
473
+ else
474
+ raise AuthorizationError, "Unknown operator #{value[0]}"
475
+ end
476
+ else
477
+ raise AuthorizationError, "Wrong conditions hash format"
478
+ end
479
+ end
480
+ end
481
+
482
+ # resolves all the values in condition_hash
483
+ def obligation (attr_validator, hash = nil)
484
+ hash = (hash || @conditions_hash).clone
485
+ hash.each do |attr, value|
486
+ if value.is_a?(Hash)
487
+ hash[attr] = obligation(attr_validator, value)
488
+ elsif value.is_a?(Array) and value.length == 2
489
+ hash[attr] = [value[0], attr_validator.evaluate(value[1])]
490
+ else
491
+ raise AuthorizationError, "Wrong conditions hash format"
492
+ end
493
+ end
494
+ hash
495
+ end
496
+
497
+ def to_long_s (hash = nil)
498
+ if hash
499
+ hash.inject({}) do |memo, key_val|
500
+ key, val = key_val
501
+ memo[key] = case val
502
+ when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
503
+ when Hash then to_long_s(val)
504
+ end
505
+ memo
506
+ end
507
+ else
508
+ "if_attribute #{to_long_s(@conditions_hash).inspect}"
509
+ end
510
+ end
511
+
512
+ protected
513
+ def object_attribute_value (object, attr)
514
+ begin
515
+ object.send(attr)
516
+ rescue ArgumentError, NoMethodError => e
517
+ raise AuthorizationUsageError, "Error when calling #{attr} on " +
518
+ "#{object.inspect} for validating attribute: #{e}"
519
+ end
520
+ end
521
+
522
+ def deep_hash_clone (hash)
523
+ hash.inject({}) do |memo, (key, val)|
524
+ memo[key] = case val
525
+ when Hash
526
+ deep_hash_clone(val)
527
+ when NilClass, Symbol
528
+ val
529
+ else
530
+ val.clone
531
+ end
532
+ memo
533
+ end
534
+ end
535
+ end
536
+
537
+ # An attribute condition that uses existing rules to decide validation
538
+ # and create obligations.
539
+ class AttributeWithPermission < Attribute
540
+ # E.g. privilege :read, attr_or_hash either :attribute or
541
+ # { :attribute => :deeper_attribute }
542
+ def initialize (privilege, attr_or_hash, context = nil)
543
+ @privilege = privilege
544
+ @context = context
545
+ @attr_hash = attr_or_hash
546
+ end
547
+
548
+ def initialize_copy (from)
549
+ @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
550
+ end
551
+
552
+ def validate? (attr_validator, object = nil, hash_or_attr = nil)
553
+ object ||= attr_validator.object
554
+ hash_or_attr ||= @attr_hash
555
+ return false unless object
556
+
557
+ case hash_or_attr
558
+ when Symbol
559
+ attr_value = object_attribute_value(object, hash_or_attr)
560
+ if attr_value.nil?
561
+ raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
562
+ end
563
+ attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
564
+ when Hash
565
+ hash_or_attr.all? do |attr, sub_hash|
566
+ attr_value = object_attribute_value(object, attr)
567
+ if attr_value.nil?
568
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
569
+ end
570
+ validate?(attr_validator, attr_value, sub_hash)
571
+ end
572
+ when NilClass
573
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
574
+ else
575
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
576
+ end
577
+ end
578
+
579
+ # may return an array of obligations to be OR'ed
580
+ def obligation (attr_validator, hash_or_attr = nil)
581
+ hash_or_attr ||= @attr_hash
582
+ case hash_or_attr
583
+ when Symbol
584
+ obligations = attr_validator.engine.obligations(@privilege,
585
+ :context => @context || hash_or_attr.to_s.pluralize.to_sym,
586
+ :user => attr_validator.user)
587
+ obligations.collect {|obl| {hash_or_attr => obl} }
588
+ when Hash
589
+ obligations_array_attrs = []
590
+ obligations =
591
+ hash_or_attr.inject({}) do |all, pair|
592
+ attr, sub_hash = pair
593
+ all[attr] = obligation(attr_validator, sub_hash)
594
+ if all[attr].length > 1
595
+ obligations_array_attrs << attr
596
+ else
597
+ all[attr] = all[attr].first
598
+ end
599
+ all
600
+ end
601
+ obligations = [obligations]
602
+ obligations_array_attrs.each do |attr|
603
+ next_array_size = obligations.first[attr].length
604
+ obligations = obligations.collect do |obls|
605
+ (0...next_array_size).collect do |idx|
606
+ obls_wo_array = obls.clone
607
+ obls_wo_array[attr] = obls_wo_array[attr][idx]
608
+ obls_wo_array
609
+ end
610
+ end.flatten
611
+ end
612
+ obligations
613
+ when NilClass
614
+ attr_validator.engine.obligations(@privilege,
615
+ :context => attr_validator.context,
616
+ :user => attr_validator.user)
617
+ else
618
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
619
+ end
620
+ end
621
+
622
+ def to_long_s
623
+ "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
624
+ end
625
+ end
626
+
627
+ # Represents a pseudo-user to facilitate guest users in applications
628
+ class GuestUser
629
+ attr_reader :role_symbols
630
+ def initialize (roles = [:guest])
631
+ @role_symbols = roles
632
+ end
633
+ end
634
+ end