grape-security 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +45 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +70 -0
  5. data/.travis.yml +18 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +314 -0
  8. data/CONTRIBUTING.md +118 -0
  9. data/Gemfile +21 -0
  10. data/Guardfile +14 -0
  11. data/LICENSE +20 -0
  12. data/README.md +1777 -0
  13. data/RELEASING.md +105 -0
  14. data/Rakefile +69 -0
  15. data/UPGRADING.md +124 -0
  16. data/grape-security.gemspec +39 -0
  17. data/grape.png +0 -0
  18. data/lib/grape.rb +99 -0
  19. data/lib/grape/api.rb +646 -0
  20. data/lib/grape/cookies.rb +39 -0
  21. data/lib/grape/endpoint.rb +533 -0
  22. data/lib/grape/error_formatter/base.rb +31 -0
  23. data/lib/grape/error_formatter/json.rb +15 -0
  24. data/lib/grape/error_formatter/txt.rb +16 -0
  25. data/lib/grape/error_formatter/xml.rb +15 -0
  26. data/lib/grape/exceptions/base.rb +66 -0
  27. data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
  28. data/lib/grape/exceptions/invalid_formatter.rb +10 -0
  29. data/lib/grape/exceptions/invalid_versioner_option.rb +10 -0
  30. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +10 -0
  31. data/lib/grape/exceptions/missing_mime_type.rb +10 -0
  32. data/lib/grape/exceptions/missing_option.rb +10 -0
  33. data/lib/grape/exceptions/missing_vendor_option.rb +10 -0
  34. data/lib/grape/exceptions/unknown_options.rb +10 -0
  35. data/lib/grape/exceptions/unknown_validator.rb +10 -0
  36. data/lib/grape/exceptions/validation.rb +26 -0
  37. data/lib/grape/exceptions/validation_errors.rb +43 -0
  38. data/lib/grape/formatter/base.rb +31 -0
  39. data/lib/grape/formatter/json.rb +12 -0
  40. data/lib/grape/formatter/serializable_hash.rb +35 -0
  41. data/lib/grape/formatter/txt.rb +11 -0
  42. data/lib/grape/formatter/xml.rb +12 -0
  43. data/lib/grape/http/request.rb +26 -0
  44. data/lib/grape/locale/en.yml +32 -0
  45. data/lib/grape/middleware/auth/base.rb +30 -0
  46. data/lib/grape/middleware/auth/basic.rb +13 -0
  47. data/lib/grape/middleware/auth/digest.rb +13 -0
  48. data/lib/grape/middleware/auth/oauth2.rb +83 -0
  49. data/lib/grape/middleware/base.rb +62 -0
  50. data/lib/grape/middleware/error.rb +89 -0
  51. data/lib/grape/middleware/filter.rb +17 -0
  52. data/lib/grape/middleware/formatter.rb +150 -0
  53. data/lib/grape/middleware/globals.rb +13 -0
  54. data/lib/grape/middleware/versioner.rb +32 -0
  55. data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
  56. data/lib/grape/middleware/versioner/header.rb +132 -0
  57. data/lib/grape/middleware/versioner/param.rb +42 -0
  58. data/lib/grape/middleware/versioner/path.rb +52 -0
  59. data/lib/grape/namespace.rb +23 -0
  60. data/lib/grape/parser/base.rb +29 -0
  61. data/lib/grape/parser/json.rb +11 -0
  62. data/lib/grape/parser/xml.rb +11 -0
  63. data/lib/grape/path.rb +70 -0
  64. data/lib/grape/route.rb +27 -0
  65. data/lib/grape/util/content_types.rb +18 -0
  66. data/lib/grape/util/deep_merge.rb +23 -0
  67. data/lib/grape/util/hash_stack.rb +120 -0
  68. data/lib/grape/validations.rb +322 -0
  69. data/lib/grape/validations/coerce.rb +63 -0
  70. data/lib/grape/validations/default.rb +25 -0
  71. data/lib/grape/validations/exactly_one_of.rb +26 -0
  72. data/lib/grape/validations/mutual_exclusion.rb +25 -0
  73. data/lib/grape/validations/presence.rb +16 -0
  74. data/lib/grape/validations/regexp.rb +12 -0
  75. data/lib/grape/validations/values.rb +23 -0
  76. data/lib/grape/version.rb +3 -0
  77. data/spec/grape/api_spec.rb +2571 -0
  78. data/spec/grape/endpoint_spec.rb +784 -0
  79. data/spec/grape/entity_spec.rb +324 -0
  80. data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
  81. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
  82. data/spec/grape/exceptions/missing_mime_type_spec.rb +18 -0
  83. data/spec/grape/exceptions/missing_option_spec.rb +18 -0
  84. data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
  85. data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
  86. data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
  87. data/spec/grape/middleware/auth/basic_spec.rb +31 -0
  88. data/spec/grape/middleware/auth/digest_spec.rb +47 -0
  89. data/spec/grape/middleware/auth/oauth2_spec.rb +135 -0
  90. data/spec/grape/middleware/base_spec.rb +58 -0
  91. data/spec/grape/middleware/error_spec.rb +45 -0
  92. data/spec/grape/middleware/exception_spec.rb +184 -0
  93. data/spec/grape/middleware/formatter_spec.rb +258 -0
  94. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
  95. data/spec/grape/middleware/versioner/header_spec.rb +302 -0
  96. data/spec/grape/middleware/versioner/param_spec.rb +58 -0
  97. data/spec/grape/middleware/versioner/path_spec.rb +44 -0
  98. data/spec/grape/middleware/versioner_spec.rb +22 -0
  99. data/spec/grape/path_spec.rb +229 -0
  100. data/spec/grape/util/hash_stack_spec.rb +132 -0
  101. data/spec/grape/validations/coerce_spec.rb +208 -0
  102. data/spec/grape/validations/default_spec.rb +123 -0
  103. data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
  104. data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
  105. data/spec/grape/validations/presence_spec.rb +142 -0
  106. data/spec/grape/validations/regexp_spec.rb +40 -0
  107. data/spec/grape/validations/values_spec.rb +152 -0
  108. data/spec/grape/validations/zh-CN.yml +10 -0
  109. data/spec/grape/validations_spec.rb +994 -0
  110. data/spec/shared/versioning_examples.rb +121 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/basic_auth_encode_helpers.rb +3 -0
  113. data/spec/support/content_type_helpers.rb +11 -0
  114. data/spec/support/versioned_helpers.rb +50 -0
  115. metadata +421 -0
@@ -0,0 +1,27 @@
1
+ module Grape
2
+ # A compiled route for inspection.
3
+ class Route
4
+ def initialize(options = {})
5
+ @options = options || {}
6
+ end
7
+
8
+ def method_missing(method_id, *arguments)
9
+ match = /route_([_a-zA-Z]\w*)/.match(method_id.to_s)
10
+ if match
11
+ @options[match.captures.last.to_sym]
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ def to_s
18
+ "version=#{route_version}, method=#{route_method}, path=#{route_path}"
19
+ end
20
+
21
+ private
22
+
23
+ def to_ary
24
+ nil
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module Grape
2
+ module ContentTypes
3
+ # Content types are listed in order of preference.
4
+ CONTENT_TYPES = ActiveSupport::OrderedHash[
5
+ :xml, 'application/xml',
6
+ :serializable_hash, 'application/json',
7
+ :json, 'application/json',
8
+ :jsonapi, 'application/vnd.api+json',
9
+ :atom, 'application/atom+xml',
10
+ :rss, 'application/rss+xml',
11
+ :txt, 'text/plain',
12
+ ]
13
+
14
+ def self.content_types_for(from_settings)
15
+ from_settings || Grape::ContentTypes::CONTENT_TYPES
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ class Hash
2
+ # deep_merge from rails
3
+ # activesupport/lib/active_support/core_ext/hash/deep_merge.rb
4
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
5
+ #
6
+ # h1 = {x: {y: [4,5,6]}, z: [7,8,9]}
7
+ # h2 = {x: {y: [7,8,9]}, z: "xyz"}
8
+ #
9
+ # h1.deep_merge(h2) #=> { x: {y: [7, 8, 9]}, z: "xyz" }
10
+ # h2.deep_merge(h1) #=> { x: {y: [4, 5, 6]}, z: [7, 8, 9] }
11
+ def deep_merge(other_hash)
12
+ dup.deep_merge!(other_hash)
13
+ end
14
+
15
+ # Same as +deep_merge+, but modifies +self+.
16
+ def deep_merge!(other_hash)
17
+ other_hash.each_pair do |k, v|
18
+ tv = self[k]
19
+ self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
20
+ end
21
+ self
22
+ end
23
+ end
@@ -0,0 +1,120 @@
1
+ module Grape
2
+ module Util
3
+ # HashStack is a stack of hashes. When retrieving a value, keys of the top
4
+ # hash on the stack take precendent over the lower keys.
5
+ class HashStack
6
+ # Unmerged array of hashes to represent the stack.
7
+ # The top of the stack is the last element.
8
+ attr_reader :stack
9
+
10
+ # TODO: handle aggregates
11
+ def initialize
12
+ @stack = [{}]
13
+ end
14
+
15
+ # Returns the top hash on the stack
16
+ def peek
17
+ @stack.last
18
+ end
19
+
20
+ # Add a new hash to the top of the stack.
21
+ #
22
+ # @param hash [Hash] optional hash to be pushed. Defaults to empty hash
23
+ # @return [HashStack]
24
+ def push(hash = {})
25
+ @stack.push(hash)
26
+ self
27
+ end
28
+
29
+ def pop
30
+ @stack.pop
31
+ end
32
+
33
+ # Looks through the stack for the first frame that matches :key
34
+ #
35
+ # @param key [Symbol] key to look for in hash frames
36
+ # @return value of given key after merging the stack
37
+ def get(key)
38
+ (@stack.length - 1).downto(0).each do |i|
39
+ return @stack[i][key] if @stack[i].key? key
40
+ end
41
+ nil
42
+ end
43
+ alias_method :[], :get
44
+
45
+ # Looks through the stack for the first frame that matches :key
46
+ #
47
+ # @param key [Symbol] key to look for in hash frames
48
+ # @return true if key exists, false otherwise
49
+ def key?(key)
50
+ (@stack.length - 1).downto(0).each do |i|
51
+ return true if @stack[i].key? key
52
+ end
53
+ false
54
+ end
55
+
56
+ # Replace a value on the top hash of the stack.
57
+ #
58
+ # @param key [Symbol] The key to set.
59
+ # @param value [Object] The value to set.
60
+ def set(key, value)
61
+ peek[key.to_sym] = value
62
+ end
63
+ alias_method :[]=, :set
64
+
65
+ # Replace multiple values on the top hash of the stack.
66
+ #
67
+ # @param hash [Hash] Hash of values to be merged in.
68
+ def update(hash)
69
+ peek.merge!(hash)
70
+ self
71
+ end
72
+
73
+ # Adds addition value into the top hash of the stack
74
+ def imbue(key, value)
75
+ current = peek[key.to_sym]
76
+ if current.is_a?(Array)
77
+ current.concat(value)
78
+ elsif current.is_a?(Hash)
79
+ current.merge!(value)
80
+ else
81
+ set(key, value)
82
+ end
83
+ end
84
+
85
+ # Prepend another HashStack's to self
86
+ def prepend(hash_stack)
87
+ @stack.unshift(*hash_stack.stack)
88
+ self
89
+ end
90
+
91
+ # Concatenate another HashStack's to self
92
+ def concat(hash_stack)
93
+ @stack.concat hash_stack.stack
94
+ self
95
+ end
96
+
97
+ # Looks through the stack for all instances of a given key and returns
98
+ # them as a flat Array.
99
+ #
100
+ # @param key [Symbol] The key to gather
101
+ # @return [Array]
102
+ def gather(key)
103
+ stack.flat_map { |s| s[key] }.compact.uniq
104
+ end
105
+
106
+ def to_s
107
+ @stack.to_s
108
+ end
109
+
110
+ def clone
111
+ new_stack = HashStack.new
112
+ stack.each do |frame|
113
+ new_stack.push frame.clone
114
+ end
115
+ new_stack.stack.shift
116
+ new_stack
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,322 @@
1
+ module Grape
2
+ module Validations
3
+ ##
4
+ # All validators must inherit from this class.
5
+ #
6
+ class Validator
7
+ attr_reader :attrs
8
+
9
+ def initialize(attrs, options, required, scope)
10
+ @attrs = Array(attrs)
11
+ @required = required
12
+ @scope = scope
13
+
14
+ if options.is_a?(Hash) && !options.empty?
15
+ raise Grape::Exceptions.UnknownOptions.new(options.keys)
16
+ end
17
+ end
18
+
19
+ def validate!(params)
20
+ attributes = AttributesIterator.new(self, @scope, params)
21
+ attributes.each do |resource_params, attr_name|
22
+ if @required || resource_params.key?(attr_name)
23
+ validate_param!(attr_name, resource_params)
24
+ end
25
+ end
26
+ end
27
+
28
+ class AttributesIterator
29
+ include Enumerable
30
+
31
+ def initialize(validator, scope, params)
32
+ @attrs = validator.attrs
33
+ @params = scope.params(params)
34
+ @params = (@params.is_a?(Array) ? @params : [@params])
35
+ end
36
+
37
+ def each
38
+ @params.each do |resource_params|
39
+ @attrs.each do |attr_name|
40
+ yield resource_params, attr_name
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.convert_to_short_name(klass)
47
+ ret = klass.name.gsub(/::/, '/')
48
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
49
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
50
+ .tr("-", "_")
51
+ .downcase
52
+ File.basename(ret, '_validator')
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Base class for all validators taking only one param.
58
+ class SingleOptionValidator < Validator
59
+ def initialize(attrs, options, required, scope)
60
+ @option = options
61
+ super
62
+ end
63
+ end
64
+
65
+ # We define Validator::inherited here so SingleOptionValidator
66
+ # will not be considered a validator.
67
+ class Validator
68
+ def self.inherited(klass)
69
+ short_name = convert_to_short_name(klass)
70
+ Validations.register_validator(short_name, klass)
71
+ end
72
+ end
73
+
74
+ class << self
75
+ attr_accessor :validators
76
+ end
77
+
78
+ self.validators = {}
79
+
80
+ def self.register_validator(short_name, klass)
81
+ validators[short_name] = klass
82
+ end
83
+
84
+ class ParamsScope
85
+ attr_accessor :element, :parent
86
+
87
+ def initialize(opts, &block)
88
+ @element = opts[:element]
89
+ @parent = opts[:parent]
90
+ @api = opts[:api]
91
+ @optional = opts[:optional] || false
92
+ @type = opts[:type]
93
+ @declared_params = []
94
+
95
+ instance_eval(&block)
96
+
97
+ configure_declared_params
98
+ end
99
+
100
+ def should_validate?(parameters)
101
+ return false if @optional && params(parameters).respond_to?(:all?) && params(parameters).all?(&:blank?)
102
+ return true if parent.nil?
103
+ parent.should_validate?(parameters)
104
+ end
105
+
106
+ def requires(*attrs, &block)
107
+ orig_attrs = attrs.clone
108
+
109
+ opts = attrs.last.is_a?(Hash) ? attrs.pop : nil
110
+
111
+ if opts && opts[:using]
112
+ require_required_and_optional_fields(attrs.first, opts)
113
+ else
114
+ validate_attributes(attrs, opts, &block)
115
+
116
+ block_given? ? new_scope(orig_attrs, &block) :
117
+ push_declared_params(attrs)
118
+ end
119
+ end
120
+
121
+ def optional(*attrs, &block)
122
+ orig_attrs = attrs
123
+
124
+ validations = {}
125
+ validations.merge!(attrs.pop) if attrs.last.is_a?(Hash)
126
+ validations[:type] ||= Array if block_given?
127
+ validates(attrs, validations)
128
+
129
+ block_given? ? new_scope(orig_attrs, true, &block) :
130
+ push_declared_params(attrs)
131
+ end
132
+
133
+ def mutually_exclusive(*attrs)
134
+ validates(attrs, mutual_exclusion: true)
135
+ end
136
+
137
+ def exactly_one_of(*attrs)
138
+ validates(attrs, exactly_one_of: true)
139
+ end
140
+
141
+ def group(*attrs, &block)
142
+ requires(*attrs, &block)
143
+ end
144
+
145
+ def params(params)
146
+ params = @parent.params(params) if @parent
147
+ if @element
148
+ if params.is_a?(Array)
149
+ params = params.flat_map { |el| el[@element] || {} }
150
+ elsif params.is_a?(Hash)
151
+ params = params[@element] || {}
152
+ else
153
+ params = {}
154
+ end
155
+ end
156
+ params
157
+ end
158
+
159
+ def use(*names)
160
+ named_params = @api.settings[:named_params] || {}
161
+ options = names.last.is_a?(Hash) ? names.pop : {}
162
+ names.each do |name|
163
+ params_block = named_params.fetch(name) do
164
+ raise "Params :#{name} not found!"
165
+ end
166
+ instance_exec(options, &params_block)
167
+ end
168
+ end
169
+ alias_method :use_scope, :use
170
+ alias_method :includes, :use
171
+
172
+ def full_name(name)
173
+ return "#{@parent.full_name(@element)}[#{name}]" if @parent
174
+ name.to_s
175
+ end
176
+
177
+ def root?
178
+ !@parent
179
+ end
180
+
181
+ protected
182
+
183
+ def push_declared_params(attrs)
184
+ @declared_params.concat attrs
185
+ end
186
+
187
+ private
188
+
189
+ def require_required_and_optional_fields(context, opts)
190
+ if context == :all
191
+ optional_fields = Array(opts[:except])
192
+ required_fields = opts[:using].keys - optional_fields
193
+ else # context == :none
194
+ required_fields = Array(opts[:except])
195
+ optional_fields = opts[:using].keys - required_fields
196
+ end
197
+ required_fields.each do |field|
198
+ field_opts = opts[:using][field]
199
+ raise ArgumentError, "required field not exist: #{field}" unless field_opts
200
+ requires(field, field_opts)
201
+ end
202
+ optional_fields.each do |field|
203
+ field_opts = opts[:using][field]
204
+ optional(field, field_opts) if field_opts
205
+ end
206
+ end
207
+
208
+ def validate_attributes(attrs, opts, &block)
209
+ validations = { presence: true }
210
+ validations.merge!(opts) if opts
211
+ validations[:type] ||= Array if block
212
+ validates(attrs, validations)
213
+ end
214
+
215
+ def new_scope(attrs, optional = false, &block)
216
+ opts = attrs[1] || { type: Array }
217
+ raise ArgumentError unless opts.keys.to_set.subset? [:type].to_set
218
+ ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
219
+ end
220
+
221
+ # Pushes declared params to parent or settings
222
+ def configure_declared_params
223
+ if @parent
224
+ @parent.push_declared_params [element => @declared_params]
225
+ else
226
+ @api.settings.peek[:declared_params] ||= []
227
+ @api.settings[:declared_params].concat @declared_params
228
+ end
229
+ end
230
+
231
+ def validates(attrs, validations)
232
+ doc_attrs = { required: validations.keys.include?(:presence) }
233
+
234
+ # special case (type = coerce)
235
+ validations[:coerce] = validations.delete(:type) if validations.key?(:type)
236
+
237
+ coerce_type = validations[:coerce]
238
+ doc_attrs[:type] = coerce_type.to_s if coerce_type
239
+
240
+ desc = validations.delete(:desc)
241
+ doc_attrs[:desc] = desc if desc
242
+
243
+ default = validations[:default]
244
+ doc_attrs[:default] = default if default
245
+
246
+ values = validations[:values]
247
+ doc_attrs[:values] = values if values
248
+
249
+ values = (values.is_a?(Proc) ? values.call : values)
250
+
251
+ # default value should be present in values array, if both exist
252
+ if default && values && !values.include?(default)
253
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values)
254
+ end
255
+
256
+ # type should be compatible with values array, if both exist
257
+ if coerce_type && values && values.any? { |v| !v.kind_of?(coerce_type) }
258
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
259
+ end
260
+
261
+ doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation)
262
+
263
+ full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
264
+ @api.document_attribute(full_attrs, doc_attrs)
265
+
266
+ # Validate for presence before any other validators
267
+ if validations.key?(:presence) && validations[:presence]
268
+ validate('presence', validations[:presence], attrs, doc_attrs)
269
+ validations.delete(:presence)
270
+ end
271
+
272
+ # Before we run the rest of the validators, lets handle
273
+ # whatever coercion so that we are working with correctly
274
+ # type casted values
275
+ if validations.key? :coerce
276
+ validate('coerce', validations[:coerce], attrs, doc_attrs)
277
+ validations.delete(:coerce)
278
+ end
279
+
280
+ validations.each do |type, options|
281
+ validate(type, options, attrs, doc_attrs)
282
+ end
283
+ end
284
+
285
+ def validate(type, options, attrs, doc_attrs)
286
+ validator_class = Validations.validators[type.to_s]
287
+
288
+ if validator_class
289
+ (@api.settings.peek[:validations] ||= []) << validator_class.new(attrs, options, doc_attrs[:required], self)
290
+ else
291
+ raise Grape::Exceptions::UnknownValidator.new(type)
292
+ end
293
+ end
294
+ end
295
+
296
+ # This module is mixed into the API Class.
297
+ module ClassMethods
298
+ def reset_validations!
299
+ settings.peek[:declared_params] = []
300
+ settings.peek[:validations] = []
301
+ end
302
+
303
+ def params(&block)
304
+ ParamsScope.new(api: self, type: Hash, &block)
305
+ end
306
+
307
+ def document_attribute(names, opts)
308
+ @last_description ||= {}
309
+ @last_description[:params] ||= {}
310
+ Array(names).each do |name|
311
+ @last_description[:params][name[:full_name].to_s] ||= {}
312
+ @last_description[:params][name[:full_name].to_s].merge!(opts)
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ # Load all defined validations.
320
+ Dir[File.expand_path('../validations/*.rb', __FILE__)].each do |path|
321
+ require(path)
322
+ end