remembering_strong_parameters 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +63 -0
- data/Rakefile +28 -0
- data/lib/action_controller/parameters.rb +291 -0
- data/lib/active_model/forbidden_attributes_protection.rb +17 -0
- data/lib/remembering_strong_parameters/version.rb +3 -0
- data/lib/remembering_strong_parameters.rb +2 -0
- data/test/action_controller_required_params_test.rb +52 -0
- data/test/action_controller_tainted_params_test.rb +25 -0
- data/test/active_model_mass_assignment_taint_protection_test.rb +43 -0
- data/test/chained_require_and_permit_test.rb +85 -0
- data/test/gemfiles/Gemfile.rails-3.0.x +6 -0
- data/test/gemfiles/Gemfile.rails-3.0.x.lock +62 -0
- data/test/gemfiles/Gemfile.rails-3.1.x +6 -0
- data/test/gemfiles/Gemfile.rails-3.2.x +6 -0
- data/test/hash_from_test.rb +25 -0
- data/test/multi_parameter_attributes_test.rb +39 -0
- data/test/nested_parameters_test.rb +157 -0
- data/test/parameters_require_test.rb +10 -0
- data/test/parameters_taint_test.rb +94 -0
- data/test/strengthen_test.rb +147 -0
- data/test/strong_array_test.rb +49 -0
- data/test/test_helper.rb +28 -0
- metadata +149 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 David Heinemeier Hansson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
= Strong Parameters
|
2
|
+
|
3
|
+
With this plugin Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been whitelisted. This means you'll have to make a conscious choice about which attributes to allow for mass updating and thus prevent accidentally exposing that which shouldn't be exposed.
|
4
|
+
|
5
|
+
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.
|
6
|
+
|
7
|
+
class PeopleController < ActionController::Base
|
8
|
+
# This will raise an ActiveModel::ForbiddenAttributes exception because it's using mass assignment
|
9
|
+
# without an explicit permit step.
|
10
|
+
def create
|
11
|
+
Person.create(params[:person])
|
12
|
+
end
|
13
|
+
|
14
|
+
# This will pass with flying colors as long as there's a person key in the parameters, otherwise
|
15
|
+
# it'll raise a ActionController::MissingParameter exception, which will get caught by
|
16
|
+
# ActionController::Base and turned into that 400 Bad Request reply.
|
17
|
+
def update
|
18
|
+
redirect_to current_account.people.find(params[:id]).tap { |person|
|
19
|
+
person.update_attributes!(person_params)
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
# Using a private method to encapsulate the permissible parameters is just a good pattern
|
25
|
+
# since you'll be able to reuse the same permit list between create and update. Also, you
|
26
|
+
# can specialize this method with per-user checking of permissible attributes.
|
27
|
+
def person_params
|
28
|
+
params.require(:person).permit(:name, :age)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
You can also use permit on nested parameters, like:
|
33
|
+
|
34
|
+
params.permit(:name, friends: [ :name, { family: [ :name ] }])
|
35
|
+
|
36
|
+
Thanks to Nick Kallen for the permit idea!
|
37
|
+
|
38
|
+
You can also used strengthen to set permit and require together:
|
39
|
+
|
40
|
+
params.strengthen(:person => {:name => :require, :age => :permit})
|
41
|
+
|
42
|
+
== Installation
|
43
|
+
|
44
|
+
In Gemfile:
|
45
|
+
|
46
|
+
gem 'remembering_strong_parameters'
|
47
|
+
|
48
|
+
and then run `bundle`. To activate the strong parameters, you need to include this module in
|
49
|
+
every model you want protected.
|
50
|
+
|
51
|
+
class Post < ActiveRecord::Base
|
52
|
+
include ActiveModel::ForbiddenAttributesProtection
|
53
|
+
end
|
54
|
+
|
55
|
+
If you want to now disable the default whitelisting that occurs in later versions of Rails, change the +config.active_record.whitelist_attributes+ property in your +config/application.rb+:
|
56
|
+
|
57
|
+
config.active_record.whitelist_attributes = false
|
58
|
+
|
59
|
+
This will allow you to remove / not have to use +attr_accessible+ and do mass assignment inside your code and tests.
|
60
|
+
|
61
|
+
== Compatibility
|
62
|
+
|
63
|
+
This plugin is only fully compatible with Rails versions 3.0, 3.1 and 3.2 but not 4.0+, as it is part of Rails Core in 4.0.
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
rescue LoadError
|
6
|
+
raise 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rdoc/task'
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
13
|
+
rdoc.title = 'RememberingStrongParameters'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.rdoc')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
t.libs << 'lib'
|
23
|
+
t.libs << 'test'
|
24
|
+
t.pattern = 'test/**/*_test.rb'
|
25
|
+
t.verbose = false
|
26
|
+
end
|
27
|
+
|
28
|
+
task :default => :test
|
@@ -0,0 +1,291 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
require 'action_controller'
|
4
|
+
|
5
|
+
module ActionController
|
6
|
+
class ParameterMissing < IndexError
|
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
|
+
|
17
|
+
REQUIRED_FLAGS = [:required, :require]
|
18
|
+
PERMITTED_FLAGS = [:permitted, :permit] + REQUIRED_FLAGS
|
19
|
+
|
20
|
+
def strengthened?
|
21
|
+
@strengthened
|
22
|
+
end
|
23
|
+
alias :permitted? :strengthened?
|
24
|
+
|
25
|
+
|
26
|
+
def initialize(attributes = nil)
|
27
|
+
super(attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
def permit!
|
31
|
+
replace to_check
|
32
|
+
@strengthened = true
|
33
|
+
each_pair do |key, value|
|
34
|
+
convert_hashes_to_parameters(key, value)
|
35
|
+
self[key].permit! if self[key].respond_to? :permit!
|
36
|
+
end
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def strengthen(filter = {})
|
42
|
+
return unless filter.kind_of? Hash
|
43
|
+
|
44
|
+
filter = filter.with_indifferent_access
|
45
|
+
|
46
|
+
if keys_all_numbers?
|
47
|
+
strengthen_numbered_hash_as_array(filter)
|
48
|
+
else
|
49
|
+
strengthen_hash(filter)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def require(key)
|
55
|
+
strengthen(key => REQUIRED_FLAGS.first)
|
56
|
+
to_check[key].presence
|
57
|
+
end
|
58
|
+
|
59
|
+
def permit(*filters)
|
60
|
+
strengthen(hash_from(filters, PERMITTED_FLAGS.first))
|
61
|
+
end
|
62
|
+
|
63
|
+
def original
|
64
|
+
to_check
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](key)
|
68
|
+
convert_hashes_to_parameters(key, super)
|
69
|
+
end
|
70
|
+
|
71
|
+
def fetch(key, *args)
|
72
|
+
convert_hashes_to_parameters(key, super)
|
73
|
+
rescue KeyError, IndexError
|
74
|
+
raise ActionController::ParameterMissing.new(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
def slice(*keys)
|
78
|
+
self.class.new(super).tap do |new_instance|
|
79
|
+
copy_instance_variables_to new_instance
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def dup
|
84
|
+
self.class.new(self).tap do |duplicate|
|
85
|
+
duplicate.default = default
|
86
|
+
copy_instance_variables_to duplicate
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def check_required(filter)
|
91
|
+
if filter.kind_of? Hash
|
92
|
+
filter.each do |key, value|
|
93
|
+
flag_error_if_abscent(key) if required_flag?(value) or value.kind_of? Hash
|
94
|
+
to_check[key].check_required(value) if value.respond_to? :check_required and to_check.has_key? key
|
95
|
+
end
|
96
|
+
end
|
97
|
+
unless missing_required_fields.empty?
|
98
|
+
raise ActionController::ParameterMissing.new("'#{missing_required_fields.join("', '")}' required by #{filter}")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
def convert_value(value)
|
104
|
+
if value.class == Hash
|
105
|
+
self.class.new_from_hash_copying_default(value)
|
106
|
+
elsif value.is_a?(Array)
|
107
|
+
StrongArray.new(value).replace(value.map { |e| convert_value(e) })
|
108
|
+
else
|
109
|
+
value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def each_element(object)
|
114
|
+
if object.is_a?(Array)
|
115
|
+
object = StrongArray.new(object)
|
116
|
+
object.map { |el| yield el }.compact
|
117
|
+
# fields_for on an array of records uses numeric hash keys
|
118
|
+
elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ }
|
119
|
+
hash = object.class.new
|
120
|
+
object.each { |k,v| hash[k] = yield v }
|
121
|
+
hash
|
122
|
+
else
|
123
|
+
yield object
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def been_checked
|
128
|
+
@been_checked ||= self.class.new
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_check
|
132
|
+
@to_check ||= clone
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def hash_from(array, value)
|
137
|
+
array = [array] unless array.kind_of? Array
|
138
|
+
array.collect! do |a|
|
139
|
+
if a.kind_of?(Hash)
|
140
|
+
key = a.keys.first
|
141
|
+
[key, hash_from(a[key], value)]
|
142
|
+
else
|
143
|
+
[a, value]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
Hash[array]
|
147
|
+
end
|
148
|
+
|
149
|
+
def keys_all_numbers?
|
150
|
+
/^[\-\d]+$/ =~ to_check.keys.join
|
151
|
+
end
|
152
|
+
|
153
|
+
def strengthen_numbered_hash_as_array(filter = {})
|
154
|
+
strong_array = StrongArray.new(to_check.values)
|
155
|
+
Hash[[to_check.keys.collect{|k| k.to_sym}, strong_array.strengthen(filter)].transpose]
|
156
|
+
end
|
157
|
+
|
158
|
+
def strengthen_hash(filter = {})
|
159
|
+
check_required(filter)
|
160
|
+
|
161
|
+
to_check.each do |key, value|
|
162
|
+
multiparameterless_key = key.gsub(/\(\d+[fi]?\)$/, "")
|
163
|
+
|
164
|
+
if filter[multiparameterless_key]
|
165
|
+
if value.respond_to?(:strengthen)
|
166
|
+
stengthened_value = value.strengthen(filter[multiparameterless_key])
|
167
|
+
been_checked[key] = stengthened_value if stengthened_value
|
168
|
+
else
|
169
|
+
check_key(key) if permitted_flag?(filter[multiparameterless_key])
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
@strengthened = true
|
175
|
+
|
176
|
+
replace been_checked
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
def convert_hashes_to_parameters(key, value)
|
181
|
+
if value.is_a?(Parameters) || !value.is_a?(Hash)
|
182
|
+
value
|
183
|
+
else
|
184
|
+
# Convert to Parameters on first access
|
185
|
+
self[key] = self.class.new(value)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def missing_required_fields
|
190
|
+
@missing_required_fields ||= []
|
191
|
+
end
|
192
|
+
|
193
|
+
def check_key(key)
|
194
|
+
check_matching_key(key)
|
195
|
+
check_matching_multi_parameter_keys(key)
|
196
|
+
end
|
197
|
+
|
198
|
+
def check_matching_key(key)
|
199
|
+
been_checked[key] = to_check[key] if to_check.has_key?(key)
|
200
|
+
end
|
201
|
+
|
202
|
+
def check_matching_multi_parameter_keys(key)
|
203
|
+
to_check.keys.grep(/\A#{Regexp.escape(key.to_s)}\(\d+[fi]?\)\z/).each { |key| been_checked[key] = to_check[key] }
|
204
|
+
end
|
205
|
+
|
206
|
+
def copy_instance_variables_to(other_instance)
|
207
|
+
other_instance.instance_variable_set :@to_check, @to_check
|
208
|
+
other_instance.instance_variable_set :@been_checked, @been_checked
|
209
|
+
other_instance.instance_variable_set :@strengthened, @strengthened
|
210
|
+
end
|
211
|
+
|
212
|
+
def flag_error_if_abscent(key)
|
213
|
+
if !to_check.has_key? key
|
214
|
+
missing_required_fields << key
|
215
|
+
elsif to_check[key].kind_of? Hash and to_check[key].empty?
|
216
|
+
missing_required_fields << key
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def required_flag?(value)
|
221
|
+
value.respond_to? :to_sym and REQUIRED_FLAGS.include?(value.to_sym)
|
222
|
+
end
|
223
|
+
|
224
|
+
def permitted_flag?(value)
|
225
|
+
value.respond_to? :to_sym and PERMITTED_FLAGS.include?(value.to_sym)
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
module RememberingStrongParameters
|
231
|
+
extend ActiveSupport::Concern
|
232
|
+
|
233
|
+
included do
|
234
|
+
rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception|
|
235
|
+
render :text => "Required parameter missing: #{parameter_missing_exception.param}", :status => :bad_request
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def params
|
240
|
+
@_params ||= Parameters.new(request.parameters)
|
241
|
+
end
|
242
|
+
|
243
|
+
def params=(val)
|
244
|
+
@_params = val.is_a?(Hash) ? Parameters.new(val) : val
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
class StrongArray < Array
|
249
|
+
|
250
|
+
def strengthen(filter = {})
|
251
|
+
original.each do |element|
|
252
|
+
case element
|
253
|
+
when Hash
|
254
|
+
element = ActionController::Parameters.new element
|
255
|
+
when Array
|
256
|
+
element = self.class.new element
|
257
|
+
end
|
258
|
+
|
259
|
+
if element.respond_to? :strengthen
|
260
|
+
been_checked << element.strengthen(filter)
|
261
|
+
else
|
262
|
+
been_checked << element if Parameters::PERMITTED_FLAGS.include?(filter)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
@strengthened = true
|
267
|
+
been_checked
|
268
|
+
end
|
269
|
+
|
270
|
+
def been_checked
|
271
|
+
@been_checked ||= self.class.new
|
272
|
+
end
|
273
|
+
|
274
|
+
def original
|
275
|
+
@original ||= self.clone
|
276
|
+
end
|
277
|
+
|
278
|
+
def strengthened?
|
279
|
+
@strengthened
|
280
|
+
end
|
281
|
+
alias :permitted? :strengthened?
|
282
|
+
|
283
|
+
|
284
|
+
def check_required(filter = {})
|
285
|
+
each{|e| e.check_required(filter) if e.respond_to? :check_required}
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
ActionController::Base.send :include, ActionController::RememberingStrongParameters
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
class ForbiddenAttributes < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
module ForbiddenAttributesProtection
|
6
|
+
def sanitize_for_mass_assignment(*options)
|
7
|
+
new_attributes = options.first
|
8
|
+
if !new_attributes.respond_to?(:strengthened?) || new_attributes.strengthened?
|
9
|
+
super
|
10
|
+
else
|
11
|
+
raise ActiveModel::ForbiddenAttributes
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveModel.autoload :ForbiddenAttributesProtection
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BooksController < ActionController::Base
|
4
|
+
def create
|
5
|
+
params.require(:book).require(:name)
|
6
|
+
head :ok
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_with_chained_require
|
10
|
+
params.strengthen(:hat => :require).strengthen(:book => {:name => :require})
|
11
|
+
head :ok
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ActionControllerRequiredParamsTest < ActionController::TestCase
|
16
|
+
tests BooksController
|
17
|
+
|
18
|
+
test "missing required parameters will raise exception" do
|
19
|
+
post :create, { :magazine => { :name => "Mjallo!" } }
|
20
|
+
assert_response :bad_request
|
21
|
+
end
|
22
|
+
|
23
|
+
test "missing second lavel required parameters will raise exception" do
|
24
|
+
post :create, { :book => { :title => "Mjallo!" } }
|
25
|
+
assert_response :bad_request
|
26
|
+
end
|
27
|
+
|
28
|
+
test "missing required parameters will raise exception when require chained" do
|
29
|
+
post :create_with_chained_require, { :magazine => { :name => "Mjallo!" }, :hat => 'bowler' }
|
30
|
+
assert_response :bad_request
|
31
|
+
|
32
|
+
post :create_with_chained_require, { :book => { :title => "Mjallo!" }, :hat => 'bowler' }
|
33
|
+
assert_response :bad_request
|
34
|
+
|
35
|
+
post :create_with_chained_require, { :book => { :name => "Mjallo!" } }
|
36
|
+
assert_response :bad_request
|
37
|
+
end
|
38
|
+
|
39
|
+
test "required parameters that are present will not raise" do
|
40
|
+
post :create, { :book => { :name => "Mjallo!" } }
|
41
|
+
assert_response :ok
|
42
|
+
|
43
|
+
post :create_with_chained_require, { :book => { :name => "Mjallo!" }, :hat => 'bowler' }
|
44
|
+
assert_response :ok
|
45
|
+
end
|
46
|
+
|
47
|
+
test "missing parameters will be mentioned in the return" do
|
48
|
+
post :create, { :magazine => { :name => "Mjallo!" } }
|
49
|
+
assert_match "Required parameter missing: 'book'", response.body
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|