tarsolya-declarative_authorization 0.4.1

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