ae_declarative_authorization 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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