zeiv-declarative_authorization 1.0.0.pre

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