Skip to content
This repository
Browse code

Merge pull request #7251 from rails/integrate-strong_parameters

Integrate strong_parameters in Rails 4
  • Loading branch information...
commit c49d959e9d40101f1712a452004695f4ce27d84c 2 parents ade7010 + 3919fcd
David Heinemeier Hansson authored September 18, 2012

Showing 74 changed files with 662 additions and 2,232 deletions. Show diff stats Hide diff stats

  1. 2  actionpack/lib/action_controller.rb
  2. 1  actionpack/lib/action_controller/base.rb
  3. 9  actionpack/lib/action_controller/metal/params_wrapper.rb
  4. 125  actionpack/lib/action_controller/metal/strong_parameters.rb
  5. 4  actionpack/lib/action_controller/railtie.rb
  6. 113  actionpack/test/controller/parameters/nested_parameters_test.rb
  7. 73  actionpack/test/controller/parameters/parameters_permit_test.rb
  8. 10  actionpack/test/controller/parameters/parameters_require_test.rb
  9. 40  actionpack/test/controller/params_wrapper_test.rb
  10. 25  actionpack/test/controller/permitted_params_test.rb
  11. 30  actionpack/test/controller/required_params_test.rb
  12. 1  actionpack/test/fixtures/company.rb
  13. 3  activemodel/lib/active_model.rb
  14. 19  activemodel/lib/active_model/deprecated_mass_assignment_security.rb
  15. 14  activemodel/lib/active_model/forbidden_attributes_protection.rb
  16. 350  activemodel/lib/active_model/mass_assignment_security.rb
  17. 40  activemodel/lib/active_model/mass_assignment_security/permission_set.rb
  18. 74  activemodel/lib/active_model/mass_assignment_security/sanitizer.rb
  19. 16  activemodel/test/cases/deprecated_mass_assignment_security_test.rb
  20. 36  activemodel/test/cases/forbidden_attributes_protection_test.rb
  21. 20  activemodel/test/cases/mass_assignment_security/black_list_test.rb
  22. 36  activemodel/test/cases/mass_assignment_security/permission_set_test.rb
  23. 50  activemodel/test/cases/mass_assignment_security/sanitizer_test.rb
  24. 19  activemodel/test/cases/mass_assignment_security/white_list_test.rb
  25. 118  activemodel/test/cases/mass_assignment_security_test.rb
  26. 12  activemodel/test/cases/secure_password_test.rb
  27. 5  activemodel/test/models/account.rb
  28. 4  activemodel/test/models/administrator.rb
  29. 76  activemodel/test/models/mass_assignment_specific.rb
  30. 3  activemodel/test/models/project.rb
  31. 3  activemodel/test/models/visitor.rb
  32. 6  activerecord/lib/active_record/associations/association.rb
  33. 20  activerecord/lib/active_record/associations/collection_association.rb
  34. 13  activerecord/lib/active_record/associations/collection_proxy.rb
  35. 4  activerecord/lib/active_record/associations/has_many_through_association.rb
  36. 16  activerecord/lib/active_record/associations/singular_association.rb
  37. 96  activerecord/lib/active_record/attribute_assignment.rb
  38. 5  activerecord/lib/active_record/attribute_methods/primary_key.rb
  39. 6  activerecord/lib/active_record/core.rb
  40. 38  activerecord/lib/active_record/nested_attributes.rb
  41. 18  activerecord/lib/active_record/persistence.rb
  42. 4  activerecord/lib/active_record/reflection.rb
  43. 12  activerecord/lib/active_record/relation.rb
  44. 1  activerecord/lib/active_record/schema_migration.rb
  45. 6  activerecord/lib/active_record/validations.rb
  46. 5  activerecord/lib/rails/generators/active_record/model/templates/model.rb
  47. 35  activerecord/test/cases/associations/has_many_associations_test.rb
  48. 15  activerecord/test/cases/associations/has_many_through_associations_test.rb
  49. 32  activerecord/test/cases/associations/has_one_associations_test.rb
  50. 4  activerecord/test/cases/base_test.rb
  51. 28  activerecord/test/cases/deprecated_dynamic_methods_test.rb
  52. 2  activerecord/test/cases/dup_test.rb
  53. 49  activerecord/test/cases/forbidden_attributes_protection_test.rb
  54. 966  activerecord/test/cases/mass_assignment_security_test.rb
  55. 40  activerecord/test/cases/persistence_test.rb
  56. 6  activerecord/test/cases/validations_test.rb
  57. 8  activerecord/test/models/bulb.rb
  58. 1  activerecord/test/models/company.rb
  59. 1  activerecord/test/models/company_in_module.rb
  60. 12  activerecord/test/models/person.rb
  61. 2  activerecord/test/models/reader.rb
  62. 2  activerecord/test/models/reply.rb
  63. 5  railties/lib/rails/generators/rails/app/templates/config/application.rb
  64. 3  railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
  65. 5  railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
  66. 2  railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
  67. 16  railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
  68. 35  railties/test/application/configuration_test.rb
  69. 1  railties/test/application/loading_test.rb
  70. 15  railties/test/generators/app_generator_test.rb
  71. 10  railties/test/generators/model_generator_test.rb
  72. 7  railties/test/generators/scaffold_controller_generator_test.rb
  73. 8  railties/test/generators/scaffold_generator_test.rb
  74. 3  railties/test/isolation/abstract_unit.rb
2  actionpack/lib/action_controller.rb
@@ -2,6 +2,7 @@
2 2
 require 'abstract_controller'
3 3
 require 'action_dispatch'
4 4
 require 'action_controller/metal/live'
  5
+require 'action_controller/metal/strong_parameters'
5 6
 
6 7
 module ActionController
7 8
   extend ActiveSupport::Autoload
@@ -34,6 +35,7 @@ module ActionController
34 35
     autoload :Rescue
35 36
     autoload :Responder
36 37
     autoload :Streaming
  38
+    autoload :StrongParameters
37 39
     autoload :Testing
38 40
     autoload :UrlFor
39 41
   end
1  actionpack/lib/action_controller/base.rb
@@ -214,6 +214,7 @@ def self.without_modules(*modules)
214 214
       Caching,
215 215
       MimeResponds,
216 216
       ImplicitRender,
  217
+      StrongParameters,
217 218
 
218 219
       Cookies,
219 220
       Flash,
9  actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -42,9 +42,7 @@ module ActionController
42 42
   #     end
43 43
   #
44 44
   # On ActiveRecord models with no +:include+ or +:exclude+ option set,
45  
-  # if attr_accessible is set on that model, it will only wrap the accessible
46  
-  # parameters, else it will only wrap the parameters returned by the class
47  
-  # method attribute_names.
  45
+  # it will only wrap the parameters returned by the class method attribute_names.
48 46
   #
49 47
   # If you're going to pass the parameters to an +ActiveModel+ object (such as
50 48
   # <tt>User.new(params[:user])</tt>), you might consider passing the model class to
@@ -165,10 +163,7 @@ def _set_wrapper_defaults(options, model=nil)
165 163
 
166 164
         unless options[:include] || options[:exclude]
167 165
           model ||= _default_wrap_model
168  
-          role = options.fetch(:as, :default)
169  
-          if model.respond_to?(:accessible_attributes) && model.accessible_attributes(role).present?
170  
-            options[:include] = model.accessible_attributes(role).to_a
171  
-          elsif model.respond_to?(:attribute_names) && model.attribute_names.present?
  166
+          if model.respond_to?(:attribute_names) && model.attribute_names.present?
172 167
             options[:include] = model.attribute_names
173 168
           end
174 169
         end
125  actionpack/lib/action_controller/metal/strong_parameters.rb
... ...
@@ -0,0 +1,125 @@
  1
+require 'active_support/concern'
  2
+require 'active_support/core_ext/hash/indifferent_access'
  3
+require 'active_support/rescuable'
  4
+
  5
+module ActionController
  6
+  class ParameterMissing < KeyError
  7
+    attr_reader :param
  8
+
  9
+    def initialize(param)
  10
+      @param = param
  11
+      super("key not found: #{param}")
  12
+    end
  13
+  end
  14
+
  15
+  class Parameters < ActiveSupport::HashWithIndifferentAccess
  16
+    cattr_accessor :permit_all_parameters, instance_accessor: false
  17
+    attr_accessor :permitted
  18
+    alias :permitted? :permitted
  19
+
  20
+    def initialize(attributes = nil)
  21
+      super(attributes)
  22
+      @permitted = self.class.permit_all_parameters
  23
+    end
  24
+
  25
+    def permit!
  26
+      @permitted = true
  27
+      self
  28
+    end
  29
+
  30
+    def require(key)
  31
+      self[key].presence || raise(ParameterMissing.new(key))
  32
+    end
  33
+
  34
+    alias :required :require
  35
+
  36
+    def permit(*filters)
  37
+      params = self.class.new
  38
+
  39
+      filters.each do |filter|
  40
+        case filter
  41
+        when Symbol, String then
  42
+          params[filter] = self[filter] if has_key?(filter)
  43
+        when Hash then
  44
+          self.slice(*filter.keys).each do |key, value|
  45
+            return unless value
  46
+
  47
+            key = key.to_sym
  48
+
  49
+            params[key] = each_element(value) do |value|
  50
+              # filters are a Hash, so we expect value to be a Hash too
  51
+              next if filter.is_a?(Hash) && !value.is_a?(Hash)
  52
+
  53
+              value = self.class.new(value) if !value.respond_to?(:permit)
  54
+
  55
+              value.permit(*Array.wrap(filter[key]))
  56
+            end
  57
+          end
  58
+        end
  59
+      end
  60
+
  61
+      params.permit!
  62
+    end
  63
+
  64
+    def [](key)
  65
+      convert_hashes_to_parameters(key, super)
  66
+    end
  67
+
  68
+    def fetch(key, *args)
  69
+      convert_hashes_to_parameters(key, super)
  70
+    rescue KeyError
  71
+      raise ActionController::ParameterMissing.new(key)
  72
+    end
  73
+
  74
+    def slice(*keys)
  75
+      self.class.new(super)
  76
+    end
  77
+
  78
+    def dup
  79
+      super.tap do |duplicate|
  80
+        duplicate.instance_variable_set :@permitted, @permitted
  81
+      end
  82
+    end
  83
+
  84
+    private
  85
+      def convert_hashes_to_parameters(key, value)
  86
+        if value.is_a?(Parameters) || !value.is_a?(Hash)
  87
+          value
  88
+        else
  89
+          # Convert to Parameters on first access
  90
+          self[key] = self.class.new(value)
  91
+        end
  92
+      end
  93
+
  94
+      def each_element(object)
  95
+        if object.is_a?(Array)
  96
+          object.map { |el| yield el }.compact
  97
+        elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ }
  98
+          hash = object.class.new
  99
+          object.each { |k,v| hash[k] = yield v }
  100
+          hash
  101
+        else
  102
+          yield object
  103
+        end
  104
+      end
  105
+  end
  106
+
  107
+  module StrongParameters
  108
+    extend ActiveSupport::Concern
  109
+    include ActiveSupport::Rescuable
  110
+
  111
+    included do
  112
+      rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception|
  113
+        render text: "Required parameter missing: #{parameter_missing_exception.param}", status: :bad_request
  114
+      end
  115
+    end
  116
+
  117
+    def params
  118
+      @_params ||= Parameters.new(request.parameters)
  119
+    end
  120
+
  121
+    def params=(val)
  122
+      @_params = val.is_a?(Hash) ? Parameters.new(val) : val
  123
+    end
  124
+  end
  125
+end
4  actionpack/lib/action_controller/railtie.rb
@@ -19,6 +19,10 @@ class Railtie < Rails::Railtie #:nodoc:
19 19
       ActionController::Helpers.helpers_path = app.helpers_paths
20 20
     end
21 21
 
  22
+    initializer "action_controller.parameters_config" do |app|
  23
+      ActionController::Parameters.permit_all_parameters = app.config.action_controller.delete(:permit_all_parameters)
  24
+    end
  25
+
22 26
     initializer "action_controller.set_configs" do |app|
23 27
       paths   = app.config.paths
24 28
       options = app.config.action_controller
113  actionpack/test/controller/parameters/nested_parameters_test.rb
... ...
@@ -0,0 +1,113 @@
  1
+require 'abstract_unit'
  2
+require 'action_controller/metal/strong_parameters'
  3
+
  4
+class NestedParametersTest < ActiveSupport::TestCase
  5
+  test "permitted nested parameters" do
  6
+    params = ActionController::Parameters.new({
  7
+      book: {
  8
+        title: "Romeo and Juliet",
  9
+        authors: [{
  10
+          name: "William Shakespeare",
  11
+          born: "1564-04-26"
  12
+        }, {
  13
+          name: "Christopher Marlowe"
  14
+        }],
  15
+        details: {
  16
+          pages: 200,
  17
+          genre: "Tragedy"
  18
+        }
  19
+      },
  20
+      magazine: "Mjallo!"
  21
+    })
  22
+
  23
+    permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages } ]
  24
+
  25
+    assert permitted.permitted?
  26
+    assert_equal "Romeo and Juliet", permitted[:book][:title]
  27
+    assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
  28
+    assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
  29
+    assert_equal 200, permitted[:book][:details][:pages]
  30
+    assert_nil permitted[:book][:details][:genre]
  31
+    assert_nil permitted[:book][:authors][1][:born]
  32
+    assert_nil permitted[:magazine]
  33
+  end
  34
+
  35
+  test "nested arrays with strings" do
  36
+    params = ActionController::Parameters.new({
  37
+      :book => {
  38
+        :genres => ["Tragedy"]
  39
+      }
  40
+    })
  41
+
  42
+    permitted = params.permit :book => :genres
  43
+    assert_equal ["Tragedy"], permitted[:book][:genres]
  44
+  end
  45
+
  46
+  test "permit may specify symbols or strings" do
  47
+    params = ActionController::Parameters.new({
  48
+      :book => {
  49
+        :title => "Romeo and Juliet",
  50
+        :author => "William Shakespeare"
  51
+      },
  52
+      :magazine => "Shakespeare Today"
  53
+    })
  54
+
  55
+    permitted = params.permit({:book => ["title", :author]}, "magazine")
  56
+    assert_equal "Romeo and Juliet", permitted[:book][:title]
  57
+    assert_equal "William Shakespeare", permitted[:book][:author]
  58
+    assert_equal "Shakespeare Today", permitted[:magazine]
  59
+  end
  60
+
  61
+  test "nested array with strings that should be hashes" do
  62
+    params = ActionController::Parameters.new({
  63
+      book: {
  64
+        genres: ["Tragedy"]
  65
+      }
  66
+    })
  67
+
  68
+    permitted = params.permit book: { genres: :type }
  69
+    assert_empty permitted[:book][:genres]
  70
+  end
  71
+
  72
+  test "nested array with strings that should be hashes and additional values" do
  73
+    params = ActionController::Parameters.new({
  74
+      book: {
  75
+        title: "Romeo and Juliet",
  76
+        genres: ["Tragedy"]
  77
+      }
  78
+    })
  79
+
  80
+    permitted = params.permit book: [ :title, { genres: :type } ]
  81
+    assert_equal "Romeo and Juliet", permitted[:book][:title]
  82
+    assert_empty permitted[:book][:genres]
  83
+  end
  84
+
  85
+  test "nested string that should be a hash" do
  86
+    params = ActionController::Parameters.new({
  87
+      book: {
  88
+        genre: "Tragedy"
  89
+      }
  90
+    })
  91
+
  92
+    permitted = params.permit book: { genre: :type }
  93
+    assert_nil permitted[:book][:genre]
  94
+  end
  95
+
  96
+  test "fields_for-style nested params" do
  97
+    params = ActionController::Parameters.new({
  98
+      book: {
  99
+        authors_attributes: {
  100
+          :'0' => { name: 'William Shakespeare', age_of_death: '52' },
  101
+          :'-1' => { name: 'Unattributed Assistant' }
  102
+        }
  103
+      }
  104
+    })
  105
+    permitted = params.permit book: { authors_attributes: [ :name ] }
  106
+
  107
+    assert_not_nil permitted[:book][:authors_attributes]['0']
  108
+    assert_not_nil permitted[:book][:authors_attributes]['-1']
  109
+    assert_nil permitted[:book][:authors_attributes]['0'][:age_of_death]
  110
+    assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['0'][:name]
  111
+    assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['-1'][:name]
  112
+  end
  113
+end
73  actionpack/test/controller/parameters/parameters_permit_test.rb
... ...
@@ -0,0 +1,73 @@
  1
+require 'abstract_unit'
  2
+require 'action_controller/metal/strong_parameters'
  3
+
  4
+class ParametersPermitTest < ActiveSupport::TestCase
  5
+  setup do
  6
+    @params = ActionController::Parameters.new({ person: { 
  7
+      age: "32", name: { first: "David", last: "Heinemeier Hansson" }
  8
+    }})
  9
+  end
  10
+
  11
+  test "fetch raises ParameterMissing exception" do
  12
+    e = assert_raises(ActionController::ParameterMissing) do
  13
+      @params.fetch :foo
  14
+    end
  15
+    assert_equal :foo, e.param
  16
+  end
  17
+
  18
+  test "fetch doesnt raise ParameterMissing exception if there is a default" do
  19
+    assert_equal "monkey", @params.fetch(:foo, "monkey")
  20
+    assert_equal "monkey", @params.fetch(:foo) { "monkey" }
  21
+  end
  22
+
  23
+  test "permitted is sticky on accessors" do
  24
+    assert !@params.slice(:person).permitted?
  25
+    assert !@params[:person][:name].permitted?
  26
+
  27
+    @params.each { |key, value| assert(value.permitted?) if key == :person }
  28
+
  29
+    assert !@params.fetch(:person).permitted?
  30
+
  31
+    assert !@params.values_at(:person).first.permitted?
  32
+  end
  33
+
  34
+  test "permitted is sticky on mutators" do
  35
+    assert !@params.delete_if { |k| k == :person }.permitted?
  36
+    assert !@params.keep_if { |k,v| k == :person }.permitted?
  37
+  end
  38
+
  39
+  test "permitted is sticky beyond merges" do
  40
+    assert !@params.merge(a: "b").permitted?
  41
+  end
  42
+
  43
+  test "modifying the parameters" do
  44
+    @params[:person][:hometown] = "Chicago"
  45
+    @params[:person][:family] = { brother: "Jonas" }
  46
+
  47
+    assert_equal "Chicago", @params[:person][:hometown]
  48
+    assert_equal "Jonas", @params[:person][:family][:brother]
  49
+  end
  50
+
  51
+  test "permitting parameters that are not there should not include the keys" do
  52
+    assert !@params.permit(:person, :funky).has_key?(:funky)
  53
+  end
  54
+
  55
+  test "permit state is kept on a dup" do
  56
+    @params.permit!
  57
+    assert_equal @params.permitted?, @params.dup.permitted?
  58
+  end
  59
+
  60
+  test "permitted takes a default value when Parameters.permit_all_parameters is set" do
  61
+    begin
  62
+      ActionController::Parameters.permit_all_parameters = true
  63
+      params = ActionController::Parameters.new({ person: {
  64
+        age: "32", name: { first: "David", last: "Heinemeier Hansson" }
  65
+      }})
  66
+
  67
+      assert params.slice(:person).permitted?
  68
+      assert params[:person][:name].permitted?
  69
+    ensure
  70
+      ActionController::Parameters.permit_all_parameters = false
  71
+    end
  72
+  end
  73
+end
10  actionpack/test/controller/parameters/parameters_require_test.rb
... ...
@@ -0,0 +1,10 @@
  1
+require 'abstract_unit'
  2
+require 'action_controller/metal/strong_parameters'
  3
+
  4
+class ParametersRequireTest < ActiveSupport::TestCase
  5
+  test "required parameters must be present not merely not nil" do
  6
+    assert_raises(ActionController::ParameterMissing) do
  7
+      ActionController::Parameters.new(person: {}).require(:person)
  8
+    end
  9
+  end
  10
+end
40  actionpack/test/controller/params_wrapper_test.rb
@@ -155,7 +155,6 @@ def test_nested_params
155 155
   end
156 156
 
157 157
   def test_derived_wrapped_keys_from_matching_model
158  
-    User.expects(:respond_to?).with(:accessible_attributes).returns(false)
159 158
     User.expects(:respond_to?).with(:attribute_names).returns(true)
160 159
     User.expects(:attribute_names).twice.returns(["username"])
161 160
 
@@ -168,7 +167,6 @@ def test_derived_wrapped_keys_from_matching_model
168 167
 
169 168
   def test_derived_wrapped_keys_from_specified_model
170 169
     with_default_wrapper_options do
171  
-      Person.expects(:respond_to?).with(:accessible_attributes).returns(false)
172 170
       Person.expects(:respond_to?).with(:attribute_names).returns(true)
173 171
       Person.expects(:attribute_names).twice.returns(["username"])
174 172
 
@@ -179,46 +177,8 @@ def test_derived_wrapped_keys_from_specified_model
179 177
       assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }})
180 178
     end
181 179
   end
182  
-  
183  
-  def test_accessible_wrapped_keys_from_matching_model
184  
-    User.expects(:respond_to?).with(:accessible_attributes).returns(true)
185  
-    User.expects(:accessible_attributes).with(:default).twice.returns(["username"])
186  
-    
187  
-    with_default_wrapper_options do
188  
-      @request.env['CONTENT_TYPE'] = 'application/json'
189  
-      post :parse, { 'username' => 'sikachu', 'title' => 'Developer' }
190  
-      assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }})
191  
-    end
192  
-  end
193  
-  
194  
-  def test_accessible_wrapped_keys_from_specified_model
195  
-    with_default_wrapper_options do
196  
-      Person.expects(:respond_to?).with(:accessible_attributes).returns(true)
197  
-      Person.expects(:accessible_attributes).with(:default).twice.returns(["username"])
198  
-
199  
-      UsersController.wrap_parameters Person
200  
-
201  
-      @request.env['CONTENT_TYPE'] = 'application/json'
202  
-      post :parse, { 'username' => 'sikachu', 'title' => 'Developer' }
203  
-      assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }})
204  
-    end
205  
-  end
206  
-  
207  
-  def test_accessible_wrapped_keys_with_role_from_specified_model
208  
-    with_default_wrapper_options do
209  
-      Person.expects(:respond_to?).with(:accessible_attributes).returns(true)
210  
-      Person.expects(:accessible_attributes).with(:admin).twice.returns(["username"])
211  
-
212  
-      UsersController.wrap_parameters Person, :as => :admin
213  
-
214  
-      @request.env['CONTENT_TYPE'] = 'application/json'
215  
-      post :parse, { 'username' => 'sikachu', 'title' => 'Developer' }
216  
-      assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }})
217  
-    end
218  
-  end
219 180
 
220 181
   def test_not_wrapping_abstract_model
221  
-    User.expects(:respond_to?).with(:accessible_attributes).returns(false)
222 182
     User.expects(:respond_to?).with(:attribute_names).returns(true)
223 183
     User.expects(:attribute_names).returns([])
224 184
 
25  actionpack/test/controller/permitted_params_test.rb
... ...
@@ -0,0 +1,25 @@
  1
+require 'abstract_unit'
  2
+
  3
+class PeopleController < ActionController::Base
  4
+  def create
  5
+    render text: params[:person].permitted? ? "permitted" : "forbidden"
  6
+  end
  7
+
  8
+  def create_with_permit
  9
+    render text: params[:person].permit(:name).permitted? ? "permitted" : "forbidden"
  10
+  end
  11
+end
  12
+
  13
+class ActionControllerPermittedParamsTest < ActionController::TestCase
  14
+  tests PeopleController
  15
+
  16
+  test "parameters are forbidden" do
  17
+    post :create, { person: { name: "Mjallo!" } }
  18
+    assert_equal "forbidden", response.body
  19
+  end
  20
+
  21
+  test "parameters can be permitted and are then not forbidden" do
  22
+    post :create_with_permit, { person: { name: "Mjallo!" } }
  23
+    assert_equal "permitted", response.body
  24
+  end
  25
+end
30  actionpack/test/controller/required_params_test.rb
... ...
@@ -0,0 +1,30 @@
  1
+require 'abstract_unit'
  2
+
  3
+class BooksController < ActionController::Base
  4
+  def create
  5
+    params.require(:book).require(:name)
  6
+    head :ok
  7
+  end
  8
+end
  9
+
  10
+class ActionControllerRequiredParamsTest < ActionController::TestCase
  11
+  tests BooksController
  12
+
  13
+  test "missing required parameters will raise exception" do
  14
+    post :create, { magazine: { name: "Mjallo!" } }
  15
+    assert_response :bad_request
  16
+
  17
+    post :create, { book: { title: "Mjallo!" } }
  18
+    assert_response :bad_request
  19
+  end
  20
+
  21
+  test "required parameters that are present will not raise" do
  22
+    post :create, { book: { name: "Mjallo!" } }
  23
+    assert_response :ok
  24
+  end
  25
+
  26
+  test "missing parameters will be mentioned in the return" do
  27
+    post :create, { magazine: { name: "Mjallo!" } }
  28
+    assert_equal "Required parameter missing: book", response.body
  29
+  end
  30
+end
1  actionpack/test/fixtures/company.rb
... ...
@@ -1,6 +1,5 @@
1 1
 class Company < ActiveRecord::Base
2 2
   has_one :mascot
3  
-  attr_protected :rating
4 3
   self.sequence_name = :companies_nonstd_seq
5 4
 
6 5
   validates_presence_of :name
3  activemodel/lib/active_model.rb
@@ -34,9 +34,10 @@ module ActiveModel
34 34
   autoload :Conversion
35 35
   autoload :Dirty
36 36
   autoload :EachValidator, 'active_model/validator'
  37
+  autoload :ForbiddenAttributesProtection
37 38
   autoload :Lint
38  
-  autoload :MassAssignmentSecurity
39 39
   autoload :Model
  40
+  autoload :DeprecatedMassAssignmentSecurity
40 41
   autoload :Name, 'active_model/naming'
41 42
   autoload :Naming
42 43
   autoload :Observer, 'active_model/observing'
19  activemodel/lib/active_model/deprecated_mass_assignment_security.rb
... ...
@@ -0,0 +1,19 @@
  1
+module ActiveModel
  2
+  module DeprecatedMassAssignmentSecurity
  3
+    extend ActiveSupport::Concern
  4
+
  5
+     module ClassMethods
  6
+       def attr_protected(*args)
  7
+         raise "`attr_protected` is extracted out of Rails into a gem. " \
  8
+           "Please use new recommended protection model for params " \
  9
+           "or add `protected_attributes` to your Gemfile to use old one."
  10
+       end
  11
+
  12
+       def attr_accessible(*args)
  13
+         raise "`attr_accessible` is extracted out of Rails into a gem. " \
  14
+           "Please use new recommended protection model for params " \
  15
+           "or add `protected_attributes` to your Gemfile to use old one."
  16
+       end
  17
+     end
  18
+  end
  19
+end
14  activemodel/lib/active_model/forbidden_attributes_protection.rb
... ...
@@ -0,0 +1,14 @@
  1
+module ActiveModel
  2
+  class ForbiddenAttributesError < StandardError
  3
+  end
  4
+
  5
+  module ForbiddenAttributesProtection
  6
+    def sanitize_for_mass_assignment(attributes, options = {})
  7
+      if attributes.respond_to?(:permitted?) && !attributes.permitted?
  8
+        raise ActiveModel::ForbiddenAttributesError
  9
+      else
  10
+        attributes
  11
+      end
  12
+    end
  13
+  end
  14
+end
350  activemodel/lib/active_model/mass_assignment_security.rb
... ...
@@ -1,350 +0,0 @@
1  
-require 'active_support/core_ext/string/inflections'
2  
-require 'active_model/mass_assignment_security/permission_set'
3  
-require 'active_model/mass_assignment_security/sanitizer'
4  
-
5  
-module ActiveModel
6  
-  # == Active Model Mass-Assignment Security
7  
-  #
8  
-  # Mass assignment security provides an interface for protecting attributes
9  
-  # from end-user assignment. For more complex permissions, mass assignment
10  
-  # security may be handled outside the model by extending a non-ActiveRecord
11  
-  # class, such as a controller, with this behavior.
12  
-  #
13  
-  # For example, a logged in user may need to assign additional attributes
14  
-  # depending on their role:
15  
-  #
16  
-  #   class AccountsController < ApplicationController
17  
-  #     include ActiveModel::MassAssignmentSecurity
18  
-  #
19  
-  #     attr_accessible :first_name, :last_name
20  
-  #     attr_accessible :first_name, :last_name, :plan_id, as: :admin
21  
-  #
22  
-  #     def update
23  
-  #       ...
24  
-  #       @account.update_attributes(account_params)
25  
-  #       ...
26  
-  #     end
27  
-  #
28  
-  #     protected
29  
-  #
30  
-  #     def account_params
31  
-  #       role = admin ? :admin : :default
32  
-  #       sanitize_for_mass_assignment(params[:account], role)
33  
-  #     end
34  
-  #
35  
-  #   end
36  
-  #
37  
-  # === Configuration options
38  
-  #
39  
-  # * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible
40  
-  #   values are:
41  
-  #   * <tt>:logger</tt> (default) - writes filtered attributes to logger
42  
-  #   * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt>
43  
-  #     on any protected attribute update.
44  
-  #
45  
-  # You can specify your own sanitizer object eg. <tt>MySanitizer.new</tt>.
46  
-  # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for
47  
-  # example implementation.
48  
-  module MassAssignmentSecurity
49  
-    extend ActiveSupport::Concern
50  
-
51  
-    included do
52  
-      class_attribute :_accessible_attributes, instance_writer: false
53  
-      class_attribute :_protected_attributes,  instance_writer: false
54  
-      class_attribute :_active_authorizer,     instance_writer: false
55  
-
56  
-      class_attribute :_mass_assignment_sanitizer, instance_writer: false
57  
-      self.mass_assignment_sanitizer = :logger
58  
-    end
59  
-
60  
-    module ClassMethods
61  
-      # Attributes named in this macro are protected from mass-assignment
62  
-      # whenever attributes are sanitized before assignment. A role for the
63  
-      # attributes is optional, if no role is provided then <tt>:default</tt>
64  
-      # is used. A role can be defined by using the <tt>:as</tt> option with a
65  
-      # symbol or an array of symbols as the value.
66  
-      #
67  
-      # Mass-assignment to these attributes will simply be ignored, to assign
68  
-      # to them you can use direct writer methods. This is meant to protect
69  
-      # sensitive attributes from being overwritten by malicious users
70  
-      # tampering with URLs or forms.
71  
-      #
72  
-      #   class Customer
73  
-      #     include ActiveModel::MassAssignmentSecurity
74  
-      #
75  
-      #     attr_accessor :name, :email, :logins_count
76  
-      #
77  
-      #     attr_protected :logins_count
78  
-      #     # Suppose that admin can not change email for customer
79  
-      #     attr_protected :logins_count, :email, as: :admin
80  
-      #
81  
-      #     def assign_attributes(values, options = {})
82  
-      #       sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
83  
-      #         send("#{k}=", v)
84  
-      #       end
85  
-      #     end
86  
-      #   end
87  
-      #
88  
-      # When using the <tt>:default</tt> role:
89  
-      #
90  
-      #   customer = Customer.new
91  
-      #   customer.assign_attributes({ name: 'David', email: 'a@b.com', logins_count: 5 }, as: :default)
92  
-      #   customer.name         # => "David"
93  
-      #   customer.email        # => "a@b.com"
94  
-      #   customer.logins_count # => nil
95  
-      #
96  
-      # And using the <tt>:admin</tt> role:
97  
-      #
98  
-      #   customer = Customer.new
99  
-      #   customer.assign_attributes({ name: 'David', email: 'a@b.com', logins_count: 5}, as: :admin)
100  
-      #   customer.name         # => "David"
101  
-      #   customer.email        # => nil
102  
-      #   customer.logins_count # => nil
103  
-      #
104  
-      #   customer.email = 'c@d.com'
105  
-      #   customer.email # => "c@d.com"
106  
-      #
107  
-      # To start from an all-closed default and enable attributes as needed,
108  
-      # have a look at +attr_accessible+.
109  
-      #
110  
-      # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of
111  
-      # +attr_protected+ to sanitize attributes provides basically the same
112  
-      # functionality, but it makes a bit tricky to deal with nested attributes.
113  
-      def attr_protected(*args)
114  
-        options = args.extract_options!
115  
-        role = options[:as] || :default
116  
-
117  
-        self._protected_attributes = protected_attributes_configs.dup
118  
-
119  
-        Array(role).each do |name|
120  
-          self._protected_attributes[name] = self.protected_attributes(name) + args
121  
-        end
122  
-
123  
-        self._active_authorizer = self._protected_attributes
124  
-      end
125  
-
126  
-      # Specifies a white list of model attributes that can be set via
127  
-      # mass-assignment.
128  
-      #
129  
-      # Like +attr_protected+, a role for the attributes is optional,
130  
-      # if no role is provided then <tt>:default</tt> is used. A role can be
131  
-      # defined by using the <tt>:as</tt> option with a symbol or an array of
132  
-      # symbols as the value.
133  
-      #
134  
-      # This is the opposite of the +attr_protected+ macro: Mass-assignment
135  
-      # will only set attributes in this list, to assign to the rest of
136  
-      # attributes you can use direct writer methods. This is meant to protect
137  
-      # sensitive attributes from being overwritten by malicious users
138  
-      # tampering with URLs or forms. If you'd rather start from an all-open
139  
-      # default and restrict attributes as needed, have a look at
140  
-      # +attr_protected+.
141  
-      #
142  
-      #   class Customer
143  
-      #     include ActiveModel::MassAssignmentSecurity
144  
-      #
145  
-      #     attr_accessor :name, :credit_rating
146  
-      #
147  
-      #     # Both admin and default user can change name of a customer
148  
-      #     attr_accessible :name, as: [:admin, :default]
149  
-      #     # Only admin can change credit rating of a customer
150  
-      #     attr_accessible :credit_rating, as: :admin
151  
-      #
152  
-      #     def assign_attributes(values, options = {})
153  
-      #       sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
154  
-      #         send("#{k}=", v)
155  
-      #       end
156  
-      #     end
157  
-      #   end
158  
-      #
159  
-      # When using the <tt>:default</tt> role:
160  
-      #
161  
-      #   customer = Customer.new
162  
-      #   customer.assign_attributes({ name: 'David', credit_rating: 'Excellent', last_login: 1.day.ago }, as: :default)
163  
-      #   customer.name          # => "David"
164  
-      #   customer.credit_rating # => nil
165  
-      #
166  
-      #   customer.credit_rating = 'Average'
167  
-      #   customer.credit_rating # => "Average"
168  
-      #
169  
-      # And using the <tt>:admin</tt> role:
170  
-      #
171  
-      #   customer = Customer.new
172  
-      #   customer.assign_attributes({ name: 'David', credit_rating: 'Excellent', last_login: 1.day.ago }, as: :admin)
173  
-      #   customer.name          # => "David"
174  
-      #   customer.credit_rating # => "Excellent"
175  
-      #
176  
-      # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of
177  
-      # +attr_accessible+ to sanitize attributes provides basically the same
178  
-      # functionality, but it makes a bit tricky to deal with nested attributes.
179  
-      def attr_accessible(*args)
180  
-        options = args.extract_options!
181  
-        role = options[:as] || :default
182  
-
183  
-        self._accessible_attributes = accessible_attributes_configs.dup
184  
-
185  
-        Array(role).each do |name|
186  
-          self._accessible_attributes[name] = self.accessible_attributes(name) + args
187  
-        end
188  
-
189  
-        self._active_authorizer = self._accessible_attributes
190  
-      end
191  
-
192  
-      # Returns an instance of <tt>ActiveModel::MassAssignmentSecurity::BlackList</tt>
193  
-      # with the attributes protected by #attr_protected method. If no +role+
194  
-      # is provided, then <tt>:default</tt> is used.
195  
-      #
196  
-      #   class Customer
197  
-      #     include ActiveModel::MassAssignmentSecurity
198  
-      #
199  
-      #     attr_accessor :name, :email, :logins_count
200  
-      #
201  
-      #     attr_protected :logins_count
202  
-      #     attr_protected :logins_count, :email, as: :admin
203  
-      #   end
204  
-      #
205  
-      #   Customer.protected_attributes
206  
-      #   # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count"}>
207  
-      #
208  
-      #   Customer.protected_attributes(:default)
209  
-      #   # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count"}>
210  
-      #
211  
-      #   Customer.protected_attributes(:admin)
212  
-      #   # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count", "email"}>
213  
-      def protected_attributes(role = :default)
214  
-        protected_attributes_configs[role]
215  
-      end
216  
-
217  
-      # Returns an instance of <tt>ActiveModel::MassAssignmentSecurity::WhiteList</tt>
218  
-      # with the attributes protected by #attr_accessible method. If no +role+
219  
-      # is provided, then <tt>:default</tt> is used.
220  
-      #
221  
-      #   class Customer
222  
-      #     include ActiveModel::MassAssignmentSecurity
223  
-      #
224  
-      #     attr_accessor :name, :credit_rating
225  
-      #
226  
-      #     attr_accessible :name, as: [:admin, :default]
227  
-      #     attr_accessible :credit_rating, as: :admin
228  
-      #   end
229  
-      #
230  
-      #   Customer.accessible_attributes
231  
-      #   # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}>
232  
-      #
233  
-      #   Customer.accessible_attributes(:default)
234  
-      #   # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}>
235  
-      #
236  
-      #   Customer.accessible_attributes(:admin)
237  
-      #   # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name", "credit_rating"}>
238  
-      def accessible_attributes(role = :default)
239  
-        accessible_attributes_configs[role]
240  
-      end
241  
-
242  
-      # Returns a hash with the protected attributes (by #attr_accessible or
243  
-      # #attr_protected) per role.
244  
-      #
245  
-      #   class Customer
246  
-      #     include ActiveModel::MassAssignmentSecurity
247  
-      #
248  
-      #     attr_accessor :name, :credit_rating
249  
-      #
250  
-      #     attr_accessible :name, as: [:admin, :default]
251  
-      #     attr_accessible :credit_rating, as: :admin
252  
-      #   end
253  
-      #
254  
-      #   Customer.active_authorizers
255  
-      #   # => {
256  
-      #   #       :admin=> #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name", "credit_rating"}>,
257  
-      #   #       :default=>#<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}>
258  
-      #   #    }
259  
-      def active_authorizers
260  
-        self._active_authorizer ||= protected_attributes_configs
261  
-      end
262  
-      alias active_authorizer active_authorizers
263  
-
264  
-      # Returns an empty array by default. You can still override this to define
265  
-      # the default attributes protected by #attr_protected method.
266  
-      #
267  
-      #   class Customer
268  
-      #     include ActiveModel::MassAssignmentSecurity
269  
-      #
270  
-      #     def self.attributes_protected_by_default
271  
-      #       [:name]
272  
-      #     end
273  
-      #   end
274  
-      #
275  
-      #   Customer.protected_attributes
276  
-      #   # => #<ActiveModel::MassAssignmentSecurity::BlackList: {:name}>
277  
-      def attributes_protected_by_default
278  
-        []
279  
-      end
280  
-
281  
-      # Defines sanitize method.
282  
-      #
283  
-      #   class Customer
284  
-      #     include ActiveModel::MassAssignmentSecurity
285  
-      #
286  
-      #     attr_accessor :name
287  
-      #
288  
-      #     attr_protected :name
289  
-      #
290  
-      #     def assign_attributes(values)
291  
-      #       sanitize_for_mass_assignment(values).each do |k, v|
292  
-      #         send("#{k}=", v)
293  
-      #       end
294  
-      #     end
295  
-      #   end
296  
-      #
297  
-      #   # See ActiveModel::MassAssignmentSecurity::StrictSanitizer for more information.
298  
-      #   Customer.mass_assignment_sanitizer = :strict
299  
-      #
300  
-      #   customer = Customer.new
301  
-      #   customer.assign_attributes(name: 'David')
302  
-      #   # => ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes for Customer: name
303  
-      #
304  
-      # Also, you can specify your own sanitizer object.
305  
-      #
306  
-      #   class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer
307  
-      #     def process_removed_attributes(klass, attrs)
308  
-      #       raise StandardError
309  
-      #     end
310  
-      #   end
311  
-      #
312  
-      #   Customer.mass_assignment_sanitizer = CustomSanitizer.new
313  
-      #
314  
-      #   customer = Customer.new
315  
-      #   customer.assign_attributes(name: 'David')
316  
-      #   # => StandardError: StandardError
317  
-      def mass_assignment_sanitizer=(value)
318  
-        self._mass_assignment_sanitizer = if value.is_a?(Symbol)
319  
-          const_get(:"#{value.to_s.camelize}Sanitizer").new(self)
320  
-        else
321  
-          value
322  
-        end
323  
-      end
324  
-
325  
-      private
326  
-
327  
-      def protected_attributes_configs
328  
-        self._protected_attributes ||= begin
329  
-          Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) }
330  
-        end
331  
-      end
332  
-
333  
-      def accessible_attributes_configs
334  
-        self._accessible_attributes ||= begin
335  
-          Hash.new { |h,k| h[k] = WhiteList.new }
336  
-        end
337  
-      end
338  
-    end
339  
-
340  
-  protected
341  
-
342  
-    def sanitize_for_mass_assignment(attributes, role = nil) #:nodoc:
343  
-      _mass_assignment_sanitizer.sanitize(self.class, attributes, mass_assignment_authorizer(role))
344  
-    end
345  
-
346  
-    def mass_assignment_authorizer(role) #:nodoc:
347  
-      self.class.active_authorizer[role || :default]
348  
-    end
349  
-  end
350  
-end
40  activemodel/lib/active_model/mass_assignment_security/permission_set.rb
... ...
@@ -1,40 +0,0 @@
1  
-require 'set'
2  
-
3  
-module ActiveModel
4  
-  module MassAssignmentSecurity
5  
-    class PermissionSet < Set #:nodoc:
6  
-
7  
-      def +(values)
8  
-        super(values.compact.map(&:to_s))
9  
-      end
10  
-
11  
-      def include?(key)
12  
-        super(remove_multiparameter_id(key))
13  
-      end
14  
-
15  
-      def deny?(key)
16  
-        raise NotImplementedError, "#deny?(key) supposed to be overwritten"
17  
-      end
18  
-
19  
-    protected
20  
-
21  
-      def remove_multiparameter_id(key)
22  
-        key.to_s.gsub(/\(.+/, '')
23  
-      end
24  
-    end
25  
-
26  
-    class WhiteList < PermissionSet #:nodoc:
27  
-
28  
-      def deny?(key)
29  
-        !include?(key)
30  
-      end
31  
-    end
32  
-
33  
-    class BlackList < PermissionSet #:nodoc:
34  
-
35  
-      def deny?(key)
36  
-        include?(key)
37  
-      end
38  
-    end
39  
-  end
40  
-end
74  activemodel/lib/active_model/mass_assignment_security/sanitizer.rb
... ...
@@ -1,74 +0,0 @@
1  
-module ActiveModel
2  
-  module MassAssignmentSecurity
3  
-    class Sanitizer #:nodoc:
4  
-      # Returns all attributes not denied by the authorizer.
5  
-      def sanitize(klass, attributes, authorizer)
6  
-        rejected = []
7  
-        sanitized_attributes = attributes.reject do |key, value|
8  
-          rejected << key if authorizer.deny?(key)
9  
-        end
10  
-        process_removed_attributes(klass, rejected) unless rejected.empty?
11  
-        sanitized_attributes
12  
-      end