ae_declarative_authorization 0.7.1 → 0.8.0

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 (56) hide show
  1. checksums.yaml +5 -5
  2. data/Appraisals +31 -21
  3. data/CHANGELOG +189 -189
  4. data/Gemfile +7 -7
  5. data/Gemfile.lock +68 -60
  6. data/LICENSE.txt +20 -20
  7. data/README.md +620 -620
  8. data/README.rdoc +597 -597
  9. data/Rakefile +35 -33
  10. data/authorization_rules.dist.rb +20 -20
  11. data/declarative_authorization.gemspec +24 -24
  12. data/gemfiles/rails4252.gemfile +10 -10
  13. data/gemfiles/rails4252.gemfile.lock +126 -0
  14. data/gemfiles/rails4271.gemfile +10 -10
  15. data/gemfiles/rails4271.gemfile.lock +126 -0
  16. data/gemfiles/rails507.gemfile +11 -11
  17. data/gemfiles/rails507.gemfile.lock +136 -0
  18. data/gemfiles/rails516.gemfile +11 -0
  19. data/gemfiles/rails516.gemfile.lock +136 -0
  20. data/gemfiles/rails521.gemfile +11 -0
  21. data/gemfiles/rails521.gemfile.lock +144 -0
  22. data/init.rb +5 -5
  23. data/lib/declarative_authorization.rb +18 -18
  24. data/lib/declarative_authorization/authorization.rb +821 -821
  25. data/lib/declarative_authorization/helper.rb +78 -78
  26. data/lib/declarative_authorization/in_controller.rb +713 -713
  27. data/lib/declarative_authorization/in_model.rb +156 -156
  28. data/lib/declarative_authorization/maintenance.rb +215 -215
  29. data/lib/declarative_authorization/obligation_scope.rb +348 -345
  30. data/lib/declarative_authorization/railsengine.rb +5 -5
  31. data/lib/declarative_authorization/reader.rb +549 -549
  32. data/lib/declarative_authorization/test/helpers.rb +261 -261
  33. data/lib/declarative_authorization/version.rb +3 -3
  34. data/lib/generators/authorization/install/install_generator.rb +77 -77
  35. data/lib/generators/authorization/rules/rules_generator.rb +13 -13
  36. data/lib/generators/authorization/rules/templates/authorization_rules.rb +27 -27
  37. data/lib/tasks/authorization_tasks.rake +89 -89
  38. data/log/test.log +15246 -0
  39. data/pkg/ae_declarative_authorization-0.7.1.gem +0 -0
  40. data/pkg/ae_declarative_authorization-0.8.0.gem +0 -0
  41. data/test/authorization_test.rb +1121 -1121
  42. data/test/controller_filter_resource_access_test.rb +573 -573
  43. data/test/controller_test.rb +478 -478
  44. data/test/database.yml +3 -3
  45. data/test/dsl_reader_test.rb +178 -178
  46. data/test/functional/filter_access_to_with_id_in_scope_test.rb +88 -88
  47. data/test/functional/no_filter_access_to_test.rb +79 -79
  48. data/test/functional/params_block_arity_test.rb +39 -39
  49. data/test/helper_test.rb +248 -248
  50. data/test/maintenance_test.rb +46 -46
  51. data/test/model_test.rb +1840 -1840
  52. data/test/profiles/access_checking +20 -0
  53. data/test/schema.sql +60 -60
  54. data/test/test_helper.rb +174 -174
  55. data/test/test_support/minitest_compatibility.rb +26 -26
  56. metadata +17 -5
@@ -1,78 +1,78 @@
1
- # Authorization::AuthorizationHelper
2
- require File.dirname(__FILE__) + '/authorization.rb'
3
-
4
- module Authorization
5
- module AuthorizationHelper
6
-
7
- # If the current user meets the given privilege, permitted_to? returns true
8
- # and yields to the optional block. The attribute checks that are defined
9
- # in the authorization rules are only evaluated if an object is given
10
- # for context.
11
- #
12
- # Examples:
13
- # <% permitted_to? :create, :users do %>
14
- # <%= link_to 'New', new_user_path %>
15
- # <% end %>
16
- # ...
17
- # <% if permitted_to? :create, :users %>
18
- # <%= link_to 'New', new_user_path %>
19
- # <% else %>
20
- # You are not allowed to create new users!
21
- # <% end %>
22
- # ...
23
- # <% for user in @users %>
24
- # <%= link_to 'Edit', edit_user_path(user) if permitted_to? :update, user %>
25
- # <% end %>
26
- #
27
- # To pass in an object and override the context, you can use the optional
28
- # options:
29
- # permitted_to? :update, user, :context => :account
30
- #
31
- def permitted_to?(privilege, object_or_sym = nil, options = {})
32
- controller.permitted_to?(privilege, object_or_sym, options) do
33
- yield if block_given?
34
- end
35
- end
36
-
37
- # While permitted_to? is used for authorization in views, in some cases
38
- # content should only be shown to some users without being concerned
39
- # with authorization. E.g. to only show the most relevant menu options
40
- # to a certain group of users. That is what has_role? should be used for.
41
- #
42
- # Examples:
43
- # <% has_role?(:sales) do %>
44
- # <%= link_to 'All contacts', contacts_path %>
45
- # <% end %>
46
- # ...
47
- # <% if has_role?(:sales) %>
48
- # <%= link_to 'Customer contacts', contacts_path %>
49
- # <% else %>
50
- # ...
51
- # <% end %>
52
- #
53
- def has_role?(*roles)
54
- controller.has_role?(*roles) do
55
- yield if block_given?
56
- end
57
- end
58
-
59
- # As has_role? except checks all roles included in the role hierarchy
60
- def has_role_with_hierarchy?(*roles)
61
- controller.has_role_with_hierarchy?(*roles) do
62
- yield if block_given?
63
- end
64
- end
65
-
66
- def has_any_role?(*roles)
67
- controller.has_any_role?(*roles) do
68
- yield if block_given?
69
- end
70
- end
71
-
72
- def has_any_role_with_hierarchy?(*roles)
73
- controller.has_any_role_with_hierarchy?(*roles) do
74
- yield if block_given?
75
- end
76
- end
77
- end
78
- end
1
+ # Authorization::AuthorizationHelper
2
+ require File.dirname(__FILE__) + '/authorization.rb'
3
+
4
+ module Authorization
5
+ module AuthorizationHelper
6
+
7
+ # If the current user meets the given privilege, permitted_to? returns true
8
+ # and yields to the optional block. The attribute checks that are defined
9
+ # in the authorization rules are only evaluated if an object is given
10
+ # for context.
11
+ #
12
+ # Examples:
13
+ # <% permitted_to? :create, :users do %>
14
+ # <%= link_to 'New', new_user_path %>
15
+ # <% end %>
16
+ # ...
17
+ # <% if permitted_to? :create, :users %>
18
+ # <%= link_to 'New', new_user_path %>
19
+ # <% else %>
20
+ # You are not allowed to create new users!
21
+ # <% end %>
22
+ # ...
23
+ # <% for user in @users %>
24
+ # <%= link_to 'Edit', edit_user_path(user) if permitted_to? :update, user %>
25
+ # <% end %>
26
+ #
27
+ # To pass in an object and override the context, you can use the optional
28
+ # options:
29
+ # permitted_to? :update, user, :context => :account
30
+ #
31
+ def permitted_to?(privilege, object_or_sym = nil, options = {})
32
+ controller.permitted_to?(privilege, object_or_sym, options) do
33
+ yield if block_given?
34
+ end
35
+ end
36
+
37
+ # While permitted_to? is used for authorization in views, in some cases
38
+ # content should only be shown to some users without being concerned
39
+ # with authorization. E.g. to only show the most relevant menu options
40
+ # to a certain group of users. That is what has_role? should be used for.
41
+ #
42
+ # Examples:
43
+ # <% has_role?(:sales) do %>
44
+ # <%= link_to 'All contacts', contacts_path %>
45
+ # <% end %>
46
+ # ...
47
+ # <% if has_role?(:sales) %>
48
+ # <%= link_to 'Customer contacts', contacts_path %>
49
+ # <% else %>
50
+ # ...
51
+ # <% end %>
52
+ #
53
+ def has_role?(*roles)
54
+ controller.has_role?(*roles) do
55
+ yield if block_given?
56
+ end
57
+ end
58
+
59
+ # As has_role? except checks all roles included in the role hierarchy
60
+ def has_role_with_hierarchy?(*roles)
61
+ controller.has_role_with_hierarchy?(*roles) do
62
+ yield if block_given?
63
+ end
64
+ end
65
+
66
+ def has_any_role?(*roles)
67
+ controller.has_any_role?(*roles) do
68
+ yield if block_given?
69
+ end
70
+ end
71
+
72
+ def has_any_role_with_hierarchy?(*roles)
73
+ controller.has_any_role_with_hierarchy?(*roles) do
74
+ yield if block_given?
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,713 +1,713 @@
1
- # Authorization::AuthorizationInController
2
- require File.dirname(__FILE__) + '/authorization.rb'
3
-
4
- module Authorization
5
- module AuthorizationInController
6
-
7
- def self.included(base) # :nodoc:
8
- base.extend(ClassMethods)
9
- base.module_eval do
10
- before_action(:filter_access_filter) if method_defined?(:filter_access_filter)
11
- end
12
- end
13
-
14
- DEFAULT_DENY = false
15
-
16
- # If attribute_check is set for filter_access_to, decl_auth_context will try to
17
- # load the appropriate object from the current controller's model with
18
- # the id from params[:id]. If that fails, a 404 Not Found is often the
19
- # right way to handle the error. If you have additional measures in place
20
- # that restricts the find scope, handling this error as a permission denied
21
- # might be a better way. Set failed_auto_loading_is_not_found to false
22
- # for the latter behavior.
23
- @@failed_auto_loading_is_not_found = true
24
- def self.failed_auto_loading_is_not_found?
25
- @@failed_auto_loading_is_not_found
26
- end
27
- def self.failed_auto_loading_is_not_found=(new_value)
28
- @@failed_auto_loading_is_not_found = new_value
29
- end
30
-
31
- # Returns the Authorization::Engine for the current controller.
32
- def authorization_engine
33
- @authorization_engine ||= Authorization::Engine.instance
34
- end
35
-
36
- # If the current user meets the given privilege, permitted_to? returns true
37
- # and yields to the optional block. The attribute checks that are defined
38
- # in the authorization rules are only evaluated if an object is given
39
- # for context.
40
- #
41
- # See examples for Authorization::AuthorizationHelper #permitted_to?
42
- #
43
- # If no object or context is specified, the controller_name is used as
44
- # context.
45
- #
46
- def permitted_to?(privilege, object_or_sym = nil, options = {})
47
- if authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, false))
48
- yield if block_given?
49
- true
50
- else
51
- false
52
- end
53
- end
54
-
55
- # Works similar to the permitted_to? method, but
56
- # throws the authorization exceptions, just like Engine#permit!
57
- def permitted_to!(privilege, object_or_sym = nil, options = {})
58
- authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, true))
59
- end
60
-
61
- # While permitted_to? is used for authorization, in some cases
62
- # content should only be shown to some users without being concerned
63
- # with authorization. E.g. to only show the most relevant menu options
64
- # to a certain group of users. That is what has_role? should be used for.
65
- def has_role?(*roles)
66
- user_roles = authorization_engine.roles_for(current_user)
67
- result = roles.all? do |role|
68
- user_roles.include?(role)
69
- end
70
- yield if result and block_given?
71
- result
72
- end
73
-
74
- # Intended to be used where you want to allow users with any single listed role to view
75
- # the content in question
76
- def has_any_role?(*roles)
77
- user_roles = authorization_engine.roles_for(current_user)
78
- result = roles.any? do |role|
79
- user_roles.include?(role)
80
- end
81
- yield if result and block_given?
82
- result
83
- end
84
-
85
- # As has_role? except checks all roles included in the role hierarchy
86
- def has_role_with_hierarchy?(*roles)
87
- user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
88
- result = roles.all? do |role|
89
- user_roles.include?(role)
90
- end
91
- yield if result and block_given?
92
- result
93
- end
94
-
95
- # As has_any_role? except checks all roles included in the role hierarchy
96
- def has_any_role_with_hierarchy?(*roles)
97
- user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
98
- result = roles.any? do |role|
99
- user_roles.include?(role)
100
- end
101
- yield if result and block_given?
102
- result
103
- end
104
-
105
- protected
106
- def filter_access_filter # :nodoc:
107
- permissions = self.class.all_filter_access_permissions
108
- all_permissions = permissions.select {|p| p.actions.include?(:all)}
109
- matching_permissions = permissions.select {|p| p.matches?(action_name)}
110
- allowed = false
111
- auth_exception = nil
112
- begin
113
- allowed = if !matching_permissions.empty?
114
- matching_permissions.all? {|perm| perm.permit!(self)}
115
- elsif !all_permissions.empty?
116
- all_permissions.all? {|perm| perm.permit!(self)}
117
- else
118
- !DEFAULT_DENY
119
- end
120
- rescue NotAuthorized => e
121
- auth_exception = e
122
- end
123
-
124
- unless allowed
125
- if all_permissions.empty? and matching_permissions.empty?
126
- logger.warn "Permission denied: No matching filter access " +
127
- "rule found for #{self.class.controller_name}.#{action_name}"
128
- elsif auth_exception
129
- logger.info "Permission denied: #{auth_exception}"
130
- end
131
- if respond_to?(:permission_denied, true)
132
- # permission_denied needs to render or redirect
133
- send(:permission_denied)
134
- else
135
- send(:render, :plain => "You are not allowed to access this action.",
136
- :status => :forbidden)
137
- end
138
- end
139
- end
140
-
141
- def load_controller_object(context_without_namespace = nil, model = nil) # :nodoc:
142
- instance_var = :"@#{context_without_namespace.to_s.singularize}"
143
- model = model ? model.classify.constantize : context_without_namespace.to_s.classify.constantize
144
- instance_variable_set(instance_var, model.find(params[:id]))
145
- end
146
-
147
- def load_parent_controller_object(parent_context_without_namespace) # :nodoc:
148
- instance_var = :"@#{parent_context_without_namespace.to_s.singularize}"
149
- model = parent_context_without_namespace.to_s.classify.constantize
150
- instance_variable_set(instance_var, model.find(params[:"#{parent_context_without_namespace.to_s.singularize}_id"]))
151
- end
152
-
153
- def new_controller_object_from_params(context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc:
154
- model_or_proxy = parent_context_without_namespace ?
155
- instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
156
- context_without_namespace.to_s.classify.constantize
157
- instance_var = :"@#{context_without_namespace.to_s.singularize}"
158
- instance_variable_set(instance_var,
159
- model_or_proxy.new(params[context_without_namespace.to_s.singularize]))
160
- end
161
-
162
- def new_blank_controller_object(context_without_namespace, parent_context_without_namespace, strong_params, model) # :nodoc:
163
- if model
164
- model_or_proxy = model.to_s.classify.constantize
165
- else
166
- model_or_proxy = parent_context_without_namespace ?
167
- instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
168
- context_without_namespace.to_s.classify.constantize
169
- end
170
- instance_var = :"@#{context_without_namespace.to_s.singularize}"
171
- instance_variable_set(instance_var,
172
- model_or_proxy.new())
173
- end
174
-
175
- def new_controller_object_for_collection(context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc:
176
- model_or_proxy = parent_context_without_namespace ?
177
- instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
178
- context_without_namespace.to_s.classify.constantize
179
- instance_var = :"@#{context_without_namespace.to_s.singularize}"
180
- instance_variable_set(instance_var, model_or_proxy.new)
181
- end
182
-
183
- def options_for_permit(object_or_sym = nil, options = {}, bang = true)
184
- context = object = nil
185
- if object_or_sym.nil?
186
- context = self.class.decl_auth_context
187
- elsif !Authorization.is_a_association_proxy?(object_or_sym) and object_or_sym.is_a?(Symbol)
188
- context = object_or_sym
189
- else
190
- object = object_or_sym
191
- end
192
-
193
- result = {:object => object,
194
- :context => context,
195
- :skip_attribute_test => object.nil?,
196
- :bang => bang}.merge(options)
197
- result[:user] = current_user unless result.key?(:user)
198
- result
199
- end
200
-
201
- module ClassMethods
202
- #
203
- # Defines a filter to be applied according to the authorization of the
204
- # current user. Requires at least one symbol corresponding to an
205
- # action as parameter. The special symbol :+all+ refers to all actions.
206
- # The all :+all+ statement is only employed if no specific statement is
207
- # present.
208
- # class UserController < ApplicationController
209
- # filter_access_to :index
210
- # filter_access_to :new, :edit
211
- # filter_access_to :all
212
- # ...
213
- # end
214
- #
215
- # The default is to allow access unconditionally if no rule matches.
216
- # Thus, including the +filter_access_to+ :+all+ statement is a good
217
- # idea, implementing a default-deny policy.
218
- #
219
- # When the access is denied, the method +permission_denied+ is called
220
- # on the current controller, if defined. Else, a simple "you are not
221
- # allowed" string is output. Log.info is given more information on the
222
- # reasons of denial.
223
- #
224
- # def permission_denied
225
- # flash[:error] = 'Sorry, you are not allowed to the requested page.'
226
- # respond_to do |format|
227
- # format.html { redirect_to(:back) rescue redirect_to('/') }
228
- # format.xml { head :unauthorized }
229
- # format.js { head :unauthorized }
230
- # end
231
- # end
232
- #
233
- # By default, required privileges are inferred from the action name and
234
- # the controller name. Thus, in UserController :+edit+ requires
235
- # :+edit+ +users+. To specify required privilege, use the option :+require+
236
- # filter_access_to :new, :create, :require => :create, :context => :users
237
- #
238
- # Without the :+attribute_check+ option, no constraints from the
239
- # authorization rules are enforced because for some actions (collections,
240
- # +new+, +create+), there is no object to evaluate conditions against. To
241
- # allow attribute checks on all actions, it is a common pattern to provide
242
- # custom objects through +before_actions+:
243
- # class BranchesController < ApplicationController
244
- # before_action :load_company
245
- # before_action :new_branch_from_company_and_params,
246
- # :only => [:index, :new, :create]
247
- # filter_access_to :all, :attribute_check => true
248
- #
249
- # protected
250
- # def new_branch_from_company_and_params
251
- # @branch = @company.branches.new(params[:branch])
252
- # end
253
- # end
254
- # NOTE: +before_actions+ need to be defined before the first
255
- # +filter_access_to+ call.
256
- #
257
- # For further customization, a custom filter expression may be formulated
258
- # in a block, which is then evaluated in the context of the controller
259
- # on a matching request. That is, for checking two objects, use the
260
- # following:
261
- # filter_access_to :merge do
262
- # permitted_to!(:update, User.find(params[:original_id])) and
263
- # permitted_to!(:delete, User.find(params[:id]))
264
- # end
265
- # The block should raise a Authorization::AuthorizationError or return
266
- # false if the access is to be denied.
267
- #
268
- # Later calls to filter_access_to with overlapping actions overwrite
269
- # previous ones for that action.
270
- #
271
- # All options:
272
- # [:+require+]
273
- # Privilege required; defaults to action_name
274
- # [:+context+]
275
- # The privilege's context, defaults to decl_auth_context, which consists
276
- # of controller_name, prepended by any namespaces
277
- # [:+attribute_check+]
278
- # Enables the check of attributes defined in the authorization rules.
279
- # Defaults to false. If enabled, filter_access_to will use a context
280
- # object from one of the following sources (in that order):
281
- # * the method from the :+load_method+ option,
282
- # * an instance variable named after the singular of the context
283
- # (by default from the controller name, e.g. @post for PostsController),
284
- # * a find on the context model, using +params+[:id] as id value.
285
- # Any of these methods will only be employed if :+attribute_check+
286
- # is enabled.
287
- # [:+model+]
288
- # The data model to load a context object from. Defaults to the
289
- # context, singularized.
290
- # [:+load_method+]
291
- # Specify a method by symbol or a Proc object which should be used
292
- # to load the object. Both should return the loaded object.
293
- # If a Proc object is given, e.g. by way of
294
- # +lambda+, it is called in the instance of the controller.
295
- # Example demonstrating the default behavior:
296
- # filter_access_to :show, :attribute_check => true,
297
- # :load_method => lambda { User.find(params[:id]) }
298
- #
299
-
300
- def filter_access_to(*args, &filter_block)
301
- options = args.last.is_a?(Hash) ? args.pop : {}
302
- options = {
303
- :require => nil,
304
- :context => nil,
305
- :attribute_check => false,
306
- :model => nil,
307
- :load_method => nil,
308
- :strong_parameters => nil
309
- }.merge!(options)
310
- privilege = options[:require]
311
- context = options[:context]
312
- actions = args.flatten
313
-
314
- # prevent setting filter_access_filter multiple times
315
- skip_before_action(:filter_access_filter) if method_defined?(:filter_access_filter)
316
- before_action :filter_access_filter
317
-
318
- filter_access_permissions.each do |perm|
319
- perm.remove_actions(actions)
320
- end
321
- filter_access_permissions <<
322
- ControllerPermission.new(actions, privilege, context,
323
- options[:strong_parameters],
324
- options[:attribute_check],
325
- options[:model],
326
- options[:load_method],
327
- filter_block)
328
- end
329
-
330
- # Disables authorization entirely. Requires at least one symbol corresponding
331
- # to an action as parameter. The special symbol :+all+ refers to all actions.
332
- # The all :+all+ statement is only employed if no specific statement is
333
- # present.
334
- def no_filter_access_to(*args)
335
- filter_access_to args do
336
- true
337
- end
338
- end
339
-
340
- # Collecting all the ControllerPermission objects from the controller
341
- # hierarchy. Permissions for actions are overwritten by calls to
342
- # filter_access_to in child controllers with the same action.
343
- def all_filter_access_permissions # :nodoc:
344
- ancestors.inject([]) do |perms, mod|
345
- if mod.respond_to?(:filter_access_permissions, true)
346
- perms +
347
- mod.filter_access_permissions.collect do |p1|
348
- p1.clone.remove_actions(perms.inject(Set.new) {|actions, p2| actions + p2.actions})
349
- end
350
- else
351
- perms
352
- end
353
- end
354
- end
355
-
356
- # To DRY up the filter_access_to statements in restful controllers,
357
- # filter_resource_access combines typical filter_access_to and
358
- # before_action calls, which set up the instance variables.
359
- #
360
- # The simplest case are top-level resource controllers with only the
361
- # seven CRUD methods, e.g.
362
- # class CompanyController < ApplicationController
363
- # filter_resource_access
364
- #
365
- # def index...
366
- # end
367
- # Here, all CRUD actions are protected through a filter_access_to :all
368
- # statement. :+attribute_check+ is enabled for all actions except for
369
- # the collection action :+index+. To have an object for attribute checks
370
- # available, filter_resource_access will set the instance variable
371
- # @+company+ in before filters. For the member actions (:+show+, :+edit+,
372
- # :+update+, :+destroy+) @company is set to Company.find(params[:id]).
373
- # For +new+ actions (:+new+, :+create+), filter_resource_access creates
374
- # a new object from company parameters: Company.new(params[:company].
375
- #
376
- # For nested resources, the parent object may be loaded automatically.
377
- # class BranchController < ApplicationController
378
- # filter_resource_access :nested_in => :companies
379
- # end
380
- # Again, the CRUD actions are protected. Now, for all CRUD actions,
381
- # the parent object @company is loaded from params[:company_id]. It is
382
- # also used when creating @branch for +new+ actions. Here, attribute_check
383
- # is enabled for the collection :+index+ as well, checking attributes on a
384
- # @company.branches.new method.
385
- #
386
- # In many cases, the default seven CRUD actions are not sufficient. As in
387
- # the resource definition for routing you may thus give additional member,
388
- # new and collection methods. The +options+ allow you to specify the
389
- # required privileges for each action by providing a hash or an array of
390
- # pairs. By default, for each action the action name is taken as privilege
391
- # (action search in the example below requires the privilege :index
392
- # :companies). Any controller action that is not specified and does not
393
- # belong to the seven CRUD actions is handled as a member method.
394
- # class CompanyController < ApplicationController
395
- # filter_resource_access :collection => [[:search, :index], :index],
396
- # :additional_member => {:mark_as_key_company => :update}
397
- # end
398
- # The +additional_+* options add to the respective CRUD actions,
399
- # the other options (:+member+, :+collection+, :+new+) replace their
400
- # respective CRUD actions.
401
- # filter_resource_access :member => { :toggle_open => :update }
402
- # Would declare :toggle_open as the only member action in the controller and
403
- # require that permission :update is granted for the current user.
404
- # filter_resource_access :additional_member => { :toggle_open => :update }
405
- # Would add a member action :+toggle_open+ to the default members, such as :+show+.
406
- #
407
- # If :+collection+ is an array of method names filter_resource_access will
408
- # associate a permission with the method that is the same as the method
409
- # name and no attribute checks will be performed unless
410
- # :attribute_check => true
411
- # is added in the options.
412
- #
413
- # You can override the default object loading by implementing any of the
414
- # following instance methods on the controller. Examples are given for the
415
- # BranchController (with +nested_in+ set to :+companies+):
416
- # [+new_branch_from_params+]
417
- # Used for +new+ actions.
418
- # [+new_branch_for_collection+]
419
- # Used for +collection+ actions if the +nested_in+ option is set.
420
- # [+load_branch+]
421
- # Used for +member+ actions.
422
- # [+load_company+]
423
- # Used for all +new+, +member+, and +collection+ actions if the
424
- # +nested_in+ option is set.
425
- #
426
- # All options:
427
- # [:+member+]
428
- # Member methods are actions like +show+, which have an params[:id] from
429
- # which to load the controller object and assign it to @controller_name,
430
- # e.g. @+branch+.
431
- #
432
- # By default, member actions are [:+show+, :+edit+, :+update+,
433
- # :+destroy+]. Also, any action not belonging to the seven CRUD actions
434
- # are handled as member actions.
435
- #
436
- # There are three different syntax to specify member, collection and
437
- # new actions.
438
- # * Hash: Lets you set the required privilege for each action:
439
- # {:+show+ => :+show+, :+mark_as_important+ => :+update+}
440
- # * Array of actions or pairs: [:+show+, [:+mark_as_important+, :+update+]],
441
- # with single actions requiring the privilege of the same name as the method.
442
- # * Single method symbol: :+show+
443
- # [:+additional_member+]
444
- # Allows to add additional member actions to the default resource +member+
445
- # actions.
446
- # [:+collection+]
447
- # Collection actions are like :+index+, actions without any controller object
448
- # to check attributes of. If +nested_in+ is given, a new object is
449
- # created from the parent object, e.g. @company.branches.new. Without
450
- # +nested_in+, attribute check is deactivated for these actions. By
451
- # default, collection is set to :+index+.
452
- # [:+additional_collection+]
453
- # Allows to add additional collection actions to the default resource +collection+
454
- # actions.
455
- # [:+new+]
456
- # +new+ methods are actions such as +new+ and +create+, which don't
457
- # receive a params[:id] to load an object from, but
458
- # a params[:controller_name_singular] hash with attributes for a new
459
- # object. The attributes will be used here to create a new object and
460
- # check the object against the authorization rules. The object is
461
- # assigned to @controller_name_singular, e.g. @branch.
462
- #
463
- # If +nested_in+ is given, the new object
464
- # is created from the parent_object.controller_name
465
- # proxy, e.g. company.branches.new(params[:branch]). By default,
466
- # +new+ is set to [:new, :create].
467
- # [:+additional_new+]
468
- # Allows to add additional new actions to the default resource +new+ actions.
469
- # [:+context+]
470
- # The context is used to determine the model to load objects from for the
471
- # before_actions and the context of privileges to use in authorization
472
- # checks.
473
- # [:+nested_in+]
474
- # Specifies the parent controller if the resource is nested in another
475
- # one. This is used to automatically load the parent object, e.g.
476
- # @+company+ from params[:company_id] for a BranchController nested in
477
- # a CompanyController.
478
- # [:+shallow+]
479
- # Only relevant when used in conjunction with +nested_in+. Specifies a nested resource
480
- # as being a shallow nested resource, resulting in the controller not attempting to
481
- # load a parent object for all member actions defined by +member+ and
482
- # +additional_member+ or rather the default member actions (:+show+, :+edit+,
483
- # :+update+, :+destroy+).
484
- # [:+no_attribute_check+]
485
- # Allows to set actions for which no attribute check should be performed.
486
- # See filter_access_to on details. By default, with no +nested_in+,
487
- # +no_attribute_check+ is set to all collections. If +nested_in+ is given
488
- # +no_attribute_check+ is empty by default.
489
- # [:+strong_parameters+]
490
- # If set to true, relies on controller to provide instance variable and
491
- # create new object in :create action. Set true if you use strong_params
492
- # and false if you use protected_attributes.
493
- #
494
- def filter_resource_access(options = {})
495
- options = {
496
- :new => [:new, :create],
497
- :additional_new => nil,
498
- :member => [:show, :edit, :update, :destroy],
499
- :additional_member => nil,
500
- :collection => [:index],
501
- :additional_collection => nil,
502
- #:new_method_for_collection => nil, # only symbol method name
503
- #:new_method => nil, # only symbol method name
504
- #:load_method => nil, # only symbol method name
505
- :no_attribute_check => nil,
506
- :context => nil,
507
- :model => nil,
508
- :nested_in => nil,
509
- :strong_parameters => nil
510
- }.merge(options)
511
- options.merge!({ :strong_parameters => true }) if options[:strong_parameters] == nil
512
-
513
- new_actions = actions_from_option( options[:new] ).merge(
514
- actions_from_option(options[:additional_new]) )
515
- members = actions_from_option(options[:member]).merge(
516
- actions_from_option(options[:additional_member]))
517
- collections = actions_from_option(options[:collection]).merge(
518
- actions_from_option(options[:additional_collection]))
519
-
520
- no_attribute_check_actions = options[:strong_parameters] ? actions_from_option(options[:collection]).merge(actions_from_option([:create])) : collections
521
-
522
- options[:no_attribute_check] ||= no_attribute_check_actions.keys unless options[:nested_in]
523
-
524
- unless options[:nested_in].blank?
525
- load_parent_method = :"load_#{options[:nested_in].to_s.singularize}"
526
- shallow_exceptions = options[:shallow] ? {:except => members.keys} : {}
527
- before_action shallow_exceptions do |controller|
528
- if controller.respond_to?(load_parent_method, true)
529
- controller.send(load_parent_method)
530
- else
531
- controller.send(:load_parent_controller_object, options[:nested_in])
532
- end
533
- end
534
-
535
- new_for_collection_method = :"new_#{controller_name.singularize}_for_collection"
536
- before_action :only => collections.keys do |controller|
537
- # new_for_collection
538
- if controller.respond_to?(new_for_collection_method, true)
539
- controller.send(new_for_collection_method)
540
- else
541
- controller.send(:new_controller_object_for_collection,
542
- options[:context] || controller_name, options[:nested_in], options[:strong_parameters])
543
- end
544
- end
545
- end
546
-
547
- unless options[:strong_parameters]
548
- new_from_params_method = :"new_#{controller_name.singularize}_from_params"
549
- before_action :only => new_actions.keys do |controller|
550
- # new_from_params
551
- if controller.respond_to?(new_from_params_method, true)
552
- controller.send(new_from_params_method)
553
- else
554
- controller.send(:new_controller_object_from_params,
555
- options[:context] || controller_name, options[:nested_in], options[:strong_parameters])
556
- end
557
- end
558
- else
559
- new_object_method = :"new_#{controller_name.singularize}"
560
- before_action :only => :new do |controller|
561
- # new_from_params
562
- if controller.respond_to?(new_object_method, true)
563
- controller.send(new_object_method)
564
- else
565
- controller.send(:new_blank_controller_object,
566
- options[:context] || controller_name, options[:nested_in], options[:strong_parameters], options[:model])
567
- end
568
- end
569
- end
570
-
571
- load_method = :"load_#{controller_name.singularize}"
572
- before_action :only => members.keys do |controller|
573
- # load controller object
574
- if controller.respond_to?(load_method, true)
575
- controller.send(load_method)
576
- else
577
- controller.send(:load_controller_object, options[:context] || controller_name, options[:model])
578
- end
579
- end
580
- filter_access_to :all, :attribute_check => true, :context => options[:context], :model => options[:model]
581
-
582
- members.merge(new_actions).merge(collections).each do |action, privilege|
583
- if action != privilege or (options[:no_attribute_check] and options[:no_attribute_check].include?(action))
584
- filter_options = {
585
- :strong_parameters => options[:strong_parameters],
586
- :context => options[:context],
587
- :attribute_check => !options[:no_attribute_check] || !options[:no_attribute_check].include?(action),
588
- :model => options[:model]
589
- }
590
- filter_options[:require] = privilege if action != privilege
591
- filter_access_to(action, filter_options)
592
- end
593
- end
594
- end
595
-
596
- # Returns the context for authorization checks in the current controller.
597
- # Uses the controller_name and prepends any namespaces underscored and
598
- # joined with underscores.
599
- #
600
- # E.g.
601
- # AllThosePeopleController => :all_those_people
602
- # AnyName::Space::ThingsController => :any_name_space_things
603
- #
604
- def decl_auth_context
605
- prefixes = name.split('::')[0..-2].map(&:underscore)
606
- ((prefixes + [controller_name]) * '_').to_sym
607
- end
608
-
609
- protected
610
- def filter_access_permissions # :nodoc:
611
- unless filter_access_permissions?
612
- ancestors[1..-1].reverse.each do |mod|
613
- mod.filter_access_permissions if mod.respond_to?(:filter_access_permissions, true)
614
- end
615
- end
616
- class_variable_set(:@@declarative_authorization_permissions, {}) unless filter_access_permissions?
617
- class_variable_get(:@@declarative_authorization_permissions)[self.name] ||= []
618
- end
619
-
620
- def filter_access_permissions? # :nodoc:
621
- class_variable_defined?(:@@declarative_authorization_permissions)
622
- end
623
-
624
- def actions_from_option(option) # :nodoc:
625
- case option
626
- when nil
627
- {}
628
- when Symbol, String
629
- {option.to_sym => option.to_sym}
630
- when Hash
631
- option
632
- when Enumerable
633
- option.each_with_object({}) do |action, hash|
634
- if action.is_a?(Array)
635
- raise "Unexpected option format: #{option.inspect}" if action.length != 2
636
- hash[action.first] = action.last
637
- else
638
- hash[action.to_sym] = action.to_sym
639
- end
640
- end
641
- end
642
- end
643
- end
644
- end
645
-
646
- class ControllerPermission # :nodoc:
647
- attr_reader :actions, :privilege, :context, :attribute_check, :strong_params
648
- def initialize(actions, privilege, context, strong_params, attribute_check = false,
649
- load_object_model = nil, load_object_method = nil,
650
- filter_block = nil)
651
- @actions = actions.to_set
652
- @privilege = privilege
653
- @context = context
654
- @load_object_model = load_object_model
655
- @load_object_method = load_object_method
656
- @filter_block = filter_block
657
- @attribute_check = attribute_check
658
- @strong_params = strong_params
659
- end
660
-
661
- def matches?(action_name)
662
- @actions.include?(action_name.to_sym)
663
- end
664
-
665
- def permit!(contr)
666
- if @filter_block
667
- return contr.instance_eval(&@filter_block)
668
- end
669
- object = @attribute_check ? load_object(contr) : nil
670
- privilege = @privilege || :"#{contr.action_name}"
671
-
672
- contr.authorization_engine.permit!(privilege,
673
- :user => contr.send(:current_user),
674
- :object => object,
675
- :skip_attribute_test => !@attribute_check,
676
- :context => @context || contr.class.decl_auth_context)
677
- end
678
-
679
- def remove_actions(actions)
680
- @actions -= actions
681
- self
682
- end
683
-
684
- private
685
-
686
- def load_object(contr)
687
- if @load_object_method and @load_object_method.is_a?(Symbol)
688
- contr.send(@load_object_method)
689
- elsif @load_object_method and @load_object_method.is_a?(Proc)
690
- contr.instance_eval(&@load_object_method)
691
- else
692
- load_object_model = @load_object_model ||
693
- (@context ? @context.to_s.classify.constantize : contr.class.controller_name.classify.constantize)
694
- load_object_model = load_object_model.classify.constantize if load_object_model.is_a?(String)
695
- instance_var = "@#{load_object_model.name.demodulize.underscore}"
696
- object = contr.instance_variable_get(instance_var)
697
- unless object
698
- begin
699
- object = @strong_params ? load_object_model.find_or_initialize_by(:id => contr.params[:id]) : load_object_model.find(contr.params[:id])
700
- rescue => e
701
- contr.logger.debug("filter_access_to tried to find " +
702
- "#{load_object_model} from params[:id] " +
703
- "(#{contr.params[:id].inspect}), because attribute_check is enabled " +
704
- "and #{instance_var.to_s} isn't set, but failed: #{e.class.name}: #{e}")
705
- raise if AuthorizationInController.failed_auto_loading_is_not_found?
706
- end
707
- contr.instance_variable_set(instance_var, object)
708
- end
709
- object
710
- end
711
- end
712
- end
713
- end
1
+ # Authorization::AuthorizationInController
2
+ require File.dirname(__FILE__) + '/authorization.rb'
3
+
4
+ module Authorization
5
+ module AuthorizationInController
6
+
7
+ def self.included(base) # :nodoc:
8
+ base.extend(ClassMethods)
9
+ base.module_eval do
10
+ before_action(:filter_access_filter) if method_defined?(:filter_access_filter)
11
+ end
12
+ end
13
+
14
+ DEFAULT_DENY = false
15
+
16
+ # If attribute_check is set for filter_access_to, decl_auth_context will try to
17
+ # load the appropriate object from the current controller's model with
18
+ # the id from params[:id]. If that fails, a 404 Not Found is often the
19
+ # right way to handle the error. If you have additional measures in place
20
+ # that restricts the find scope, handling this error as a permission denied
21
+ # might be a better way. Set failed_auto_loading_is_not_found to false
22
+ # for the latter behavior.
23
+ @@failed_auto_loading_is_not_found = true
24
+ def self.failed_auto_loading_is_not_found?
25
+ @@failed_auto_loading_is_not_found
26
+ end
27
+ def self.failed_auto_loading_is_not_found=(new_value)
28
+ @@failed_auto_loading_is_not_found = new_value
29
+ end
30
+
31
+ # Returns the Authorization::Engine for the current controller.
32
+ def authorization_engine
33
+ @authorization_engine ||= Authorization::Engine.instance
34
+ end
35
+
36
+ # If the current user meets the given privilege, permitted_to? returns true
37
+ # and yields to the optional block. The attribute checks that are defined
38
+ # in the authorization rules are only evaluated if an object is given
39
+ # for context.
40
+ #
41
+ # See examples for Authorization::AuthorizationHelper #permitted_to?
42
+ #
43
+ # If no object or context is specified, the controller_name is used as
44
+ # context.
45
+ #
46
+ def permitted_to?(privilege, object_or_sym = nil, options = {})
47
+ if authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, false))
48
+ yield if block_given?
49
+ true
50
+ else
51
+ false
52
+ end
53
+ end
54
+
55
+ # Works similar to the permitted_to? method, but
56
+ # throws the authorization exceptions, just like Engine#permit!
57
+ def permitted_to!(privilege, object_or_sym = nil, options = {})
58
+ authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, true))
59
+ end
60
+
61
+ # While permitted_to? is used for authorization, in some cases
62
+ # content should only be shown to some users without being concerned
63
+ # with authorization. E.g. to only show the most relevant menu options
64
+ # to a certain group of users. That is what has_role? should be used for.
65
+ def has_role?(*roles)
66
+ user_roles = authorization_engine.roles_for(current_user)
67
+ result = roles.all? do |role|
68
+ user_roles.include?(role)
69
+ end
70
+ yield if result and block_given?
71
+ result
72
+ end
73
+
74
+ # Intended to be used where you want to allow users with any single listed role to view
75
+ # the content in question
76
+ def has_any_role?(*roles)
77
+ user_roles = authorization_engine.roles_for(current_user)
78
+ result = roles.any? do |role|
79
+ user_roles.include?(role)
80
+ end
81
+ yield if result and block_given?
82
+ result
83
+ end
84
+
85
+ # As has_role? except checks all roles included in the role hierarchy
86
+ def has_role_with_hierarchy?(*roles)
87
+ user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
88
+ result = roles.all? do |role|
89
+ user_roles.include?(role)
90
+ end
91
+ yield if result and block_given?
92
+ result
93
+ end
94
+
95
+ # As has_any_role? except checks all roles included in the role hierarchy
96
+ def has_any_role_with_hierarchy?(*roles)
97
+ user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
98
+ result = roles.any? do |role|
99
+ user_roles.include?(role)
100
+ end
101
+ yield if result and block_given?
102
+ result
103
+ end
104
+
105
+ protected
106
+ def filter_access_filter # :nodoc:
107
+ permissions = self.class.all_filter_access_permissions
108
+ all_permissions = permissions.select {|p| p.actions.include?(:all)}
109
+ matching_permissions = permissions.select {|p| p.matches?(action_name)}
110
+ allowed = false
111
+ auth_exception = nil
112
+ begin
113
+ allowed = if !matching_permissions.empty?
114
+ matching_permissions.all? {|perm| perm.permit!(self)}
115
+ elsif !all_permissions.empty?
116
+ all_permissions.all? {|perm| perm.permit!(self)}
117
+ else
118
+ !DEFAULT_DENY
119
+ end
120
+ rescue NotAuthorized => e
121
+ auth_exception = e
122
+ end
123
+
124
+ unless allowed
125
+ if all_permissions.empty? and matching_permissions.empty?
126
+ logger.warn "Permission denied: No matching filter access " +
127
+ "rule found for #{self.class.controller_name}.#{action_name}"
128
+ elsif auth_exception
129
+ logger.info "Permission denied: #{auth_exception}"
130
+ end
131
+ if respond_to?(:permission_denied, true)
132
+ # permission_denied needs to render or redirect
133
+ send(:permission_denied)
134
+ else
135
+ send(:render, :plain => "You are not allowed to access this action.",
136
+ :status => :forbidden)
137
+ end
138
+ end
139
+ end
140
+
141
+ def load_controller_object(context_without_namespace = nil, model = nil) # :nodoc:
142
+ instance_var = :"@#{context_without_namespace.to_s.singularize}"
143
+ model = model ? model.classify.constantize : context_without_namespace.to_s.classify.constantize
144
+ instance_variable_set(instance_var, model.find(params[:id]))
145
+ end
146
+
147
+ def load_parent_controller_object(parent_context_without_namespace) # :nodoc:
148
+ instance_var = :"@#{parent_context_without_namespace.to_s.singularize}"
149
+ model = parent_context_without_namespace.to_s.classify.constantize
150
+ instance_variable_set(instance_var, model.find(params[:"#{parent_context_without_namespace.to_s.singularize}_id"]))
151
+ end
152
+
153
+ def new_controller_object_from_params(context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc:
154
+ model_or_proxy = parent_context_without_namespace ?
155
+ instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
156
+ context_without_namespace.to_s.classify.constantize
157
+ instance_var = :"@#{context_without_namespace.to_s.singularize}"
158
+ instance_variable_set(instance_var,
159
+ model_or_proxy.new(params[context_without_namespace.to_s.singularize]))
160
+ end
161
+
162
+ def new_blank_controller_object(context_without_namespace, parent_context_without_namespace, strong_params, model) # :nodoc:
163
+ if model
164
+ model_or_proxy = model.to_s.classify.constantize
165
+ else
166
+ model_or_proxy = parent_context_without_namespace ?
167
+ instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
168
+ context_without_namespace.to_s.classify.constantize
169
+ end
170
+ instance_var = :"@#{context_without_namespace.to_s.singularize}"
171
+ instance_variable_set(instance_var,
172
+ model_or_proxy.new())
173
+ end
174
+
175
+ def new_controller_object_for_collection(context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc:
176
+ model_or_proxy = parent_context_without_namespace ?
177
+ instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
178
+ context_without_namespace.to_s.classify.constantize
179
+ instance_var = :"@#{context_without_namespace.to_s.singularize}"
180
+ instance_variable_set(instance_var, model_or_proxy.new)
181
+ end
182
+
183
+ def options_for_permit(object_or_sym = nil, options = {}, bang = true)
184
+ context = object = nil
185
+ if object_or_sym.nil?
186
+ context = self.class.decl_auth_context
187
+ elsif !Authorization.is_a_association_proxy?(object_or_sym) and object_or_sym.is_a?(Symbol)
188
+ context = object_or_sym
189
+ else
190
+ object = object_or_sym
191
+ end
192
+
193
+ result = {:object => object,
194
+ :context => context,
195
+ :skip_attribute_test => object.nil?,
196
+ :bang => bang}.merge(options)
197
+ result[:user] = current_user unless result.key?(:user)
198
+ result
199
+ end
200
+
201
+ module ClassMethods
202
+ #
203
+ # Defines a filter to be applied according to the authorization of the
204
+ # current user. Requires at least one symbol corresponding to an
205
+ # action as parameter. The special symbol :+all+ refers to all actions.
206
+ # The all :+all+ statement is only employed if no specific statement is
207
+ # present.
208
+ # class UserController < ApplicationController
209
+ # filter_access_to :index
210
+ # filter_access_to :new, :edit
211
+ # filter_access_to :all
212
+ # ...
213
+ # end
214
+ #
215
+ # The default is to allow access unconditionally if no rule matches.
216
+ # Thus, including the +filter_access_to+ :+all+ statement is a good
217
+ # idea, implementing a default-deny policy.
218
+ #
219
+ # When the access is denied, the method +permission_denied+ is called
220
+ # on the current controller, if defined. Else, a simple "you are not
221
+ # allowed" string is output. Log.info is given more information on the
222
+ # reasons of denial.
223
+ #
224
+ # def permission_denied
225
+ # flash[:error] = 'Sorry, you are not allowed to the requested page.'
226
+ # respond_to do |format|
227
+ # format.html { redirect_to(:back) rescue redirect_to('/') }
228
+ # format.xml { head :unauthorized }
229
+ # format.js { head :unauthorized }
230
+ # end
231
+ # end
232
+ #
233
+ # By default, required privileges are inferred from the action name and
234
+ # the controller name. Thus, in UserController :+edit+ requires
235
+ # :+edit+ +users+. To specify required privilege, use the option :+require+
236
+ # filter_access_to :new, :create, :require => :create, :context => :users
237
+ #
238
+ # Without the :+attribute_check+ option, no constraints from the
239
+ # authorization rules are enforced because for some actions (collections,
240
+ # +new+, +create+), there is no object to evaluate conditions against. To
241
+ # allow attribute checks on all actions, it is a common pattern to provide
242
+ # custom objects through +before_actions+:
243
+ # class BranchesController < ApplicationController
244
+ # before_action :load_company
245
+ # before_action :new_branch_from_company_and_params,
246
+ # :only => [:index, :new, :create]
247
+ # filter_access_to :all, :attribute_check => true
248
+ #
249
+ # protected
250
+ # def new_branch_from_company_and_params
251
+ # @branch = @company.branches.new(params[:branch])
252
+ # end
253
+ # end
254
+ # NOTE: +before_actions+ need to be defined before the first
255
+ # +filter_access_to+ call.
256
+ #
257
+ # For further customization, a custom filter expression may be formulated
258
+ # in a block, which is then evaluated in the context of the controller
259
+ # on a matching request. That is, for checking two objects, use the
260
+ # following:
261
+ # filter_access_to :merge do
262
+ # permitted_to!(:update, User.find(params[:original_id])) and
263
+ # permitted_to!(:delete, User.find(params[:id]))
264
+ # end
265
+ # The block should raise a Authorization::AuthorizationError or return
266
+ # false if the access is to be denied.
267
+ #
268
+ # Later calls to filter_access_to with overlapping actions overwrite
269
+ # previous ones for that action.
270
+ #
271
+ # All options:
272
+ # [:+require+]
273
+ # Privilege required; defaults to action_name
274
+ # [:+context+]
275
+ # The privilege's context, defaults to decl_auth_context, which consists
276
+ # of controller_name, prepended by any namespaces
277
+ # [:+attribute_check+]
278
+ # Enables the check of attributes defined in the authorization rules.
279
+ # Defaults to false. If enabled, filter_access_to will use a context
280
+ # object from one of the following sources (in that order):
281
+ # * the method from the :+load_method+ option,
282
+ # * an instance variable named after the singular of the context
283
+ # (by default from the controller name, e.g. @post for PostsController),
284
+ # * a find on the context model, using +params+[:id] as id value.
285
+ # Any of these methods will only be employed if :+attribute_check+
286
+ # is enabled.
287
+ # [:+model+]
288
+ # The data model to load a context object from. Defaults to the
289
+ # context, singularized.
290
+ # [:+load_method+]
291
+ # Specify a method by symbol or a Proc object which should be used
292
+ # to load the object. Both should return the loaded object.
293
+ # If a Proc object is given, e.g. by way of
294
+ # +lambda+, it is called in the instance of the controller.
295
+ # Example demonstrating the default behavior:
296
+ # filter_access_to :show, :attribute_check => true,
297
+ # :load_method => lambda { User.find(params[:id]) }
298
+ #
299
+
300
+ def filter_access_to(*args, &filter_block)
301
+ options = args.last.is_a?(Hash) ? args.pop : {}
302
+ options = {
303
+ :require => nil,
304
+ :context => nil,
305
+ :attribute_check => false,
306
+ :model => nil,
307
+ :load_method => nil,
308
+ :strong_parameters => nil
309
+ }.merge!(options)
310
+ privilege = options[:require]
311
+ context = options[:context]
312
+ actions = args.flatten
313
+
314
+ # prevent setting filter_access_filter multiple times
315
+ skip_before_action(:filter_access_filter) if method_defined?(:filter_access_filter)
316
+ before_action :filter_access_filter
317
+
318
+ filter_access_permissions.each do |perm|
319
+ perm.remove_actions(actions)
320
+ end
321
+ filter_access_permissions <<
322
+ ControllerPermission.new(actions, privilege, context,
323
+ options[:strong_parameters],
324
+ options[:attribute_check],
325
+ options[:model],
326
+ options[:load_method],
327
+ filter_block)
328
+ end
329
+
330
+ # Disables authorization entirely. Requires at least one symbol corresponding
331
+ # to an action as parameter. The special symbol :+all+ refers to all actions.
332
+ # The all :+all+ statement is only employed if no specific statement is
333
+ # present.
334
+ def no_filter_access_to(*args)
335
+ filter_access_to args do
336
+ true
337
+ end
338
+ end
339
+
340
+ # Collecting all the ControllerPermission objects from the controller
341
+ # hierarchy. Permissions for actions are overwritten by calls to
342
+ # filter_access_to in child controllers with the same action.
343
+ def all_filter_access_permissions # :nodoc:
344
+ ancestors.inject([]) do |perms, mod|
345
+ if mod.respond_to?(:filter_access_permissions, true)
346
+ perms +
347
+ mod.filter_access_permissions.collect do |p1|
348
+ p1.clone.remove_actions(perms.inject(Set.new) {|actions, p2| actions + p2.actions})
349
+ end
350
+ else
351
+ perms
352
+ end
353
+ end
354
+ end
355
+
356
+ # To DRY up the filter_access_to statements in restful controllers,
357
+ # filter_resource_access combines typical filter_access_to and
358
+ # before_action calls, which set up the instance variables.
359
+ #
360
+ # The simplest case are top-level resource controllers with only the
361
+ # seven CRUD methods, e.g.
362
+ # class CompanyController < ApplicationController
363
+ # filter_resource_access
364
+ #
365
+ # def index...
366
+ # end
367
+ # Here, all CRUD actions are protected through a filter_access_to :all
368
+ # statement. :+attribute_check+ is enabled for all actions except for
369
+ # the collection action :+index+. To have an object for attribute checks
370
+ # available, filter_resource_access will set the instance variable
371
+ # @+company+ in before filters. For the member actions (:+show+, :+edit+,
372
+ # :+update+, :+destroy+) @company is set to Company.find(params[:id]).
373
+ # For +new+ actions (:+new+, :+create+), filter_resource_access creates
374
+ # a new object from company parameters: Company.new(params[:company].
375
+ #
376
+ # For nested resources, the parent object may be loaded automatically.
377
+ # class BranchController < ApplicationController
378
+ # filter_resource_access :nested_in => :companies
379
+ # end
380
+ # Again, the CRUD actions are protected. Now, for all CRUD actions,
381
+ # the parent object @company is loaded from params[:company_id]. It is
382
+ # also used when creating @branch for +new+ actions. Here, attribute_check
383
+ # is enabled for the collection :+index+ as well, checking attributes on a
384
+ # @company.branches.new method.
385
+ #
386
+ # In many cases, the default seven CRUD actions are not sufficient. As in
387
+ # the resource definition for routing you may thus give additional member,
388
+ # new and collection methods. The +options+ allow you to specify the
389
+ # required privileges for each action by providing a hash or an array of
390
+ # pairs. By default, for each action the action name is taken as privilege
391
+ # (action search in the example below requires the privilege :index
392
+ # :companies). Any controller action that is not specified and does not
393
+ # belong to the seven CRUD actions is handled as a member method.
394
+ # class CompanyController < ApplicationController
395
+ # filter_resource_access :collection => [[:search, :index], :index],
396
+ # :additional_member => {:mark_as_key_company => :update}
397
+ # end
398
+ # The +additional_+* options add to the respective CRUD actions,
399
+ # the other options (:+member+, :+collection+, :+new+) replace their
400
+ # respective CRUD actions.
401
+ # filter_resource_access :member => { :toggle_open => :update }
402
+ # Would declare :toggle_open as the only member action in the controller and
403
+ # require that permission :update is granted for the current user.
404
+ # filter_resource_access :additional_member => { :toggle_open => :update }
405
+ # Would add a member action :+toggle_open+ to the default members, such as :+show+.
406
+ #
407
+ # If :+collection+ is an array of method names filter_resource_access will
408
+ # associate a permission with the method that is the same as the method
409
+ # name and no attribute checks will be performed unless
410
+ # :attribute_check => true
411
+ # is added in the options.
412
+ #
413
+ # You can override the default object loading by implementing any of the
414
+ # following instance methods on the controller. Examples are given for the
415
+ # BranchController (with +nested_in+ set to :+companies+):
416
+ # [+new_branch_from_params+]
417
+ # Used for +new+ actions.
418
+ # [+new_branch_for_collection+]
419
+ # Used for +collection+ actions if the +nested_in+ option is set.
420
+ # [+load_branch+]
421
+ # Used for +member+ actions.
422
+ # [+load_company+]
423
+ # Used for all +new+, +member+, and +collection+ actions if the
424
+ # +nested_in+ option is set.
425
+ #
426
+ # All options:
427
+ # [:+member+]
428
+ # Member methods are actions like +show+, which have an params[:id] from
429
+ # which to load the controller object and assign it to @controller_name,
430
+ # e.g. @+branch+.
431
+ #
432
+ # By default, member actions are [:+show+, :+edit+, :+update+,
433
+ # :+destroy+]. Also, any action not belonging to the seven CRUD actions
434
+ # are handled as member actions.
435
+ #
436
+ # There are three different syntax to specify member, collection and
437
+ # new actions.
438
+ # * Hash: Lets you set the required privilege for each action:
439
+ # {:+show+ => :+show+, :+mark_as_important+ => :+update+}
440
+ # * Array of actions or pairs: [:+show+, [:+mark_as_important+, :+update+]],
441
+ # with single actions requiring the privilege of the same name as the method.
442
+ # * Single method symbol: :+show+
443
+ # [:+additional_member+]
444
+ # Allows to add additional member actions to the default resource +member+
445
+ # actions.
446
+ # [:+collection+]
447
+ # Collection actions are like :+index+, actions without any controller object
448
+ # to check attributes of. If +nested_in+ is given, a new object is
449
+ # created from the parent object, e.g. @company.branches.new. Without
450
+ # +nested_in+, attribute check is deactivated for these actions. By
451
+ # default, collection is set to :+index+.
452
+ # [:+additional_collection+]
453
+ # Allows to add additional collection actions to the default resource +collection+
454
+ # actions.
455
+ # [:+new+]
456
+ # +new+ methods are actions such as +new+ and +create+, which don't
457
+ # receive a params[:id] to load an object from, but
458
+ # a params[:controller_name_singular] hash with attributes for a new
459
+ # object. The attributes will be used here to create a new object and
460
+ # check the object against the authorization rules. The object is
461
+ # assigned to @controller_name_singular, e.g. @branch.
462
+ #
463
+ # If +nested_in+ is given, the new object
464
+ # is created from the parent_object.controller_name
465
+ # proxy, e.g. company.branches.new(params[:branch]). By default,
466
+ # +new+ is set to [:new, :create].
467
+ # [:+additional_new+]
468
+ # Allows to add additional new actions to the default resource +new+ actions.
469
+ # [:+context+]
470
+ # The context is used to determine the model to load objects from for the
471
+ # before_actions and the context of privileges to use in authorization
472
+ # checks.
473
+ # [:+nested_in+]
474
+ # Specifies the parent controller if the resource is nested in another
475
+ # one. This is used to automatically load the parent object, e.g.
476
+ # @+company+ from params[:company_id] for a BranchController nested in
477
+ # a CompanyController.
478
+ # [:+shallow+]
479
+ # Only relevant when used in conjunction with +nested_in+. Specifies a nested resource
480
+ # as being a shallow nested resource, resulting in the controller not attempting to
481
+ # load a parent object for all member actions defined by +member+ and
482
+ # +additional_member+ or rather the default member actions (:+show+, :+edit+,
483
+ # :+update+, :+destroy+).
484
+ # [:+no_attribute_check+]
485
+ # Allows to set actions for which no attribute check should be performed.
486
+ # See filter_access_to on details. By default, with no +nested_in+,
487
+ # +no_attribute_check+ is set to all collections. If +nested_in+ is given
488
+ # +no_attribute_check+ is empty by default.
489
+ # [:+strong_parameters+]
490
+ # If set to true, relies on controller to provide instance variable and
491
+ # create new object in :create action. Set true if you use strong_params
492
+ # and false if you use protected_attributes.
493
+ #
494
+ def filter_resource_access(options = {})
495
+ options = {
496
+ :new => [:new, :create],
497
+ :additional_new => nil,
498
+ :member => [:show, :edit, :update, :destroy],
499
+ :additional_member => nil,
500
+ :collection => [:index],
501
+ :additional_collection => nil,
502
+ #:new_method_for_collection => nil, # only symbol method name
503
+ #:new_method => nil, # only symbol method name
504
+ #:load_method => nil, # only symbol method name
505
+ :no_attribute_check => nil,
506
+ :context => nil,
507
+ :model => nil,
508
+ :nested_in => nil,
509
+ :strong_parameters => nil
510
+ }.merge(options)
511
+ options.merge!({ :strong_parameters => true }) if options[:strong_parameters] == nil
512
+
513
+ new_actions = actions_from_option( options[:new] ).merge(
514
+ actions_from_option(options[:additional_new]) )
515
+ members = actions_from_option(options[:member]).merge(
516
+ actions_from_option(options[:additional_member]))
517
+ collections = actions_from_option(options[:collection]).merge(
518
+ actions_from_option(options[:additional_collection]))
519
+
520
+ no_attribute_check_actions = options[:strong_parameters] ? actions_from_option(options[:collection]).merge(actions_from_option([:create])) : collections
521
+
522
+ options[:no_attribute_check] ||= no_attribute_check_actions.keys unless options[:nested_in]
523
+
524
+ unless options[:nested_in].blank?
525
+ load_parent_method = :"load_#{options[:nested_in].to_s.singularize}"
526
+ shallow_exceptions = options[:shallow] ? {:except => members.keys} : {}
527
+ before_action shallow_exceptions do |controller|
528
+ if controller.respond_to?(load_parent_method, true)
529
+ controller.send(load_parent_method)
530
+ else
531
+ controller.send(:load_parent_controller_object, options[:nested_in])
532
+ end
533
+ end
534
+
535
+ new_for_collection_method = :"new_#{controller_name.singularize}_for_collection"
536
+ before_action :only => collections.keys do |controller|
537
+ # new_for_collection
538
+ if controller.respond_to?(new_for_collection_method, true)
539
+ controller.send(new_for_collection_method)
540
+ else
541
+ controller.send(:new_controller_object_for_collection,
542
+ options[:context] || controller_name, options[:nested_in], options[:strong_parameters])
543
+ end
544
+ end
545
+ end
546
+
547
+ unless options[:strong_parameters]
548
+ new_from_params_method = :"new_#{controller_name.singularize}_from_params"
549
+ before_action :only => new_actions.keys do |controller|
550
+ # new_from_params
551
+ if controller.respond_to?(new_from_params_method, true)
552
+ controller.send(new_from_params_method)
553
+ else
554
+ controller.send(:new_controller_object_from_params,
555
+ options[:context] || controller_name, options[:nested_in], options[:strong_parameters])
556
+ end
557
+ end
558
+ else
559
+ new_object_method = :"new_#{controller_name.singularize}"
560
+ before_action :only => :new do |controller|
561
+ # new_from_params
562
+ if controller.respond_to?(new_object_method, true)
563
+ controller.send(new_object_method)
564
+ else
565
+ controller.send(:new_blank_controller_object,
566
+ options[:context] || controller_name, options[:nested_in], options[:strong_parameters], options[:model])
567
+ end
568
+ end
569
+ end
570
+
571
+ load_method = :"load_#{controller_name.singularize}"
572
+ before_action :only => members.keys do |controller|
573
+ # load controller object
574
+ if controller.respond_to?(load_method, true)
575
+ controller.send(load_method)
576
+ else
577
+ controller.send(:load_controller_object, options[:context] || controller_name, options[:model])
578
+ end
579
+ end
580
+ filter_access_to :all, :attribute_check => true, :context => options[:context], :model => options[:model]
581
+
582
+ members.merge(new_actions).merge(collections).each do |action, privilege|
583
+ if action != privilege or (options[:no_attribute_check] and options[:no_attribute_check].include?(action))
584
+ filter_options = {
585
+ :strong_parameters => options[:strong_parameters],
586
+ :context => options[:context],
587
+ :attribute_check => !options[:no_attribute_check] || !options[:no_attribute_check].include?(action),
588
+ :model => options[:model]
589
+ }
590
+ filter_options[:require] = privilege if action != privilege
591
+ filter_access_to(action, filter_options)
592
+ end
593
+ end
594
+ end
595
+
596
+ # Returns the context for authorization checks in the current controller.
597
+ # Uses the controller_name and prepends any namespaces underscored and
598
+ # joined with underscores.
599
+ #
600
+ # E.g.
601
+ # AllThosePeopleController => :all_those_people
602
+ # AnyName::Space::ThingsController => :any_name_space_things
603
+ #
604
+ def decl_auth_context
605
+ prefixes = name.split('::')[0..-2].map(&:underscore)
606
+ ((prefixes + [controller_name]) * '_').to_sym
607
+ end
608
+
609
+ protected
610
+ def filter_access_permissions # :nodoc:
611
+ unless filter_access_permissions?
612
+ ancestors[1..-1].reverse.each do |mod|
613
+ mod.filter_access_permissions if mod.respond_to?(:filter_access_permissions, true)
614
+ end
615
+ end
616
+ class_variable_set(:@@declarative_authorization_permissions, {}) unless filter_access_permissions?
617
+ class_variable_get(:@@declarative_authorization_permissions)[self.name] ||= []
618
+ end
619
+
620
+ def filter_access_permissions? # :nodoc:
621
+ class_variable_defined?(:@@declarative_authorization_permissions)
622
+ end
623
+
624
+ def actions_from_option(option) # :nodoc:
625
+ case option
626
+ when nil
627
+ {}
628
+ when Symbol, String
629
+ {option.to_sym => option.to_sym}
630
+ when Hash
631
+ option
632
+ when Enumerable
633
+ option.each_with_object({}) do |action, hash|
634
+ if action.is_a?(Array)
635
+ raise "Unexpected option format: #{option.inspect}" if action.length != 2
636
+ hash[action.first] = action.last
637
+ else
638
+ hash[action.to_sym] = action.to_sym
639
+ end
640
+ end
641
+ end
642
+ end
643
+ end
644
+ end
645
+
646
+ class ControllerPermission # :nodoc:
647
+ attr_reader :actions, :privilege, :context, :attribute_check, :strong_params
648
+ def initialize(actions, privilege, context, strong_params, attribute_check = false,
649
+ load_object_model = nil, load_object_method = nil,
650
+ filter_block = nil)
651
+ @actions = actions.to_set
652
+ @privilege = privilege
653
+ @context = context
654
+ @load_object_model = load_object_model
655
+ @load_object_method = load_object_method
656
+ @filter_block = filter_block
657
+ @attribute_check = attribute_check
658
+ @strong_params = strong_params
659
+ end
660
+
661
+ def matches?(action_name)
662
+ @actions.include?(action_name.to_sym)
663
+ end
664
+
665
+ def permit!(contr)
666
+ if @filter_block
667
+ return contr.instance_eval(&@filter_block)
668
+ end
669
+ object = @attribute_check ? load_object(contr) : nil
670
+ privilege = @privilege || :"#{contr.action_name}"
671
+
672
+ contr.authorization_engine.permit!(privilege,
673
+ :user => contr.send(:current_user),
674
+ :object => object,
675
+ :skip_attribute_test => !@attribute_check,
676
+ :context => @context || contr.class.decl_auth_context)
677
+ end
678
+
679
+ def remove_actions(actions)
680
+ @actions -= actions
681
+ self
682
+ end
683
+
684
+ private
685
+
686
+ def load_object(contr)
687
+ if @load_object_method and @load_object_method.is_a?(Symbol)
688
+ contr.send(@load_object_method)
689
+ elsif @load_object_method and @load_object_method.is_a?(Proc)
690
+ contr.instance_eval(&@load_object_method)
691
+ else
692
+ load_object_model = @load_object_model ||
693
+ (@context ? @context.to_s.classify.constantize : contr.class.controller_name.classify.constantize)
694
+ load_object_model = load_object_model.classify.constantize if load_object_model.is_a?(String)
695
+ instance_var = "@#{load_object_model.name.demodulize.underscore}"
696
+ object = contr.instance_variable_get(instance_var)
697
+ unless object
698
+ begin
699
+ object = @strong_params ? load_object_model.find_or_initialize_by(:id => contr.params[:id]) : load_object_model.find(contr.params[:id])
700
+ rescue => e
701
+ contr.logger.debug("filter_access_to tried to find " +
702
+ "#{load_object_model} from params[:id] " +
703
+ "(#{contr.params[:id].inspect}), because attribute_check is enabled " +
704
+ "and #{instance_var.to_s} isn't set, but failed: #{e.class.name}: #{e}")
705
+ raise if AuthorizationInController.failed_auto_loading_is_not_found?
706
+ end
707
+ contr.instance_variable_set(instance_var, object)
708
+ end
709
+ object
710
+ end
711
+ end
712
+ end
713
+ end