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