Set binding credential permissions in CredHub. Delete credentials on unbind.

Fixes #190
This commit is contained in:
Scott Frederick
2019-01-14 14:45:56 -06:00
parent 3cfb342960
commit ec01ee143b
18 changed files with 697 additions and 60 deletions

View File

@@ -23,7 +23,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.appbroker.extensions.credentials.CredHubCredentialsGenerator;
import org.springframework.cloud.appbroker.service.CreateServiceInstanceAppBindingWorkflow;
import org.springframework.cloud.appbroker.service.DeleteServiceInstanceBindingWorkflow;
import org.springframework.cloud.appbroker.workflow.binding.CredHubPersistingCreateServiceInstanceAppBindingWorkflow;
import org.springframework.cloud.appbroker.workflow.binding.CredHubPersistingDeleteServiceInstanceBindingWorkflow;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.credhub.autoconfig.CredHubTemplateAutoConfiguration;
@@ -44,6 +46,11 @@ public class CredHubAutoConfiguration {
return new CredHubPersistingCreateServiceInstanceAppBindingWorkflow(credHubOperations, appName);
}
@Bean
public DeleteServiceInstanceBindingWorkflow credhubPersistingDeleteServiceInstanceAppBindingWorkflow(CredHubOperations credHubOperations) {
return new CredHubPersistingDeleteServiceInstanceBindingWorkflow(credHubOperations, appName);
}
@Bean
public CredHubCredentialsGenerator credHubCredentialsGenerator(CredHubOperations credHubOperations) {
return new CredHubCredentialsGenerator(credHubOperations);

View File

@@ -25,6 +25,7 @@ import org.springframework.cloud.appbroker.extensions.credentials.CredHubCredent
import org.springframework.cloud.appbroker.extensions.credentials.CredentialGenerator;
import org.springframework.cloud.appbroker.extensions.credentials.SimpleCredentialGenerator;
import org.springframework.cloud.appbroker.workflow.binding.CredHubPersistingCreateServiceInstanceAppBindingWorkflow;
import org.springframework.cloud.appbroker.workflow.binding.CredHubPersistingDeleteServiceInstanceBindingWorkflow;
import org.springframework.context.annotation.Bean;
import org.springframework.credhub.core.CredHubOperations;
@@ -57,7 +58,8 @@ class CredHubAutoConfigurationTest {
.isExactlyInstanceOf(SimpleCredentialGenerator.class);
assertThat(context)
.doesNotHaveBean(CredHubPersistingCreateServiceInstanceAppBindingWorkflow.class);
.doesNotHaveBean(CredHubPersistingCreateServiceInstanceAppBindingWorkflow.class)
.doesNotHaveBean(CredHubPersistingDeleteServiceInstanceBindingWorkflow.class);
});
}
@@ -71,7 +73,8 @@ class CredHubAutoConfigurationTest {
.isExactlyInstanceOf(CredHubCredentialsGenerator.class);
assertThat(context)
.hasSingleBean(CredHubPersistingCreateServiceInstanceAppBindingWorkflow.class);
.hasSingleBean(CredHubPersistingCreateServiceInstanceAppBindingWorkflow.class)
.hasSingleBean(CredHubPersistingDeleteServiceInstanceBindingWorkflow.class);
});
}

View File

@@ -111,13 +111,11 @@ public class WorkflowServiceInstanceBindingService implements ServiceInstanceBin
private boolean isAppBindingRequest(CreateServiceInstanceBindingRequest request) {
return request.getBindResource() != null
&& StringUtils.hasText(request.getBindResource().getAppGuid())
&& StringUtils.isEmpty(request.getBindResource().getRoute());
}
private boolean isRouteBindingRequest(CreateServiceInstanceBindingRequest request) {
return request.getBindResource() != null
&& StringUtils.isEmpty(request.getBindResource().getAppGuid())
&& StringUtils.hasText(request.getBindResource().getRoute());
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.appbroker.integration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.appbroker.integration.fixtures.CredHubStubFixture;
import org.springframework.cloud.appbroker.integration.fixtures.OpenServiceBrokerApiFixture;
import org.springframework.cloud.appbroker.integration.fixtures.TestBindingCredentialsProviderFixture;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource;
import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath;
import static io.restassured.RestAssured.given;
import static org.springframework.cloud.appbroker.integration.CreateInstanceWithBasicAuthCredentialsComponentTest.APP_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=example",
"spring.cloud.appbroker.services[0].plan-name=standard",
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME,
"spring.credhub.url=http://localhost:8888"
})
class CreateBindingWithCredHubComponentTest extends WiremockComponentTest {
private static final String SERVICE_INSTANCE_ID = "instance-id";
private static final String BINDING_ID = "binding-id";
private static final String CREDENTIAL_NAME = "credentials-json";
private static final String APP_ID = "app-id";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@Autowired
private CredHubStubFixture credHubFixture;
@Autowired
private TestBindingCredentialsProviderFixture bindingFixture;
@Value("${spring.application.name}")
private String brokerAppName;
@Value("${spring.cloud.openservicebroker.catalog.services[0].id}")
String serviceDefinitionId;
@Test
void createAppBindingWithCredHub() {
String credentialName = credHubFixture.bindingCredentialName(brokerAppName, serviceDefinitionId,
BINDING_ID, CREDENTIAL_NAME);
credHubFixture.stubWriteCredential(credentialName,
matchingJsonPath("$.[?(@.value.credential1 == '" +
bindingFixture.getCredentials().get("credential1") +
"')]"),
matchingJsonPath("$.[?(@.value.credential2 == '" +
bindingFixture.getCredentials().get("credential2") +
"')]"));
credHubFixture.stubAddAppPermission(credentialName, "mtls-app:" + APP_ID);
// when a service binding is created
given(brokerFixture.serviceAppBindingRequest())
.when()
.put(brokerFixture.createBindingUrl(), SERVICE_INSTANCE_ID, BINDING_ID)
.then()
.statusCode(HttpStatus.CREATED.value());
}
@Test
void createServiceKeyWithCredHub() {
String credentialName = credHubFixture.bindingCredentialName(brokerAppName, serviceDefinitionId,
BINDING_ID, CREDENTIAL_NAME);
credHubFixture.stubWriteCredential(credentialName,
matchingJsonPath("$.[?(@.value.credential1 == '" +
bindingFixture.getCredentials().get("credential1") +
"')]"),
matchingJsonPath("$.[?(@.value.credential2 == '" +
bindingFixture.getCredentials().get("credential2") +
"')]"));
credHubFixture.stubAddAppPermission(credentialName, "uaa-client:service-key-client-id");
// when a service binding is created
given(brokerFixture.serviceKeyRequest())
.when()
.put(brokerFixture.createBindingUrl(), SERVICE_INSTANCE_ID, BINDING_ID)
.then()
.statusCode(HttpStatus.CREATED.value());
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.appbroker.integration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.appbroker.integration.fixtures.CredHubStubFixture;
import org.springframework.cloud.appbroker.integration.fixtures.OpenServiceBrokerApiFixture;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource;
import static io.restassured.RestAssured.given;
import static org.springframework.cloud.appbroker.integration.CreateInstanceWithBasicAuthCredentialsComponentTest.APP_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=example",
"spring.cloud.appbroker.services[0].plan-name=standard",
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME,
"spring.credhub.url=http://localhost:8888"
})
class DeleteBindingWithCredHubComponentTest extends WiremockComponentTest {
private static final String SERVICE_INSTANCE_ID = "instance-id";
private static final String BINDING_ID = "binding-id";
private static final String CREDENTIAL_NAME = "credentials-json";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@Autowired
private CredHubStubFixture credHubFixture;
@Value("${spring.application.name}")
private String brokerAppName;
@Value("${spring.cloud.openservicebroker.catalog.services[0].id}")
String serviceDefinitionId;
@Test
void deleteAppBindingWithCredHub() {
String credentialName = credHubFixture.bindingCredentialName(brokerAppName, serviceDefinitionId,
BINDING_ID, CREDENTIAL_NAME);
credHubFixture.stubFindCredential(credentialName);
credHubFixture.stubDeleteCredential(credentialName);
// when a service binding is deleted
given(brokerFixture.serviceAppBindingRequest())
.when()
.delete(brokerFixture.deleteBindingUrl(), SERVICE_INSTANCE_ID, BINDING_ID)
.then()
.statusCode(HttpStatus.OK.value());
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.appbroker.integration.fixtures.CloudControllerStubFixture;
import org.springframework.cloud.appbroker.integration.fixtures.CredHubStubFixture;
import org.springframework.cloud.appbroker.integration.fixtures.OpenServiceBrokerApiFixture;
import org.springframework.cloud.appbroker.integration.fixtures.TestBindingCredentialsProviderFixture;
import org.springframework.cloud.appbroker.integration.fixtures.UaaStubFixture;
import org.springframework.cloud.appbroker.integration.fixtures.WiremockServerFixture;
import org.springframework.test.context.ActiveProfiles;
@@ -40,7 +41,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
OpenServiceBrokerApiFixture.class,
CloudControllerStubFixture.class,
UaaStubFixture.class,
CredHubStubFixture.class},
CredHubStubFixture.class,
TestBindingCredentialsProviderFixture.class},
properties = {
"spring.cloud.appbroker.deployer.cloudfoundry.api-host=localhost",
"spring.cloud.appbroker.deployer.cloudfoundry.api-port=8080",

View File

@@ -16,14 +16,18 @@
package org.springframework.cloud.appbroker.integration.fixtures;
import com.github.tomakehurst.wiremock.client.MappingBuilder;
import com.github.tomakehurst.wiremock.matching.ContentPattern;
import org.springframework.boot.test.context.TestComponent;
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath;
import static com.github.tomakehurst.wiremock.client.WireMock.noContent;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.put;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
@TestComponent
@@ -32,6 +36,45 @@ public class CredHubStubFixture extends WiremockStubFixture {
super(8888);
}
public void stubWriteCredential(String credentialName, ContentPattern<?>... appMetadataPatterns) {
MappingBuilder mappingBuilder = put(urlPathEqualTo("/api/v1/data"))
.withRequestBody(matchingJsonPath("$.[?(@.name == '" + credentialName + "')]"))
.withRequestBody(matchingJsonPath("$.[?(@.type == 'json')]"));
for (ContentPattern<?> appMetadataPattern : appMetadataPatterns) {
mappingBuilder.withRequestBody(appMetadataPattern);
}
stubFor(mappingBuilder
.willReturn(ok()
.withHeader("Content-type", "application/json")
.withBody(credhub("put-data-json"))));
}
public void stubFindCredential(String credentialName) {
stubFor(get(urlPathEqualTo("/api/v1/data"))
.withQueryParam("name-like", equalTo(credentialName))
.willReturn(ok()
.withHeader("Content-type", "application/json")
.withBody(credhub("get-data-find"))));
}
public void stubDeleteCredential(String credentialName) {
stubFor(delete(urlPathEqualTo("/api/v1/data"))
.withQueryParam("name", equalTo(credentialName))
.willReturn(noContent()
.withHeader("Content-type", "application/json")
.withBody(credhub("put-data-json"))));
}
public void stubAddAppPermission(String path, String actor) {
stubFor(post(urlPathEqualTo("/api/v2/permissions"))
.withRequestBody(matchingJsonPath("$.[?(@.path == '" + path + "')]"))
.withRequestBody(matchingJsonPath("$.[?(@.actor == '" + actor + "')]"))
.withRequestBody(matchingJsonPath("$.[?(@.operations[0] == 'read')]"))
.willReturn(ok()
.withHeader("Content-type", "application/json")
.withBody(credhub("post-permission"))));
}
public void stubGenerateUser(String appId, String serviceInstanceId,
String descriptor, int length) {
stubGenerate(appId, serviceInstanceId, descriptor, length, "user", "post-data-user");
@@ -64,6 +107,11 @@ public class CredHubStubFixture extends WiremockStubFixture {
.willReturn(noContent()));
}
public String bindingCredentialName(String brokerAppName, String serviceDefinitionId,
String bindingId, String credentialName) {
return "/c/" + brokerAppName + "/" + serviceDefinitionId + "/" + bindingId + "/" + credentialName;
}
private String credhub(String fileRoot) {
return readResponseFromFile(fileRoot, "credhub");
}

View File

@@ -36,6 +36,8 @@ import static io.restassured.RestAssured.with;
public class OpenServiceBrokerApiFixture implements ApplicationListener<ApplicationStartedEvent> {
private static final String ORG_ID = "org-id";
private static final String SPACE_ID = "space-id";
private static final String APP_ID = "app-id";
private static final String SERVICE_KEY_CLIENT_ID = "service-key-client-id";
@Value("${spring.cloud.openservicebroker.catalog.services[0].plans[0].id}")
String planId;
@@ -59,20 +61,57 @@ public class OpenServiceBrokerApiFixture implements ApplicationListener<Applicat
"&plan_id=" + planId;
}
public String createBindingUrl() {
return "/service_instances/{instance_id}/service_bindings/{binding_id}";
}
public String deleteBindingUrl() {
return "/service_instances/{instance_id}/service_bindings/{binding_id}" +
"?service_id=" + serviceDefinitionId +
"&plan_id=" + planId;
}
public RequestSpecification serviceInstanceRequest() {
return serviceBrokerSpecification()
.body(buildServiceInstanceRequestBody());
.body("{" +
"\"service_id\": \"" + serviceDefinitionId + "\"," +
"\"plan_id\": \"" + planId + "\"," +
"\"organization_guid\": \"" + ORG_ID + "\"," +
"\"space_guid\": \"" + SPACE_ID + "\"" +
"}\n");
}
public RequestSpecification serviceInstanceRequest(Map<String, Object> params) {
String stringParams = new JSONObject(params).toString();
return serviceBrokerSpecification()
.body("{" +
" \"service_id\": \"" + serviceDefinitionId + "\"," +
" \"plan_id\": \"" + planId + "\"," +
" \"organization_guid\": \"" + ORG_ID + "\"," +
" \"space_guid\": \"" + SPACE_ID + "\"," +
" \"parameters\": " + stringParams +
"\"service_id\": \"" + serviceDefinitionId + "\"," +
"\"plan_id\": \"" + planId + "\"," +
"\"organization_guid\": \"" + ORG_ID + "\"," +
"\"space_guid\": \"" + SPACE_ID + "\"," +
"\"parameters\": " + stringParams +
"}");
}
public RequestSpecification serviceAppBindingRequest() {
return serviceBrokerSpecification()
.body("{" +
"\"service_id\": \"" + serviceDefinitionId + "\"," +
"\"plan_id\": \"" + planId + "\"," +
"\"bind_resource\": {" +
"\"app_guid\": \"" + APP_ID + "\"" +
"}" +
"}");
}
public RequestSpecification serviceKeyRequest() {
return serviceBrokerSpecification()
.body("{" +
"\"service_id\": \"" + serviceDefinitionId + "\"," +
"\"plan_id\": \"" + planId + "\"," +
"\"bind_resource\": {" +
"\"credential_client_id\": \"" + SERVICE_KEY_CLIENT_ID + "\"" +
"}" +
"}");
}
@@ -83,15 +122,6 @@ public class OpenServiceBrokerApiFixture implements ApplicationListener<Applicat
.contentType(ContentType.JSON);
}
private String buildServiceInstanceRequestBody() {
return "{\n" +
" \"service_id\": \"" + serviceDefinitionId + "\",\n" +
" \"plan_id\": \"" + planId + "\",\n" +
" \"organization_guid\": \"" + ORG_ID + "\",\n" +
" \"space_guid\": \"" + SPACE_ID + "\"\n" +
"}\n";
}
public String waitForAsyncOperationComplete(String serviceInstanceId) {
try {
String state;

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.appbroker.integration.fixtures;
import org.springframework.boot.test.context.TestComponent;
import org.springframework.cloud.appbroker.service.CreateServiceInstanceAppBindingWorkflow;
import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceAppBindingResponse.CreateServiceInstanceAppBindingResponseBuilder;
import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceBindingRequest;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@TestComponent
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TestBindingCredentialsProviderFixture implements CreateServiceInstanceAppBindingWorkflow {
private final Map<String, Object> credentials;
public TestBindingCredentialsProviderFixture() {
this.credentials = new HashMap<>();
this.credentials.put("credential1", "value1");
this.credentials.put("credential2", "value2");
}
@Override
public Mono<Boolean> accept(CreateServiceInstanceBindingRequest request) {
return Mono.just(true);
}
@Override
public Mono<CreateServiceInstanceAppBindingResponseBuilder> buildResponse(CreateServiceInstanceBindingRequest request,
CreateServiceInstanceAppBindingResponseBuilder responseBuilder) {
return Mono.just(responseBuilder.credentials(this.credentials));
}
public Map<String, Object> getCredentials() {
return credentials;
}
}

View File

@@ -60,14 +60,20 @@ public class WiremockServerFixture {
}
public void verifyAllRequiredStubsUsed() {
Set<UUID> servedStubIds = ccUaaWiremockServer.getServeEvents().getRequests().stream()
// todo: cleanup CC and UAA stubbing so used/unused can be verified
// verifyStubs(ccUaaWiremockServer);
verifyStubs(credHubWiremockServer);
}
private void verifyStubs(WireMockServer wireMockServer) {
Set<UUID> servedStubIds = wireMockServer.getServeEvents().getRequests().stream()
.filter(event -> event.getStubMapping() != null)
.map(event -> event.getStubMapping().getId())
.collect(Collectors.toSet());
List<StubMapping> unusedStubs = ccUaaWiremockServer.listAllStubMappings().getMappings().stream()
List<StubMapping> unusedStubs = wireMockServer.listAllStubMappings().getMappings().stream()
.filter(stub -> !servedStubIds.contains(stub.getId()))
.filter(this::stubIsOptional)
.filter(this::stubIsRequired)
.collect(Collectors.toList());
assertThat(unusedStubs.size())
@@ -75,10 +81,10 @@ public class WiremockServerFixture {
.isEqualTo(0);
}
private boolean stubIsOptional(StubMapping stub) {
private boolean stubIsRequired(StubMapping stub) {
if (stub.getMetadata() != null) {
return !stub.getMetadata().getBoolean("optional");
}
return false;
return true;
}
}

View File

@@ -0,0 +1,8 @@
{
"credentials": [
{
"version_created_at": "2017-05-09T21:09:26Z",
"name": "/example-json"
}
]
}

View File

@@ -0,0 +1,6 @@
{
"uuid": "example-uuid",
"path": "/example-directory/*",
"actor": "uaa-user:106f52e2-5d01-4675-8d7a-c05ff9a2c081",
"operations": ["read","write"]
}

View File

@@ -0,0 +1,14 @@
{
"type": "json",
"version_created_at": "2017-01-01T04:07:18Z",
"id": "67fc3def-bbfb-4953-83f8-4ab0682ad675",
"name": "/example-json",
"value": {
"key": 123,
"key_list": [
"val1",
"val2"
],
"is_true": true
}
}

View File

@@ -16,75 +16,112 @@
package org.springframework.cloud.appbroker.workflow.binding;
import org.springframework.credhub.support.permissions.Operation;
import reactor.core.publisher.Mono;
import reactor.util.Logger;
import reactor.util.Loggers;
import org.springframework.cloud.servicebroker.model.binding.BindResource;
import org.springframework.cloud.appbroker.service.CreateServiceInstanceAppBindingWorkflow;
import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceAppBindingResponse;
import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceAppBindingResponse.CreateServiceInstanceAppBindingResponseBuilder;
import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceBindingRequest;
import org.springframework.core.annotation.Order;
import org.springframework.credhub.core.CredHubOperations;
import org.springframework.credhub.support.ServiceInstanceCredentialName;
import org.springframework.credhub.support.CredentialName;
import org.springframework.credhub.support.permissions.Permission;
import org.springframework.credhub.support.json.JsonCredentialRequest;
import org.springframework.util.CollectionUtils;
@Order(50)
public class CredHubPersistingCreateServiceInstanceAppBindingWorkflow implements CreateServiceInstanceAppBindingWorkflow {
public class CredHubPersistingCreateServiceInstanceAppBindingWorkflow
extends CredHubPersistingWorkflow
implements CreateServiceInstanceAppBindingWorkflow {
private static final Logger LOG = Loggers.getLogger(CredHubPersistingCreateServiceInstanceAppBindingWorkflow.class);
private static final String CREDENTIALS_KEY = "credhub-ref";
private static final String CREDHUB_REF_KEY = "credhub-ref";
private static final String CREDENTIALS_NAME = "credentials-json";
private final String appName;
private static final String CREDENTIAL_CLIENT_ID = "credential_client_id";
private final CredHubOperations credHubOperations;
public CredHubPersistingCreateServiceInstanceAppBindingWorkflow(CredHubOperations credHubOperations, String appName) {
super(appName);
this.credHubOperations = credHubOperations;
this.appName = appName;
}
@Override
public Mono<CreateServiceInstanceAppBindingResponseBuilder> buildResponse(CreateServiceInstanceBindingRequest request,
CreateServiceInstanceAppBindingResponseBuilder responseBuilder) {
return Mono.just(responseBuilder.build())
.flatMap(response -> {
if (!CollectionUtils.isEmpty(response.getCredentials())) {
return persistBindingCredentials(request, response)
.doOnRequest(l -> LOG.debug("Storing binding credentials in CredHub"))
.doOnSuccess(r -> LOG.debug("Finished storing binding credentials in CredHub"))
.doOnError(exception -> LOG.debug("Error storing binding credentials in CredHub with error {}", exception));
}
return Mono.just(responseBuilder);
});
.flatMap(response -> {
if (!CollectionUtils.isEmpty(response.getCredentials())) {
return buildCredentialName(request.getServiceDefinitionId(), request.getBindingId())
.flatMap(credentialName -> persistBindingCredentials(request, response, credentialName)
.doOnRequest(l -> LOG.debug("Storing binding credentials with name '{}' in CredHub", credentialName.getName()))
.doOnSuccess(r -> LOG.debug("Finished storing binding credentials with name '{}' in CredHub", credentialName.getName()))
.doOnError(exception -> LOG.error("Error storing binding credentials with name '{}' in CredHub with error: {}",
credentialName.getName(), exception.getMessage())));
}
return Mono.just(responseBuilder);
});
}
private Mono<CreateServiceInstanceAppBindingResponseBuilder> persistBindingCredentials(CreateServiceInstanceBindingRequest request,
CreateServiceInstanceAppBindingResponse response) {
return Mono.just(ServiceInstanceCredentialName.builder()
.serviceBrokerName(this.appName)
.serviceOfferingName(request.getServiceDefinitionId())
.serviceBindingId(request.getBindingId())
.credentialName(CREDENTIALS_NAME)
.build())
.flatMap(credentialName -> Mono.fromCallable(() -> credHubOperations
.credentials()
CreateServiceInstanceAppBindingResponse response,
CredentialName credentialName) {
return writeCredential(response, credentialName)
.then(writePermissions(request, credentialName))
.thenReturn(buildReplacementBindingResponse(response, credentialName));
}
private Mono<Void> writeCredential(CreateServiceInstanceAppBindingResponse response,
CredentialName credentialName) {
return Mono.fromCallable(() -> {
credHubOperations.credentials()
.write(JsonCredentialRequest.builder()
.name(credentialName)
.value(response.getCredentials())
.build()))
.thenReturn(CreateServiceInstanceAppBindingResponse
.builder()
.async(response.isAsync())
.bindingExisted(response.isBindingExisted())
.credentials(CREDENTIALS_KEY, credentialName.getName())
.operation(response.getOperation())
.syslogDrainUrl(response.getSyslogDrainUrl())
.volumeMounts(response.getVolumeMounts())));
.build());
return null;
});
}
private Mono<Void> writePermissions(CreateServiceInstanceBindingRequest request,
CredentialName credentialName) {
return Mono.fromCallable(() -> {
BindResource bindResource = request.getBindResource();
if (bindResource.getAppGuid() != null) {
Permission permission = Permission.builder()
.app(bindResource.getAppGuid())
.operation(Operation.READ)
.build();
credHubOperations.permissionsV2().addPermissions(credentialName, permission);
}
if (bindResource.getProperty(CREDENTIAL_CLIENT_ID) != null) {
Permission permission = Permission.builder()
.client(bindResource.getProperty(CREDENTIAL_CLIENT_ID).toString())
.operation(Operation.READ)
.build();
credHubOperations.permissionsV2().addPermissions(credentialName, permission);
}
return null;
});
}
private CreateServiceInstanceAppBindingResponseBuilder buildReplacementBindingResponse(CreateServiceInstanceAppBindingResponse response,
CredentialName credentialName) {
return CreateServiceInstanceAppBindingResponse.builder()
.async(response.isAsync())
.bindingExisted(response.isBindingExisted())
.credentials(CREDHUB_REF_KEY, credentialName.getName())
.operation(response.getOperation())
.syslogDrainUrl(response.getSyslogDrainUrl())
.volumeMounts(response.getVolumeMounts());
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.appbroker.workflow.binding;
import org.springframework.cloud.appbroker.service.DeleteServiceInstanceBindingWorkflow;
import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingRequest;
import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingResponse.DeleteServiceInstanceBindingResponseBuilder;
import org.springframework.core.annotation.Order;
import org.springframework.credhub.core.CredHubOperations;
import org.springframework.credhub.support.CredentialName;
import reactor.core.publisher.Mono;
import reactor.util.Logger;
import reactor.util.Loggers;
@Order(50)
public class CredHubPersistingDeleteServiceInstanceBindingWorkflow
extends CredHubPersistingWorkflow
implements DeleteServiceInstanceBindingWorkflow {
private static final Logger LOG = Loggers.getLogger(CredHubPersistingDeleteServiceInstanceBindingWorkflow.class);
private final CredHubOperations credHubOperations;
public CredHubPersistingDeleteServiceInstanceBindingWorkflow(CredHubOperations credHubOperations, String appName) {
super(appName);
this.credHubOperations = credHubOperations;
}
@Override
public Mono<DeleteServiceInstanceBindingResponseBuilder> buildResponse(DeleteServiceInstanceBindingRequest request,
DeleteServiceInstanceBindingResponseBuilder responseBuilder) {
return buildCredentialName(request.getServiceDefinitionId(), request.getBindingId())
.filter(this::credentialExists)
.flatMap(credentialName -> deleteBindingCredentials(credentialName)
.doOnRequest(l -> LOG.debug("Deleting binding credentials with name '{}' in CredHub", credentialName.getName()))
.doOnSuccess(r -> LOG.debug("Finished deleting binding credentials with name '{}' in CredHub", credentialName.getName()))
.doOnError(exception -> LOG.error("Error deleting binding credentials with name '{}' in CredHub with error: {}",
credentialName.getName(), exception.getMessage())))
.thenReturn(responseBuilder);
}
private boolean credentialExists(CredentialName credentialName) {
return !credHubOperations.credentials().findByName(credentialName).isEmpty();
}
private Mono<Void> deleteBindingCredentials(CredentialName credentialName) {
return Mono.fromCallable(() -> {
credHubOperations.credentials().deleteByName(credentialName);
return null;
});
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.appbroker.workflow.binding;
import org.springframework.credhub.support.ServiceInstanceCredentialName;
import reactor.core.publisher.Mono;
class CredHubPersistingWorkflow {
private static final String CREDENTIALS_NAME = "credentials-json";
private final String appName;
CredHubPersistingWorkflow(String appName) {
this.appName = appName;
}
Mono<ServiceInstanceCredentialName> buildCredentialName(String serviceDefinitionId, String bindingId) {
return Mono.just(ServiceInstanceCredentialName.builder()
.serviceBrokerName(this.appName)
.serviceOfferingName(serviceDefinitionId)
.serviceBindingId(bindingId)
.credentialName(CREDENTIALS_NAME)
.build());
}
}

View File

@@ -25,6 +25,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.servicebroker.model.binding.BindResource;
import org.springframework.credhub.core.permissionV2.CredHubPermissionV2Operations;
import org.springframework.credhub.support.CredentialName;
import org.springframework.credhub.support.ServiceInstanceCredentialName;
import reactor.test.StepVerifier;
import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceAppBindingResponse;
@@ -38,6 +42,7 @@ import org.springframework.credhub.support.CredentialDetails;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -51,6 +56,9 @@ class CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest {
@Mock
private CredHubCredentialOperations credHubCredentialOperations;
@Mock
private CredHubPermissionV2Operations credHubPermissionOperations;
private CredHubPersistingCreateServiceInstanceAppBindingWorkflow workflow;
@BeforeEach
@@ -96,11 +104,22 @@ class CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest {
@Test
@SuppressWarnings("serial")
void storeCredentialsInCredHub() {
CredentialName credentialName = ServiceInstanceCredentialName.builder()
.serviceBrokerName("test-app-name")
.serviceOfferingName("foo-definition-id")
.serviceBindingId("foo-binding-id")
.credentialName("credentials-json")
.build();
CreateServiceInstanceBindingRequest request = CreateServiceInstanceBindingRequest
.builder()
.bindingId("foo-binding-id")
.serviceInstanceId("foo-instance-id")
.serviceDefinitionId("foo-definition-id")
.bindResource(BindResource.builder()
.appGuid("app-id")
.properties("credential_client_id", "client-id")
.build())
.build();
Map<String, Object> credentials = new HashMap<String, Object>() {{
@@ -125,6 +144,8 @@ class CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest {
given(this.credHubOperations.credentials())
.willReturn(credHubCredentialOperations);
given(this.credHubOperations.permissionsV2())
.willReturn(credHubPermissionOperations);
given(this.credHubCredentialOperations.write(any()))
.willReturn(new CredentialDetails<>());
@@ -135,7 +156,7 @@ class CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest {
CreateServiceInstanceAppBindingResponse response = createServiceInstanceAppBindingResponseBuilder.build();
assertThat(response.isBindingExisted()).isEqualTo(true);
assertThat(response.getCredentials()).hasSize(1);
assertThat(response.getCredentials().get("credhub-ref")).isEqualTo("/c/test-app-name/foo-definition-id/foo-binding-id/credentials-json");
assertThat(response.getCredentials().get("credhub-ref")).isEqualTo(credentialName.getName());
assertThat(response.getSyslogDrainUrl()).isEqualTo("https://logs.example.com");
assertThat(response.getVolumeMounts()).hasSize(4);
@@ -143,6 +164,7 @@ class CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest {
.verifyComplete();
verify(this.credHubCredentialOperations).write(any());
verify(this.credHubPermissionOperations, times(2)).addPermissions(any(), any());
verifyNoMoreInteractions(this.credHubCredentialOperations);
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.appbroker.workflow.binding;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingRequest;
import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingResponse;
import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingResponse.DeleteServiceInstanceBindingResponseBuilder;
import org.springframework.credhub.core.CredHubOperations;
import org.springframework.credhub.core.credential.CredHubCredentialOperations;
import org.springframework.credhub.support.CredentialName;
import org.springframework.credhub.support.CredentialSummary;
import org.springframework.credhub.support.ServiceInstanceCredentialName;
import reactor.test.StepVerifier;
import java.util.Collections;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@ExtendWith(MockitoExtension.class)
class CredHubPersistingDeleteServiceInstanceBindingWorkflowTest {
@Mock
private CredHubOperations credHubOperations;
@Mock
private CredHubCredentialOperations credHubCredentialOperations;
private CredHubPersistingDeleteServiceInstanceBindingWorkflow workflow;
@BeforeEach
void setUp() {
this.workflow = new CredHubPersistingDeleteServiceInstanceBindingWorkflow(credHubOperations, "test-app-name");
}
@Test
void deleteCredentialsFromCredHubWhenFound() {
CredentialName credentialName = ServiceInstanceCredentialName.builder()
.serviceBrokerName("test-app-name")
.serviceOfferingName("foo-definition-id")
.serviceBindingId("foo-binding-id")
.credentialName("credentials-json")
.build();
DeleteServiceInstanceBindingRequest request = DeleteServiceInstanceBindingRequest
.builder()
.bindingId("foo-binding-id")
.serviceInstanceId("foo-instance-id")
.serviceDefinitionId("foo-definition-id")
.build();
DeleteServiceInstanceBindingResponseBuilder responseBuilder =
DeleteServiceInstanceBindingResponse.builder();
given(this.credHubOperations.credentials())
.willReturn(credHubCredentialOperations);
given(this.credHubCredentialOperations.findByName(credentialName))
.willReturn(Collections.singletonList(new CredentialSummary(credentialName)));
StepVerifier
.create(this.workflow.buildResponse(request, responseBuilder))
.expectNext(responseBuilder)
.verifyComplete();
verify(this.credHubCredentialOperations).deleteByName(eq(credentialName));
verifyNoMoreInteractions(this.credHubCredentialOperations);
}
@Test
void deleteCredentialsFromCredHubWhenNotFound() {
DeleteServiceInstanceBindingRequest request = DeleteServiceInstanceBindingRequest
.builder()
.bindingId("foo-binding-id")
.serviceInstanceId("foo-instance-id")
.serviceDefinitionId("foo-definition-id")
.build();
DeleteServiceInstanceBindingResponseBuilder responseBuilder =
DeleteServiceInstanceBindingResponse.builder();
given(this.credHubOperations.credentials())
.willReturn(credHubCredentialOperations);
given(this.credHubCredentialOperations.findByName(any()))
.willReturn(Collections.emptyList());
StepVerifier
.create(this.workflow.buildResponse(request, responseBuilder))
.expectNext(responseBuilder)
.verifyComplete();
verifyNoMoreInteractions(this.credHubCredentialOperations);
}
}