graylog2-declarative_authorization 0.5.2

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