diff --git a/build.gradle b/build.gradle index 31d9e0e..f38a0b5 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { ext { - springBootVersion = '2.0.3.RELEASE' + springBootVersion = '2.1.6.RELEASE' } repositories { mavenCentral() @@ -51,11 +51,12 @@ dependencies { compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.boot:spring-boot-starter-actuator') compile('org.springframework.boot:spring-boot-starter-security') - compile('org.springframework.hateoas:spring-hateoas:0.24.0.RELEASE') + compile('org.springframework.hateoas:spring-hateoas:0.25.1.RELEASE') + compile('org.springframework.credhub:spring-credhub-starter:2.0.1.RELEASE') runtime('org.springframework.boot:spring-boot-devtools') - compile('org.springframework.cloud:spring-cloud-starter-open-service-broker-webmvc:2.0.1.RELEASE') + compile('org.springframework.cloud:spring-cloud-starter-open-service-broker:3.0.3.RELEASE') compile('org.springframework.boot:spring-boot-starter-data-jpa') runtime('org.hsqldb:hsqldb') diff --git a/deploy/cloudfoundry/README.adoc b/deploy/cloudfoundry/README.adoc index b8c8bbf..16ea101 100644 --- a/deploy/cloudfoundry/README.adoc +++ b/deploy/cloudfoundry/README.adoc @@ -58,6 +58,70 @@ routes: bookstore-service-broker.apps.example.com #0 running 2018-02-13T21:58:44Z 0.0% 290.8M of 1G 144.7M of 1G ---- +== Deploy the service broker application with Secure Service Credential support + +The service broker does support storing credentials in Cloud Foundry`s Credhub optionally. The broker needs UAA client_credentials, which are authorized to `/c/bookstore-service-broker/bdb1be2e-360b-495c-8115-d7697f9c6a9e/*` in Credhub, to access and store credentials securely. +More information about Secure Service Credential in Cloud Foundry can be found here: https://github.com/cloudfoundry-incubator/credhub/blob/master/docs/secure-service-credentials.md + +=== Create client credentials in UAA + +---- +$ uaac target uaa. +$ uaac token client get admin -s # UAA Admin Client +$ uaac client add bookstore-service-broker --name bookstore-service-broker --authorized_grant_types client_credentials --authorities "credhub.write","credhub.read" +---- + +=== Set permissions in Credhub + +A documentation how to access Credhub in Pivotal PAS from Operations Manager can be found here: https://community.pivotal.io/s/article/how-to-login-and-access-credhub-in-pcf + +---- +$ credhub login -s credhub.service.cf.internal:8844 --client-name credhub_admin_client --client-secret --skip-tls-validation +$ credhub set-permission -a uaa-client:bookstore-service-broker -p /c/bookstore-service-broker/bdb1be2e-360b-495c-8115-d7697f9c6a9e/* -o read,write,delete,read_acl,write_acl +---- + +==== Verify Credhub permissions +---- +$ credhub login -s credhub.service.cf.internal:8844 --client-name bookstore-service-broker --client-secret --skip-tls-validation +$ credhub set -n /c/bookstore-service-broker/bdb1be2e-360b-495c-8115-d7697f9c6a9e/test -t value -v test +$ credhub delete -n '/c/bookstore-service-broker/bdb1be2e-360b-495c-8115-d7697f9c6a9e/test' +---- + +=== Push Service Broker + +Modify value of `CREDHUB_CLIENT_SECRET` in `deploy/cloudfoundry/manifest-credhub.yml` to match your chosen client_secret for the service broker. +If necessary, `CREDHUB_URL`, `CREDHUB_CLIENT_ID` and `UAA_TOKEN_URI` can be configured optionally. + +---- +$ cf push -f deploy/cloudfoundry/manifest-credhub.yml +Pushing from manifest to org sample / space test as user@example.com... +Using manifest file deploy/cloudfoundry/manifest.yml +Getting app info... +Creating app with these attributes... ++ name: bookstore-service-broker + path: build/libs/bookstore-service-broker-0.0.1.BUILD-SNAPSHOT.jar ++ memory: 1G + env: + CREDHUB_CLIENT_SECRET + SPRING_PROFILES_ACTIVE + routes: ++ bookstore-service-broker.apps.example.com + +... + +name: bookstore-service-broker +requested state: started +instances: 1/1 +usage: 1G x 1 instances +routes: bookstore-service-broker.apps.example.com + +... + + state since cpu memory disk details +#0 running 2018-02-13T21:58:44Z 0.0% 290.8M of 1G 144.7M of 1G +---- + + == Verify the service broker application Note the value of the `route` row in the output from the command above. diff --git a/deploy/cloudfoundry/manifest-credhub.yml b/deploy/cloudfoundry/manifest-credhub.yml new file mode 100644 index 0000000..c79c232 --- /dev/null +++ b/deploy/cloudfoundry/manifest-credhub.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: bookstore-service-broker + memory: 1G + path: ../../build/libs/bookstore-service-broker-0.0.1.BUILD-SNAPSHOT.jar + env: + SPRING_PROFILES_ACTIVE: cloud,credhub + CREDHUB_CLIENT_SECRET: secret diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 933b647..478d2ff 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip diff --git a/src/main/java/org/springframework/cloud/sample/bookstore/config/CredHubAutoConfiguration.java b/src/main/java/org/springframework/cloud/sample/bookstore/config/CredHubAutoConfiguration.java new file mode 100644 index 0000000..b9dfd81 --- /dev/null +++ b/src/main/java/org/springframework/cloud/sample/bookstore/config/CredHubAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.sample.bookstore.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.sample.bookstore.servicebroker.credhub.CredhubCreateServiceInstanceBinding; +import org.springframework.cloud.sample.bookstore.servicebroker.credhub.CredhubDeleteServiceInstanceBinding; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.credhub.autoconfig.CredHubTemplateAutoConfiguration; +import org.springframework.credhub.core.CredHubOperations; + +@Configuration +@AutoConfigureAfter(CredHubTemplateAutoConfiguration.class) +@ConditionalOnClass(CredHubOperations.class) +@ConditionalOnBean(CredHubOperations.class) +public class CredHubAutoConfiguration { + + @Value("${spring.application.name}") + private String appName; + + @Bean + public CredhubCreateServiceInstanceBinding credhubPersistingCreateServiceInstanceAppBindingWorkflow(CredHubOperations credHubOperations) { + return new CredhubCreateServiceInstanceBinding(credHubOperations, appName); + } + + @Bean + public CredhubDeleteServiceInstanceBinding credhubPersistingDeleteServiceInstanceAppBindingWorkflow(CredHubOperations credHubOperations) { + return new CredhubDeleteServiceInstanceBinding(credHubOperations, appName); + } + +} diff --git a/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredHubPersistingWorkflow.java b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredHubPersistingWorkflow.java new file mode 100644 index 0000000..eabe569 --- /dev/null +++ b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredHubPersistingWorkflow.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.sample.bookstore.servicebroker.credhub; + +import org.springframework.credhub.support.ServiceInstanceCredentialName; +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; + +class CredHubPersistingWorkflow { + + private static final Logger LOG = Loggers.getLogger(CredHubPersistingWorkflow.class); + + private static final String CREDENTIALS_NAME = "credentials-json"; + private final String appName; + + CredHubPersistingWorkflow(String appName) { + this.appName = appName; + } + + Mono buildCredentialName(String serviceDefinitionId, String bindingId) { + LOG.debug("Building credentials name for service_id '{}' and binding_id '{}'", serviceDefinitionId, bindingId); + return Mono.just(ServiceInstanceCredentialName.builder() + .serviceBrokerName(this.appName) + .serviceOfferingName(serviceDefinitionId) + .serviceBindingId(bindingId) + .credentialName(CREDENTIALS_NAME) + .build()); + } +} diff --git a/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredhubCreateServiceInstanceBinding.java b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredhubCreateServiceInstanceBinding.java new file mode 100644 index 0000000..672a524 --- /dev/null +++ b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredhubCreateServiceInstanceBinding.java @@ -0,0 +1,103 @@ +package org.springframework.cloud.sample.bookstore.servicebroker.credhub; + +import org.springframework.cloud.servicebroker.model.binding.BindResource; +import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceAppBindingResponse; +import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceBindingRequest; +import org.springframework.credhub.core.CredHubOperations; +import org.springframework.credhub.support.CredentialName; +import org.springframework.credhub.support.json.JsonCredentialRequest; +import org.springframework.credhub.support.permissions.Operation; +import org.springframework.credhub.support.permissions.Permission; +import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; + +public class CredhubCreateServiceInstanceBinding extends CredHubPersistingWorkflow{ + + private static final Logger LOG = Loggers.getLogger(CredhubCreateServiceInstanceBinding.class); + + private static final String CREDHUB_REF_KEY = "credhub-ref"; + + private static final String CREDENTIAL_CLIENT_ID = "credential_client_id"; + + private final CredHubOperations credHubOperations; + + public CredhubCreateServiceInstanceBinding(CredHubOperations credHubOperations, String appName) { + super(appName); + this.credHubOperations = credHubOperations; + } + + public Mono buildResponse(CreateServiceInstanceBindingRequest request, + CreateServiceInstanceAppBindingResponse.CreateServiceInstanceAppBindingResponseBuilder responseBuilder) { + return Mono.just(responseBuilder.build()) + .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 persistBindingCredentials(CreateServiceInstanceBindingRequest request, + CreateServiceInstanceAppBindingResponse response, + CredentialName credentialName) { + return writeCredential(response, credentialName) + .then(writePermissions(request, credentialName)) + .thenReturn(buildReplacementBindingResponse(response, credentialName)); + } + + private Mono writeCredential(CreateServiceInstanceAppBindingResponse response, + CredentialName credentialName) { + return Mono.fromCallable(() -> { + credHubOperations.credentials() + .write(JsonCredentialRequest.builder() + .name(credentialName) + .value(response.getCredentials()) + .build()); + return null; + }); + } + + private Mono 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 CreateServiceInstanceAppBindingResponse.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()); + } + +} diff --git a/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredhubDeleteServiceInstanceBinding.java b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredhubDeleteServiceInstanceBinding.java new file mode 100644 index 0000000..f7313e3 --- /dev/null +++ b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/credhub/CredhubDeleteServiceInstanceBinding.java @@ -0,0 +1,46 @@ +package org.springframework.cloud.sample.bookstore.servicebroker.credhub; + +import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingRequest; +import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingResponse; +import org.springframework.credhub.core.CredHubOperations; +import org.springframework.credhub.support.CredentialName; +import org.springframework.credhub.support.ServiceInstanceCredentialName; +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; + +public class CredhubDeleteServiceInstanceBinding extends CredHubPersistingWorkflow { + + private static final Logger LOG = Loggers.getLogger(CredhubDeleteServiceInstanceBinding.class); + + private final CredHubOperations credHubOperations; + + public CredhubDeleteServiceInstanceBinding(CredHubOperations credHubOperations, String appName) { + super(appName); + this.credHubOperations = credHubOperations; + } + + public Mono buildResponse(DeleteServiceInstanceBindingRequest request, DeleteServiceInstanceBindingResponse.DeleteServiceInstanceBindingResponseBuilder responseBuilder) { + LOG.debug("Preparing delete of credentials for service_id '{}' and binding_id '{}'", request.getServiceDefinitionId(), request.getBindingId()); + + Mono credentialNameMono = buildCredentialName(request.getServiceDefinitionId(), request.getBindingId()); + return credentialNameMono + .filter(this::credentialExists) + .map(this::deleteBindingCredentials) + .thenReturn(responseBuilder); + } + + private boolean credentialExists(CredentialName credentialName) { + if (credentialName == null) { + return false; + } + return !credHubOperations.credentials().findByName(credentialName).isEmpty(); + } + + private Mono deleteBindingCredentials(CredentialName credentialName) { + LOG.debug("Deleting credentials with name '{}'", credentialName.getName()); + credHubOperations.credentials().deleteByName(credentialName); + return Mono.never(); + } + +} diff --git a/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookStoreServiceInstanceBindingService.java b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookStoreServiceInstanceBindingService.java index 41a2540..15a195b 100644 --- a/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookStoreServiceInstanceBindingService.java +++ b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookStoreServiceInstanceBindingService.java @@ -16,32 +16,31 @@ package org.springframework.cloud.sample.bookstore.servicebroker.service; -import org.springframework.cloud.sample.bookstore.web.model.ApplicationInformation; +import org.springframework.cloud.sample.bookstore.servicebroker.credhub.CredhubCreateServiceInstanceBinding; +import org.springframework.cloud.sample.bookstore.servicebroker.credhub.CredhubDeleteServiceInstanceBinding; import org.springframework.cloud.sample.bookstore.servicebroker.model.ServiceBinding; import org.springframework.cloud.sample.bookstore.servicebroker.repository.ServiceBindingRepository; +import org.springframework.cloud.sample.bookstore.web.model.ApplicationInformation; import org.springframework.cloud.sample.bookstore.web.model.User; import org.springframework.cloud.sample.bookstore.web.service.UserService; import org.springframework.cloud.servicebroker.exception.ServiceInstanceBindingDoesNotExistException; -import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceAppBindingResponse; +import org.springframework.cloud.servicebroker.model.binding.*; import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceAppBindingResponse.CreateServiceInstanceAppBindingResponseBuilder; -import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceBindingRequest; -import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceBindingResponse; -import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingRequest; -import org.springframework.cloud.servicebroker.model.binding.GetServiceInstanceAppBindingResponse; -import org.springframework.cloud.servicebroker.model.binding.GetServiceInstanceBindingRequest; -import org.springframework.cloud.servicebroker.model.binding.GetServiceInstanceBindingResponse; +import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingResponse.DeleteServiceInstanceBindingResponseBuilder; import org.springframework.cloud.servicebroker.service.ServiceInstanceBindingService; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import static org.springframework.cloud.sample.bookstore.web.security.SecurityAuthorities.FULL_ACCESS; import static org.springframework.cloud.sample.bookstore.web.security.SecurityAuthorities.BOOK_STORE_ID_PREFIX; +import static org.springframework.cloud.sample.bookstore.web.security.SecurityAuthorities.FULL_ACCESS; @Service +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class BookStoreServiceInstanceBindingService implements ServiceInstanceBindingService { private static final String URI_KEY = "uri"; private static final String USERNAME_KEY = "username"; @@ -51,62 +50,79 @@ public class BookStoreServiceInstanceBindingService implements ServiceInstanceBi private final UserService userService; private final ApplicationInformation applicationInformation; + private final Optional credhubCreate; + private final Optional credhubDelete; + public BookStoreServiceInstanceBindingService(ServiceBindingRepository bindingRepository, UserService userService, - ApplicationInformation applicationInformation) { + ApplicationInformation applicationInformation, + Optional credhubCreate, + Optional credhubDelete) { this.bindingRepository = bindingRepository; this.userService = userService; this.applicationInformation = applicationInformation; + this.credhubCreate = credhubCreate; + this.credhubDelete = credhubDelete; } @Override - public CreateServiceInstanceBindingResponse createServiceInstanceBinding(CreateServiceInstanceBindingRequest request) { + public Mono createServiceInstanceBinding(CreateServiceInstanceBindingRequest request) { CreateServiceInstanceAppBindingResponseBuilder responseBuilder = - CreateServiceInstanceAppBindingResponse.builder(); + CreateServiceInstanceAppBindingResponse.builder(); - Optional binding = bindingRepository.findById(request.getBindingId()); + Optional bindingRecord = bindingRepository.findById(request.getBindingId()); - if (binding.isPresent()) { - responseBuilder - .bindingExisted(true) - .credentials(binding.get().getCredentials()); + if (bindingRecord.isPresent()) { + responseBuilder.bindingExisted(true).credentials(bindingRecord.get().getCredentials()); + return Mono.just(responseBuilder.build()); } else { User user = createUser(request); Map credentials = buildCredentials(request.getServiceInstanceId(), user); - saveBinding(request, credentials); - - responseBuilder - .bindingExisted(false) - .credentials(credentials); + responseBuilder.bindingExisted(false).credentials(credentials); + + return this.credhubCreate.map(credhub -> credhub.buildResponse(request, responseBuilder) + .map(convertedBuilder -> + { + CreateServiceInstanceAppBindingResponse response = convertedBuilder.build(); + saveBinding(request, response.getCredentials()); + return (CreateServiceInstanceBindingResponse) response; + }) + ).orElseGet(() -> { + saveBinding(request, credentials); + return Mono.just(responseBuilder.build()); + }); } - - return responseBuilder.build(); } @Override - public GetServiceInstanceBindingResponse getServiceInstanceBinding(GetServiceInstanceBindingRequest request) { + public Mono getServiceInstanceBinding(GetServiceInstanceBindingRequest request) { String bindingId = request.getBindingId(); - Optional serviceBinding = bindingRepository.findById(bindingId); + Optional bindingRecord = bindingRepository.findById(bindingId); - if (serviceBinding.isPresent()) { - return GetServiceInstanceAppBindingResponse.builder() - .parameters(serviceBinding.get().getParameters()) - .credentials(serviceBinding.get().getCredentials()) - .build(); + if (bindingRecord.isPresent()) { + return Mono.just(GetServiceInstanceAppBindingResponse.builder() + .parameters(bindingRecord.get().getParameters()) + .credentials(bindingRecord.get().getCredentials()) + .build()); } else { throw new ServiceInstanceBindingDoesNotExistException(bindingId); } } @Override - public void deleteServiceInstanceBinding(DeleteServiceInstanceBindingRequest request) { + public Mono deleteServiceInstanceBinding(DeleteServiceInstanceBindingRequest request) { String bindingId = request.getBindingId(); + DeleteServiceInstanceBindingResponseBuilder builder = DeleteServiceInstanceBindingResponse.builder(); if (bindingRepository.existsById(bindingId)) { bindingRepository.deleteById(bindingId); userService.deleteUser(bindingId); + return this.credhubDelete + .map(credhub -> credhub.buildResponse(request, builder)) + .orElseGet(() -> Mono.just(DeleteServiceInstanceBindingResponse.builder())) + .map(DeleteServiceInstanceBindingResponseBuilder::build); } else { throw new ServiceInstanceBindingDoesNotExistException(bindingId); } @@ -114,7 +130,7 @@ public void deleteServiceInstanceBinding(DeleteServiceInstanceBindingRequest req private User createUser(CreateServiceInstanceBindingRequest request) { return userService.createUser(request.getBindingId(), - FULL_ACCESS, BOOK_STORE_ID_PREFIX + request.getServiceInstanceId()); + FULL_ACCESS, BOOK_STORE_ID_PREFIX + request.getServiceInstanceId()); } private Map buildCredentials(String instanceId, User user) { @@ -129,15 +145,15 @@ private Map buildCredentials(String instanceId, User user) { private String buildUri(String instanceId) { return UriComponentsBuilder - .fromUriString(applicationInformation.getBaseUrl()) - .pathSegment("bookstores", instanceId) - .build() - .toUriString(); + .fromUriString(applicationInformation.getBaseUrl()) + .pathSegment("bookstores", instanceId) + .build() + .toUriString(); } private void saveBinding(CreateServiceInstanceBindingRequest request, Map credentials) { ServiceBinding serviceBinding = - new ServiceBinding(request.getBindingId(), request.getParameters(), credentials); + new ServiceBinding(request.getBindingId(), request.getParameters(), credentials); bindingRepository.save(serviceBinding); } -} \ No newline at end of file +} diff --git a/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookStoreServiceInstanceService.java b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookStoreServiceInstanceService.java index 96ced34..25a6880 100644 --- a/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookStoreServiceInstanceService.java +++ b/src/main/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookStoreServiceInstanceService.java @@ -29,6 +29,7 @@ import org.springframework.cloud.servicebroker.model.instance.GetServiceInstanceResponse; import org.springframework.cloud.servicebroker.service.ServiceInstanceService; import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; import java.util.Optional; @@ -43,7 +44,7 @@ public BookStoreServiceInstanceService(BookStoreService storeService, ServiceIns } @Override - public CreateServiceInstanceResponse createServiceInstance(CreateServiceInstanceRequest request) { + public Mono createServiceInstance(CreateServiceInstanceRequest request) { String instanceId = request.getServiceInstanceId(); CreateServiceInstanceResponseBuilder responseBuilder = CreateServiceInstanceResponse.builder(); @@ -56,35 +57,35 @@ public CreateServiceInstanceResponse createServiceInstance(CreateServiceInstance saveInstance(request, instanceId); } - return responseBuilder.build(); + return Mono.just(responseBuilder.build()); } @Override - public GetServiceInstanceResponse getServiceInstance(GetServiceInstanceRequest request) { + public Mono getServiceInstance(GetServiceInstanceRequest request) { String instanceId = request.getServiceInstanceId(); Optional serviceInstance = instanceRepository.findById(instanceId); if (serviceInstance.isPresent()) { - return GetServiceInstanceResponse.builder() + return Mono.just(GetServiceInstanceResponse.builder() .serviceDefinitionId(serviceInstance.get().getServiceDefinitionId()) .planId(serviceInstance.get().getPlanId()) .parameters(serviceInstance.get().getParameters()) - .build(); + .build()); } else { throw new ServiceInstanceDoesNotExistException(instanceId); } } @Override - public DeleteServiceInstanceResponse deleteServiceInstance(DeleteServiceInstanceRequest request) { + public Mono deleteServiceInstance(DeleteServiceInstanceRequest request) { String instanceId = request.getServiceInstanceId(); if (instanceRepository.existsById(instanceId)) { storeService.deleteBookStore(instanceId); instanceRepository.deleteById(instanceId); - return DeleteServiceInstanceResponse.builder().build(); + return Mono.just(DeleteServiceInstanceResponse.builder().build()); } else { throw new ServiceInstanceDoesNotExistException(instanceId); } diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..3bec598 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.sample.bookstore.config.CredHubAutoConfiguration diff --git a/src/main/resources/application-credhub.yml b/src/main/resources/application-credhub.yml new file mode 100644 index 0000000..762b4a1 --- /dev/null +++ b/src/main/resources/application-credhub.yml @@ -0,0 +1,17 @@ +spring: + credhub: + url: ${CREDHUB_URL:https://credhub.service.cf.internal:8844} + oauth2: + registration-id: credhub-client + security: + oauth2: + client: + registration: + credhub-client: + provider: uaa + client-id: ${CREDHUB_CLIENT_ID:bookstore-service-broker} + client-secret: ${CREDHUB_CLIENT_SECRET:password} + authorization-grant-type: client_credentials + provider: + uaa: + token-uri: ${UAA_TOKEN_URI:https://uaa.service.cf.internal:8443/oauth/token} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7434bc9..99a5867 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,7 +9,8 @@ spring: hibernate: ddl-auto: update show-sql: true - + application: + name: bookstore-service-broker --- spring: profiles: @@ -17,4 +18,4 @@ spring: datasource: url: ${vcap.services.broker-db.credentials.jdbcurl:jdbc:hsqldb:mem:broker-db} username: ${vcap.services.broker-db.credentials.username:sa} - password: ${vcap.services.broker-db.credentials.password:} \ No newline at end of file + password: ${vcap.services.broker-db.credentials.password:} diff --git a/src/test/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookstoreServiceInstanceBindingServiceTests.java b/src/test/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookstoreServiceInstanceBindingServiceTests.java index 7fc008d..627c1d7 100644 --- a/src/test/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookstoreServiceInstanceBindingServiceTests.java +++ b/src/test/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookstoreServiceInstanceBindingServiceTests.java @@ -16,40 +16,44 @@ package org.springframework.cloud.sample.bookstore.servicebroker.service; +import org.assertj.core.util.Lists; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.springframework.cloud.sample.bookstore.web.model.ApplicationInformation; +import org.springframework.cloud.sample.bookstore.servicebroker.credhub.CredhubCreateServiceInstanceBinding; +import org.springframework.cloud.sample.bookstore.servicebroker.credhub.CredhubDeleteServiceInstanceBinding; import org.springframework.cloud.sample.bookstore.servicebroker.model.ServiceBinding; import org.springframework.cloud.sample.bookstore.servicebroker.repository.ServiceBindingRepository; +import org.springframework.cloud.sample.bookstore.web.model.ApplicationInformation; import org.springframework.cloud.sample.bookstore.web.model.User; import org.springframework.cloud.sample.bookstore.web.service.UserService; import org.springframework.cloud.servicebroker.exception.ServiceInstanceBindingDoesNotExistException; -import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceAppBindingResponse; -import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceBindingRequest; -import org.springframework.cloud.servicebroker.model.binding.CreateServiceInstanceBindingResponse; -import org.springframework.cloud.servicebroker.model.binding.DeleteServiceInstanceBindingRequest; -import org.springframework.cloud.servicebroker.model.binding.GetServiceInstanceAppBindingResponse; -import org.springframework.cloud.servicebroker.model.binding.GetServiceInstanceBindingRequest; -import org.springframework.cloud.servicebroker.model.binding.GetServiceInstanceBindingResponse; +import org.springframework.cloud.servicebroker.model.binding.*; +import org.springframework.credhub.core.CredHubOperations; +import org.springframework.credhub.core.credential.CredHubCredentialOperations; +import org.springframework.credhub.core.permissionV2.CredHubPermissionV2Operations; +import org.springframework.credhub.support.CredentialDetails; +import org.springframework.credhub.support.CredentialName; +import org.springframework.credhub.support.ServiceInstanceCredentialName; import java.util.HashMap; import java.util.Map; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static org.springframework.cloud.sample.bookstore.web.security.SecurityAuthorities.BOOK_STORE_ID_PREFIX; import static org.springframework.cloud.sample.bookstore.web.security.SecurityAuthorities.FULL_ACCESS; public class BookstoreServiceInstanceBindingServiceTests { + private static final String APP_NAME = "test-app"; private static final String SERVICE_INSTANCE_ID = "instance-id"; private static final String SERVICE_BINDING_ID = "binding-id"; private static final String BASE_URL = "https://localhost:8080"; + private static final String SERVICE_DEFINITION_ID = "service-definition-id"; + private static final String CREDENTIAL_NAME = "credentials-json"; @Mock private ServiceBindingRepository repository; @@ -57,7 +61,17 @@ public class BookstoreServiceInstanceBindingServiceTests { @Mock private UserService userService; + @Mock + private CredHubOperations credHubOperations; + + @Mock + private CredHubCredentialOperations credHubCredentialOperations; + + @Mock + private CredHubPermissionV2Operations credHubPermissionV2Operations; + private BookStoreServiceInstanceBindingService service; + private BookStoreServiceInstanceBindingService credHubEnabledService; private final Map credentials = new HashMap() {{ put("uri", "https://example.com"); @@ -68,26 +82,32 @@ public class BookstoreServiceInstanceBindingServiceTests { @Before public void setUp() { initMocks(this); + when(credHubOperations.credentials()).thenReturn(credHubCredentialOperations); + when(credHubOperations.permissionsV2()).thenReturn(credHubPermissionV2Operations); ApplicationInformation appInfo = new ApplicationInformation(BASE_URL); - service = new BookStoreServiceInstanceBindingService(repository, userService, appInfo); + service = new BookStoreServiceInstanceBindingService(repository, userService, appInfo, Optional.empty(), Optional.empty()); + + CredhubCreateServiceInstanceBinding credHubCreate = new CredhubCreateServiceInstanceBinding(credHubOperations, APP_NAME); + CredhubDeleteServiceInstanceBinding credHubDelete = new CredhubDeleteServiceInstanceBinding(credHubOperations, APP_NAME); + credHubEnabledService = new BookStoreServiceInstanceBindingService(repository, userService, appInfo, Optional.of(credHubCreate), Optional.of(credHubDelete)); } @Test public void createBindingWhenBindingDoesNotExist() { when(repository.findById(SERVICE_BINDING_ID)) - .thenReturn(Optional.empty()); + .thenReturn(Optional.empty()); when(userService.createUser(SERVICE_BINDING_ID, FULL_ACCESS, BOOK_STORE_ID_PREFIX + SERVICE_INSTANCE_ID)) .thenReturn(new User(SERVICE_BINDING_ID, "password", FULL_ACCESS, BOOK_STORE_ID_PREFIX + SERVICE_INSTANCE_ID)); CreateServiceInstanceBindingRequest request = CreateServiceInstanceBindingRequest.builder() - .serviceInstanceId(SERVICE_INSTANCE_ID) - .bindingId(SERVICE_BINDING_ID) - .build(); + .serviceInstanceId(SERVICE_INSTANCE_ID) + .bindingId(SERVICE_BINDING_ID) + .build(); - CreateServiceInstanceBindingResponse response = service.createServiceInstanceBinding(request); + CreateServiceInstanceBindingResponse response = service.createServiceInstanceBinding(request).block(); assertThat(response).isInstanceOf(CreateServiceInstanceAppBindingResponse.class); @@ -96,8 +116,8 @@ public void createBindingWhenBindingDoesNotExist() { Map credentials = appResponse.getCredentials(); assertThat(credentials) - .hasSize(3) - .containsOnlyKeys("uri", "username", "password"); + .hasSize(3) + .containsOnlyKeys("uri", "username", "password"); assertThat(credentials.get("uri").toString()) .startsWith(BASE_URL) @@ -119,14 +139,14 @@ public void createBindingWhenBindingExists() { ServiceBinding binding = new ServiceBinding(SERVICE_BINDING_ID, null, credentials); when(repository.findById(SERVICE_BINDING_ID)) - .thenReturn(Optional.of(binding)); + .thenReturn(Optional.of(binding)); CreateServiceInstanceBindingRequest request = CreateServiceInstanceBindingRequest.builder() - .serviceInstanceId(SERVICE_INSTANCE_ID) - .bindingId(SERVICE_BINDING_ID) - .build(); + .serviceInstanceId(SERVICE_INSTANCE_ID) + .bindingId(SERVICE_BINDING_ID) + .build(); - CreateServiceInstanceBindingResponse response = service.createServiceInstanceBinding(request); + CreateServiceInstanceBindingResponse response = service.createServiceInstanceBinding(request).block(); assertThat(response).isInstanceOf(CreateServiceInstanceAppBindingResponse.class); @@ -139,19 +159,58 @@ public void createBindingWhenBindingExists() { verifyNoMoreInteractions(repository); } + @Test + public void createBindingWithCredhubWhenBindingDoesNotExist() { + when(repository.findById(SERVICE_BINDING_ID)).thenReturn(Optional.empty()); + + when(userService.createUser(SERVICE_BINDING_ID, FULL_ACCESS, BOOK_STORE_ID_PREFIX + SERVICE_INSTANCE_ID)) + .thenReturn(new User(SERVICE_BINDING_ID, "password", FULL_ACCESS, BOOK_STORE_ID_PREFIX + SERVICE_INSTANCE_ID)); + + CreateServiceInstanceBindingRequest request = CreateServiceInstanceBindingRequest.builder() + .serviceDefinitionId(SERVICE_DEFINITION_ID) + .serviceInstanceId(SERVICE_INSTANCE_ID) + .bindingId(SERVICE_BINDING_ID) + .bindResource(BindResource.builder().appGuid("app-guid").properties("credential_client_id", "moo").build()) + .build(); + + CreateServiceInstanceBindingResponse response = credHubEnabledService.createServiceInstanceBinding(request).block(); + + assertThat(response).isInstanceOf(CreateServiceInstanceAppBindingResponse.class); + + CreateServiceInstanceAppBindingResponse appResponse = (CreateServiceInstanceAppBindingResponse) response; + assertThat(appResponse.isBindingExisted()).isFalse(); + + Map credentials = appResponse.getCredentials(); + + assertThat(credentials).hasSize(1).containsOnlyKeys("credhub-ref"); + assertThat(credentials.get("credhub-ref").toString()).isEqualTo("/c/" + APP_NAME + "/" + SERVICE_DEFINITION_ID + "/" + SERVICE_BINDING_ID + "/" + CREDENTIAL_NAME); + + verify(repository).findById(SERVICE_BINDING_ID); + verify(credHubOperations, times(1)).credentials(); + verify(credHubOperations, times(2)).permissionsV2(); + + ArgumentCaptor repositoryCaptor = ArgumentCaptor.forClass(ServiceBinding.class); + verify(repository).save(repositoryCaptor.capture()); + ServiceBinding actualBinding = repositoryCaptor.getValue(); + assertThat(actualBinding.getBindingId()).isEqualTo(SERVICE_BINDING_ID); + assertThat(actualBinding.getCredentials()).isEqualTo(credentials); + + verifyNoMoreInteractions(repository); + } + @Test public void getBindingWhenBindingExists() { HashMap parameters = new HashMap<>(); ServiceBinding serviceBinding = new ServiceBinding(SERVICE_BINDING_ID, parameters, credentials); when(repository.findById(SERVICE_BINDING_ID)) - .thenReturn(Optional.of(serviceBinding)); + .thenReturn(Optional.of(serviceBinding)); GetServiceInstanceBindingRequest request = GetServiceInstanceBindingRequest.builder() - .bindingId(SERVICE_BINDING_ID) - .build(); + .bindingId(SERVICE_BINDING_ID) + .build(); - GetServiceInstanceBindingResponse response = service.getServiceInstanceBinding(request); + GetServiceInstanceBindingResponse response = service.getServiceInstanceBinding(request).block(); assertThat(response).isInstanceOf(GetServiceInstanceAppBindingResponse.class); @@ -167,26 +226,58 @@ public void getBindingWhenBindingExists() { @Test(expected = ServiceInstanceBindingDoesNotExistException.class) public void getBindingWhenBindingDoesNotExist() { when(repository.findById(SERVICE_BINDING_ID)) - .thenReturn(Optional.empty()); + .thenReturn(Optional.empty()); GetServiceInstanceBindingRequest request = GetServiceInstanceBindingRequest.builder() - .bindingId(SERVICE_BINDING_ID) - .build(); + .bindingId(SERVICE_BINDING_ID) + .build(); - service.getServiceInstanceBinding(request); + service.getServiceInstanceBinding(request).block(); } @Test public void deleteBindingWhenBindingExists() { when(repository.existsById(SERVICE_BINDING_ID)) - .thenReturn(true); + .thenReturn(true); DeleteServiceInstanceBindingRequest request = DeleteServiceInstanceBindingRequest.builder() - .serviceInstanceId(SERVICE_INSTANCE_ID) - .bindingId(SERVICE_BINDING_ID) - .build(); + .serviceInstanceId(SERVICE_INSTANCE_ID) + .bindingId(SERVICE_BINDING_ID) + .build(); + + service.deleteServiceInstanceBinding(request).block(); + + verify(repository).existsById(SERVICE_BINDING_ID); + verify(repository).deleteById(SERVICE_BINDING_ID); + verifyNoMoreInteractions(repository); + + verify(userService).deleteUser(SERVICE_BINDING_ID); + verifyNoMoreInteractions(userService); + } + + @Test + public void deleteBindingWithCredHubWhenBindingExists() { + when(repository.existsById(SERVICE_BINDING_ID)).thenReturn(true); + when(credHubCredentialOperations.findByName(any())).thenReturn(Lists.list(new CredentialDetails<>())); + + DeleteServiceInstanceBindingRequest request = DeleteServiceInstanceBindingRequest.builder() + .serviceDefinitionId(SERVICE_DEFINITION_ID) + .serviceInstanceId(SERVICE_INSTANCE_ID) + .bindingId(SERVICE_BINDING_ID) + .build(); + + CredentialName name = ServiceInstanceCredentialName.builder() + .serviceBrokerName(APP_NAME) + .serviceOfferingName(SERVICE_DEFINITION_ID) + .serviceBindingId(SERVICE_BINDING_ID) + .credentialName(CREDENTIAL_NAME) + .build(); + + credHubEnabledService.deleteServiceInstanceBinding(request).block(); - service.deleteServiceInstanceBinding(request); + verify(credHubOperations, times(2)).credentials(); + verify(credHubCredentialOperations, times(1)).findByName(name); + verify(credHubCredentialOperations, times(1)).deleteByName(name); verify(repository).existsById(SERVICE_BINDING_ID); verify(repository).deleteById(SERVICE_BINDING_ID); @@ -199,13 +290,13 @@ public void deleteBindingWhenBindingExists() { @Test(expected = ServiceInstanceBindingDoesNotExistException.class) public void deleteBindingWhenBindingDoesNotExist() { when(repository.existsById(SERVICE_BINDING_ID)) - .thenReturn(false); + .thenReturn(false); DeleteServiceInstanceBindingRequest request = DeleteServiceInstanceBindingRequest.builder() - .serviceInstanceId(SERVICE_INSTANCE_ID) - .bindingId(SERVICE_BINDING_ID) - .build(); + .serviceInstanceId(SERVICE_INSTANCE_ID) + .bindingId(SERVICE_BINDING_ID) + .build(); - service.deleteServiceInstanceBinding(request); + service.deleteServiceInstanceBinding(request).block(); } -} \ No newline at end of file +} diff --git a/src/test/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookstoreServiceInstanceServiceTests.java b/src/test/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookstoreServiceInstanceServiceTests.java index 38bc937..410d072 100644 --- a/src/test/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookstoreServiceInstanceServiceTests.java +++ b/src/test/java/org/springframework/cloud/sample/bookstore/servicebroker/service/BookstoreServiceInstanceServiceTests.java @@ -74,7 +74,7 @@ public void createServiceInstanceWhenInstanceExists() { .serviceInstanceId(SERVICE_INSTANCE_ID) .build(); - CreateServiceInstanceResponse response = service.createServiceInstance(request); + CreateServiceInstanceResponse response = service.createServiceInstance(request).block(); assertThat(response.isInstanceExisted()).isTrue(); assertThat(response.getDashboardUrl()).isNull(); @@ -101,7 +101,7 @@ public void createServiceInstanceWhenInstanceDoesNotExist() { .context(context) .build(); - CreateServiceInstanceResponse response = service.createServiceInstance(request); + CreateServiceInstanceResponse response = service.createServiceInstance(request).block(); assertThat(response.isInstanceExisted()).isFalse(); assertThat(response.getDashboardUrl()).isNull(); @@ -132,7 +132,7 @@ public void getServiceInstanceWhenInstanceExists() { .serviceInstanceId(SERVICE_INSTANCE_ID) .build(); - GetServiceInstanceResponse response = service.getServiceInstance(request); + GetServiceInstanceResponse response = service.getServiceInstance(request).block(); assertThat(response.getServiceDefinitionId()).isEqualTo(serviceInstance.getServiceDefinitionId()); assertThat(response.getPlanId()).isEqualTo(serviceInstance.getPlanId()); @@ -163,7 +163,7 @@ public void deleteServiceInstanceWhenInstanceExists() { .serviceInstanceId(SERVICE_INSTANCE_ID) .build(); - DeleteServiceInstanceResponse response = service.deleteServiceInstance(request); + DeleteServiceInstanceResponse response = service.deleteServiceInstance(request).block(); assertThat(response.isAsync()).isFalse(); assertThat(response.getOperation()).isNull();