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