diff --git a/build.gradle b/build.gradle index c59f7cc..82799a0 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ configure(allprojects) { springVersion = project.findProperty("springVersion") ?: "5.0.10.RELEASE" reactorVersion = project.findProperty("reactorVersion") ?: "Bismuth-SR14" openServiceBrokerVersion = "3.0.0.BUILD-SNAPSHOT" + springCredhubVersion = "2.0.0.BUILD-SNAPSHOT" cfJavaClientVersion = "3.13.0.RELEASE" mockitoVersion = "2.23.4" immutablesVersion = "2.7.3" diff --git a/settings.gradle b/settings.gradle index 64050c6..c6f8e4a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,5 +6,5 @@ include "spring-cloud-app-broker-core" include "spring-cloud-app-broker-autoconfigure" include "spring-cloud-app-broker-sample" include "spring-cloud-app-broker-acceptance-tests" +include "spring-cloud-app-broker-security-credhub" include 'spring-cloud-starter-app-broker' - diff --git a/spring-cloud-app-broker-autoconfigure/build.gradle b/spring-cloud-app-broker-autoconfigure/build.gradle index 30ddc46..c7e9064 100644 --- a/spring-cloud-app-broker-autoconfigure/build.gradle +++ b/spring-cloud-app-broker-autoconfigure/build.gradle @@ -26,20 +26,29 @@ dependencies { implementation project(":spring-cloud-app-broker-core") implementation project(":spring-cloud-app-broker-deployer") implementation project(":spring-cloud-app-broker-deployer-cloudfoundry") + implementation project(":spring-cloud-app-broker-security-credhub") implementation("org.springframework.boot:spring-boot-starter") - implementation("org.cloudfoundry:cloudfoundry-client-reactor:${cfJavaClientVersion}") - implementation("org.cloudfoundry:cloudfoundry-operations:${cfJavaClientVersion}") implementation("io.projectreactor:reactor-core") implementation("io.projectreactor.ipc:reactor-netty") api("org.immutables:value:${immutablesVersion}") + compileOnly("org.cloudfoundry:cloudfoundry-client-reactor:${cfJavaClientVersion}") + compileOnly("org.cloudfoundry:cloudfoundry-operations:${cfJavaClientVersion}") + compileOnly("org.springframework.credhub:spring-credhub-starter:${springCredhubVersion}") + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.boot:spring-boot-starter-webflux") testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") + + testCompileOnly("org.cloudfoundry:cloudfoundry-client-reactor:${cfJavaClientVersion}") + testCompileOnly("org.cloudfoundry:cloudfoundry-operations:${cfJavaClientVersion}") + testCompileOnly("org.springframework.credhub:spring-credhub-starter:${springCredhubVersion}") } compileJava.dependsOn(processResources) diff --git a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java index 0b31e2a..42cef81 100644 --- a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java +++ b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java @@ -211,13 +211,13 @@ public class AppBrokerAutoConfiguration { @Bean public WorkflowServiceInstanceBindingService serviceInstanceBindingService( - ServiceInstanceBindingStateRepository stateRepository, - @Autowired(required = false) List createServiceInstanceAppBindingWorkflows, - @Autowired(required = false) List createServiceInstanceRouteBindingWorkflows, - @Autowired(required = false) List deleteServiceInstanceBindingWorkflows) { + ServiceInstanceBindingStateRepository stateRepository, + @Autowired(required = false) List createServiceInstanceAppBindingWorkflows, + @Autowired(required = false) List createServiceInstanceRouteBindingWorkflows, + @Autowired(required = false) List deleteServiceInstanceBindingWorkflows) { return new WorkflowServiceInstanceBindingService(stateRepository, - createServiceInstanceAppBindingWorkflows, - createServiceInstanceRouteBindingWorkflows, - deleteServiceInstanceBindingWorkflows); + createServiceInstanceAppBindingWorkflows, + createServiceInstanceRouteBindingWorkflows, + deleteServiceInstanceBindingWorkflows); } } diff --git a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/CredHubAutoConfiguration.java b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/CredHubAutoConfiguration.java new file mode 100644 index 0000000..975d9dc --- /dev/null +++ b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/CredHubAutoConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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.autoconfigure; + +import org.springframework.cloud.appbroker.workflow.binding.CredHubPersistingCreateServiceInstanceAppBindingWorkflow; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.cloud.appbroker.service.CreateServiceInstanceAppBindingWorkflow; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.credhub.core.ReactiveCredHubOperations; + +@Configuration +@AutoConfigureBefore(AppBrokerAutoConfiguration.class) +public class CredHubAutoConfiguration { + + @Value("${spring.application.name}") + private String appName; + + @Bean + @ConditionalOnBean(ReactiveCredHubOperations.class) + public CreateServiceInstanceAppBindingWorkflow credhubPersistingCreateServiceInstanceAppBindingWorkflow(ReactiveCredHubOperations credHubOperations) { + return new CredHubPersistingCreateServiceInstanceAppBindingWorkflow(credHubOperations, appName); + } + +} diff --git a/spring-cloud-app-broker-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-app-broker-autoconfigure/src/main/resources/META-INF/spring.factories index 4fe3741..dc4d0d0 100644 --- a/spring-cloud-app-broker-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-app-broker-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,3 +1,4 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.appbroker.autoconfigure.CloudFoundryAppDeployerAutoConfiguration,\ +org.springframework.cloud.appbroker.autoconfigure.CredHubAutoConfiguration,\ org.springframework.cloud.appbroker.autoconfigure.AppBrokerAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java b/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java index 3862658..9c61752 100644 --- a/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java +++ b/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java @@ -17,8 +17,11 @@ package org.springframework.cloud.appbroker.autoconfigure; import org.junit.jupiter.api.Test; +import org.springframework.cloud.appbroker.workflow.binding.CredHubPersistingCreateServiceInstanceAppBindingWorkflow; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; import org.springframework.cloud.appbroker.deployer.BackingApplications; @@ -35,6 +38,7 @@ import org.springframework.cloud.appbroker.extensions.parameters.ParameterMappin import org.springframework.cloud.appbroker.extensions.parameters.PropertyMappingParametersTransformerFactory; import org.springframework.cloud.appbroker.extensions.targets.SpacePerServiceInstance; import org.springframework.cloud.appbroker.extensions.targets.TargetService; +import org.springframework.cloud.appbroker.service.CreateServiceInstanceAppBindingWorkflow; import org.springframework.cloud.appbroker.service.WorkflowServiceInstanceBindingService; import org.springframework.cloud.appbroker.service.WorkflowServiceInstanceService; import org.springframework.cloud.appbroker.state.ServiceInstanceBindingStateRepository; @@ -42,17 +46,47 @@ import org.springframework.cloud.appbroker.state.ServiceInstanceStateRepository; import org.springframework.cloud.appbroker.workflow.instance.AppDeploymentCreateServiceInstanceWorkflow; import org.springframework.cloud.appbroker.workflow.instance.AppDeploymentDeleteServiceInstanceWorkflow; import org.springframework.cloud.appbroker.workflow.instance.AppDeploymentUpdateServiceInstanceWorkflow; +import org.springframework.context.annotation.Bean; +import org.springframework.credhub.core.ReactiveCredHubOperations; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; class AppBrokerAutoConfigurationTest { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AppBrokerAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(AppBrokerAutoConfiguration.class, CredHubAutoConfiguration.class)); @Test void servicesAreCreatedWithCloudFoundryConfigured() { + configuredContext() + .run(this::assertContext); + } + + @Test + void servicesAreCreatedWithCloudFoundryAndCredHubConfigured() { + configuredContext() + .withUserConfiguration(CredHubConfiguration.class) + .run((context) -> { + assertContext(context); + assertThat(context) + .hasSingleBean(CreateServiceInstanceAppBindingWorkflow.class) + .getBean(CreateServiceInstanceAppBindingWorkflow.class) + .isExactlyInstanceOf(CredHubPersistingCreateServiceInstanceAppBindingWorkflow.class); + }); + } + + @Test + void servicesAreNotCreatedWithoutDeployerConfiguration() { this.contextRunner + .run((context) -> { + assertThat(context).doesNotHaveBean(BackingApplications.class); + assertThat(context).doesNotHaveBean(DeployerClient.class); + }); + } + + private ApplicationContextRunner configuredContext() { + return this.contextRunner .withConfiguration(AutoConfigurations.of(CloudFoundryAppDeployerAutoConfiguration.class)) .withPropertyValues( "spring.cloud.appbroker.services[0].service-name=service1", @@ -76,70 +110,70 @@ class AppBrokerAutoConfigurationTest { "spring.cloud.appbroker.deployer.cloudfoundry.api-host=https://api.example.com", "spring.cloud.appbroker.deployer.cloudfoundry.username=user", "spring.cloud.appbroker.deployer.cloudfoundry.password=secret" - ) - .run((context) -> { - assertThat(context).hasSingleBean(BrokeredServices.class); - BrokeredServices brokeredServices = context.getBean(BrokeredServices.class); - assertThat(brokeredServices).hasSize(2); - - assertThat(brokeredServices.get(0).getServiceName()).isEqualTo("service1"); - assertThat(brokeredServices.get(0).getPlanName()).isEqualTo("service1-plan1"); - - assertThat(brokeredServices.get(0).getApps().get(0).getName()).isEqualTo("app1"); - assertThat(brokeredServices.get(0).getApps().get(0).getPath()).isEqualTo("classpath:app1.jar"); - assertThat(brokeredServices.get(0).getApps().get(0).getProperties().get("memory")).isEqualTo("1G"); - assertThat(brokeredServices.get(0).getApps().get(0).getProperties().get("instances")).isNull(); - - assertThat(brokeredServices.get(0).getApps().get(1).getName()).isEqualTo("app2"); - assertThat(brokeredServices.get(0).getApps().get(1).getPath()).isEqualTo("classpath:app2.jar"); - assertThat(brokeredServices.get(0).getApps().get(1).getProperties().get("memory")).isEqualTo("2G"); - assertThat(brokeredServices.get(0).getApps().get(1).getProperties().get("instances")).isEqualTo("2"); - - assertThat(brokeredServices.get(1).getServiceName()).isEqualTo("service2"); - assertThat(brokeredServices.get(1).getPlanName()).isEqualTo("service2-plan1"); - - assertThat(brokeredServices.get(1).getApps().get(0).getName()).isEqualTo("app3"); - assertThat(brokeredServices.get(1).getApps().get(0).getPath()).isEqualTo("classpath:app3.jar"); - - assertThat(context).hasSingleBean(DeployerClient.class); - assertThat(context).hasSingleBean(BrokeredServices.class); - - assertThat(context).hasSingleBean(ServiceInstanceStateRepository.class); - assertThat(context).hasSingleBean(ServiceInstanceBindingStateRepository.class); - - assertThat(context).hasSingleBean(BackingAppDeploymentService.class); - assertThat(context).hasSingleBean(BackingServicesProvisionService.class); - - assertThat(context).hasSingleBean(BackingApplicationsParametersTransformationService.class); - assertThat(context).hasSingleBean(EnvironmentMappingParametersTransformerFactory.class); - assertThat(context).hasSingleBean(PropertyMappingParametersTransformerFactory.class); - assertThat(context).hasSingleBean(ParameterMappingParametersTransformerFactory.class); - - assertThat(context).hasSingleBean(BackingServicesParametersTransformationService.class); - - assertThat(context).hasSingleBean(CredentialProviderService.class); - assertThat(context).hasSingleBean(SpringSecurityBasicAuthCredentialProviderFactory.class); - assertThat(context).hasSingleBean(SpringSecurityOAuth2CredentialProviderFactory.class); - - assertThat(context).hasSingleBean(TargetService.class); - assertThat(context).hasSingleBean(SpacePerServiceInstance.class); - - assertThat(context).hasSingleBean(WorkflowServiceInstanceService.class); - assertThat(context).hasSingleBean(WorkflowServiceInstanceBindingService.class); - - assertThat(context).hasSingleBean(AppDeploymentCreateServiceInstanceWorkflow.class); - assertThat(context).hasSingleBean(AppDeploymentDeleteServiceInstanceWorkflow.class); - assertThat(context).hasSingleBean(AppDeploymentUpdateServiceInstanceWorkflow.class); - }); + ); } - @Test - void servicesAreNotCreatedWithoutDeployerConfiguration() { - this.contextRunner - .run((context) -> { - assertThat(context).doesNotHaveBean(BackingApplications.class); - assertThat(context).doesNotHaveBean(DeployerClient.class); - }); + private void assertContext(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(BrokeredServices.class); + BrokeredServices brokeredServices = context.getBean(BrokeredServices.class); + assertThat(brokeredServices).hasSize(2); + + assertThat(brokeredServices.get(0).getServiceName()).isEqualTo("service1"); + assertThat(brokeredServices.get(0).getPlanName()).isEqualTo("service1-plan1"); + + assertThat(brokeredServices.get(0).getApps().get(0).getName()).isEqualTo("app1"); + assertThat(brokeredServices.get(0).getApps().get(0).getPath()).isEqualTo("classpath:app1.jar"); + assertThat(brokeredServices.get(0).getApps().get(0).getProperties().get("memory")).isEqualTo("1G"); + assertThat(brokeredServices.get(0).getApps().get(0).getProperties().get("instances")).isNull(); + + assertThat(brokeredServices.get(0).getApps().get(1).getName()).isEqualTo("app2"); + assertThat(brokeredServices.get(0).getApps().get(1).getPath()).isEqualTo("classpath:app2.jar"); + assertThat(brokeredServices.get(0).getApps().get(1).getProperties().get("memory")).isEqualTo("2G"); + assertThat(brokeredServices.get(0).getApps().get(1).getProperties().get("instances")).isEqualTo("2"); + + assertThat(brokeredServices.get(1).getServiceName()).isEqualTo("service2"); + assertThat(brokeredServices.get(1).getPlanName()).isEqualTo("service2-plan1"); + + assertThat(brokeredServices.get(1).getApps().get(0).getName()).isEqualTo("app3"); + assertThat(brokeredServices.get(1).getApps().get(0).getPath()).isEqualTo("classpath:app3.jar"); + + assertThat(context).hasSingleBean(DeployerClient.class); + assertThat(context).hasSingleBean(BrokeredServices.class); + + assertThat(context).hasSingleBean(ServiceInstanceStateRepository.class); + assertThat(context).hasSingleBean(ServiceInstanceBindingStateRepository.class); + + assertThat(context).hasSingleBean(BackingAppDeploymentService.class); + assertThat(context).hasSingleBean(BackingServicesProvisionService.class); + + assertThat(context).hasSingleBean(BackingApplicationsParametersTransformationService.class); + assertThat(context).hasSingleBean(EnvironmentMappingParametersTransformerFactory.class); + assertThat(context).hasSingleBean(PropertyMappingParametersTransformerFactory.class); + assertThat(context).hasSingleBean(ParameterMappingParametersTransformerFactory.class); + + assertThat(context).hasSingleBean(BackingServicesParametersTransformationService.class); + + assertThat(context).hasSingleBean(CredentialProviderService.class); + assertThat(context).hasSingleBean(SpringSecurityBasicAuthCredentialProviderFactory.class); + assertThat(context).hasSingleBean(SpringSecurityOAuth2CredentialProviderFactory.class); + + assertThat(context).hasSingleBean(TargetService.class); + assertThat(context).hasSingleBean(SpacePerServiceInstance.class); + + assertThat(context).hasSingleBean(WorkflowServiceInstanceService.class); + assertThat(context).hasSingleBean(WorkflowServiceInstanceBindingService.class); + + assertThat(context).hasSingleBean(AppDeploymentCreateServiceInstanceWorkflow.class); + assertThat(context).hasSingleBean(AppDeploymentDeleteServiceInstanceWorkflow.class); + assertThat(context).hasSingleBean(AppDeploymentUpdateServiceInstanceWorkflow.class); + } + + @TestConfiguration + public static class CredHubConfiguration { + @Bean + public ReactiveCredHubOperations credHubOperations() { + return mock(ReactiveCredHubOperations.class); + } } } \ No newline at end of file diff --git a/spring-cloud-app-broker-sample/src/test/resources/application.yml b/spring-cloud-app-broker-sample/src/test/resources/application.yml index 3474d8a..c7a231e 100644 --- a/spring-cloud-app-broker-sample/src/test/resources/application.yml +++ b/spring-cloud-app-broker-sample/src/test/resources/application.yml @@ -1,3 +1,6 @@ +spring: + application: + name: foo-app logging: level: cloudfoundry-client: DEBUG \ No newline at end of file diff --git a/spring-cloud-app-broker-security-credhub/build.gradle b/spring-cloud-app-broker-security-credhub/build.gradle new file mode 100644 index 0000000..aa13671 --- /dev/null +++ b/spring-cloud-app-broker-security-credhub/build.gradle @@ -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. + */ + +description = "Spring Cloud App Broker Deployer Security CredHub" + +dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}" + } +} + +dependencies { + implementation project(":spring-cloud-app-broker-core") + implementation("io.projectreactor:reactor-core") + implementation("org.springframework.cloud:spring-cloud-open-service-broker-core:${openServiceBrokerVersion}") + implementation("org.springframework.credhub:spring-credhub-starter:${springCredhubVersion}") + + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + + testImplementation("org.springframework.boot:spring-boot-starter-webflux") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("io.projectreactor:reactor-test") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") +} \ No newline at end of file diff --git a/spring-cloud-app-broker-security-credhub/src/main/java/org/springframework/cloud/appbroker/workflow/binding/CredHubPersistingCreateServiceInstanceAppBindingWorkflow.java b/spring-cloud-app-broker-security-credhub/src/main/java/org/springframework/cloud/appbroker/workflow/binding/CredHubPersistingCreateServiceInstanceAppBindingWorkflow.java new file mode 100644 index 0000000..b61c090 --- /dev/null +++ b/spring-cloud-app-broker-security-credhub/src/main/java/org/springframework/cloud/appbroker/workflow/binding/CredHubPersistingCreateServiceInstanceAppBindingWorkflow.java @@ -0,0 +1,93 @@ +/* + * 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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; + +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.ReactiveCredHubOperations; +import org.springframework.credhub.support.json.JsonCredentialRequest; +import org.springframework.util.CollectionUtils; + +@Order(50) +public class CredHubPersistingCreateServiceInstanceAppBindingWorkflow implements CreateServiceInstanceAppBindingWorkflow { + + private static final Logger LOG = Loggers.getLogger(CredHubPersistingCreateServiceInstanceAppBindingWorkflow.class); + + private static final String CREDENTIALS_KEY = "credhub-ref"; + + private static final String CREDENTIALS_VALUE_TEMPLATE = "/c/%s/%s/%s/credentials-json"; + + private final String appName; + + private final ReactiveCredHubOperations credHubOperations; + + public CredHubPersistingCreateServiceInstanceAppBindingWorkflow(ReactiveCredHubOperations credHubOperations, String appName) { + this.credHubOperations = credHubOperations; + this.appName = appName; + } + + @Override + public Flux create(CreateServiceInstanceBindingRequest request) { + return Flux.empty(); + } + + @Override + public Mono 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); + }); + } + + private Mono persistBindingCredentials(CreateServiceInstanceBindingRequest request, + CreateServiceInstanceAppBindingResponse response) { + return credHubOperations + .credentials() + .write(JsonCredentialRequest + .builder() + .value(response.getCredentials()) + .build()) + .thenReturn(CreateServiceInstanceAppBindingResponse + .builder() + .async(response.isAsync()) + .bindingExisted(response.isBindingExisted()) + .credentials(CREDENTIALS_KEY, formatCredentials(request)) + .operation(response.getOperation()) + .syslogDrainUrl(response.getSyslogDrainUrl()) + .volumeMounts(response.getVolumeMounts())); + } + + private String formatCredentials(CreateServiceInstanceBindingRequest request) { + return String.format(CREDENTIALS_VALUE_TEMPLATE, this.appName, request.getServiceDefinitionId(), request.getBindingId()); + } + +} diff --git a/spring-cloud-app-broker-security-credhub/src/test/java/org/springframework/cloud/appbroker/workflow/binding/CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest.java b/spring-cloud-app-broker-security-credhub/src/test/java/org/springframework/cloud/appbroker/workflow/binding/CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest.java new file mode 100644 index 0000000..2b3e4d9 --- /dev/null +++ b/spring-cloud-app-broker-security-credhub/src/test/java/org/springframework/cloud/appbroker/workflow/binding/CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest.java @@ -0,0 +1,148 @@ +/* + * 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 java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +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 reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +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.cloud.servicebroker.model.binding.VolumeMount; +import org.springframework.credhub.core.ReactiveCredHubOperations; +import org.springframework.credhub.core.credential.ReactiveCredHubCredentialOperations; + +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.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +@ExtendWith(MockitoExtension.class) +class CredHubPersistingCreateServiceInstanceAppBindingWorkflowTest { + + @Mock + private ReactiveCredHubOperations credHubOperations; + + @Mock + private ReactiveCredHubCredentialOperations credHubCredentialOperations; + + private CredHubPersistingCreateServiceInstanceAppBindingWorkflow workflow; + + @BeforeEach + void setUp() { + this.workflow = new CredHubPersistingCreateServiceInstanceAppBindingWorkflow(credHubOperations, "test-app-name"); + } + + @Test + void noBindingCredentials() { + CreateServiceInstanceBindingRequest request = CreateServiceInstanceBindingRequest + .builder() + .bindingId("foo-binding-id") + .serviceInstanceId("foo-instance-id") + .serviceDefinitionId("foo-definition-id") + .build(); + + CreateServiceInstanceAppBindingResponseBuilder responseBuilder = CreateServiceInstanceAppBindingResponse + .builder() + .bindingExisted(true) + .syslogDrainUrl("https://logs.example.com") + .volumeMounts(VolumeMount.builder().build()) + .volumeMounts(VolumeMount.builder().build()) + .volumeMounts(Arrays.asList( + VolumeMount.builder().build(), + VolumeMount.builder().build() + )); + + StepVerifier + .create(this.workflow.buildResponse(request, responseBuilder)) + .assertNext(createServiceInstanceAppBindingResponseBuilder -> { + CreateServiceInstanceAppBindingResponse response = createServiceInstanceAppBindingResponseBuilder.build(); + assertThat(response.isBindingExisted()).isEqualTo(true); + assertThat(response.getCredentials()).hasSize(0); + assertThat(response.getSyslogDrainUrl()).isEqualTo("https://logs.example.com"); + assertThat(response.getVolumeMounts()).hasSize(4); + + }) + .verifyComplete(); + + verifyZeroInteractions(this.credHubCredentialOperations); + } + + @Test + @SuppressWarnings("serial") + void storeCredentialsInCredHub() { + CreateServiceInstanceBindingRequest request = CreateServiceInstanceBindingRequest + .builder() + .bindingId("foo-binding-id") + .serviceInstanceId("foo-instance-id") + .serviceDefinitionId("foo-definition-id") + .build(); + + Map credentials = new HashMap() {{ + put("credential4", "value4"); + put("credential5", "value5"); + }}; + + CreateServiceInstanceAppBindingResponseBuilder responseBuilder = CreateServiceInstanceAppBindingResponse + .builder() + .bindingExisted(true) + .credentials("credential1", "value1") + .credentials("credential2", 2) + .credentials("credential3", true) + .credentials(credentials) + .syslogDrainUrl("https://logs.example.com") + .volumeMounts(VolumeMount.builder().build()) + .volumeMounts(VolumeMount.builder().build()) + .volumeMounts(Arrays.asList( + VolumeMount.builder().build(), + VolumeMount.builder().build() + )); + + given(this.credHubOperations.credentials()) + .willReturn(credHubCredentialOperations); + + given(this.credHubCredentialOperations.write(any())) + .willReturn(Mono.empty()); + + StepVerifier + .create(this.workflow.buildResponse(request, responseBuilder)) + .assertNext(createServiceInstanceAppBindingResponseBuilder -> { + 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.getSyslogDrainUrl()).isEqualTo("https://logs.example.com"); + assertThat(response.getVolumeMounts()).hasSize(4); + + }) + .verifyComplete(); + + verify(this.credHubCredentialOperations).write(any()); + verifyNoMoreInteractions(this.credHubCredentialOperations); + } +} \ No newline at end of file