remembering_strong_parameters 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ module RememberingStrongParameters
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,2 @@
1
+ require 'action_controller/parameters'
2
+ require 'active_model/forbidden_attributes_protection'
@@ -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