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,31 @@
1
+ require 'spec_helper'
2
+
3
+ require 'base64'
4
+
5
+ describe Grape::Middleware::Auth::Basic do
6
+ def app
7
+ Rack::Builder.new do |b|
8
+ b.use Grape::Middleware::Error
9
+ b.use(Grape::Middleware::Auth::Basic) do |u, p|
10
+ u && p && u == p
11
+ end
12
+ b.run lambda { |env| [200, {}, ["Hello there."]] }
13
+ end
14
+ end
15
+
16
+ it 'throws a 401 if no auth is given' do
17
+ @proc = lambda { false }
18
+ get '/whatever'
19
+ expect(last_response.status).to eq(401)
20
+ end
21
+
22
+ it 'authenticates if given valid creds' do
23
+ get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin')
24
+ expect(last_response.status).to eq(200)
25
+ end
26
+
27
+ it 'throws a 401 is wrong auth is given' do
28
+ get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong')
29
+ expect(last_response.status).to eq(401)
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec::Matchers.define :be_challenge do
4
+ match do |actual_response|
5
+ actual_response.status == 401 &&
6
+ actual_response['WWW-Authenticate'] =~ /^Digest / &&
7
+ actual_response.body.empty?
8
+ end
9
+ end
10
+
11
+ class Test < Grape::API
12
+ http_digest(realm: 'Test Api', opaque: 'secret') do |username|
13
+ { 'foo' => 'bar' }[username]
14
+ end
15
+
16
+ get '/test' do
17
+ [{ hey: 'you' }, { there: 'bar' }, { foo: 'baz' }]
18
+ end
19
+ end
20
+
21
+ describe Grape::Middleware::Auth::Digest do
22
+ def app
23
+ Test
24
+ end
25
+
26
+ it 'is a digest authentication challenge' do
27
+ get '/test'
28
+ expect(last_response).to be_challenge
29
+ end
30
+
31
+ it 'throws a 401 if no auth is given' do
32
+ get '/test'
33
+ expect(last_response.status).to eq(401)
34
+ end
35
+
36
+ it 'authenticates if given valid creds' do
37
+ digest_authorize "foo", "bar"
38
+ get '/test'
39
+ expect(last_response.status).to eq(200)
40
+ end
41
+
42
+ it 'throws a 401 if given invalid creds' do
43
+ digest_authorize "bar", "foo"
44
+ get '/test'
45
+ expect(last_response.status).to eq(401)
46
+ end
47
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Middleware::Auth::OAuth2 do
4
+ class FakeToken
5
+ attr_accessor :token
6
+
7
+ def self.verify(token)
8
+ FakeToken.new(token) if !!token && %w(g e).include?(token[0..0])
9
+ end
10
+
11
+ def initialize(token)
12
+ @token = token
13
+ end
14
+
15
+ def expired?
16
+ @token[0..0] == 'e'
17
+ end
18
+
19
+ def permission_for?(env)
20
+ env['PATH_INFO'] == '/forbidden' ? false : true
21
+ end
22
+ end
23
+
24
+ def app
25
+ Rack::Builder.app do
26
+ use Grape::Middleware::Auth::OAuth2, token_class: 'FakeToken'
27
+ run lambda { |env| [200, {}, [(env['api.token'].token if env['api.token'])]] }
28
+ end
29
+ end
30
+
31
+ context 'with the token in the query string' do
32
+ context 'and a valid token' do
33
+ before { get '/awesome?access_token=g123' }
34
+
35
+ it 'sets env["api.token"]' do
36
+ expect(last_response.body).to eq('g123')
37
+ end
38
+ end
39
+
40
+ context 'and an invalid token' do
41
+ before do
42
+ @err = catch :error do
43
+ get '/awesome?access_token=b123'
44
+ end
45
+ end
46
+
47
+ it 'throws an error' do
48
+ expect(@err[:status]).to eq(401)
49
+ end
50
+
51
+ it 'sets the WWW-Authenticate header in the response' do
52
+ expect(@err[:headers]['WWW-Authenticate']).to eq("OAuth realm='OAuth API', error='invalid_grant'")
53
+ end
54
+ end
55
+ end
56
+
57
+ context 'with an expired token' do
58
+ before do
59
+ @err = catch :error do
60
+ get '/awesome?access_token=e123'
61
+ end
62
+ end
63
+
64
+ it 'throws an error' do
65
+ expect(@err[:status]).to eq(401)
66
+ end
67
+
68
+ it 'sets the WWW-Authenticate header in the response to error' do
69
+ expect(@err[:headers]['WWW-Authenticate']).to eq("OAuth realm='OAuth API', error='invalid_grant'")
70
+ end
71
+ end
72
+
73
+ %w(HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION REDIRECT_X_HTTP_AUTHORIZATION).each do |head|
74
+ context "with the token in the #{head} header" do
75
+ before do
76
+ get '/awesome', {}, head => 'OAuth g123'
77
+ end
78
+
79
+ it 'sets env["api.token"]' do
80
+ expect(last_response.body).to eq('g123')
81
+ end
82
+ end
83
+ end
84
+
85
+ context 'with the token in the POST body' do
86
+ before do
87
+ post '/awesome', 'access_token' => 'g123'
88
+ end
89
+
90
+ it 'sets env["api.token"]' do
91
+ expect(last_response.body).to eq('g123')
92
+ end
93
+ end
94
+
95
+ context 'when accessing something outside its scope' do
96
+ before do
97
+ @err = catch :error do
98
+ get '/forbidden?access_token=g123'
99
+ end
100
+ end
101
+
102
+ it 'throws an error' do
103
+ expect(@err[:status]).to eq(403)
104
+ end
105
+
106
+ it 'sets the WWW-Authenticate header in the response to error' do
107
+ expect(@err[:headers]['WWW-Authenticate']).to eq("OAuth realm='OAuth API', error='insufficient_scope'")
108
+ end
109
+ end
110
+
111
+ context 'when authorization is not required' do
112
+ def app
113
+ Rack::Builder.app do
114
+ use Grape::Middleware::Auth::OAuth2, token_class: 'FakeToken', required: false
115
+ run lambda { |env| [200, {}, [(env['api.token'].token if env['api.token'])]] }
116
+ end
117
+ end
118
+
119
+ context 'with no token' do
120
+ before { post '/awesome' }
121
+
122
+ it 'succeeds anyway' do
123
+ expect(last_response.status).to eq(200)
124
+ end
125
+ end
126
+
127
+ context 'with a valid token' do
128
+ before { get '/awesome?access_token=g123' }
129
+
130
+ it 'sets env["api.token"]' do
131
+ expect(last_response.body).to eq('g123')
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Middleware::Base do
4
+ subject { Grape::Middleware::Base.new(blank_app) }
5
+ let(:blank_app) { lambda { |_| [200, {}, 'Hi there.'] } }
6
+
7
+ before do
8
+ # Keep it one object for testing.
9
+ allow(subject).to receive(:dup).and_return(subject)
10
+ end
11
+
12
+ it 'has the app as an accessor' do
13
+ expect(subject.app).to eq(blank_app)
14
+ end
15
+
16
+ it 'calls through to the app' do
17
+ expect(subject.call({})).to eq([200, {}, 'Hi there.'])
18
+ end
19
+
20
+ context 'callbacks' do
21
+ it 'calls #before' do
22
+ expect(subject).to receive(:before)
23
+ end
24
+
25
+ it 'calls #after' do
26
+ expect(subject).to receive(:after)
27
+ end
28
+
29
+ after { subject.call!({}) }
30
+ end
31
+
32
+ it 'is able to access the response' do
33
+ subject.call({})
34
+ expect(subject.response).to be_kind_of(Rack::Response)
35
+ end
36
+
37
+ context 'options' do
38
+ it 'persists options passed at initialization' do
39
+ expect(Grape::Middleware::Base.new(blank_app, abc: true).options[:abc]).to be true
40
+ end
41
+
42
+ context 'defaults' do
43
+ class ExampleWare < Grape::Middleware::Base
44
+ def default_options
45
+ { monkey: true }
46
+ end
47
+ end
48
+
49
+ it 'persists the default options' do
50
+ expect(ExampleWare.new(blank_app).options[:monkey]).to be true
51
+ end
52
+
53
+ it 'overrides default options when provided' do
54
+ expect(ExampleWare.new(blank_app, monkey: false).options[:monkey]).to be false
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Middleware::Error do
4
+ class ErrApp
5
+ class << self
6
+ attr_accessor :error
7
+ attr_accessor :format
8
+
9
+ def call(env)
10
+ throw :error, error
11
+ end
12
+ end
13
+ end
14
+
15
+ def app
16
+ Rack::Builder.app do
17
+ use Grape::Middleware::Error, default_message: 'Aww, hamburgers.'
18
+ run ErrApp
19
+ end
20
+ end
21
+
22
+ it 'sets the status code appropriately' do
23
+ ErrApp.error = { status: 410 }
24
+ get '/'
25
+ expect(last_response.status).to eq(410)
26
+ end
27
+
28
+ it 'sets the error message appropriately' do
29
+ ErrApp.error = { message: 'Awesome stuff.' }
30
+ get '/'
31
+ expect(last_response.body).to eq('Awesome stuff.')
32
+ end
33
+
34
+ it 'defaults to a 500 status' do
35
+ ErrApp.error = {}
36
+ get '/'
37
+ expect(last_response.status).to eq(500)
38
+ end
39
+
40
+ it 'has a default message' do
41
+ ErrApp.error = {}
42
+ get '/'
43
+ expect(last_response.body).to eq('Aww, hamburgers.')
44
+ end
45
+ end
@@ -0,0 +1,184 @@
1
+ require 'spec_helper'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ describe Grape::Middleware::Error do
5
+
6
+ # raises a text exception
7
+ class ExceptionApp
8
+ class << self
9
+ def call(env)
10
+ raise "rain!"
11
+ end
12
+ end
13
+ end
14
+
15
+ # raises a hash error
16
+ class ErrorHashApp
17
+ class << self
18
+ def error!(message, status)
19
+ throw :error, message: { error: message, detail: "missing widget" }, status: status
20
+ end
21
+
22
+ def call(env)
23
+ error!("rain!", 401)
24
+ end
25
+ end
26
+ end
27
+
28
+ # raises an error!
29
+ class AccessDeniedApp
30
+ class << self
31
+ def error!(message, status)
32
+ throw :error, message: message, status: status
33
+ end
34
+
35
+ def call(env)
36
+ error!("Access Denied", 401)
37
+ end
38
+ end
39
+ end
40
+
41
+ # raises a custom error
42
+ class CustomError < Grape::Exceptions::Base
43
+ end
44
+
45
+ class CustomErrorApp
46
+ class << self
47
+ def call(env)
48
+ raise CustomError, status: 400, message: 'failed validation'
49
+ end
50
+ end
51
+ end
52
+
53
+ attr_reader :app
54
+
55
+ it 'does not trap errors by default' do
56
+ @app ||= Rack::Builder.app do
57
+ use Grape::Middleware::Error
58
+ run ExceptionApp
59
+ end
60
+ expect { get '/' }.to raise_error
61
+ end
62
+
63
+ context 'with rescue_all set to true' do
64
+ it 'sets the message appropriately' do
65
+ @app ||= Rack::Builder.app do
66
+ use Grape::Middleware::Error, rescue_all: true
67
+ run ExceptionApp
68
+ end
69
+ get '/'
70
+ expect(last_response.body).to eq("rain!")
71
+ end
72
+
73
+ it 'defaults to a 500 status' do
74
+ @app ||= Rack::Builder.app do
75
+ use Grape::Middleware::Error, rescue_all: true
76
+ run ExceptionApp
77
+ end
78
+ get '/'
79
+ expect(last_response.status).to eq(500)
80
+ end
81
+
82
+ it 'is possible to specify a different default status code' do
83
+ @app ||= Rack::Builder.app do
84
+ use Grape::Middleware::Error, rescue_all: true, default_status: 500
85
+ run ExceptionApp
86
+ end
87
+ get '/'
88
+ expect(last_response.status).to eq(500)
89
+ end
90
+
91
+ it 'is possible to return errors in json format' do
92
+ @app ||= Rack::Builder.app do
93
+ use Grape::Middleware::Error, rescue_all: true, format: :json
94
+ run ExceptionApp
95
+ end
96
+ get '/'
97
+ expect(last_response.body).to eq('{"error":"rain!"}')
98
+ end
99
+
100
+ it 'is possible to return hash errors in json format' do
101
+ @app ||= Rack::Builder.app do
102
+ use Grape::Middleware::Error, rescue_all: true, format: :json
103
+ run ErrorHashApp
104
+ end
105
+ get '/'
106
+ expect(['{"error":"rain!","detail":"missing widget"}',
107
+ '{"detail":"missing widget","error":"rain!"}']).to include(last_response.body)
108
+ end
109
+
110
+ it 'is possible to return errors in jsonapi format' do
111
+ @app ||= Rack::Builder.app do
112
+ use Grape::Middleware::Error, rescue_all: true, format: :jsonapi
113
+ run ExceptionApp
114
+ end
115
+ get '/'
116
+ expect(last_response.body).to eq('{"error":"rain!"}')
117
+ end
118
+
119
+ it 'is possible to return hash errors in jsonapi format' do
120
+ @app ||= Rack::Builder.app do
121
+ use Grape::Middleware::Error, rescue_all: true, format: :jsonapi
122
+ run ErrorHashApp
123
+ end
124
+ get '/'
125
+ expect(['{"error":"rain!","detail":"missing widget"}',
126
+ '{"detail":"missing widget","error":"rain!"}']).to include(last_response.body)
127
+ end
128
+
129
+ it 'is possible to return errors in xml format' do
130
+ @app ||= Rack::Builder.app do
131
+ use Grape::Middleware::Error, rescue_all: true, format: :xml
132
+ run ExceptionApp
133
+ end
134
+ get '/'
135
+ expect(last_response.body).to eq("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>\n <message>rain!</message>\n</error>\n")
136
+ end
137
+
138
+ it 'is possible to return hash errors in xml format' do
139
+ @app ||= Rack::Builder.app do
140
+ use Grape::Middleware::Error, rescue_all: true, format: :xml
141
+ run ErrorHashApp
142
+ end
143
+ get '/'
144
+ expect(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>\n <detail>missing widget</detail>\n <error>rain!</error>\n</error>\n",
145
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>\n <error>rain!</error>\n <detail>missing widget</detail>\n</error>\n"]).to include(last_response.body)
146
+ end
147
+
148
+ it 'is possible to specify a custom formatter' do
149
+ @app ||= Rack::Builder.app do
150
+ use Grape::Middleware::Error, rescue_all: true,
151
+ format: :custom,
152
+ error_formatters: {
153
+ custom: lambda { |message, backtrace, options, env|
154
+ { custom_formatter: message }.inspect
155
+ }
156
+ }
157
+ run ExceptionApp
158
+ end
159
+ get '/'
160
+ expect(last_response.body).to eq('{:custom_formatter=>"rain!"}')
161
+ end
162
+
163
+ it 'does not trap regular error! codes' do
164
+ @app ||= Rack::Builder.app do
165
+ use Grape::Middleware::Error
166
+ run AccessDeniedApp
167
+ end
168
+ get '/'
169
+ expect(last_response.status).to eq(401)
170
+ end
171
+
172
+ it 'responds to custom Grape exceptions appropriately' do
173
+ @app ||= Rack::Builder.app do
174
+ use Grape::Middleware::Error, rescue_all: false
175
+ run CustomErrorApp
176
+ end
177
+
178
+ get '/'
179
+ expect(last_response.status).to eq(400)
180
+ expect(last_response.body).to eq('failed validation')
181
+ end
182
+
183
+ end
184
+ end