Skip to content
This repository was archived by the owner on Jun 2, 2021. It is now read-only.

Commit 6588654

Browse files
author
Derik Evangelista
authored
v3(bindings): resolve credhub refs for keys (cloudfoundry#1801)
[#173529481](https://www.pivotaltracker.com/story/show/173529481)
1 parent 9c6b8f8 commit 6588654

4 files changed

Lines changed: 167 additions & 27 deletions

File tree

app/controllers/v3/service_credential_bindings_controller.rb

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,18 @@ def details
3838
ensure_service_credential_binding_is_accessible!
3939
not_found! unless can_read_secrets_in_the_binding_space?
4040

41-
render status: :ok, json: credential_binding_details
41+
credentials = if service_credential_binding[:type] == 'key' && service_credential_binding.credhub_reference?
42+
fetch_credentials_value(service_credential_binding.credhub_reference)
43+
else
44+
service_credential_binding.credentials
45+
end
46+
47+
details = Presenters::V3::ServiceCredentialBindingDetailsPresenter.new(
48+
binding: service_credential_binding,
49+
credentials: credentials
50+
).to_hash
51+
52+
render status: :ok, json: details
4253
end
4354

4455
private
@@ -52,6 +63,29 @@ def decorators(message)
5263
AVAILABLE_DECORATORS.select { |d| d.match?(message.include) }.reduce([]) { |decorators, d| decorators << d }
5364
end
5465

66+
def config
67+
@config ||= VCAP::CloudController::Config.config
68+
end
69+
70+
def uaa_client
71+
@uaa_client ||= UaaClient.new(
72+
uaa_target: config.get(:uaa, :internal_url),
73+
client_id: config.get(:cc_service_key_client_name),
74+
secret: config.get(:cc_service_key_client_secret),
75+
ca_file: config.get(:uaa, :ca_file),
76+
)
77+
end
78+
79+
def credhub_client
80+
@credhub_client ||= Credhub::Client.new(config.get(:credhub_api, :internal_url), uaa_client)
81+
end
82+
83+
def fetch_credentials_value(name)
84+
credhub_client.get_credential_by_name(name)
85+
rescue => e
86+
unprocessable!(e.message)
87+
end
88+
5589
def service_credential_binding
5690
@service_credential_binding ||= fetcher.fetch(hashed_params[:guid], space_guids: space_guids)
5791
end
@@ -73,10 +107,6 @@ def serialized(message)
73107
Presenters::V3::ServiceCredentialBindingPresenter.new(service_credential_binding, decorators: decorators(message)).to_hash
74108
end
75109

76-
def credential_binding_details
77-
Presenters::V3::ServiceCredentialBindingDetailsPresenter.new(service_credential_binding).to_hash
78-
end
79-
80110
def ensure_service_credential_binding_is_accessible!
81111
not_found! unless service_credential_binding_exists?
82112
end

app/presenters/v3/service_credential_binding_details_presenter.rb

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,18 @@ module CloudController
55
module Presenters
66
module V3
77
class ServiceCredentialBindingDetailsPresenter < BasePresenter
8+
def initialize(binding:, credentials:)
9+
super(binding)
10+
@credentials = credentials
11+
end
12+
813
def to_hash
914
{
10-
credentials: credentials,
11-
syslog_drain_url: @resource.syslog_drain_url,
12-
volume_mounts: @resource.volume_mounts || nil
15+
credentials: @credentials,
16+
syslog_drain_url: @resource.try(:syslog_drain_url) || nil,
17+
volume_mounts: @resource.try(:volume_mounts) || nil
1318
}.compact
1419
end
15-
16-
private
17-
18-
def credentials
19-
JSON.parse(@resource.credentials)
20-
rescue JSON::ParserError
21-
nil
22-
end
2320
end
2421
end
2522
end

spec/request/service_credential_bindings_spec.rb

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,12 +456,13 @@ def check_filtered_bindings(*bindings)
456456
service_instance: instance,
457457
volume_mounts: ['foo', 'bar'],
458458
syslog_drain_url: 'some-drain-url',
459-
credentials: '{"cred_key": "creds-val-64", "magic": true}'
459+
credentials: { 'cred_key' => 'creds-val-64', 'magic' => true }
460460
}
461461
}
462462
let(:app_binding) { VCAP::CloudController::ServiceBinding.make(**details) }
463+
let(:guid) { app_binding.guid }
463464
let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }
464-
let(:api_call) { ->(user_headers) { get "/v3/service_credential_bindings/#{app_binding.guid}/details", nil, user_headers } }
465+
let(:api_call) { ->(user_headers) { get "/v3/service_credential_bindings/#{guid}/details", nil, user_headers } }
465466
let(:binding_credentials) {
466467
{
467468
credentials: {
@@ -541,6 +542,94 @@ def check_filtered_bindings(*bindings)
541542
end
542543
end
543544
end
545+
546+
describe 'credhub interaction' do
547+
let(:key_binding) { VCAP::CloudController::ServiceKey.make(:credhub_reference, service_instance: instance) }
548+
let(:credhub_url) { VCAP::CloudController::Config.config.get(:credhub_api, :internal_url) }
549+
let(:uaa_url) { VCAP::CloudController::Config.config.get(:uaa, :internal_url) }
550+
let(:credentials) { { 'username' => 'cinnamon', 'password' => 'roll' } }
551+
let(:credhub_response_status) { 200 }
552+
let(:credhub_response_body) { { data: [value: credentials] }.to_json }
553+
let!(:credhub_server_stub) {
554+
stub_request(:get, "#{credhub_url}/api/v1/data?name=#{key_binding.credhub_reference}&current=true").
555+
with(headers: {
556+
'Authorization' => 'Bearer my-favourite-access-token',
557+
'Content-Type' => 'application/json'
558+
}).to_return(status: credhub_response_status, body: credhub_response_body)
559+
}
560+
561+
before do
562+
token = { token_type: 'Bearer', access_token: 'my-favourite-access-token' }
563+
stub_request(:post, "#{uaa_url}/oauth/token").
564+
to_return(status: 200, body: token.to_json, headers: { 'Content-Type' => 'application/json' })
565+
end
566+
567+
context 'for key bindings with a credhub reference' do
568+
let(:guid) { key_binding.guid }
569+
570+
it 'fetches and returns the credentials values from credhub' do
571+
api_call.call(admin_headers)
572+
expect(last_response).to have_status_code(200)
573+
expect(credhub_server_stub).to have_been_requested
574+
expect(parsed_response['credentials']).to eq(credentials)
575+
end
576+
end
577+
578+
context 'for key bindings without a credhub reference' do
579+
let(:key_binding) { VCAP::CloudController::ServiceKey.make(service_instance: instance) }
580+
let(:guid) { key_binding.guid }
581+
582+
it 'returns the credentials as found in the datbase' do
583+
api_call.call(admin_headers)
584+
expect(last_response).to have_status_code(200)
585+
expect(credhub_server_stub).not_to have_been_requested
586+
expect(parsed_response['credentials']).to eq(key_binding.credentials)
587+
end
588+
end
589+
590+
context 'for app bindings with credhub references' do
591+
let(:details) { { service_instance: instance, credentials: { 'credhub-ref' => '/secret/super/morish' } } }
592+
593+
it 'returns the credentials as found in the database' do
594+
api_call.call(admin_headers)
595+
expect(last_response).to have_status_code(200)
596+
expect(credhub_server_stub).not_to have_been_requested
597+
expect(parsed_response['credentials']).to eq({ 'credhub-ref' => '/secret/super/morish' })
598+
end
599+
end
600+
601+
context 'when the reference cannot be found on credhub' do
602+
let(:guid) { key_binding.guid }
603+
let(:credhub_response_status) { 404 }
604+
let(:credhub_response_body) { { error: 'cred does not exist' }.to_json }
605+
606+
it 'returns an error' do
607+
api_call.call(admin_headers)
608+
expect(last_response).to have_status_code(422)
609+
expect(parsed_response['errors']).to include(include({
610+
'detail' => 'cred does not exist',
611+
'title' => 'CF-UnprocessableEntity',
612+
'code' => 10008,
613+
}))
614+
end
615+
end
616+
617+
context 'when credhub returns an error' do
618+
let(:guid) { key_binding.guid }
619+
let(:credhub_response_status) { 500 }
620+
let(:credhub_response_body) { nil }
621+
622+
it 'returns an error' do
623+
api_call.call(admin_headers)
624+
expect(last_response).to have_status_code(422)
625+
expect(parsed_response['errors']).to include(include({
626+
'detail' => 'Server error, status: 500',
627+
'title' => 'CF-UnprocessableEntity',
628+
'code' => 10008,
629+
}))
630+
end
631+
end
632+
end
544633
end
545634

546635
def expected_json(binding)

spec/unit/presenters/v3/service_credential_binding_details_presenter_spec.rb

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,28 @@ module CloudController
66
RSpec.describe Presenters::V3::ServiceCredentialBindingDetailsPresenter do
77
let(:instance) { ServiceInstance.make(guid: 'instance-guid') }
88
let(:app) { AppModel.make(guid: 'app-guid', space: instance.space) }
9-
let(:json_creds) { { password: 'super secret avocado toast' }.to_json }
9+
let(:credentials) { { password: 'super secret avocado toast' } }
1010
let(:credential_binding) do
1111
ServiceBinding.make(
1212
name: 'some-name',
1313
app: app,
1414
service_instance: instance,
15-
credentials: json_creds,
15+
credentials: credentials,
1616
volume_mounts: %w{super good},
1717
syslog_drain_url: 'http://banana.example.com/drain'
1818
)
1919
end
2020

21-
it 'returns the binding details' do
22-
presenter = described_class.new(credential_binding)
21+
let(:key_binding) {
22+
ServiceKey.make(
23+
name: 'some-key',
24+
service_instance: instance,
25+
credentials: credentials
26+
)
27+
}
28+
29+
it 'returns the app binding details' do
30+
presenter = described_class.new(binding: credential_binding, credentials: credential_binding.credentials)
2331
expect(presenter.to_hash.deep_symbolize_keys).to match(
2432
{
2533
credentials: {
@@ -31,11 +39,22 @@ module CloudController
3139
)
3240
end
3341

42+
it 'returns the key binding detials' do
43+
presenter = described_class.new(binding: key_binding, credentials: key_binding.credentials)
44+
expect(presenter.to_hash.deep_symbolize_keys).to match(
45+
{
46+
credentials: {
47+
password: 'super secret avocado toast'
48+
}
49+
}
50+
)
51+
end
52+
3453
context 'when syslog drain is not set' do
3554
let(:credential_binding) { ServiceBinding.make }
3655

3756
it 'does not include syslog_drain_url in the response' do
38-
presenter = described_class.new(credential_binding)
57+
presenter = described_class.new(binding: credential_binding, credentials: credential_binding.credentials)
3958
expect(presenter.to_hash).to_not have_key(:syslog_drain_url)
4059
end
4160
end
@@ -44,19 +63,24 @@ module CloudController
4463
let(:credential_binding) { ServiceBinding.make }
4564

4665
it 'does not include volume_mounts in the response' do
47-
presenter = described_class.new(credential_binding)
66+
presenter = described_class.new(binding: credential_binding, credentials: credential_binding.credentials)
4867
expect(presenter.to_hash).to_not have_key(:volume_mounts)
4968
end
5069
end
5170

5271
context 'when credentials are not set' do
53-
let(:credential_binding) { ServiceBinding.make }
54-
5572
it 'does not include credentials in the response' do
56-
presenter = described_class.new(credential_binding)
73+
presenter = described_class.new(binding: credential_binding, credentials: nil)
5774
expect(presenter.to_hash).to_not have_key(:credentials)
5875
end
5976
end
77+
78+
context 'when some other credentials are passed in' do
79+
it 'includes those in the response' do
80+
presenter = described_class.new(binding: credential_binding, credentials: { hello: 'my friend' })
81+
expect(presenter.to_hash[:credentials]).to eq({ hello: 'my friend' })
82+
end
83+
end
6084
end
6185
end
6286
end

0 commit comments

Comments
 (0)