diff options
3 files changed, 225 insertions, 14 deletions
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index bbb711b49d..88b9e78da7 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -15,7 +15,7 @@ module ActionController # a non-empty array: # # class UsersController < ApplicationController - # wrap_parameters :format => [:json, :xml] + # wrap_parameters format: [:json, :xml] # end # # If you enable +ParamsWrapper+ for +:json+ format, instead of having to @@ -38,11 +38,12 @@ module ActionController # +:exclude+ options like this: # # class UsersController < ApplicationController - # wrap_parameters :person, :include => [:username, :password] + # wrap_parameters :person, include: [:username, :password] # end # # On ActiveRecord models with no +:include+ or +:exclude+ option set, - # it will only wrap the parameters returned by the class method attribute_names. + # it will only wrap the parameters returned by the class method + # <tt>attribute_names</tt>. # # If you're going to pass the parameters to an +ActiveModel+ object (such as # <tt>User.new(params[:user])</tt>), you might consider passing the model class to diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index ebbacfc455..24768b23a8 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -3,36 +3,169 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/rescuable' module ActionController + # Raised when a required parameter is missing. + # + # params = ActionController::Parameters.new(a: {}) + # params.fetch(:b) + # # => ActionController::ParameterMissing: key not found: b + # params.require(:a) + # # => ActionController::ParameterMissing: key not found: a class ParameterMissing < KeyError - attr_reader :param + attr_reader :param # :nodoc: - def initialize(param) + def initialize(param) # :nodoc: @param = param super("key not found: #{param}") end end + # == Action Controller Parameters + # + # Allows to choose which attributes should be whitelisted for mass updating + # and thus prevent accidentally exposing that which shouldn’t be exposed. + # Provides two methods for this purpose: #require and #permit. The former is + # used to mark parameters as required. The latter is used to set the parameter + # as permitted and limit which attributes should be allowed for mass updating. + # + # params = ActionController::Parameters.new({ + # person: { + # name: 'Francesco', + # age: 22, + # role: 'admin' + # } + # }) + # + # permitted = params.require(:person).permit(:name, :age) + # permitted # => {"name"=>"Francesco", "age"=>22} + # permitted.class # => ActionController::Parameters + # permitted.permitted? # => true + # + # Person.first.update_attributes!(permitted) + # # => #<Person id: 1, name: "Francesco", age: 22, role: "user"> + # + # It provides a +permit_all_parameters+ option that controls the top-level + # behaviour of new instances. If it's +true+, all the parameters will be + # permitted by default. The default value for +permit_all_parameters+ + # option is +false+. + # + # params = ActionController::Parameters.new + # params.permitted? # => false + # + # ActionController::Parameters.permit_all_parameters = true + # + # params = ActionController::Parameters.new + # params.permitted? # => true + # + # <tt>ActionController::Parameters</tt> is inherited from + # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means + # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>. + # + # params = ActionController::Parameters.new(key: 'value') + # params[:key] # => "value" + # params["key"] # => "value" class Parameters < ActiveSupport::HashWithIndifferentAccess cattr_accessor :permit_all_parameters, instance_accessor: false - attr_accessor :permitted - alias :permitted? :permitted + attr_accessor :permitted # :nodoc: + # Returns a new instance of <tt>ActionController::Parameters</tt>. + # Also, sets the +permitted+ attribute to the default value of + # <tt>ActionController::Parameters.permit_all_parameters</tt>. + # + # class Person + # include ActiveRecord::Base + # end + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => false + # Person.new(params) # => ActiveModel::ForbiddenAttributesError + # + # ActionController::Parameters.permit_all_parameters = true + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => true + # Person.new(params) # => #<Person id: nil, name: "Francesco"> def initialize(attributes = nil) super(attributes) @permitted = self.class.permit_all_parameters end + # Returns +true+ if the parameter is permitted, +false+ otherwise. + # + # params = ActionController::Parameters.new + # params.permitted? # => false + # params.permit! + # params.permitted? # => true + def permitted? + @permitted + end + + # Sets the +permitted+ attribute to +true+. This can be used to pass + # mass assignment. Returns +self+. + # + # class Person < ActiveRecord::Base + # end + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => false + # Person.new(params) # => ActiveModel::ForbiddenAttributesError + # params.permit! + # params.permitted? # => true + # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! @permitted = true self end + # Ensures that a parameter is present. If it's present, returns + # the parameter at the given +key+, otherwise raises an + # <tt>ActionController::ParameterMissing</tt> error. + # + # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) + # # => {"name"=>"Francesco"} + # + # ActionController::Parameters.new(person: nil).require(:person) + # # => ActionController::ParameterMissing: key not found: person + # + # ActionController::Parameters.new(person: {}).require(:person) + # # => ActionController::ParameterMissing: key not found: person def require(key) self[key].presence || raise(ParameterMissing.new(key)) end + # Alias of #require. alias :required :require + # Returns a new <tt>ActionController::Parameters</tt> instance that + # includes only the given +filters+ and sets the +permitted+ for the + # object to +true+. This is useful for limiting which attributes + # should be allowed for mass updating. + # + # params = ActionController::Parameters.new(user: { name: 'Francesco', age: 22, role: 'admin' }) + # permitted = params.require(:user).permit(:name, :age) + # permitted.permitted? # => true + # permitted.has_key?(:name) # => true + # permitted.has_key?(:age) # => true + # permitted.has_key?(:role) # => false + # + # You can also use +permit+ on nested parameters, like: + # + # params = ActionController::Parameters.new({ + # person: { + # name: 'Francesco', + # age: 22, + # pets: [{ + # name: 'Purplish', + # category: 'dogs' + # }] + # } + # }) + # + # permitted = params.permit(person: [ :name, { pets: [ :name ] } ]) + # permitted.permitted? # => true + # permitted[:person][:name] # => "Francesco" + # permitted[:person][:age] # => nil + # permitted[:person][:pets][0][:name] # => "Purplish" + # permitted[:person][:pets][0][:category] # => nil def permit(*filters) params = self.class.new @@ -61,20 +194,52 @@ module ActionController params.permit! end + # Returns a parameter for the given +key+. If not found, + # returns +nil+. + # + # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params[:person] # => {"name"=>"Francesco"} + # params[:none] # => nil def [](key) convert_hashes_to_parameters(key, super) end + # Returns a parameter for the given +key+. If the +key+ + # can't be found, there are several options: With no other arguments, + # it will raise an <tt>ActionController::ParameterMissing</tt> error; + # if more arguments are given, then that will be returned; if a block + # is given, then that will be run and its result returned. + # + # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params.fetch(:person) # => {"name"=>"Francesco"} + # params.fetch(:none) # => ActionController::ParameterMissing: key not found: none + # params.fetch(:none, 'Francesco') # => "Francesco" + # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) convert_hashes_to_parameters(key, super) rescue KeyError raise ActionController::ParameterMissing.new(key) end + # Returns a new <tt>ActionController::Parameters</tt> instance that + # includes only the given +keys+. If the given +keys+ + # don't exist, returns an empty hash. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.slice(:a, :b) # => {"a"=>1, "b"=>2} + # params.slice(:d) # => {} def slice(*keys) self.class.new(super) end + # Returns an exact copy of the <tt>ActionController::Parameters</tt> + # instance. +permitted+ state is kept on the duped object. + # + # params = ActionController::Parameters.new(a: 1) + # params.permit! + # params.permitted? # => true + # copy_params = params.dup # => {"a"=>1} + # copy_params.permitted? # => true def dup super.tap do |duplicate| duplicate.instance_variable_set :@permitted, @permitted @@ -104,6 +269,46 @@ module ActionController end end + # == Strong Parameters + # + # It provides an interface for proctecting attributes from end-user + # assignment. This makes Action Controller parameters are forbidden + # to be used in Active Model mass assignmets until they have been + # whitelisted. + # + # In addition, parameters can be marked as required and flow through a + # predefined raise/rescue flow to end up as a 400 Bad Request with no + # effort. + # + # class PeopleController < ActionController::Base + # # This will raise an ActiveModel::ForbiddenAttributes exception because + # # it's using mass assignment without an explicit permit step. + # def create + # Person.create(params[:person]) + # end + # + # # This will pass with flying colors as long as there's a person key in the + # # parameters, otherwise it'll raise a ActionController::MissingParameter + # # exception, which will get caught by ActionController::Base and turned + # # into that 400 Bad Request reply. + # def update + # redirect_to current_account.people.find(params[:id]).tap { |person| + # person.update_attributes!(person_params) + # } + # end + # + # private + # # Using a private method to encapsulate the permissible parameters is + # # just a good pattern since you'll be able to reuse the same permit + # # list between create and update. Also, you can specialize this method + # # with per-user checking of permissible attributes. + # def person_params + # params.require(:person).permit(:name, :age) + # end + # end + # + # See ActionController::Parameters.require and ActionController::Parameters.permit + # for more information. module StrongParameters extend ActiveSupport::Concern include ActiveSupport::Rescuable @@ -114,12 +319,17 @@ module ActionController end end + # Returns a new ActionController::Parameters object that + # has been instantiated with the <tt>request.parameters</tt>. def params @_params ||= Parameters.new(request.parameters) end - def params=(val) - @_params = val.is_a?(Hash) ? Parameters.new(val) : val + # Assigns the given +value+ to the +params+ hash. If +value+ + # is a Hash, this will create an ActionController::Parameters + # object that has been instantiated with the given +value+ hash. + def params=(value) + @_params = value.is_a?(Hash) ? Parameters.new(value) : value end end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index fa8c5ac95c..af13b75a9d 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -6,11 +6,11 @@ module ActiveRecord include ActiveModel::ForbiddenAttributesProtection # Allows you to set all the attributes by passing in a hash of attributes with - # keys matching the attribute names (which again matches the column names) + # keys matching the attribute names (which again matches the column names). # - # If the passed hash responds to permitted? method and the return value - # of this method is false an ActiveModel::ForbiddenAttributesError exception - # is raised. + # If the passed hash responds to <tt>permitted?</tt> method and the return value + # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> + # exception is raised. def assign_attributes(new_attributes) return if new_attributes.blank? @@ -59,7 +59,7 @@ module ActiveRecord # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the - # attribute will be set to nil. + # attribute will be set to +nil+. def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( extract_callstack_for_multiparameter_attributes(pairs) |