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