Module: Authorization::AuthorizationInController::ClassMethods

Defined in:
lib/declarative_authorization/in_controller.rb

Instance Method Summary collapse

Instance Method Details

#all_filter_access_permissionsObject

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.


332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/declarative_authorization/in_controller.rb', line 332

def all_filter_access_permissions # :nodoc:
  ancestors.inject([]) do |perms, mod|
    if mod.respond_to?(:filter_access_permissions, true)
      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

#decl_auth_contextObject

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

594
595
596
597
# File 'lib/declarative_authorization/in_controller.rb', line 594

def decl_auth_context
  prefixes = name.split('::')[0..-2].map(&:underscore)
  ((prefixes + [controller_name]) * '_').to_sym
end

#filter_access_to(*args, &filter_block) ⇒ Object

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 inferred 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 behavior:

filter_access_to :show, :attribute_check => true,
                 :load_method => lambda { User.find(params[:id]) }

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
# File 'lib/declarative_authorization/in_controller.rb', line 299

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,
    :strong_parameters => nil
  }.merge!(options)
  privilege = options[:require]
  context = options[:context]
  actions = args.flatten

  # prevent setting filter_access_filter multiple times
  skip_before_filter :filter_access_filter
  before_filter :filter_access_filter
  
  filter_access_permissions.each do |perm|
    perm.remove_actions(actions)
  end
  filter_access_permissions << 
    ControllerPermission.new(actions, privilege, context,
                             options[:strong_parameters],
                             options[:attribute_check],
                             options[:model],
                             options[:load_method],
                             filter_block)
end

#filter_resource_access(options = {}) ⇒ Object

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). For new actions (:new, :create), filter_resource_access creates a new object from company parameters: Company.new(params.

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. 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 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 (:member, :collection, :new) replace their respective CRUD actions.

filter_resource_access :member => { :toggle_open => :update }

Would declare :toggle_open as the only member action in the controller and require that permission :update is granted for the current user.

filter_resource_access :additional_member => { :toggle_open => :update }

Would add a member action :toggle_open to the default members, such as :show.

If :collection is an array of method names filter_resource_access will associate a permission with the method that is the same as the method name and no attribute checks will be performed unless

:attribute_check => true

is added in the options.

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 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, :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 collection actions to the default resource collection actions.

:new

new methods are actions such as new and create, which don't receive a params to load an object from, but a params 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). 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 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 performed. 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.

:strong_parameters

If set to true, relies on controller to provide instance variable and create new object in :create action. Set true if you use strong_params and false if you use protected_attributes.


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
# File 'lib/declarative_authorization/in_controller.rb', line 483

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,
    :model => nil,
    :nested_in  => nil,
    :strong_parameters => nil
  }.merge(options)
  options.merge!({ :strong_parameters => true }) if Rails.version >= '4' && options[:strong_parameters] == nil
  options.merge!({ :strong_parameters => false }) if Rails.version < '4' && options[:strong_parameters] == nil

  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]))

  no_attribute_check_actions = options[:strong_parameters] ? actions_from_option(options[:collection]).merge(actions_from_option([:create])) : collections

  options[:no_attribute_check] ||= no_attribute_check_actions.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, true)
        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, true)
        controller.send(new_for_collection_method)
      else
        controller.send(:new_controller_object_for_collection,
            options[:context] || controller_name, options[:nested_in], options[:strong_parameters])
      end
    end
  end

  unless options[:strong_parameters]
    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, true)
        controller.send(new_from_params_method)
      else
        controller.send(:new_controller_object_from_params,
            options[:context] || controller_name, options[:nested_in], options[:strong_parameters])
      end
    end
  else
    new_object_method = :"new_#{controller_name.singularize}"
    before_filter :only => :new do |controller|
      # new_from_params
      if controller.respond_to?(new_object_method, true)
        controller.send(new_object_method)
      else
        controller.send(:new_blank_controller_object,
            options[:context] || controller_name, options[:nested_in], options[:strong_parameters], options[:model])
      end
    end          
  end

  load_method = :"load_#{controller_name.singularize}"
  before_filter :only => members.keys do |controller|
    # load controller object
    if controller.respond_to?(load_method, true)
      controller.send(load_method)
    else
      controller.send(:load_controller_object, options[:context] || controller_name, options[:model])
    end
  end
  filter_access_to :all, :attribute_check => true, :context => options[:context], :model => options[:model]

  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 = {
        :strong_parameters => options[:strong_parameters],
        :context          => options[:context],
        :attribute_check  => !options[:no_attribute_check] || !options[:no_attribute_check].include?(action),
        :model => options[:model]
      }
      filter_options[:require] = privilege if action != privilege
      filter_access_to(action, filter_options)
    end
  end
end