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,32 @@
1
+ en:
2
+ grape:
3
+ errors:
4
+ format: ! '%{attribute} %{message}'
5
+ messages:
6
+ coerce: 'is invalid'
7
+ presence: 'is missing'
8
+ regexp: 'is invalid'
9
+ values: 'does not have a valid value'
10
+ missing_vendor_option:
11
+ problem: 'missing :vendor option.'
12
+ summary: 'when version using header, you must specify :vendor option. '
13
+ resolution: "eg: version 'v1', :using => :header, :vendor => 'twitter'"
14
+ missing_mime_type:
15
+ problem: 'missing mime type for %{new_format}'
16
+ resolution:
17
+ "you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES
18
+ or add your own with content_type :%{new_format}, 'application/%{new_format}'
19
+ "
20
+ invalid_with_option_for_represent:
21
+ problem: 'You must specify an entity class in the :with option.'
22
+ resolution: 'eg: represent User, :with => Entity::User'
23
+ missing_option: 'You must specify :%{option} options.'
24
+ invalid_formatter: 'cannot convert %{klass} to %{to_format}'
25
+ invalid_versioner_option:
26
+ problem: 'Unknown :using for versioner: %{strategy}'
27
+ resolution: 'available strategy for :using is :path, :header, :param'
28
+ unknown_validator: 'unknown validator: %{validator_type}'
29
+ unknown_options: 'unknown options: %{options}'
30
+ incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
31
+ mutual_exclusion: 'are mutually exclusive'
32
+ exactly_one: "- exactly one parameter must be provided"
@@ -0,0 +1,30 @@
1
+ require 'rack/auth/basic'
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Auth
6
+ class Base < Grape::Middleware::Base
7
+ attr_reader :authenticator
8
+
9
+ def initialize(app, options = {}, &authenticator)
10
+ super(app, options)
11
+ @authenticator = authenticator
12
+ end
13
+
14
+ def base_request
15
+ raise NotImplementedError, "You must implement base_request."
16
+ end
17
+
18
+ def credentials
19
+ base_request.provided? ? base_request.credentials : [nil, nil]
20
+ end
21
+
22
+ def before
23
+ unless authenticator.call(*credentials)
24
+ throw :error, status: 401, message: "API Authorization Failed."
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ require 'rack/auth/basic'
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Auth
6
+ class Basic < Grape::Middleware::Auth::Base
7
+ def base_request
8
+ Rack::Auth::Basic::Request.new(env)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'rack/auth/digest/md5'
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Auth
6
+ class Digest < Grape::Middleware::Auth::Base
7
+ def base_request
8
+ Rack::Auth::Digest::Request.new(env)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,83 @@
1
+ module Grape
2
+ module Middleware
3
+ module Auth
4
+ # OAuth 2.0 authorization for Grape APIs.
5
+ class OAuth2 < Grape::Middleware::Base
6
+ def default_options
7
+ {
8
+ token_class: 'AccessToken',
9
+ realm: 'OAuth API',
10
+ parameter: %w(bearer_token oauth_token access_token),
11
+ accepted_headers: %w(HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION REDIRECT_X_HTTP_AUTHORIZATION),
12
+ header: [/Bearer (.*)/i, /OAuth (.*)/i],
13
+ required: true
14
+ }
15
+ end
16
+
17
+ def before
18
+ verify_token(token_parameter || token_header)
19
+ end
20
+
21
+ def request
22
+ @request ||= Grape::Request.new(env)
23
+ end
24
+
25
+ def params
26
+ @params ||= request.params
27
+ end
28
+
29
+ def token_parameter
30
+ Array(options[:parameter]).each do |p|
31
+ return params[p] if params[p]
32
+ end
33
+ nil
34
+ end
35
+
36
+ def token_header
37
+ return false unless authorization_header
38
+ Array(options[:header]).each do |regexp|
39
+ return $1 if authorization_header =~ regexp
40
+ end
41
+ nil
42
+ end
43
+
44
+ def authorization_header
45
+ options[:accepted_headers].each do |head|
46
+ return env[head] if env[head]
47
+ end
48
+ nil
49
+ end
50
+
51
+ def token_class
52
+ @klass ||= eval(options[:token_class]) # rubocop:disable Eval
53
+ end
54
+
55
+ def verify_token(token)
56
+ token = token_class.verify(token)
57
+ if token
58
+ if token.respond_to?(:expired?) && token.expired?
59
+ error_out(401, 'invalid_grant')
60
+ else
61
+ if !token.respond_to?(:permission_for?) || token.permission_for?(env)
62
+ env['api.token'] = token
63
+ else
64
+ error_out(403, 'insufficient_scope')
65
+ end
66
+ end
67
+ elsif !!options[:required]
68
+ error_out(401, 'invalid_grant')
69
+ end
70
+ end
71
+
72
+ def error_out(status, error)
73
+ throw :error,
74
+ message: error,
75
+ status: status,
76
+ headers: {
77
+ 'WWW-Authenticate' => "OAuth realm='#{options[:realm]}', error='#{error}'"
78
+ }
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,62 @@
1
+ module Grape
2
+ module Middleware
3
+ class Base
4
+ attr_reader :app, :env, :options
5
+
6
+ # @param [Rack Application] app The standard argument for a Rack middleware.
7
+ # @param [Hash] options A hash of options, simply stored for use by subclasses.
8
+ def initialize(app, options = {})
9
+ @app = app
10
+ @options = default_options.merge(options)
11
+ end
12
+
13
+ def default_options
14
+ {}
15
+ end
16
+
17
+ def call(env)
18
+ dup.call!(env)
19
+ end
20
+
21
+ def call!(env)
22
+ @env = env
23
+ before
24
+ @app_response = @app.call(@env)
25
+ after || @app_response
26
+ end
27
+
28
+ # @abstract
29
+ # Called before the application is called in the middleware lifecycle.
30
+ def before
31
+ end
32
+
33
+ # @abstract
34
+ # Called after the application is called in the middleware lifecycle.
35
+ # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
36
+ def after
37
+ end
38
+
39
+ def response
40
+ Rack::Response.new(@app_response)
41
+ end
42
+
43
+ def content_type_for(format)
44
+ HashWithIndifferentAccess.new(content_types)[format]
45
+ end
46
+
47
+ def content_types
48
+ ContentTypes.content_types_for(options[:content_types])
49
+ end
50
+
51
+ def content_type
52
+ content_type_for(env['api.format'] || options[:format]) || 'text/html'
53
+ end
54
+
55
+ def mime_types
56
+ content_types.each_with_object({}) { |(k, v), types_without_params|
57
+ types_without_params[k] = v.split(';').first
58
+ }.invert
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,89 @@
1
+ require 'grape/middleware/base'
2
+
3
+ module Grape
4
+ module Middleware
5
+ class Error < Base
6
+ def default_options
7
+ {
8
+ default_status: 500, # default status returned on error
9
+ default_message: "",
10
+ format: :txt,
11
+ formatters: {},
12
+ error_formatters: {},
13
+ rescue_all: false, # true to rescue all exceptions
14
+ rescue_subclasses: true, # rescue subclasses of exceptions listed
15
+ rescue_options: { backtrace: false }, # true to display backtrace
16
+ rescue_handlers: {}, # rescue handler blocks
17
+ base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
18
+ all_rescue_handler: nil # rescue handler block to rescue from all exceptions
19
+ }
20
+ end
21
+
22
+ def call!(env)
23
+ @env = env
24
+
25
+ begin
26
+ error_response(catch(:error) do
27
+ return @app.call(@env)
28
+ end)
29
+ rescue StandardError => e
30
+ is_rescuable = rescuable?(e.class)
31
+ if e.is_a?(Grape::Exceptions::Base) && !is_rescuable
32
+ handler = lambda { |arg| error_response(arg) }
33
+ else
34
+ raise unless is_rescuable
35
+ handler = find_handler(e.class)
36
+ end
37
+
38
+ handler.nil? ? handle_error(e) : exec_handler(e, &handler)
39
+ end
40
+ end
41
+
42
+ def find_handler(klass)
43
+ handler = options[:rescue_handlers].find(-> { [] }) { |error, _| klass <= error }[1]
44
+ handler ||= options[:base_only_rescue_handlers][klass]
45
+ handler ||= options[:all_rescue_handler]
46
+ handler
47
+ end
48
+
49
+ def rescuable?(klass)
50
+ options[:rescue_all] || (options[:rescue_handlers] || []).any? { |error, handler| klass <= error } || (options[:base_only_rescue_handlers] || []).include?(klass)
51
+ end
52
+
53
+ def exec_handler(e, &handler)
54
+ if handler.lambda? && handler.arity == 0
55
+ instance_exec(&handler)
56
+ else
57
+ instance_exec(e, &handler)
58
+ end
59
+ end
60
+
61
+ def handle_error(e)
62
+ error_response(message: e.message, backtrace: e.backtrace)
63
+ end
64
+
65
+ def error_response(error = {})
66
+ status = error[:status] || options[:default_status]
67
+ message = error[:message] || options[:default_message]
68
+ headers = { 'Content-Type' => content_type }
69
+ headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
70
+ backtrace = error[:backtrace] || []
71
+ rack_response(format_message(message, backtrace), status, headers)
72
+ end
73
+
74
+ def rack_response(message, status = options[:default_status], headers = { 'Content-Type' => content_type })
75
+ if headers['Content-Type'] == 'text/html'
76
+ message = ERB::Util.html_escape(message)
77
+ end
78
+ Rack::Response.new([message], status, headers).finish
79
+ end
80
+
81
+ def format_message(message, backtrace)
82
+ format = env['api.format'] || options[:format]
83
+ formatter = Grape::ErrorFormatter::Base.formatter_for(format, options)
84
+ throw :error, status: 406, message: "The requested format '#{format}' is not supported." unless formatter
85
+ formatter.call(message, backtrace, options, env)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,17 @@
1
+ module Grape
2
+ module Middleware
3
+ # This is a simple middleware for adding before and after filters
4
+ # to Grape APIs. It is used like so:
5
+ #
6
+ # use Grape::Middleware::Filter, before: lambda { do_something }, after: lambda { do_something }
7
+ class Filter < Base
8
+ def before
9
+ app.instance_eval(&options[:before]) if options[:before]
10
+ end
11
+
12
+ def after
13
+ app.instance_eval(&options[:after]) if options[:after]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,150 @@
1
+ require 'grape/middleware/base'
2
+
3
+ module Grape
4
+ module Middleware
5
+ class Formatter < Base
6
+ def default_options
7
+ {
8
+ default_format: :txt,
9
+ formatters: {},
10
+ parsers: {}
11
+ }
12
+ end
13
+
14
+ def headers
15
+ env.dup.inject({}) do |h, (k, v)|
16
+ h[k.to_s.downcase[5..-1]] = v if k.to_s.downcase.start_with?('http_')
17
+ h
18
+ end
19
+ end
20
+
21
+ def before
22
+ negotiate_content_type
23
+ read_body_input
24
+ end
25
+
26
+ def after
27
+ status, headers, bodies = *@app_response
28
+ # allow content-type to be explicitly overwritten
29
+ api_format = mime_types[headers["Content-Type"]] || env['api.format']
30
+ formatter = Grape::Formatter::Base.formatter_for api_format, options
31
+ begin
32
+ bodymap = bodies.collect do |body|
33
+ formatter.call body, env
34
+ end
35
+ rescue Grape::Exceptions::InvalidFormatter => e
36
+ throw :error, status: 500, message: e.message
37
+ end
38
+ headers['Content-Type'] = content_type_for(env['api.format']) unless headers['Content-Type']
39
+ Rack::Response.new(bodymap, status, headers).to_a
40
+ end
41
+
42
+ private
43
+
44
+ def request
45
+ @request ||= Rack::Request.new(env)
46
+ end
47
+
48
+ # store read input in env['api.request.input']
49
+ def read_body_input
50
+ if (request.post? || request.put? || request.patch? || request.delete?) &&
51
+ (!request.form_data? || !request.media_type) &&
52
+ (!request.parseable_data?) &&
53
+ (request.content_length.to_i > 0 || request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
54
+
55
+ if (input = env['rack.input'])
56
+ input.rewind
57
+ body = env['api.request.input'] = input.read
58
+ begin
59
+ read_rack_input(body) if body && body.length > 0
60
+ ensure
61
+ input.rewind
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # store parsed input in env['api.request.body']
68
+ def read_rack_input(body)
69
+ fmt = mime_types[request.media_type] if request.media_type
70
+ fmt ||= options[:default_format]
71
+ if content_type_for(fmt)
72
+ parser = Grape::Parser::Base.parser_for fmt, options
73
+ if parser
74
+ begin
75
+ body = (env['api.request.body'] = parser.call(body, env))
76
+ if body.is_a?(Hash)
77
+ if env['rack.request.form_hash']
78
+ env['rack.request.form_hash'] = env['rack.request.form_hash'].merge(body)
79
+ else
80
+ env['rack.request.form_hash'] = body
81
+ end
82
+ env['rack.request.form_input'] = env['rack.input']
83
+ end
84
+ rescue StandardError => e
85
+ throw :error, status: 400, message: e.message
86
+ end
87
+ else
88
+ env['api.request.body'] = body
89
+ end
90
+ else
91
+ throw :error, status: 406, message: "The requested content-type '#{request.media_type}' is not supported."
92
+ end
93
+ end
94
+
95
+ def negotiate_content_type
96
+ fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
97
+ if content_type_for(fmt)
98
+ env['api.format'] = fmt
99
+ else
100
+ throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
101
+ end
102
+ end
103
+
104
+ def format_from_extension
105
+ parts = request.path.split('.')
106
+
107
+ if parts.size > 1
108
+ extension = parts.last
109
+ # avoid symbol memory leak on an unknown format
110
+ return extension.to_sym if content_type_for(extension)
111
+ end
112
+ nil
113
+ end
114
+
115
+ def format_from_params
116
+ fmt = Rack::Utils.parse_nested_query(env['QUERY_STRING'])["format"]
117
+ # avoid symbol memory leak on an unknown format
118
+ return fmt.to_sym if content_type_for(fmt)
119
+ fmt
120
+ end
121
+
122
+ def format_from_header
123
+ mime_array.each do |t|
124
+ return mime_types[t] if mime_types.key?(t)
125
+ end
126
+ nil
127
+ end
128
+
129
+ def mime_array
130
+ accept = headers['accept']
131
+ return [] unless accept
132
+
133
+ accept_into_mime_and_quality = %r{
134
+ (
135
+ \w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
136
+ (?:
137
+ (?:;[^,]*?)? # optionally multiple formats in a row
138
+ ;\s*q=([\d.]+) # optional "quality" preference (eg q=0.5)
139
+ )?
140
+ }x
141
+
142
+ vendor_prefix_pattern = /vnd\.[^+]+\+/
143
+
144
+ accept.scan(accept_into_mime_and_quality)
145
+ .sort_by { |_, quality_preference| -quality_preference.to_f }
146
+ .map { |mime, _| mime.sub(vendor_prefix_pattern, '') }
147
+ end
148
+ end
149
+ end
150
+ end