cred_hubble 0.0.1.pre → 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +7 -1
  4. data/.travis.yml +3 -1
  5. data/README.md +353 -13
  6. data/cred_hubble.gemspec +3 -0
  7. data/lib/cred_hubble.rb +3 -2
  8. data/lib/cred_hubble/client.rb +119 -13
  9. data/lib/cred_hubble/http/client.rb +39 -4
  10. data/lib/cred_hubble/resources/certificate_credential.rb +25 -0
  11. data/lib/cred_hubble/resources/credential.rb +32 -0
  12. data/lib/cred_hubble/resources/credential_collection.rb +21 -0
  13. data/lib/cred_hubble/resources/credential_factory.rb +41 -0
  14. data/lib/cred_hubble/resources/immutable_resource.rb +2 -2
  15. data/lib/cred_hubble/resources/json_credential.rb +13 -0
  16. data/lib/cred_hubble/resources/password_credential.rb +13 -0
  17. data/lib/cred_hubble/resources/permission.rb +10 -0
  18. data/lib/cred_hubble/resources/permission_collection.rb +21 -0
  19. data/lib/cred_hubble/resources/resource.rb +10 -0
  20. data/lib/cred_hubble/resources/resources.rb +15 -0
  21. data/lib/cred_hubble/resources/{base_resource.rb → rest_resource.rb} +6 -2
  22. data/lib/cred_hubble/resources/rsa_credential.rb +24 -0
  23. data/lib/cred_hubble/resources/ssh_credential.rb +39 -0
  24. data/lib/cred_hubble/resources/user_credential.rb +39 -0
  25. data/lib/cred_hubble/resources/value_credential.rb +13 -0
  26. data/lib/cred_hubble/version.rb +1 -1
  27. data/spec/cred_hubble/client_spec.rb +487 -3
  28. data/spec/cred_hubble/http/client_spec.rb +347 -53
  29. data/spec/cred_hubble/resources/certificate_credential_spec.rb +49 -0
  30. data/spec/cred_hubble/resources/credential_collection_spec.rb +59 -0
  31. data/spec/cred_hubble/resources/credential_factory_spec.rb +154 -0
  32. data/spec/cred_hubble/resources/credential_spec.rb +10 -0
  33. data/spec/cred_hubble/resources/json_credential_spec.rb +52 -0
  34. data/spec/cred_hubble/resources/password_credential_spec.rb +41 -0
  35. data/spec/cred_hubble/resources/permission_collection_spec.rb +87 -0
  36. data/spec/cred_hubble/resources/permission_spec.rb +36 -0
  37. data/spec/cred_hubble/resources/rsa_credential_spec.rb +46 -0
  38. data/spec/cred_hubble/resources/ssh_credential_spec.rb +73 -0
  39. data/spec/cred_hubble/resources/user_credential_spec.rb +72 -0
  40. data/spec/cred_hubble/resources/value_credential_spec.rb +42 -0
  41. data/spec/support/shared_examples/resource_examples.rb +49 -0
  42. metadata +57 -5
@@ -1,5 +1,67 @@
1
1
  require 'spec_helper'
2
2
 
3
+ RSpec.shared_examples 'a request with error handling' do
4
+ context 'when the response status is 400' do
5
+ let(:response_body) { 'Bad request' }
6
+ let(:status) { 400 }
7
+
8
+ it 'raises a CredHubble::Http::BadRequestError' do
9
+ expect { subject }
10
+ .to raise_error(CredHubble::Http::BadRequestError, "status: #{status}, body: #{response_body}")
11
+ end
12
+ end
13
+
14
+ context 'when the response status is 401' do
15
+ let(:response_body) { 'Unauthorized' }
16
+ let(:status) { 401 }
17
+
18
+ it 'raises a CredHubble::Http::UnauthorizedError' do
19
+ expect { subject }
20
+ .to raise_error(CredHubble::Http::UnauthorizedError, "status: #{status}, body: #{response_body}")
21
+ end
22
+ end
23
+
24
+ context 'when the response status is 403' do
25
+ let(:response_body) { 'Forbidden' }
26
+ let(:status) { 403 }
27
+
28
+ it 'raises a CredHubble::Http::ForbiddenError' do
29
+ expect { subject }
30
+ .to raise_error(CredHubble::Http::ForbiddenError, "status: #{status}, body: #{response_body}")
31
+ end
32
+ end
33
+
34
+ context 'when the response status is 404' do
35
+ let(:response_body) { 'Not found' }
36
+ let(:status) { 404 }
37
+
38
+ it 'raises a CredHubble::Http::NotFoundError' do
39
+ expect { subject }
40
+ .to raise_error(CredHubble::Http::NotFoundError, "status: #{status}, body: #{response_body}")
41
+ end
42
+ end
43
+
44
+ context 'when the response status is 500' do
45
+ let(:response_body) { 'Internal Server Error' }
46
+ let(:status) { 500 }
47
+
48
+ it 'raises a CredHubble::Http::InternalServerError' do
49
+ expect { subject }
50
+ .to raise_error(CredHubble::Http::InternalServerError, "status: #{status}, body: #{response_body}")
51
+ end
52
+ end
53
+
54
+ context 'when the response status is not otherwise handled' do
55
+ let(:response_body) { "I'm a teapot" }
56
+ let(:status) { 418 }
57
+
58
+ it 'raises a CredHubble::Http::InternalServerError' do
59
+ expect { subject }
60
+ .to raise_error(CredHubble::Http::UnknownError, "status: #{status}, body: #{response_body}")
61
+ end
62
+ end
63
+ end
64
+
3
65
  RSpec.describe CredHubble::Http::Client do
4
66
  let(:url) { 'https://example.com:8845' }
5
67
  subject { CredHubble::Http::Client.new(url) }
@@ -30,98 +92,330 @@ RSpec.describe CredHubble::Http::Client do
30
92
  expect(response.status).to eq(status)
31
93
  end
32
94
 
33
- context 'when a Faraday::SSLError occurrs' do
34
- let(:error) { Faraday::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed') }
35
- let(:fake_connection) { instance_double(Faraday::Connection) }
95
+ describe 'error handling' do
96
+ subject { CredHubble::Http::Client.new(url).get(path) }
97
+
98
+ context 'when a Faraday::SSLError occurs' do
99
+ let(:error) { Faraday::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed') }
100
+ let(:fake_connection) { instance_double(Faraday::Connection) }
101
+
102
+ before do
103
+ allow_any_instance_of(CredHubble::Http::Client).to receive(:connection).and_return(fake_connection)
104
+ allow(fake_connection).to receive(:get).and_raise(error)
105
+ end
36
106
 
37
- before do
38
- allow(subject).to receive(:connection).and_return(fake_connection)
39
- allow(fake_connection).to receive(:get).and_raise(error)
107
+ it 'raises a CredHubble::Exceptions::SSLError' do
108
+ expect { subject }.to raise_error(CredHubble::Http::SSLError)
109
+ end
40
110
  end
41
111
 
42
- it 'raises a CredHubble::Exceptions::SSLError' do
43
- expect { subject.get(path) }.to raise_error(CredHubble::Http::SSLError)
112
+ it_behaves_like 'a request with error handling'
113
+ end
114
+
115
+ describe 'request headers' do
116
+ context 'when client is initialized with an auth_token_header' do
117
+ let(:token) { 'meesa-jar-jar-binks-token' }
118
+ subject { CredHubble::Http::Client.new(url, auth_header_token: token) }
119
+
120
+ it 'includes an Authorization header with the provided bearer token' do
121
+ subject.get(path)
122
+ assert_requested(
123
+ :get,
124
+ "#{url}#{path}",
125
+ headers: { 'Content-Type' => 'application/json', 'Authorization' => "bearer #{token}" }
126
+ )
127
+ end
44
128
  end
129
+
130
+ context 'when client is not initialized with an auth_token_header' do
131
+ subject { CredHubble::Http::Client.new(url) }
132
+
133
+ it 'does not include an authorization header' do
134
+ subject.get(path)
135
+ assert_requested(:get, "#{url}#{path}", headers: { 'Content-Type' => 'application/json' })
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ describe '#put' do
142
+ let(:path) { '/api/v1/data' }
143
+ let(:status) { 200 }
144
+ let(:request_body) do
145
+ '{
146
+ "name": "/la-la-land",
147
+ "type": "value",
148
+ "value": "another day of sun: purple shirt parkour"
149
+ }'
150
+ end
151
+ let(:response_body) do
152
+ '{
153
+ "id": "62630719-2413-4332-9e39-a8acbd73d3b7",
154
+ "name": "/la-la-land",
155
+ "type": "value",
156
+ "value": "another day of sun: purple shirt parkour",
157
+ "version_created_at": "2017-01-01T04:07:18Z"
158
+ }'
45
159
  end
46
160
 
47
- context 'when the response status is 400' do
48
- let(:response_body) { 'Bad request' }
49
- let(:status) { 400 }
161
+ before do
162
+ stub_request(:put, "#{url}#{path}").with(body: request_body).to_return(status: status, body: response_body)
163
+ end
50
164
 
51
- it 'raises a CredHubble::Http::BadRequestError' do
52
- expect { subject.get(path) }
53
- .to raise_error(CredHubble::Http::BadRequestError, "status: #{status}, body: #{response_body}")
165
+ it 'makes a PUT request with the given body to the requested url and path' do
166
+ response = subject.put(path, request_body)
167
+ expect(response).to be_a(Faraday::Response)
168
+ expect(response.body).to eq(response_body)
169
+ expect(response.status).to eq(status)
170
+ end
171
+
172
+ describe 'error handling' do
173
+ subject { CredHubble::Http::Client.new(url).put(path, request_body) }
174
+
175
+ context 'when a Faraday::SSLError occurs' do
176
+ let(:error) { Faraday::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed') }
177
+ let(:fake_connection) { instance_double(Faraday::Connection) }
178
+
179
+ before do
180
+ allow_any_instance_of(CredHubble::Http::Client).to receive(:connection).and_return(fake_connection)
181
+ allow(fake_connection).to receive(:put).and_raise(error)
182
+ end
183
+
184
+ it 'raises a CredHubble::Exceptions::SSLError' do
185
+ expect { subject }.to raise_error(CredHubble::Http::SSLError)
186
+ end
54
187
  end
188
+
189
+ it_behaves_like 'a request with error handling'
55
190
  end
56
191
 
57
- context 'when the response status is 401' do
58
- let(:response_body) { 'Unauthorized' }
59
- let(:status) { 401 }
192
+ describe 'request headers' do
193
+ context 'when client is initialized with an auth_token_header' do
194
+ let(:token) { 'meesa-jar-jar-binks-token' }
195
+ subject { CredHubble::Http::Client.new(url, auth_header_token: token) }
60
196
 
61
- it 'raises a CredHubble::Http::UnauthorizedError' do
62
- expect { subject.get(path) }
63
- .to raise_error(CredHubble::Http::UnauthorizedError, "status: #{status}, body: #{response_body}")
197
+ it 'includes an Authorization header with the provided bearer token' do
198
+ subject.put(path, request_body)
199
+ assert_requested(
200
+ :put,
201
+ "#{url}#{path}",
202
+ body: request_body,
203
+ headers: { 'Content-Type' => 'application/json', 'Authorization' => "bearer #{token}" }
204
+ )
205
+ end
64
206
  end
207
+
208
+ context 'when client is not initialized with an auth_token_header' do
209
+ subject { CredHubble::Http::Client.new(url) }
210
+
211
+ it 'does not include an authorization header' do
212
+ subject.put(path, request_body)
213
+ assert_requested(
214
+ :put,
215
+ "#{url}#{path}",
216
+ body: request_body,
217
+ headers: { 'Content-Type' => 'application/json' }
218
+ )
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ describe '#post' do
225
+ let(:path) { '/api/v1/data' }
226
+ let(:status) { 200 }
227
+ let(:request_body) do
228
+ '{
229
+ "name": "/ron-dunn",
230
+ "type": "value",
231
+ "value": "is that your name or are you telling me you\'re finished talking?"
232
+ }'
233
+ end
234
+ let(:response_body) do
235
+ '{
236
+ "id": "62630719-2413-4332-9e39-a8acbd73d3b7",
237
+ "name": "/ron-dunn",
238
+ "type": "value",
239
+ "value": "is that your name or are you telling me you\'re finished talking?",
240
+ "version_created_at": "2017-01-01T04:07:18Z"
241
+ }'
242
+ end
243
+
244
+ before do
245
+ stub_request(:post, "#{url}#{path}").with(body: request_body).to_return(status: status, body: response_body)
65
246
  end
66
247
 
67
- context 'when the response status is 403' do
68
- let(:response_body) { 'Forbidden' }
69
- let(:status) { 403 }
248
+ it 'makes a POST request with the given body to the requested url and path' do
249
+ response = subject.post(path, request_body)
250
+ expect(response).to be_a(Faraday::Response)
251
+ expect(response.body).to eq(response_body)
252
+ expect(response.status).to eq(status)
253
+ end
254
+
255
+ describe 'error handling' do
256
+ subject { CredHubble::Http::Client.new(url).post(path, request_body) }
257
+
258
+ context 'when a Faraday::SSLError occurs' do
259
+ let(:error) { Faraday::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed') }
260
+ let(:fake_connection) { instance_double(Faraday::Connection) }
261
+
262
+ before do
263
+ allow_any_instance_of(CredHubble::Http::Client).to receive(:connection).and_return(fake_connection)
264
+ allow(fake_connection).to receive(:post).and_raise(error)
265
+ end
70
266
 
71
- it 'raises a CredHubble::Http::ForbiddenError' do
72
- expect { subject.get(path) }
73
- .to raise_error(CredHubble::Http::ForbiddenError, "status: #{status}, body: #{response_body}")
267
+ it 'raises a CredHubble::Exceptions::SSLError' do
268
+ expect { subject }.to raise_error(CredHubble::Http::SSLError)
269
+ end
74
270
  end
271
+
272
+ it_behaves_like 'a request with error handling'
75
273
  end
76
274
 
77
- context 'when the response status is 404' do
78
- let(:response_body) { 'Not found' }
79
- let(:status) { 404 }
275
+ describe 'request headers' do
276
+ context 'when client is initialized with an auth_token_header' do
277
+ let(:token) { 'meesa-jar-jar-binks-token' }
278
+ subject { CredHubble::Http::Client.new(url, auth_header_token: token) }
279
+
280
+ it 'includes an Authorization header with the provided bearer token' do
281
+ subject.post(path, request_body)
282
+ assert_requested(
283
+ :post,
284
+ "#{url}#{path}",
285
+ body: request_body,
286
+ headers: { 'Content-Type' => 'application/json', 'Authorization' => "bearer #{token}" }
287
+ )
288
+ end
289
+ end
290
+
291
+ context 'when client is not initialized with an auth_token_header' do
292
+ subject { CredHubble::Http::Client.new(url) }
80
293
 
81
- it 'raises a CredHubble::Http::NotFoundError' do
82
- expect { subject.get(path) }
83
- .to raise_error(CredHubble::Http::NotFoundError, "status: #{status}, body: #{response_body}")
294
+ it 'does not include an authorization header' do
295
+ subject.post(path, request_body)
296
+ assert_requested(
297
+ :post,
298
+ "#{url}#{path}",
299
+ body: request_body,
300
+ headers: { 'Content-Type' => 'application/json' }
301
+ )
302
+ end
84
303
  end
85
304
  end
305
+ end
306
+
307
+ describe '#delete' do
308
+ let(:path) { '/data' }
309
+ let(:status) { 204 }
310
+ let(:response_body) { '' }
311
+
312
+ before do
313
+ stub_request(:delete, "#{url}#{path}").to_return(status: status, body: response_body)
314
+ end
86
315
 
87
- context 'when the response status is 500' do
88
- let(:response_body) { 'Internal Server Error' }
89
- let(:status) { 500 }
316
+ it 'makes a GET request to the requested url and path' do
317
+ response = subject.delete(path)
318
+ expect(response).to be_a(Faraday::Response)
319
+ expect(response.body).to eq(response_body)
320
+ expect(response.status).to eq(status)
321
+ end
322
+
323
+ describe 'error handling' do
324
+ subject { CredHubble::Http::Client.new(url).delete(path) }
325
+
326
+ context 'when a Faraday::SSLError occurs' do
327
+ let(:error) { Faraday::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed') }
328
+ let(:fake_connection) { instance_double(Faraday::Connection) }
90
329
 
91
- it 'raises a CredHubble::Http::InternalServerError' do
92
- expect { subject.get(path) }
93
- .to raise_error(CredHubble::Http::InternalServerError, "status: #{status}, body: #{response_body}")
330
+ before do
331
+ allow_any_instance_of(CredHubble::Http::Client).to receive(:connection).and_return(fake_connection)
332
+ allow(fake_connection).to receive(:delete).and_raise(error)
333
+ end
334
+
335
+ it 'raises a CredHubble::Exceptions::SSLError' do
336
+ expect { subject }.to raise_error(CredHubble::Http::SSLError)
337
+ end
94
338
  end
339
+
340
+ it_behaves_like 'a request with error handling'
95
341
  end
96
342
 
97
- context 'when the response status is not otherwise handled' do
98
- let(:response_body) { "I'm a teapot" }
99
- let(:status) { 418 }
343
+ describe 'request headers' do
344
+ context 'when client is initialized with an auth_token_header' do
345
+ let(:token) { 'some-oauth2-bearer-token' }
346
+ subject { CredHubble::Http::Client.new(url, auth_header_token: token) }
347
+
348
+ it 'includes an Authorization header with the provided bearer token' do
349
+ subject.delete(path)
350
+ assert_requested(
351
+ :delete,
352
+ "#{url}#{path}",
353
+ headers: { 'Content-Type' => 'application/json', 'Authorization' => "bearer #{token}" }
354
+ )
355
+ end
356
+ end
357
+
358
+ context 'when client is not initialized with an auth_token_header' do
359
+ subject { CredHubble::Http::Client.new(url) }
100
360
 
101
- it 'raises a CredHubble::Http::InternalServerError' do
102
- expect { subject.get(path) }
103
- .to raise_error(CredHubble::Http::UnknownError, "status: #{status}, body: #{response_body}")
361
+ it 'does not include an authorization header' do
362
+ subject.delete(path)
363
+ assert_requested(:delete, "#{url}#{path}", headers: { 'Content-Type' => 'application/json' })
364
+ end
104
365
  end
105
366
  end
106
367
  end
107
368
 
108
- describe 'SSL verification' do
109
- context 'when verify_ssl is not specified' do
369
+ describe 'SSL/TLS configuration' do
370
+ subject { CredHubble::Http::Client.new(url) }
371
+
372
+ it 'has ssl verification enabled' do
373
+ connection = subject.send(:connection)
374
+ expect(connection.ssl.verify).to eq(true)
375
+ end
376
+
377
+ context 'when a file path is not provided for the CredHub CA' do
110
378
  subject { CredHubble::Http::Client.new(url) }
111
379
 
112
- it 'has ssl verification enabled by default' do
380
+ it 'does not include any additional CA certs' do
113
381
  connection = subject.send(:connection)
114
- expect(connection.ssl.verify).to eq(true)
382
+ expect(connection.ssl.ca_file).to be_nil
115
383
  end
116
384
  end
117
385
 
118
- # TODO: Remove ability to disable ssl verification
119
- context 'when verify_ssl is set to false' do
120
- subject { CredHubble::Http::Client.new(url, verify_ssl: false) }
386
+ context 'when a file path is provided for the CredHub CA' do
387
+ let(:ca_path) { '/custom/certstore/credhub_ca.crt' }
388
+ subject { CredHubble::Http::Client.new(url, ca_path: ca_path) }
121
389
 
122
- it 'has ssl verification disabled' do
390
+ it 'includes the CA cert file path in the connection ssl config' do
123
391
  connection = subject.send(:connection)
124
- expect(connection.ssl.verify).to eq(false)
392
+ expect(connection.ssl.ca_file).to eq(ca_path)
393
+ end
394
+ end
395
+
396
+ describe 'mutual TLS client cert and client key' do
397
+ let(:mock_cert) { instance_double(OpenSSL::X509::Certificate) }
398
+ let(:mock_key) { instance_double(OpenSSL::PKey::RSA) }
399
+
400
+ context 'when a client cert and client key are not provided' do
401
+ subject { CredHubble::Http::Client.new(url) }
402
+
403
+ it 'does not include any client cert or client key' do
404
+ connection = subject.send(:connection)
405
+ expect(connection.ssl.client_cert).to be_nil
406
+ expect(connection.ssl.client_key).to be_nil
407
+ end
408
+ end
409
+
410
+ context 'when a client cert and client key are provided for the CredHub CA' do
411
+ let(:ca_path) { '/custom/certstore/credhub_ca.crt' }
412
+ subject { CredHubble::Http::Client.new(url, client_cert: mock_cert, client_key: mock_key) }
413
+
414
+ it 'includes the cert file in the connection ssl config' do
415
+ connection = subject.send(:connection)
416
+ expect(connection.ssl.client_cert).to eq(mock_cert)
417
+ expect(connection.ssl.client_key).to eq(mock_key)
418
+ end
125
419
  end
126
420
  end
127
421
  end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CredHubble::Resources::CertificateCredential do
4
+ let(:json_response) do
5
+ '{
6
+ "id": "15811465-8538-460d-9682-5514d44439fd",
7
+ "name": "/load-balancer-tls-cert",
8
+ "type": "certificate",
9
+ "value": {
10
+ "ca": "-----BEGIN CERTIFICATE-----\n... CA CERT ...\n-----END CERTIFICATE-----",
11
+ "certificate": "-----BEGIN CERTIFICATE-----\n... CERTIFICATE ...\n-----END CERTIFICATE-----",
12
+ "private_key": "-----BEGIN RSA PRIVATE KEY-----\n... RSA PRIVATE KEY ...\n-----END RSA PRIVATE KEY-----"
13
+ },
14
+ "version_created_at": "1990-05-18T01:01:01Z"
15
+ }'
16
+ end
17
+
18
+ describe '.from_json' do
19
+ subject { CredHubble::Resources::CertificateCredential }
20
+
21
+ context 'when the JSON includes the required attributes' do
22
+ it 'instantiates a new CertificateCredential object' do
23
+ credential = subject.from_json(json_response)
24
+
25
+ expect(credential).to be_a(CredHubble::Resources::CertificateCredential)
26
+ expect(credential.value.ca)
27
+ .to eq("-----BEGIN CERTIFICATE-----\n... CA CERT ...\n-----END CERTIFICATE-----")
28
+ expect(credential.value.certificate)
29
+ .to eq("-----BEGIN CERTIFICATE-----\n... CERTIFICATE ...\n-----END CERTIFICATE-----")
30
+ expect(credential.value.private_key)
31
+ .to eq("-----BEGIN RSA PRIVATE KEY-----\n... RSA PRIVATE KEY ...\n-----END RSA PRIVATE KEY-----")
32
+ end
33
+ end
34
+
35
+ it_behaves_like 'a Credential resource'
36
+ it_behaves_like 'a JSON deserializing resource'
37
+ end
38
+
39
+ describe '#type' do
40
+ it 'returns "certificate"' do
41
+ subject.type = 'attempting-to-overwrite'
42
+ expect(subject.type).to eq('certificate')
43
+ end
44
+ end
45
+
46
+ describe '#to_json' do
47
+ it_behaves_like 'a JSON serializing resource'
48
+ end
49
+ end