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,13 @@
1
+ require 'grape/middleware/base'
2
+
3
+ module Grape
4
+ module Middleware
5
+ class Globals < Base
6
+ def before
7
+ @env['grape.request'] = Grape::Request.new(@env)
8
+ @env['grape.request.headers'] = request.headers
9
+ @env['grape.request.params'] = request.params if @env['rack.input']
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ # Versioners set env['api.version'] when a version is defined on an API and
2
+ # on the requests. The current methods for determining version are:
3
+ #
4
+ # :header - version from HTTP Accept header.
5
+ # :path - version from uri. e.g. /v1/resource
6
+ # :param - version from uri query string, e.g. /v1/resource?apiver=v1
7
+ #
8
+ # See individual classes for details.
9
+ module Grape
10
+ module Middleware
11
+ module Versioner
12
+ module_function
13
+
14
+ # @param strategy [Symbol] :path, :header or :param
15
+ # @return a middleware class based on strategy
16
+ def using(strategy)
17
+ case strategy
18
+ when :path
19
+ Path
20
+ when :header
21
+ Header
22
+ when :param
23
+ Param
24
+ when :accept_version_header
25
+ AcceptVersionHeader
26
+ else
27
+ raise Grape::Exceptions::InvalidVersionerOption.new(strategy)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,67 @@
1
+ require 'grape/middleware/base'
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ # This middleware sets various version related rack environment variables
7
+ # based on the HTTP Accept-Version header
8
+ #
9
+ # Example: For request header
10
+ # Accept-Version: v1
11
+ #
12
+ # The following rack env variables are set:
13
+ #
14
+ # env['api.version'] => 'v1'
15
+ #
16
+ # If version does not match this route, then a 406 is raised with
17
+ # X-Cascade header to alert Rack::Mount to attempt the next matched
18
+ # route.
19
+ class AcceptVersionHeader < Base
20
+ def before
21
+ potential_version = (env['HTTP_ACCEPT_VERSION'] || '').strip
22
+
23
+ if strict?
24
+ # If no Accept-Version header:
25
+ if potential_version.empty?
26
+ throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
27
+ end
28
+ end
29
+
30
+ unless potential_version.empty?
31
+ # If the requested version is not supported:
32
+ unless versions.any? { |v| v.to_s == potential_version }
33
+ throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.'
34
+ end
35
+
36
+ env['api.version'] = potential_version
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def versions
43
+ options[:versions] || []
44
+ end
45
+
46
+ def strict?
47
+ options[:version_options] && options[:version_options][:strict]
48
+ end
49
+
50
+ # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
51
+ # of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
52
+ # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
53
+ def cascade?
54
+ if options[:version_options] && options[:version_options].key?(:cascade)
55
+ !!options[:version_options][:cascade]
56
+ else
57
+ true
58
+ end
59
+ end
60
+
61
+ def error_headers
62
+ cascade? ? { 'X-Cascade' => 'pass' } : {}
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,132 @@
1
+ require 'grape/middleware/base'
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ # This middleware sets various version related rack environment variables
7
+ # based on the HTTP Accept header with the pattern:
8
+ # application/vnd.:vendor-:version+:format
9
+ #
10
+ # Example: For request header
11
+ # Accept: application/vnd.mycompany-v1+json
12
+ #
13
+ # The following rack env variables are set:
14
+ #
15
+ # env['api.type'] => 'application'
16
+ # env['api.subtype'] => 'vnd.mycompany-v1+json'
17
+ # env['api.vendor] => 'mycompany'
18
+ # env['api.version] => 'v1'
19
+ # env['api.format] => 'format'
20
+ #
21
+ # If version does not match this route, then a 406 is raised with
22
+ # X-Cascade header to alert Rack::Mount to attempt the next matched
23
+ # route.
24
+ class Header < Base
25
+ def before
26
+ begin
27
+ header = Rack::Accept::MediaType.new env['HTTP_ACCEPT']
28
+ rescue RuntimeError => e
29
+ throw :error, status: 406, headers: error_headers, message: e.message
30
+ end
31
+
32
+ if strict?
33
+ # If no Accept header:
34
+ if header.qvalues.empty?
35
+ throw :error, status: 406, headers: error_headers, message: 'Accept header must be set.'
36
+ end
37
+ # Remove any acceptable content types with ranges.
38
+ header.qvalues.reject! do |media_type, _|
39
+ Rack::Accept::Header.parse_media_type(media_type).find { |s| s == '*' }
40
+ end
41
+ # If all Accept headers included a range:
42
+ if header.qvalues.empty?
43
+ throw :error, status: 406, headers: error_headers, message: 'Accept header must not contain ranges ("*").'
44
+ end
45
+ end
46
+
47
+ media_type = header.best_of available_media_types
48
+
49
+ if media_type
50
+ type, subtype = Rack::Accept::Header.parse_media_type media_type
51
+ env['api.type'] = type
52
+ env['api.subtype'] = subtype
53
+
54
+ if /\Avnd\.([a-z0-9*.]+)(?:-([a-z0-9*\-.]+))?(?:\+([a-z0-9*\-.+]+))?\z/ =~ subtype
55
+ env['api.vendor'] = $1
56
+ env['api.version'] = $2
57
+ env['api.format'] = $3 # weird that Grape::Middleware::Formatter also does this
58
+ end
59
+ # If none of the available content types are acceptable:
60
+ elsif strict?
61
+ throw :error, status: 406, headers: error_headers, message: '406 Not Acceptable'
62
+ # If all acceptable content types specify a vendor or version that doesn't exist:
63
+ elsif header.values.all? { |header_value| has_vendor?(header_value) || version?(header_value) }
64
+ throw :error, status: 406, headers: error_headers, message: 'API vendor or version not found.'
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def available_media_types
71
+ available_media_types = []
72
+
73
+ content_types.each do |extension, media_type|
74
+ versions.reverse.each do |version|
75
+ available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
76
+ end
77
+ available_media_types << "application/vnd.#{vendor}+#{extension}"
78
+ end
79
+
80
+ available_media_types << "application/vnd.#{vendor}"
81
+
82
+ content_types.each do |_, media_type|
83
+ available_media_types << media_type
84
+ end
85
+
86
+ available_media_types = available_media_types.flatten
87
+ end
88
+
89
+ def versions
90
+ options[:versions] || []
91
+ end
92
+
93
+ def vendor
94
+ options[:version_options] && options[:version_options][:vendor]
95
+ end
96
+
97
+ def strict?
98
+ options[:version_options] && options[:version_options][:strict]
99
+ end
100
+
101
+ # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
102
+ # of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
103
+ # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
104
+ def cascade?
105
+ if options[:version_options] && options[:version_options].key?(:cascade)
106
+ !!options[:version_options][:cascade]
107
+ else
108
+ true
109
+ end
110
+ end
111
+
112
+ def error_headers
113
+ cascade? ? { 'X-Cascade' => 'pass' } : {}
114
+ end
115
+
116
+ # @param [String] media_type a content type
117
+ # @return [Boolean] whether the content type sets a vendor
118
+ def has_vendor?(media_type)
119
+ _, subtype = Rack::Accept::Header.parse_media_type media_type
120
+ subtype[/\Avnd\.[a-z0-9*.]+/]
121
+ end
122
+
123
+ # @param [String] media_type a content type
124
+ # @return [Boolean] whether the content type sets an API version
125
+ def version?(media_type)
126
+ _, subtype = Rack::Accept::Header.parse_media_type media_type
127
+ subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,42 @@
1
+ require 'grape/middleware/base'
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ # This middleware sets various version related rack environment variables
7
+ # based on the request parameters and removes that parameter from the
8
+ # request parameters for subsequent middleware and API.
9
+ # If the version substring does not match any potential initialized
10
+ # versions, a 404 error is thrown.
11
+ # If the version substring is not passed the version (highest mounted)
12
+ # version will be used.
13
+ #
14
+ # Example: For a uri path
15
+ # /resource?apiver=v1
16
+ #
17
+ # The following rack env variables are set and path is rewritten to
18
+ # '/resource':
19
+ #
20
+ # env['api.version'] => 'v1'
21
+ class Param < Base
22
+ def default_options
23
+ {
24
+ parameter: "apiver"
25
+ }
26
+ end
27
+
28
+ def before
29
+ paramkey = options[:parameter]
30
+ potential_version = Rack::Utils.parse_nested_query(env['QUERY_STRING'])[paramkey]
31
+ unless potential_version.nil?
32
+ if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
33
+ throw :error, status: 404, message: "404 API Version Not Found", headers: { 'X-Cascade' => 'pass' }
34
+ end
35
+ env['api.version'] = potential_version
36
+ env['rack.request.query_hash'].delete(paramkey) if env.key? 'rack.request.query_hash'
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ require 'grape/middleware/base'
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ # This middleware sets various version related rack environment variables
7
+ # based on the uri path and removes the version substring from the uri
8
+ # path. If the version substring does not match any potential initialized
9
+ # versions, a 404 error is thrown.
10
+ #
11
+ # Example: For a uri path
12
+ # /v1/resource
13
+ #
14
+ # The following rack env variables are set and path is rewritten to
15
+ # '/resource':
16
+ #
17
+ # env['api.version'] => 'v1'
18
+ #
19
+ class Path < Base
20
+ def default_options
21
+ {
22
+ pattern: /.*/i
23
+ }
24
+ end
25
+
26
+ def before
27
+ path = env['PATH_INFO'].dup
28
+
29
+ if prefix && path.index(prefix) == 0
30
+ path.sub!(prefix, '')
31
+ path = Rack::Mount::Utils.normalize_path(path)
32
+ end
33
+
34
+ pieces = path.split('/')
35
+ potential_version = pieces[1]
36
+ if potential_version =~ options[:pattern]
37
+ if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
38
+ throw :error, status: 404, message: "404 API Version Not Found"
39
+ end
40
+ env['api.version'] = potential_version
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def prefix
47
+ Rack::Mount::Utils.normalize_path(options[:prefix].to_s) if options[:prefix]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ module Grape
2
+ class Namespace
3
+ attr_reader :space, :options
4
+
5
+ # options:
6
+ # requirements: a hash
7
+ def initialize(space, options = {})
8
+ @space, @options = space.to_s, options
9
+ end
10
+
11
+ def requirements
12
+ options[:requirements] || {}
13
+ end
14
+
15
+ def self.joined_space(settings)
16
+ settings.gather(:namespace).map(&:space).join("/")
17
+ end
18
+
19
+ def self.joined_space_path(settings)
20
+ Rack::Mount::Utils.normalize_path(joined_space(settings))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module Grape
2
+ module Parser
3
+ module Base
4
+ class << self
5
+ PARSERS = {
6
+ json: Grape::Parser::Json,
7
+ jsonapi: Grape::Parser::Json,
8
+ xml: Grape::Parser::Xml
9
+ }
10
+
11
+ def parsers(options)
12
+ PARSERS.merge(options[:parsers] || {})
13
+ end
14
+
15
+ def parser_for(api_format, options = {})
16
+ spec = parsers(options)[api_format]
17
+ case spec
18
+ when nil
19
+ nil
20
+ when Symbol
21
+ method(spec)
22
+ else
23
+ spec
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ module Grape
2
+ module Parser
3
+ module Json
4
+ class << self
5
+ def call(object, env)
6
+ MultiJson.load(object)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Grape
2
+ module Parser
3
+ module Xml
4
+ class << self
5
+ def call(object, env)
6
+ MultiXml.parse(object)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ module Grape
2
+ class Path
3
+ def self.prepare(raw_path, namespace, settings)
4
+ Path.new(raw_path, namespace, settings).path_with_suffix
5
+ end
6
+
7
+ attr_reader :raw_path, :namespace, :settings
8
+
9
+ def initialize(raw_path, namespace, settings)
10
+ @raw_path = raw_path
11
+ @namespace = namespace
12
+ @settings = settings
13
+ end
14
+
15
+ def mount_path
16
+ split_setting(:mount_path, '/')
17
+ end
18
+
19
+ def root_prefix
20
+ split_setting(:root_prefix, '/')
21
+ end
22
+
23
+ def uses_path_versioning?
24
+ !!(settings[:version] && settings[:version_options][:using] == :path)
25
+ end
26
+
27
+ def has_namespace?
28
+ namespace && namespace.to_s =~ /^\S/ && namespace != '/'
29
+ end
30
+
31
+ def has_path?
32
+ raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/'
33
+ end
34
+
35
+ def suffix
36
+ if !uses_path_versioning? || (has_namespace? || has_path?)
37
+ '(.:format)'
38
+ else
39
+ '(/.:format)'
40
+ end
41
+ end
42
+
43
+ def path
44
+ Rack::Mount::Utils.normalize_path(parts.join('/'))
45
+ end
46
+
47
+ def path_with_suffix
48
+ "#{path}#{suffix}"
49
+ end
50
+
51
+ def to_s
52
+ path_with_suffix
53
+ end
54
+
55
+ private
56
+
57
+ def parts
58
+ parts = [mount_path, root_prefix].compact
59
+ parts << ':version' if uses_path_versioning?
60
+ parts << namespace.to_s
61
+ parts << raw_path.to_s
62
+ parts.flatten.reject { |part| part == '/' }
63
+ end
64
+
65
+ def split_setting(key, delimiter)
66
+ return if settings[key].nil?
67
+ settings[key].to_s.split("/")
68
+ end
69
+ end
70
+ end