strongly_typed_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.
@@ -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 -%>