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