grape-security 0.8.0

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.
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