strongly_typed_parameters 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,90 @@
1
+ = Strong(ly typed) 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
+ In this fork, the type of each parameter is also validated to avoid unexpected behavior with implicit casting.
5
+
6
+ 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.
7
+
8
+ class PeopleController < ActionController::Base
9
+ # This will raise an ActiveModel::ForbiddenAttributes exception because it's using mass assignment
10
+ # without an explicit permit step.
11
+ def create
12
+ Person.create(params[:person])
13
+ end
14
+
15
+ # This will pass with flying colors as long as there's a person key in the parameters, otherwise
16
+ # it'll raise a ActionController::MissingParameter exception, which will get caught by
17
+ # ActionController::Base and turned into that 400 Bad Request reply.
18
+ def update
19
+ person = current_account.people.find(params[:id])
20
+ person.update_attributes!(person_params)
21
+ redirect_to person
22
+ end
23
+
24
+ private
25
+ # Using a private method to encapsulate the permissible parameters is just a good pattern
26
+ # since you'll be able to reuse the same permit list between create and update. Also, you
27
+ # can specialize this method with per-user checking of permissible attributes.
28
+ def person_params
29
+ params.require(:person).permit(:name, :age)
30
+ end
31
+ end
32
+
33
+ == Permitted Types
34
+
35
+ Given
36
+
37
+ params.permit(:id)
38
+
39
+ the key +:id+ will pass the whitelisting if it appears in +params+ and is a String. Otherwise the key is going to be filtered out, so arrays, hashes, or any other objects cannot be injected.
40
+
41
+ If instead the argument is given as
42
+
43
+ params.permit(:id => Numeric)
44
+
45
+ the +:id+ value must be a number. Any class or module can be given here. The marker module Boolean is included in TrueClass and FalseClass.
46
+
47
+ To declare that the value in +params+ must be an array of values of a certain type, wrap the type constant in an Array:
48
+
49
+ params.permit(:id => [Numeric])
50
+
51
+ == Defaults with ActiveRecord
52
+
53
+ If a parameter shares a name with an ActiveRecord model, the default types for its attributes are those of that model, rather than String.
54
+
55
+ == Nested Parameters
56
+
57
+ You can also use permit on nested parameters, like:
58
+
59
+ params.permit(:name, {:emails => [String]}, :friends => [ :name, { :family => [ :name ] }])
60
+
61
+ Thanks to Nick Kallen for the permit idea!
62
+
63
+ == Handling of Unpermitted Keys
64
+
65
+ By default parameter keys that are not explicitly permitted will be logged in the development and test environment. In other environments these parameters will simply be filtered out and ignored.
66
+
67
+ Additionally, this behaviour can be changed by changing the +config.action_controller.action_on_unpermitted_parameters+ property in your environment files. If set to +:log+ the unpermitted attributes will be logged, if set to +:raise+ an exception will be raised.
68
+
69
+ == Installation
70
+
71
+ In Gemfile:
72
+
73
+ gem 'strongly_typed_parameters'
74
+
75
+ and then run `bundle`. To activate the strong parameters, you need to include this module in
76
+ every model you want protected.
77
+
78
+ class Post < ActiveRecord::Base
79
+ include ActiveModel::ForbiddenAttributesProtection
80
+ end
81
+
82
+ 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+:
83
+
84
+ config.active_record.whitelist_attributes = false
85
+
86
+ This will allow you to remove / not have to use +attr_accessible+ and do mass assignment inside your code and tests.
87
+
88
+ == Compatibility
89
+
90
+ This plugin is only fully compatible with Rails versions 3.0, 3.1 and 3.2 but not 4.0+, as the non-typechecking version is part of Rails Core in 4.0.
@@ -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 = 'StrongParameters'
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,240 @@
1
+ require 'date'
2
+ require 'bigdecimal'
3
+ require 'stringio'
4
+
5
+ require 'active_support/concern'
6
+ require 'active_support/core_ext/hash/indifferent_access'
7
+ require 'action_controller'
8
+ require 'action_dispatch/http/upload'
9
+
10
+ module ActionController
11
+ class ParameterMissing < IndexError
12
+ attr_reader :param
13
+
14
+ def initialize(param)
15
+ @param = param
16
+ super("key not found: #{param}")
17
+ end
18
+ end
19
+
20
+ class UnpermittedParameters < IndexError
21
+ attr_reader :params
22
+
23
+ def initialize(params)
24
+ @params = params
25
+ super("found unpermitted parameters: #{params.join(", ")}")
26
+ end
27
+ end
28
+
29
+ class Parameters < ActiveSupport::HashWithIndifferentAccess
30
+ attr_accessor :permitted
31
+ alias :permitted? :permitted
32
+ attr_accessor :klass
33
+
34
+ cattr_accessor :action_on_unpermitted_parameters, :instance_accessor => false
35
+
36
+ # Never raise an UnpermittedParameters exception because of these params
37
+ # are present. They are added by Rails and it's of no concern.
38
+ NEVER_UNPERMITTED_PARAMS = %w( controller action )
39
+
40
+ def initialize(attributes = nil, klass = String)
41
+ super(attributes)
42
+ @permitted = false
43
+ @klass = klass
44
+ end
45
+
46
+ def permit!
47
+ each_pair do |key, value|
48
+ convert_hashes_to_parameters(key, value)
49
+ self[key].permit! if self[key].respond_to? :permit!
50
+ end
51
+
52
+ @permitted = true
53
+ self
54
+ end
55
+
56
+ def require(key)
57
+ self[key].presence || raise(ActionController::ParameterMissing.new(key))
58
+ end
59
+
60
+ alias :required :require
61
+
62
+ def permit(*filters)
63
+ params = self.class.new
64
+ filters.each do |filter|
65
+ rule = filter.is_a?(Hash) ? filter : default_rule(filter)
66
+ if rule.values.one? && rule.values.first.is_a?(Class)
67
+ permitted_scalar_filter(params, rule.keys.first, rule.values.first)
68
+ else
69
+ apply_filter(params, rule)
70
+ end
71
+ end
72
+
73
+ unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
74
+
75
+ params.permit!
76
+ end
77
+
78
+ def [](key)
79
+ convert_hashes_to_parameters(key, super)
80
+ end
81
+
82
+ def fetch(key, *args)
83
+ convert_hashes_to_parameters(key, super)
84
+ rescue KeyError, IndexError
85
+ raise ActionController::ParameterMissing.new(key)
86
+ end
87
+
88
+ def slice(*keys)
89
+ self.class.new(super).tap do |new_instance|
90
+ new_instance.instance_variable_set :@permitted, @permitted
91
+ end
92
+ end
93
+
94
+ def dup
95
+ self.class.new(self).tap do |duplicate|
96
+ duplicate.default = default
97
+ duplicate.instance_variable_set :@permitted, @permitted
98
+ end
99
+ end
100
+
101
+ protected
102
+ def convert_value(value)
103
+ if value.class == Hash
104
+ self.class.new_from_hash_copying_default(value)
105
+ elsif value.is_a?(Array)
106
+ value.dup.replace(value.map { |e| convert_value(e) })
107
+ else
108
+ value
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def convert_hashes_to_parameters(key, value)
115
+ if value.is_a?(Parameters) || !value.is_a?(Hash)
116
+ value
117
+ else
118
+ # Convert to Parameters on first access
119
+ self[key] = self.class.new(value)
120
+ end
121
+ end
122
+
123
+ def permitted_scalar?(value, klass)
124
+ value.is_a?(klass)
125
+ end
126
+
127
+ def array_of_permitted_scalars?(value,klass)
128
+ if value.is_a?(Array)
129
+ value.all? {|element| permitted_scalar?(element,klass)}
130
+ end
131
+ end
132
+
133
+ def permitted_scalar_filter(params, key, klass)
134
+ if has_key?(key) && permitted_scalar?(self[key],klass)
135
+ params[key] = self[key]
136
+ end
137
+
138
+ keys.grep(/\A#{Regexp.escape(key.to_s)}\(\d+[if]?\)\z/).each do |key|
139
+ if permitted_scalar?(self[key],klass)
140
+ params[key] = self[key]
141
+ end
142
+ end
143
+ end
144
+
145
+ def array_of_permitted_scalars_filter(params, key, rule)
146
+ raise ArgumentError unless rule.one?
147
+ if has_key?(key) && array_of_permitted_scalars?(self[key],rule.first)
148
+ params[key] = self[key]
149
+ end
150
+ end
151
+
152
+ def apply_filter(params, filter)
153
+ filter = filter.with_indifferent_access
154
+
155
+ # Slicing filters out non-declared keys.
156
+ slice(*filter.keys).each do |key, value|
157
+
158
+ rule = filter[key]
159
+
160
+ # Declaration {:favorite_numbers => [Numeric]}
161
+ if rule.is_a?(Array) && rule.first.is_a?(Class)
162
+ array_of_permitted_scalars_filter(params, key, rule)
163
+ # Declaration {:favorite_number => Numeric} or :uuid [=> String]
164
+ elsif rule.is_a?(Class) || rule.is_a?(Module)
165
+ permitted_scalar_filter(params, key, rule)
166
+ else
167
+ # Declaration {:user => :name} or {:user => [:name, :age, {:address => ...}]}
168
+ raise ArgumentError if rule.empty?
169
+ params[key] = each_element(value) do |element|
170
+ if element.is_a?(Hash)
171
+ element = self.class.new(element) unless element.respond_to?(:permit)
172
+ element.klass = key.camelize.constantize rescue String
173
+ element.permit(*Array.wrap(rule))
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ def default_rule(filter)
181
+ if @klass.respond_to?(:columns) && (type = @klass.columns.find { |attr| attr.name == filter.to_s })
182
+ {filter => type.klass}
183
+ else
184
+ {filter => String}
185
+ end
186
+ end
187
+
188
+ def each_element(value)
189
+ if value.is_a?(Array)
190
+ value.map { |el| yield el }.compact
191
+ # fields_for on an array of records uses numeric hash keys.
192
+ elsif value.is_a?(Hash) && value.keys.all? { |k| k =~ /\A-?\d+\z/ }
193
+ hash = value.class.new
194
+ value.each { |k,v| hash[k] = yield v }
195
+ hash
196
+ else
197
+ yield value
198
+ end
199
+ end
200
+
201
+ def unpermitted_parameters!(params)
202
+ return unless self.class.action_on_unpermitted_parameters
203
+
204
+ unpermitted_keys = unpermitted_keys(params)
205
+
206
+ if unpermitted_keys.any?
207
+ case self.class.action_on_unpermitted_parameters
208
+ when :log
209
+ ActionController::Base.logger.debug "Unpermitted parameters: #{unpermitted_keys.join(", ")}"
210
+ when :raise
211
+ raise ActionController::UnpermittedParameters.new(unpermitted_keys)
212
+ end
213
+ end
214
+ end
215
+
216
+ def unpermitted_keys(params)
217
+ self.keys - params.keys - NEVER_UNPERMITTED_PARAMS
218
+ end
219
+ end
220
+
221
+ module StrongParameters
222
+ extend ActiveSupport::Concern
223
+
224
+ included do
225
+ rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception|
226
+ render :text => "Required parameter missing: #{parameter_missing_exception.param}", :status => :bad_request
227
+ end
228
+ end
229
+
230
+ def params
231
+ @_params ||= Parameters.new(request.parameters)
232
+ end
233
+
234
+ def params=(val)
235
+ @_params = val.is_a?(Hash) ? Parameters.new(val) : val
236
+ end
237
+ end
238
+ end
239
+
240
+ ActionController::Base.send :include, ActionController::StrongParameters
@@ -0,0 +1,15 @@
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?(:permitted?) || new_attributes.permitted?
9
+ super
10
+ else
11
+ raise ActiveModel::ForbiddenAttributes
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ Description:
2
+ Stubs out a scaffolded controller and its views. Different from rails
3
+ scaffold_controller, it uses strongly_typed_parameters to whitelist permissible
4
+ attributes in a private method.
5
+ Pass the model name, either CamelCased or under_scored. The controller
6
+ name is retrieved as a pluralized version of the model name.
7
+
8
+ To create a controller within a module, specify the model name as a
9
+ path like 'parent_module/controller_name'.
10
+
11
+ This generates a controller class in app/controllers and invokes helper,
12
+ template engine and test framework generators.
@@ -0,0 +1,17 @@
1
+ require 'rails/version'
2
+ require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
3
+
4
+ module Rails
5
+ module Generators
6
+ class StrongParametersControllerGenerator < ScaffoldControllerGenerator
7
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ if ::Rails::VERSION::STRING < '3.1'
11
+ def module_namespacing
12
+ yield if block_given?
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,94 @@
1
+ <% module_namespacing do -%>
2
+ class <%= controller_class_name %>Controller < ApplicationController
3
+ # GET <%= route_url %>
4
+ # GET <%= route_url %>.json
5
+ def index
6
+ @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
7
+
8
+ respond_to do |format|
9
+ format.html # index.html.erb
10
+ format.json { render json: <%= "@#{plural_table_name}" %> }
11
+ end
12
+ end
13
+
14
+ # GET <%= route_url %>/1
15
+ # GET <%= route_url %>/1.json
16
+ def show
17
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
18
+
19
+ respond_to do |format|
20
+ format.html # show.html.erb
21
+ format.json { render json: <%= "@#{singular_table_name}" %> }
22
+ end
23
+ end
24
+
25
+ # GET <%= route_url %>/new
26
+ # GET <%= route_url %>/new.json
27
+ def new
28
+ @<%= singular_table_name %> = <%= orm_class.build(class_name) %>
29
+
30
+ respond_to do |format|
31
+ format.html # new.html.erb
32
+ format.json { render json: <%= "@#{singular_table_name}" %> }
33
+ end
34
+ end
35
+
36
+ # GET <%= route_url %>/1/edit
37
+ def edit
38
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
39
+ end
40
+
41
+ # POST <%= route_url %>
42
+ # POST <%= route_url %>.json
43
+ def create
44
+ @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
45
+
46
+ respond_to do |format|
47
+ if @<%= orm_instance.save %>
48
+ format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> }
49
+ format.json { render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> }
50
+ else
51
+ format.html { render action: "new" }
52
+ format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
53
+ end
54
+ end
55
+ end
56
+
57
+ # PATCH/PUT <%= route_url %>/1
58
+ # PATCH/PUT <%= route_url %>/1.json
59
+ def update
60
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
61
+
62
+ respond_to do |format|
63
+ if @<%= orm_instance.update_attributes("#{singular_table_name}_params") %>
64
+ format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> }
65
+ format.json { head :no_content }
66
+ else
67
+ format.html { render action: "edit" }
68
+ format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
69
+ end
70
+ end
71
+ end
72
+
73
+ # DELETE <%= route_url %>/1
74
+ # DELETE <%= route_url %>/1.json
75
+ def destroy
76
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
77
+ @<%= orm_instance.destroy %>
78
+
79
+ respond_to do |format|
80
+ format.html { redirect_to <%= index_helper %>_url }
81
+ format.json { head :no_content }
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # Use this method to whitelist the permissible parameters. Example:
88
+ # params.require(:person).permit(:name, :age)
89
+ # Also, you can specialize this method with per-user checking of permissible attributes.
90
+ def <%= "#{singular_table_name}_params" %>
91
+ params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes.map {|a| ":#{a.name}" }.sort.join(', ') %>)
92
+ end
93
+ end
94
+ <% end -%>