graylog2-declarative_authorization 0.5.2

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 (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
+