ghart-declarative_authorization 0.3.2.4

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 +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 +106 -0
@@ -0,0 +1,20 @@
1
+ authorization do
2
+ role :guest do
3
+ # add permissions for guests here, e.g.
4
+ #has_permission_on :conferences, :to => :read
5
+ end
6
+
7
+ # permissions on other roles, such as
8
+ #role :admin do
9
+ # has_permission_on :conferences, :to => :manage
10
+ #end
11
+ end
12
+
13
+ privileges do
14
+ # default privilege hierarchies to facilitate RESTful Rails apps
15
+ privilege :manage, :includes => [:create, :read, :update, :delete]
16
+ privilege :read, :includes => [:index, :show]
17
+ privilege :create, :includes => :new
18
+ privilege :update, :includes => :edit
19
+ privilege :delete, :includes => :destroy
20
+ end
@@ -0,0 +1,7 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ if Authorization::activate_authorization_rules_browser?
3
+ map.resources :authorization_rules, :only => [:index],
4
+ :collection => {:graph => :get, :change => :get, :suggest_change => :get}
5
+ map.resources :authorization_usages, :only => :index
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ garlic do
2
+ repo 'rails', :url => 'git://github.com/rails/rails'#, :local => "~/dev/vendor/rails"
3
+ repo 'declarative_authorization', :path => '.'
4
+
5
+ target 'edge'
6
+ target '2.1-stable', :branch => 'origin/2-1-stable'
7
+ target '2.2.0-RC1', :tag => 'v2.2.0'
8
+
9
+ all_targets do
10
+ prepare do
11
+ plugin 'declarative_authorization', :clone => true
12
+ end
13
+
14
+ run do
15
+ cd "vendor/plugins/declarative_authorization" do
16
+ sh "rake"
17
+ end
18
+ end
19
+ end
20
+ end
data/init.rb ADDED
@@ -0,0 +1,5 @@
1
+ begin
2
+ require File.join(File.dirname(__FILE__), 'lib', 'declarative_authorization') # From here
3
+ rescue LoadError
4
+ require 'declarative_authorization' # From gem
5
+ end
@@ -0,0 +1,15 @@
1
+ require File.join(%w{declarative_authorization rails_legacy})
2
+ require File.join(%w{declarative_authorization helper})
3
+ require File.join(%w{declarative_authorization in_controller})
4
+ require File.join(%w{declarative_authorization in_model})
5
+ require File.join(%w{declarative_authorization obligation_scope})
6
+
7
+ min_rails_version = "2.1.0"
8
+ if Rails::VERSION::STRING < min_rails_version
9
+ raise "declarative_authorization requires Rails #{min_rails_version}. You are using #{Rails::VERSION::STRING}."
10
+ end
11
+
12
+ ActionController::Base.send :include, Authorization::AuthorizationInController
13
+ ActionController::Base.helper Authorization::AuthorizationHelper
14
+
15
+ ActiveRecord::Base.send :include, Authorization::AuthorizationInModel
@@ -0,0 +1,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