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