This repository has been archived by the owner on Jan 2, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
/
params_verification.rb
358 lines (316 loc) · 13.5 KB
/
params_verification.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
require 'erb' # used to sanitize the error message and avoid XSS attacks
# ParamsVerification module.
# Written to verify a service params without creating new objects.
# This module is used on all requests requiring validation and therefore performance
# security and maintainability are critical.
#
# @api public
module ParamsVerification
class ParamError < StandardError; end #:nodoc
class NoParamsDefined < ParamError; end #:nodoc
class MissingParam < ParamError; end #:nodoc
class UnexpectedParam < ParamError; end #:nodoc
class InvalidParamType < ParamError; end #:nodoc
class InvalidParamValue < ParamError; end #:nodoc
# An array of validation regular expressions.
# The array gets cached but can be accessed via the symbol key.
#
# @return [Hash] An array with all the validation types as keys and regexps as values.
# @api public
def self.type_validations
@type_validations ||= { :integer => /^-?\d+$/,
:float => /^-?(\d*\.\d+|\d+)$/,
:decimal => /^-?(\d*\.\d+|\d+)$/,
:datetime => /^[-\d:T\s\+]+[zZ]*$/, # "T" is for ISO date format
:boolean => /^(1|true|TRUE|T|Y|0|false|FALSE|F|N)$/,
#:array => /,/
}
end
# Validation against each required WeaselDiesel::Params::Rule
# and returns the potentially modified params (with default values)
#
# @param [Hash] params The params to verify (incoming request params)
# @param [WeaselDiesel::Params] service_params A Playco service param compatible object listing required and optional params
# @param [Boolean] ignore_unexpected Flag letting the validation know if unexpected params should be ignored
#
# @return [Hash]
# The passed params potentially modified by the default rules defined in the service.
#
# @example Validate request params against a service's defined param rules
# ParamsVerification.validate!(request.params, @service.defined_params)
#
# @api public
def self.validate!(params, service_params, ignore_unexpected=false)
# Verify that no garbage params are passed, if they are, an exception is raised.
# only the first level is checked at this point
unless ignore_unexpected
unexpected_params?(params, service_params.param_names)
end
# dupe the params so we don't modify the passed value
updated_params = params.dup
# Required param verification
service_params.list_required.each do |rule|
updated_params = validate_required_rule(rule, updated_params)
end
# Set optional defaults if any optional
service_params.list_optional.each do |rule|
updated_params = validate_optional_rule(rule, updated_params)
end
# check the namespaced params
service_params.namespaced_params.each do |param|
unless param.space_name.null && updated_params[param.space_name.name.to_s].nil?
param.list_required.each do |rule|
updated_params = validate_required_rule(rule, updated_params, param.space_name.name.to_s)
end
param.list_optional.each do |rule|
updated_params = validate_optional_rule(rule, updated_params, param.space_name.name.to_s)
end
end
end
# verify nested params, only 1 level deep tho
params.each_pair do |key, value|
if value.is_a?(Hash)
namespaced = service_params.namespaced_params.find{|np| np.space_name.name.to_s == key.to_s}
raise UnexpectedParam, "Request included unexpected parameter: #{ERB::Util.html_escape(key)}" if namespaced.nil?
unexpected_params?(params[key], namespaced.param_names)
end
end
updated_params
end
private
# Validates a required rule against a list of params passed.
#
#
# @param [WeaselDiesel::Params::Rule] rule The required rule to check against.
# @param [Hash] params The request params.
# @param [String] namespace Optional param namespace to check the rule against.
#
# @return [Hash]
# A hash representing the potentially modified params after going through the filter.
#
# @api private
def self.validate_required_rule(rule, params, namespace=nil)
param_name = rule.name.to_s
param_value, namespaced_params = extract_param_values(params, param_name, namespace)
# Checks presence
if !(namespaced_params || params).keys.include?(param_name)
raise MissingParam, "'#{rule.name}' is missing - passed params: #{html_escape(params.inspect)}."
end
updated_param_value, updated_params = validate_and_cast_type(param_value, param_name, rule.options[:type], params, namespace)
# check for nulls in params that don't allow them
if !valid_null_param?(param_name, updated_param_value, rule)
raise InvalidParamValue, "Value for parameter '#{param_name}' cannot be null - passed params: #{html_escape(updated_params.inspect)}."
elsif updated_param_value
value_errors = validate_ruled_param_value(param_name, updated_param_value, rule)
raise InvalidParamValue, value_errors.join(', ') if value_errors
end
updated_params
end
# Validates that an optional rule is respected.
# If the rule contains default values, the params might be updated.
#
# @param [#WeaselDiesel::Params::Rule] rule The optional rule
# @param [Hash] params The request params
# @param [String] namespace An optional namespace
#
# @return [Hash] The potentially modified params
#
# @api private
def self.validate_optional_rule(rule, params, namespace=nil)
param_name = rule.name.to_s
param_value, namespaced_params = extract_param_values(params, param_name, namespace)
if param_value && !valid_null_param?(param_name, param_value, rule)
raise InvalidParamValue, "Value for parameter '#{param_name}' cannot be null if passed - passed params: #{html_escape(params.inspect)}."
end
# Use a default value if one is available and the submitted param value is nil
if param_value.nil? && rule.options[:default]
param_value = rule.options[:default]
if namespace
params[namespace] ||= {}
params[namespace][param_name] = param_value
else
params[param_name] = param_value
end
end
updated_param_value, updated_params = validate_and_cast_type(param_value, param_name, rule.options[:type], params, namespace)
value_errors = validate_ruled_param_value(param_name, updated_param_value, rule) if updated_param_value
raise InvalidParamValue, value_errors.join(', ') if value_errors
updated_params
end
# Validates the param value against the rule and cast the param in the appropriate type.
# The modified params containing the cast value is returned along the cast param value.
#
# @param [Object] param_value The value to validate and cast.
# @param [String] param_name The name of the param we are validating.
# @param [Symbol] rule_type The expected object type.
# @param [Hash] params The params that might need to be updated.
# @param [String, Symbol] namespace The optional namespace used to access the `param_value`
#
# @return [Array<Object, Hash>] An array containing the param value and
# a hash representing the potentially modified params after going through the filter.
#
def self.validate_and_cast_type(param_value, param_name, rule_type, params, namespace=nil)
# checks type & modifies params if needed
if rule_type && param_value
# nullify empty strings for any types other than string
param_value = nil if param_value == '' && rule_type != :string
verify_cast(param_name, param_value, rule_type)
param_value = type_cast_value(rule_type, param_value)
# update the params hash with the type cast value
if namespace
params[namespace] ||= {}
params[namespace][param_name] = param_value
else
params[param_name] = param_value
end
end
[param_value, params]
end
# Validates a value against a few rule options.
#
# @return [NilClass, Array<String>] Returns an array of error messages if an option didn't validate.
def self.validate_ruled_param_value(param_name, param_value, rule)
# checks the value against a whitelist style 'in'/'options' list
if rule.options[:options] || rule.options[:in]
choices = rule.options[:options] || rule.options[:in]
unless param_value.is_a?(Array) ? (param_value & choices == param_value) : choices.include?(param_value)
errors ||= []
errors << "Value for parameter '#{param_name}' (#{html_escape(param_value)}) is not in the allowed set of values."
end
end
# enforces a minimum numeric value
if rule.options[:min_value]
min = rule.options[:min_value]
if param_value.to_i < min
errors ||= []
errors << "Value for parameter '#{param_name}' ('#{html_escape(param_value)}') is lower than the min accepted value (#{min})."
end
end
# enforces a maximum numeric value
if rule.options[:max_value]
max = rule.options[:max_value]
if param_value.to_i > max
errors ||= []
errors << "Value for parameter '#{param_name}' ('#{html_escape(param_value)}') is higher than the max accepted value (#{max})."
end
end
# enforces a minimum string length
if rule.options[:min_length]
min = rule.options[:min_length]
if param_value.to_s.length < min
errors ||= []
errors << "Length of parameter '#{param_name}' ('#{html_escape(param_value)}') is shorter than the min accepted value (#{min})."
end
end
# enforces a maximum string length
if rule.options[:max_length]
max = rule.options[:max_length]
if param_value.to_s.length > max
errors ||= []
errors << "Length of parameter '#{param_name}' ('#{html_escape(param_value)}') is longer than the max accepted value (#{max})."
end
end
errors
end
# Extract the param value and the namespaced params
# based on a passed namespace and params
#
# @param [Hash] params The passed params to extract info from.
# @param [String] param_name The param name to find the value.
# @param [NilClass, String] namespace the params' namespace.
# @return [Array<Object, String>]
#
# @api private
def self.extract_param_values(params, param_name, namespace=nil)
# Namespace check
if namespace == '' || namespace.nil?
[params[param_name], nil]
else
# puts "namespace: #{namespace} - params #{params[namespace].inspect}"
namespaced_params = params[namespace]
if namespaced_params
[namespaced_params[param_name], namespaced_params]
else
[nil, namespaced_params]
end
end
end
def self.unexpected_params?(params, param_names)
# Raise an exception unless no unexpected params were found
unexpected_keys = (params.keys - param_names)
unless unexpected_keys.empty?
raise UnexpectedParam, "Request included unexpected parameter(s): #{unexpected_keys.map{|k| ERB::Util.html_escape(k)}.join(', ')}"
end
end
def self.type_cast_value(type, value)
return value if value == nil
case type
when :integer
value.to_i
when :float, :decimal
value.to_f
when :string
value.to_s
when :boolean
if value.is_a? TrueClass
true
elsif value.is_a? FalseClass
false
else
case value.to_s
when /^(1|true|TRUE|T|Y)$/
true
when /^(0|false|FALSE|F|N)$/
false
else
raise InvalidParamValue, "Could not typecast boolean to appropriate value"
end
end
# An array type is a comma delimited string, we need to cast the passed strings.
when :array
value.respond_to?(:split) ? value.split(',') : value
when :binary, :file
value
else
value
end
end
# Checks that the value's type matches the expected type for a given param. If a nil value is passed
# the verification is skipped.
#
# @param [Symbol, String] Param name used if the verification fails and that an error is raised.
# @param [NilClass, #to_s] The value to validate.
# @param [Symbol] The expected type, such as :boolean, :integer etc...
# @raise [InvalidParamType] Custom exception raised when the validation isn't found or the value doesn't match.
#
# @return [NilClass]
# @api public
def self.verify_cast(name, value, expected_type)
return if value == nil
validation = ParamsVerification.type_validations[expected_type.to_sym]
unless validation.nil? || value.to_s =~ validation
raise InvalidParamType, "Value for parameter '#{name}' (#{html_escape(value)}) is of the wrong type (expected #{expected_type})"
end
end
# Checks that a param explicitly set to not be null is present.
# if 'null' is found in the ruleset and set to 'false' (default is 'true' to allow null),
# then confirm that the submitted value isn't nil or empty
# @param [String] param_name The name of the param to verify.
# @param [NilClass, String, Integer, TrueClass, FalseClass] param_value The value to check.
# @param [WeaselDiesel::Params::Rule] rule The rule to check against.
#
# @return [Boolean] true if the param is valid, false otherwise
def self.valid_null_param?(param_name, param_value, rule)
if rule.options.has_key?(:null) && rule.options[:null] == false
if rule.options[:type] && rule.options[:type] == :array
return false if param_value.nil? || (param_value.respond_to?(:split) && param_value.split(',').empty?)
else
return false if param_value.nil? || param_value == ''
end
end
true
end
def self.html_escape(msg)
ERB::Util.html_escape(msg)
end
end