input_sanitizer 0.1.10 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yaml +26 -0
  3. data/.github/workflows/gempush.yml +28 -0
  4. data/.gitignore +2 -1
  5. data/CHANGELOG +99 -0
  6. data/LICENSE +201 -22
  7. data/README.md +24 -4
  8. data/input_sanitizer.gemspec +10 -4
  9. data/lib/input_sanitizer.rb +5 -2
  10. data/lib/input_sanitizer/errors.rb +142 -0
  11. data/lib/input_sanitizer/extended_converters.rb +5 -42
  12. data/lib/input_sanitizer/extended_converters/comma_joined_integers_converter.rb +15 -0
  13. data/lib/input_sanitizer/extended_converters/comma_joined_strings_converter.rb +15 -0
  14. data/lib/input_sanitizer/extended_converters/positive_integer_converter.rb +12 -0
  15. data/lib/input_sanitizer/extended_converters/specific_values_converter.rb +19 -0
  16. data/lib/input_sanitizer/restricted_hash.rb +49 -8
  17. data/lib/input_sanitizer/v1.rb +22 -0
  18. data/lib/input_sanitizer/v1/clean_field.rb +38 -0
  19. data/lib/input_sanitizer/{default_converters.rb → v1/default_converters.rb} +20 -13
  20. data/lib/input_sanitizer/v1/sanitizer.rb +166 -0
  21. data/lib/input_sanitizer/v2.rb +13 -0
  22. data/lib/input_sanitizer/v2/clean_field.rb +36 -0
  23. data/lib/input_sanitizer/v2/clean_payload_collection_field.rb +41 -0
  24. data/lib/input_sanitizer/v2/clean_query_collection_field.rb +40 -0
  25. data/lib/input_sanitizer/v2/error_collection.rb +49 -0
  26. data/lib/input_sanitizer/v2/nested_sanitizer_factory.rb +19 -0
  27. data/lib/input_sanitizer/v2/payload_sanitizer.rb +130 -0
  28. data/lib/input_sanitizer/v2/payload_transform.rb +42 -0
  29. data/lib/input_sanitizer/v2/query_sanitizer.rb +33 -0
  30. data/lib/input_sanitizer/v2/types.rb +227 -0
  31. data/lib/input_sanitizer/version.rb +1 -1
  32. data/spec/extended_converters/comma_joined_integers_converter_spec.rb +18 -0
  33. data/spec/extended_converters/comma_joined_strings_converter_spec.rb +18 -0
  34. data/spec/extended_converters/positive_integer_converter_spec.rb +18 -0
  35. data/spec/extended_converters/specific_values_converter_spec.rb +27 -0
  36. data/spec/restricted_hash_spec.rb +37 -7
  37. data/spec/sanitizer_spec.rb +129 -26
  38. data/spec/spec_helper.rb +17 -2
  39. data/spec/v1/default_converters_spec.rb +141 -0
  40. data/spec/v2/converters_spec.rb +174 -0
  41. data/spec/v2/payload_sanitizer_spec.rb +534 -0
  42. data/spec/v2/payload_transform_spec.rb +98 -0
  43. data/spec/v2/query_sanitizer_spec.rb +300 -0
  44. data/v2.md +52 -0
  45. metadata +105 -40
  46. data/.travis.yml +0 -12
  47. data/lib/input_sanitizer/sanitizer.rb +0 -140
  48. data/spec/default_converters_spec.rb +0 -101
  49. data/spec/extended_converters_spec.rb +0 -62
@@ -3,11 +3,12 @@ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
3
3
  require 'input_sanitizer/version'
4
4
 
5
5
  Gem::Specification.new do |gem|
6
- gem.authors = ["Tomek Paczkowski", "Tomasz Werbicki", "Michal Bugno"]
7
- gem.email = ["tom@futuresimple.com", "tomasz@futuresimple.com", "michal@futuresimple.com"]
6
+ gem.authors = ["Zendesk"]
7
+ gem.email = ["opensource@zendesk.com"]
8
8
  gem.description = %q{Gem to sanitize hash of incoming data}
9
9
  gem.summary = %q{Gem to sanitize hash of incoming data}
10
10
  gem.homepage = ""
11
+ gem.license = "Apache-2.0"
11
12
 
12
13
  gem.files = `git ls-files`.split($\)
13
14
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -16,6 +17,11 @@ Gem::Specification.new do |gem|
16
17
  gem.require_paths = ["lib"]
17
18
  gem.version = InputSanitizer::VERSION
18
19
 
19
- gem.add_development_dependency "rspec"
20
- gem.add_development_dependency "simplecov"
20
+ gem.add_runtime_dependency "method_struct", ">= 0.2.2"
21
+
22
+ gem.add_runtime_dependency "activesupport", ">= 3.0.0"
23
+ gem.add_development_dependency "pry", "~> 0.10.1"
24
+ gem.add_development_dependency "simplecov", "~> 0.9.2"
25
+
26
+ gem.add_development_dependency "rspec", "~> 3.2.0"
21
27
  end
@@ -1,5 +1,8 @@
1
1
  module InputSanitizer
2
2
  end
3
3
 
4
- require "input_sanitizer/version"
5
- require 'input_sanitizer/sanitizer'
4
+ require 'method_struct'
5
+
6
+ require 'input_sanitizer/version'
7
+ require 'input_sanitizer/v1'
8
+ require 'input_sanitizer/v2'
@@ -0,0 +1,142 @@
1
+ module InputSanitizer
2
+ class OptionalValueOmitted < StandardError; end
3
+
4
+ class KeyNotAllowedError < ArgumentError; end
5
+
6
+ class ValidationError < StandardError
7
+ attr_accessor :field
8
+ attr_reader :value
9
+ end
10
+
11
+ class ConversionError < ValidationError; end
12
+
13
+ class ValueMissingError < ValidationError
14
+ def code
15
+ :missing
16
+ end
17
+
18
+ def initialize
19
+ super("is missing")
20
+ end
21
+ end
22
+
23
+ class BlankValueError < ValidationError
24
+ def code
25
+ :blank
26
+ end
27
+
28
+ def initialize
29
+ super("can't be blank")
30
+ end
31
+ end
32
+
33
+ class RegexpMismatchError < ValidationError
34
+ def code
35
+ :regexp
36
+ end
37
+
38
+ def initialize
39
+ super("does not match regular expression")
40
+ end
41
+ end
42
+
43
+ class ValueNotAllowedError < ValidationError
44
+ def code
45
+ :inclusion
46
+ end
47
+
48
+ def initialize(value)
49
+ @value = value
50
+ super("is not included in the list")
51
+ end
52
+ end
53
+
54
+ class TypeMismatchError < ValidationError
55
+ def code
56
+ case @type
57
+ when :integer
58
+ :not_an_integer
59
+ when :url
60
+ :invalid_uri
61
+ else
62
+ :invalid_type
63
+ end
64
+ end
65
+
66
+ def initialize(value, type)
67
+ @value = value
68
+ @type = type
69
+
70
+ message = case @type
71
+ when :integer
72
+ "must be an integer"
73
+ when :url
74
+ 'must be a valid URI (include the scheme name part, both http and https are accepted, '\
75
+ 'and the hierarchical part)'
76
+ else
77
+ "must be a value of type '#{type}'"
78
+ end
79
+
80
+ super(message)
81
+ end
82
+ end
83
+
84
+ class ExtraneousParamError < ValidationError
85
+ def code
86
+ :unknown_param
87
+ end
88
+
89
+ def initialize(name)
90
+ @field = name
91
+ super("is an unexpected parameter")
92
+ end
93
+ end
94
+
95
+ class CollectionLengthError < ValidationError
96
+ def code
97
+ :invalid_length
98
+ end
99
+
100
+ def initialize(value, min, max)
101
+ if min && max
102
+ super("must be of length between #{min} and #{max}, given: #{value}")
103
+ elsif min
104
+ super("must be of length greater than or equal to #{min}, given: #{value}")
105
+ else
106
+ super("must be of length less than or equal to #{max}, given: #{value}")
107
+ end
108
+ end
109
+ end
110
+
111
+ class ValueError < ValidationError
112
+ def code
113
+ :invalid_value
114
+ end
115
+
116
+ def initialize(value, min, max)
117
+ if min && max
118
+ super("must be between #{min} and #{max}, given: #{value}")
119
+ elsif min
120
+ super("must be higher than or equal to #{min}, given: #{value}")
121
+ else
122
+ super("must be lower than or equal to #{max}, given: #{value}")
123
+ end
124
+ end
125
+ end
126
+
127
+ class NestedError < ValidationError
128
+ attr_reader :nested_errors
129
+
130
+ def initialize(nested_errors)
131
+ @nested_errors = nested_errors
132
+ end
133
+ end
134
+
135
+ class CollectionError < ValidationError
136
+ attr_reader :collection_errors
137
+
138
+ def initialize(collection_errors)
139
+ @collection_errors = collection_errors
140
+ end
141
+ end
142
+ end
@@ -1,42 +1,5 @@
1
- module InputSanitizer
2
- class PositiveIntegerConverter < IntegerConverter
3
- def call(value)
4
- val = super
5
- raise ConversionError.new("invalid integer (neagtive or zero)") if val <= 0
6
- val
7
- end
8
- end
9
-
10
- class CommaJoinedIntegersConverter
11
- def call(value)
12
- non_valid = value.gsub(/[0-9,]/, "")
13
- if non_valid.empty?
14
- parts = value.split(",").map(&:to_i)
15
- else
16
- invalid_chars = non_valid.split(//)
17
- invalid_chars_desc = invalid_chars.join(", ")
18
- raise InputSanitizer::ConversionError.new("Invalid chars: #{invalid_chars_desc}")
19
- end
20
- end
21
- end
22
-
23
- class SpecificValuesConverter
24
- def initialize(values)
25
- @valid_values = values
26
- end
27
-
28
- def call(value)
29
- found = @valid_values.include?(value) ? value : nil
30
- if !found
31
- found = @valid_values.include?(value.to_sym) ? value.to_sym : nil
32
- end
33
- if !found
34
- values_joined = @valid_values.join(", ")
35
- error_message = "Possible values: #{values_joined}"
36
- raise InputSanitizer::ConversionError.new(error_message)
37
- else
38
- found
39
- end
40
- end
41
- end
42
- end
1
+ dir = File.dirname(__FILE__)
2
+ require File.join(dir, 'extended_converters', 'positive_integer_converter')
3
+ require File.join(dir, 'extended_converters', 'comma_joined_integers_converter')
4
+ require File.join(dir, 'extended_converters', 'comma_joined_strings_converter')
5
+ require File.join(dir, 'extended_converters', 'specific_values_converter')
@@ -0,0 +1,15 @@
1
+ module InputSanitizer
2
+ class CommaJoinedIntegersConverter
3
+ def call(value)
4
+ value = value.to_s
5
+ non_valid = value.gsub(/[0-9,]/, "")
6
+
7
+ if non_valid.empty?
8
+ value.split(",").map(&:to_i)
9
+ else
10
+ invalid_chars = non_valid.split(//).join(", ")
11
+ raise InputSanitizer::ConversionError.new("Invalid integers: #{invalid_chars}")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module InputSanitizer
2
+ class CommaJoinedStringsConverter
3
+ def call(value)
4
+ value = value.to_s
5
+ non_valid = value.gsub(/[a-zA-Z,_]/, "")
6
+
7
+ if non_valid.empty?
8
+ value.split(",").map(&:to_s)
9
+ else
10
+ invalid_chars = non_valid.split(//).join(", ")
11
+ raise InputSanitizer::ConversionError.new("Invalid strings: #{invalid_chars}")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module InputSanitizer
2
+ class PositiveIntegerConverter < V1::IntegerConverter
3
+ def call(value)
4
+ super.tap { |value| raise_error if value <= 0 }
5
+ end
6
+
7
+ private
8
+ def raise_error
9
+ raise ConversionError.new("invalid integer (neagtive or zero)")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module InputSanitizer
2
+ class SpecificValuesConverter
3
+ def initialize(values)
4
+ @valid_values = values
5
+ end
6
+
7
+ def call(value)
8
+ case
9
+ when @valid_values.include?(value)
10
+ value
11
+ when value.respond_to?(:to_sym) && @valid_values.include?(value.to_sym)
12
+ value.to_sym
13
+ else
14
+ values_joined = @valid_values.join(", ")
15
+ raise InputSanitizer::ConversionError.new("Possible values: #{values_joined}")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,24 +1,65 @@
1
1
  module InputSanitizer
2
- class KeyNotAllowedError < ArgumentError; end
3
-
4
2
  class RestrictedHash < Hash
5
3
  def initialize(allowed_keys)
6
- @allowed_keys = allowed_keys
7
- super() { |hash, key| default_for_key(key) }
4
+ @allowed_keys = Set.new(allowed_keys)
5
+ super()
6
+ end
7
+
8
+ def [](key)
9
+ raise_not_allowed(key) unless key_allowed?(key)
10
+ fetch(key, nil)
11
+ end
12
+
13
+ def []=(key, val)
14
+ @allowed_keys.add(key)
15
+ super
16
+ end
17
+
18
+ def store(key, val)
19
+ @allowed_keys.add(key)
20
+ super
21
+ end
22
+
23
+ def merge!(hash, &block)
24
+ @allowed_keys.merge(Set[*hash.keys])
25
+ super
26
+ end
27
+
28
+ def merge(hash, &block)
29
+ @allowed_keys.merge(Set[*hash.keys])
30
+ super
8
31
  end
9
32
 
10
33
  def key_allowed?(key)
11
34
  @allowed_keys.include?(key)
12
35
  end
13
36
 
14
- private
15
- def default_for_key(key)
16
- key_allowed?(key) ? nil : raise_not_allowed(key)
37
+ def transform_keys
38
+ return enum_for(:transform_keys) unless block_given?
39
+
40
+ new_allowed_keys = @allowed_keys.map { |key| yield(key) }
41
+ result = self.class.new(new_allowed_keys)
42
+ each_key do |key|
43
+ result[yield(key)] = self[key]
44
+ end
45
+ result
17
46
  end
18
47
 
48
+ def transform_keys!
49
+ return enum_for(:transform_keys!) unless block_given?
50
+
51
+ @allowed_keys.map! { |key| yield(key) }
52
+ keys.each do |key|
53
+ self[yield(key)] = delete(key)
54
+ end
55
+ self
56
+ end
57
+
58
+ private
59
+
19
60
  def raise_not_allowed(key)
20
61
  msg = "Key not allowed: #{key}"
21
- raise KeyNotAllowedError.new(msg)
62
+ raise KeyNotAllowedError, msg
22
63
  end
23
64
  end
24
65
  end
@@ -0,0 +1,22 @@
1
+ module InputSanitizer::V1
2
+ end
3
+
4
+ dir = File.dirname(__FILE__)
5
+ require File.join(dir, 'errors')
6
+ require File.join(dir, 'restricted_hash')
7
+ require File.join(dir, 'v1', 'default_converters')
8
+ require File.join(dir, 'v1', 'clean_field')
9
+ require File.join(dir, 'v1', 'sanitizer')
10
+ require File.join(dir, 'extended_converters')
11
+
12
+ # Backward compatibility
13
+ module InputSanitizer
14
+ Sanitizer = V1::Sanitizer
15
+
16
+ IntegerConverter = V1::IntegerConverter
17
+ StringConverter = V1::StringConverter
18
+ DateConverter = V1::DateConverter
19
+ TimeConverter = V1::TimeConverter
20
+ BooleanConverter = V1::BooleanConverter
21
+ AllowNil = V1::AllowNil
22
+ end
@@ -0,0 +1,38 @@
1
+ InputSanitizer::V1::CleanField = MethodStruct.new(:data, :has_key, :converter, :required, :collection, :namespace, :default, :provide, :allow) do
2
+ def call
3
+ if has_key
4
+ convert
5
+ elsif default
6
+ converter.call(default)
7
+ elsif required
8
+ raise InputSanitizer::ValueMissingError
9
+ else
10
+ raise InputSanitizer::OptionalValueOmitted
11
+ end
12
+ end
13
+
14
+ private
15
+ def convert
16
+ if collection
17
+ data.map { |value| convert_single(value) }
18
+ else
19
+ convert_single(data)
20
+ end
21
+ end
22
+
23
+ def convert_single(value)
24
+ if namespace
25
+ { namespace => convert_value(value[namespace]) }
26
+ else
27
+ convert_value(value)
28
+ end
29
+ end
30
+
31
+ def convert_value(value)
32
+ if provide
33
+ converter.call(value, provide)
34
+ else
35
+ converter.call(value)
36
+ end
37
+ end
38
+ end
@@ -1,14 +1,12 @@
1
1
  require 'time'
2
+ require 'date'
2
3
 
3
- module InputSanitizer
4
- class ConversionError < Exception
5
- end
6
-
4
+ module InputSanitizer::V1
7
5
  class IntegerConverter
8
6
  def call(value)
9
7
  cast = value.to_i
10
8
  if cast.to_s != value.to_s
11
- raise ConversionError.new("invalid integer")
9
+ raise InputSanitizer::ConversionError.new("invalid integer")
12
10
  end
13
11
  cast
14
12
  end
@@ -24,24 +22,31 @@ module InputSanitizer
24
22
  ISO_RE = /\A\d{4}-?\d{2}-?\d{2}/
25
23
 
26
24
  def call(value)
27
- raise ConversionError.new("invalid time") unless value =~ ISO_RE
25
+ raise InputSanitizer::ConversionError.new("invalid time") unless value =~ ISO_RE
28
26
  Date.parse(value)
29
27
  rescue ArgumentError
30
- raise ConversionError.new("invalid iso8601 date")
28
+ raise InputSanitizer::ConversionError.new("invalid iso8601 date")
31
29
  end
32
30
  end
33
31
 
34
32
  class TimeConverter
35
- ISO_RE = /\A\d{4}-?\d{2}-?\d{2}([T ]?\d{2}(:?\d{2}(:?\d{2})?)?)?\Z/
33
+ ISO_RE = /\A\d{4}-?\d{2}-?\d{2}([T ]?\d{2}(:?\d{2}(:?\d{2}((\.)?\d{0,3}(Z)?)?)?)?)?\Z/
36
34
 
37
35
  def call(value)
38
- if value =~ ISO_RE
39
- strip_timezone(Time.parse(value))
36
+ case value
37
+ when Time
38
+ value.getutc
39
+ when String
40
+ if value =~ ISO_RE
41
+ strip_timezone(Time.parse(value))
42
+ else
43
+ raise InputSanitizer::ConversionError.new("invalid time")
44
+ end
40
45
  else
41
- raise ConversionError.new("invalid time")
46
+ raise InputSanitizer::ConversionError.new("invalid time")
42
47
  end
43
48
  rescue ArgumentError
44
- raise ConversionError.new("invalid time")
49
+ raise InputSanitizer::ConversionError.new("invalid time")
45
50
  end
46
51
 
47
52
  def strip_timezone(time)
@@ -59,6 +64,8 @@ module InputSanitizer
59
64
  '0' => false,
60
65
  'yes' => true,
61
66
  'no' => false,
67
+ 1 => true,
68
+ 0 => false,
62
69
  }
63
70
 
64
71
  def call(value)
@@ -74,7 +81,7 @@ module InputSanitizer
74
81
  message += " for true, or "
75
82
  message += falsy.join(", ")
76
83
  message += " for false."
77
- raise ConversionError.new(message)
84
+ raise InputSanitizer::ConversionError.new(message)
78
85
  end
79
86
  end
80
87
  end