aboutsummaryrefslogblamecommitdiffstats
path: root/activemodel/lib/active_model/mass_assignment_security.rb
blob: 01eef762fd924c744f17a174b4e9711c889d15cf (plain) (tree)
1
2
3
4
5
6

                                                              
 
                  
                                           
                               







                                             







                                                                                      

                                                        
     
                                                 
                                                                          
     




                                                      
     
                   
     
                            

                                                                 
             
     
           
     
                       
                                                                         


                                                                              



                                                                             
                                              




                                                       
       




                                                                               


                                

             

                                     
                                 
                                                                                                                                      
                                             
                                         
                                         



                                               







                                                                                                                                    




                                                                                                






                                                                                   

                                                            
 
                                                                      
                        
       



                                                                                







                                                                              



                                                       
       
                                 
                                                                
       

                                                                               


                                

             

                                     
                                 
                                                                                                                                      
                                             




                                               






                                                                                                                                    

                                                                                                 






                                                                                     

                                                             
 

                                                
         
 

                                                 
         
 

                                                                
         
                                                
 

                                         
         

















                                                                                                            

       
           
 

                                                                  
       
 

                                                    
       

     
require 'active_support/core_ext/class/attribute.rb'
require 'active_model/mass_assignment_security/permission_set'

module ActiveModel
  # = Active Model Mass-Assignment Security
  module MassAssignmentSecurity
    extend ActiveSupport::Concern

    included do
      class_attribute :_accessible_attributes
      class_attribute :_protected_attributes
      class_attribute :_active_authorizer
    end

    # Mass assignment security provides an interface for protecting attributes
    # from end-user assignment. For more complex permissions, mass assignment security
    # may be handled outside the model by extending a non-ActiveRecord class,
    # such as a controller, with this behavior.
    #
    # For example, a logged in user may need to assign additional attributes depending
    # on their role:
    #
    #   class AccountsController < ApplicationController
    #     include ActiveModel::MassAssignmentSecurity
    #
    #     attr_accessible :first_name, :last_name
    #     attr_accessible :first_name, :last_name, :plan_id, :as => :admin
    #
    #     def update
    #       ...
    #       @account.update_attributes(account_params)
    #       ...
    #     end
    #
    #     protected
    #
    #     def account_params
    #       scope = admin ? :admin : :default
    #       sanitize_for_mass_assignment(params[:account], scope)
    #     end
    #
    #   end
    #
    module ClassMethods
      # Attributes named in this macro are protected from mass-assignment
      # whenever attributes are sanitized before assignment. A scope for the
      # attributes is optional, if no scope is provided then :default is used.
      # A scope can be defined by using the :as option.
      #
      # Mass-assignment to these attributes will simply be ignored, to assign
      # to them you can use direct writer methods. This is meant to protect
      # sensitive attributes from being overwritten by malicious users
      # tampering with URLs or forms. Example:
      #
      #   class Customer
      #     include ActiveModel::MassAssignmentSecurity
      #
      #     attr_accessor :name, :credit_rating
      #
      #     attr_protected :credit_rating, :last_login
      #     attr_protected :last_login, :as => :admin
      #
      #     def assign_attributes(values, options = {})
      #       sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
      #         send("#{k}=", v)
      #       end
      #     end
      #   end
      #
      # When using a :default scope :
      #
      #   customer = Customer.new
      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
      #   customer.name          # => "David"
      #   customer.credit_rating # => nil
      #   customer.last_login    # => nil
      #
      #   customer.credit_rating = "Average"
      #   customer.credit_rating # => "Average"
      #
      # And using the :admin scope :
      #
      #   customer = Customer.new
      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin)
      #   customer.name          # => "David"
      #   customer.credit_rating # => "Excellent"
      #   customer.last_login    # => nil
      #
      # To start from an all-closed default and enable attributes as needed,
      # have a look at +attr_accessible+.
      #
      # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+
      # to sanitize attributes won't provide sufficient protection.
      def attr_protected(*args)
        options = args.extract_options!
        scope = options[:as] || :default

        self._protected_attributes        = protected_attributes_configs.dup
        self._protected_attributes[scope] = self.protected_attributes(scope) + args

        self._active_authorizer = self._protected_attributes
      end

      # Specifies a white list of model attributes that can be set via
      # mass-assignment.
      #
      # Like +attr_protected+, a scope for the attributes is optional,
      # if no scope is provided then :default is used. A scope can be defined by
      # using the :as option.
      #
      # This is the opposite of the +attr_protected+ macro: Mass-assignment
      # will only set attributes in this list, to assign to the rest of
      # attributes you can use direct writer methods. This is meant to protect
      # sensitive attributes from being overwritten by malicious users
      # tampering with URLs or forms. If you'd rather start from an all-open
      # default and restrict attributes as needed, have a look at
      # +attr_protected+.
      #
      #   class Customer
      #     include ActiveModel::MassAssignmentSecurity
      #
      #     attr_accessor :name, :credit_rating
      #
      #     attr_accessible :name
      #     attr_accessible :name, :credit_rating, :as => :admin
      #
      #     def assign_attributes(values, options = {})
      #       sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
      #         send("#{k}=", v)
      #       end
      #     end
      #   end
      #
      # When using a :default scope :
      #
      #   customer = Customer.new
      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
      #   customer.name          # => "David"
      #   customer.credit_rating # => nil
      #
      #   customer.credit_rating = "Average"
      #   customer.credit_rating # => "Average"
      #
      # And using the :admin scope :
      #
      #   customer = Customer.new
      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin)
      #   customer.name          # => "David"
      #   customer.credit_rating # => "Excellent"
      #
      # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+
      # to sanitize attributes won't provide sufficient protection.
      def attr_accessible(*args)
        options = args.extract_options!
        scope = options[:as] || :default

        self._accessible_attributes        = accessible_attributes_configs.dup
        self._accessible_attributes[scope] = self.accessible_attributes(scope) + args

        self._active_authorizer = self._accessible_attributes
      end

      def protected_attributes(scope = :default)
        protected_attributes_configs[scope]
      end

      def accessible_attributes(scope = :default)
        accessible_attributes_configs[scope]
      end

      def active_authorizers
        self._active_authorizer ||= protected_attributes_configs
      end
      alias active_authorizer active_authorizers

      def attributes_protected_by_default
        []
      end

      private

      def protected_attributes_configs
        self._protected_attributes ||= begin
          default_black_list = BlackList.new(attributes_protected_by_default).tap do |w|
            w.logger = self.logger if self.respond_to?(:logger)
          end
          Hash.new(default_black_list)
        end
      end

      def accessible_attributes_configs
        self._accessible_attributes ||= begin
          default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
          Hash.new(default_white_list)
        end
      end
    end

  protected

    def sanitize_for_mass_assignment(attributes, scope = :default)
      mass_assignment_authorizer(scope).sanitize(attributes)
    end

    def mass_assignment_authorizer(scope = :default)
      self.class.active_authorizer[scope]
    end
  end
end