zeiv-declarative_authorization 1.0.0.pre

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