From 5a8930af9db42217cb5cd21b4f97f645a954fbe5 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 27 Aug 2015 10:18:06 +0100 Subject: [PATCH] Move VCAP_SERVICES processing over from spring-cloud-security --- README.adoc | 21 ++-- docs/src/main/asciidoc/intro.adoc | 14 ++- .../asciidoc/spring-cloud-cloudfoundry.adoc | 14 +++ .../CloudFoundryAutoConfigurationTest.java | 4 +- ...ceCredentialsEnvironmentPostProcessor.java | 118 ++++++++++++++++++ ...dentialsEnvironmentPostProcessorTests.java | 94 ++++++++++++++ 6 files changed, 252 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/environment/VcapServiceCredentialsEnvironmentPostProcessor.java create mode 100644 spring-cloud-cloudfoundry-web/src/test/java/org/springframework/cloud/cloudfoundry/environment/VcapServiceCredentialsEnvironmentPostProcessorTests.java diff --git a/README.adoc b/README.adoc index e7b01f9..2c719f5 100644 --- a/README.adoc +++ b/README.adoc @@ -8,10 +8,17 @@ middlware that you "bind" to an app, essentially providing it with an environment variable containing credentials (e.g. the location and username to use for the service). -Add this project as a dependency to any Spring Cloud UI app or REST -service and deploy to Cloudfoundry. If you use Spring Cloud Security -OAuth2 features this will make them bindable to Cloud Foundry services -instead of enironment properties in `spring.oauth2.*`. For a UI app you can -declare `@EnableOAuth2Sso` and bind to a service called "sso", and for -a service you can add `@EnableOAuth2Resource` and bind to a service -called "resource" (see below for how to change the names). \ No newline at end of file +The `spring-cloud-cloudfoundry-web` project provides basic support for +some enhanced features of webapps in Cloud Foundry: binding +automatically to single-sign-on services and optionally enabling +sticky routing for discovery. + +The `spring-cloud-cloudfoundry-discovery` project provides an +implementation of Spring Cloud Commons `DiscoveryClient` so you can +`@EnableDiscoveryClient` and provide your credentials as +`spring.cloud.cloudfoundry.discovery.[email,password]` and then you +can use the `DiscoveryClient` directly or via a `LoadBalancerClient` +(also `*.url` if you are not connecting to [Pivotal Web +Services](https://run.pivotal.io)). + +> NOTE: if you are looking for a way to bind to services then this is the wrong library. Check out the [Spring Cloud Connectors](https://github.com/spring-cloud/spring-cloud-connectors) instead. \ No newline at end of file diff --git a/docs/src/main/asciidoc/intro.adoc b/docs/src/main/asciidoc/intro.adoc index a910fdb..23046cf 100644 --- a/docs/src/main/asciidoc/intro.adoc +++ b/docs/src/main/asciidoc/intro.adoc @@ -6,7 +6,15 @@ middlware that you "bind" to an app, essentially providing it with an environment variable containing credentials (e.g. the location and username to use for the service). -This project provides an implementation of Spring Cloud Commons `DiscoveryClient` so you can `@EnableDiscoveryClient` -and provide your credentials as `spring.cloud.cloudfoundry.discovery.[email,password]` and then you can use the -`DiscoveryClient` directly or via a `LoadBalancerClient` (also `*.url` if you are not connecting to [Pivotal Web +The `spring-cloud-cloudfoundry-web` project provides basic support for +some enhanced features of webapps in Cloud Foundry: binding +automatically to single-sign-on services and optionally enabling +sticky routing for discovery. + +The `spring-cloud-cloudfoundry-discovery` project provides an +implementation of Spring Cloud Commons `DiscoveryClient` so you can +`@EnableDiscoveryClient` and provide your credentials as +`spring.cloud.cloudfoundry.discovery.[email,password]` and then you +can use the `DiscoveryClient` directly or via a `LoadBalancerClient` +(also `*.url` if you are not connecting to [Pivotal Web Services](https://run.pivotal.io)). \ No newline at end of file diff --git a/docs/src/main/asciidoc/spring-cloud-cloudfoundry.adoc b/docs/src/main/asciidoc/spring-cloud-cloudfoundry.adoc index 01eaa77..3c2f170 100644 --- a/docs/src/main/asciidoc/spring-cloud-cloudfoundry.adoc +++ b/docs/src/main/asciidoc/spring-cloud-cloudfoundry.adoc @@ -9,3 +9,17 @@ the fact that it has to get an access token from Cloud Foundry. include::quickstart.adoc[] +=== Single Sign On + +NOTE: All of the OAuth2 SSO and resource server features moved to Spring Boot +in version 1.3. You can find documentation in the +http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/[Spring Boot user guide]. + +This project provides automatic binding from CloudFoundry service +credentials to the Spring Boot features. If you have a CloudFoundry +service called "sso", for instance, with credentials containing +"client_id", "client_secret" and "auth_domain", it will bind +automatically to the Spring OAuth2 client that you enable with +`@EnableOAuth2Sso` (from Spring Boot). The name of the service can be +parameterized using `spring.oauth2.sso.serviceId`. + diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAutoConfigurationTest.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAutoConfigurationTest.java index 80dfc99..9cc144c 100644 --- a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAutoConfigurationTest.java +++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAutoConfigurationTest.java @@ -32,8 +32,6 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.util.Collections; - /** * @author Josh Long */ @@ -60,7 +58,7 @@ public class CloudFoundryAutoConfigurationTest { + "\"state_timestamp\":1431028810}"; this.context = new SpringApplicationBuilder() - .properties(Collections.singletonMap("VCAP_APPLICATION", vcapAppl)) + .properties("VCAP_APPLICATION:"+vcapAppl, "server.port=0") .sources(SimpleConfiguration.class).run(); } diff --git a/spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/environment/VcapServiceCredentialsEnvironmentPostProcessor.java b/spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/environment/VcapServiceCredentialsEnvironmentPostProcessor.java new file mode 100644 index 0000000..f4ade56 --- /dev/null +++ b/spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/environment/VcapServiceCredentialsEnvironmentPostProcessor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2014 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.cloudfoundry.environment; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.config.ConfigFileEnvironmentPostProcessor; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.StringUtils; + +/** + * @author Dave Syer + * + */ +public class VcapServiceCredentialsEnvironmentPostProcessor +implements EnvironmentPostProcessor, Ordered { + + // After VcapEnvironmentPostProcessor and ConfigFileEnvironmentPostProcessor so + // values here can + // use those ones + private int order = ConfigFileEnvironmentPostProcessor.DEFAULT_ORDER + 1; + + @Override + public int getOrder() { + return this.order; + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, + SpringApplication application) { + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment); + Map properties = resolver.getSubProperties("vcap.services."); + if (properties == null || properties.isEmpty()) { + return; + } + Map source = new HashMap(); + String serviceId; + if (!resolver.getSubProperties("security.oauth2.resource.").isEmpty()) { + serviceId = resolver.getProperty("security.oauth2.resource.serviceId", + "resource"); + } + else { + serviceId = resolver.getProperty("security.oauth2.sso.serviceId", "sso"); + } + String authDomain = (String) properties + .get(serviceId + ".credentials.auth_domain"); + if (authDomain != null) { + source.put("security.oauth2.resource.userInfoUri", + authDomain + "/userinfo"); + source.put("security.oauth2.resource.keyUri", authDomain + "/token_key"); + source.put("security.oauth2.resource.accessTokenUri", + authDomain + "/oauth/token"); + source.put("security.oauth2.resource.authorizationUri", + authDomain + "/oauth/authorization"); + } + else { + addProperty(source, resolver, serviceId, "resource", "userInfoUri"); + addProperty(source, resolver, serviceId, "resource", "tokenInfoUri"); + addProperty(source, resolver, serviceId, "resource", "keyUri"); + addProperty(source, resolver, serviceId, "resource", "keyValue"); + addProperty(source, resolver, serviceId, "client", "accessTokenUri", "tokenUri"); + addProperty(source, resolver, serviceId, "client", "authorizationUri"); + } + addProperty(source, resolver, serviceId, "client", "clientId"); + addProperty(source, resolver, serviceId, "client", "clientSecret"); + addProperty(source, resolver, serviceId, "client", "scope"); + String resourceId = resolver + .getProperty("vcap.services." + serviceId + ".credentials.id", ""); + if (StringUtils.hasText(resourceId)) { + source.put("security.oauth2.resource.id", resourceId); + } + environment.getPropertySources() + .addLast(new MapPropertySource("cloudDefaultSecurityBindings", source)); + } + + private void addProperty(Map source, + RelaxedPropertyResolver resolver, String serviceId, String stem, String key, String... altKeys) { + String value = resolve(resolver, serviceId, key); + if (StringUtils.hasText(value)) { + source.put("security.oauth2."+stem+"." + key, value); + return; + } + for (String altKey : altKeys) { + value = resolve(resolver, serviceId, altKey); + if (StringUtils.hasText(value)) { + source.put("security.oauth2."+stem+"." + key, value); + return; + } + } + } + + private String resolve(RelaxedPropertyResolver resolver, String serviceId, + String key) { + return resolver.getProperty( + String.format("vcap.services.%s.credentials.%s", serviceId, key), + ""); + } + +} diff --git a/spring-cloud-cloudfoundry-web/src/test/java/org/springframework/cloud/cloudfoundry/environment/VcapServiceCredentialsEnvironmentPostProcessorTests.java b/spring-cloud-cloudfoundry-web/src/test/java/org/springframework/cloud/cloudfoundry/environment/VcapServiceCredentialsEnvironmentPostProcessorTests.java new file mode 100644 index 0000000..f89bd7c --- /dev/null +++ b/spring-cloud-cloudfoundry-web/src/test/java/org/springframework/cloud/cloudfoundry/environment/VcapServiceCredentialsEnvironmentPostProcessorTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2014 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.cloudfoundry.environment; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; + +/** + * @author Dave Syer + * + */ +public class VcapServiceCredentialsEnvironmentPostProcessorTests { + + private VcapServiceCredentialsEnvironmentPostProcessor listener = new VcapServiceCredentialsEnvironmentPostProcessor(); + + private ConfigurableEnvironment environment = new StandardEnvironment(); + + @Test + public void noop() { + this.listener.postProcessEnvironment(this.environment, new SpringApplication()); + Map properties = new RelaxedPropertyResolver(this.environment) + .getSubProperties("security.oauth2"); + assertTrue(properties == null || properties.isEmpty()); + } + + @Test + public void addClientId() { + EnvironmentTestUtils.addEnvironment(this.environment, + "vcap.services.sso.credentials.clientId:foo"); + this.listener.postProcessEnvironment(this.environment, new SpringApplication()); + assertEquals("foo", this.environment + .resolvePlaceholders("${security.oauth2.client.clientId}")); + } + + @Test + public void addClientIdUnderscores() { + EnvironmentTestUtils.addEnvironment(this.environment, + "vcap.services.sso.credentials.client_id:foo"); + this.listener.postProcessEnvironment(this.environment, new SpringApplication()); + assertEquals("foo", this.environment + .resolvePlaceholders("${security.oauth2.client.clientId}")); + } + + @Test + public void addTokenUri() { + EnvironmentTestUtils.addEnvironment(this.environment, + "vcap.services.sso.credentials.accessTokenUri:http://example.com"); + this.listener.postProcessEnvironment(this.environment, new SpringApplication()); + assertEquals("http://example.com", this.environment + .resolvePlaceholders("${security.oauth2.client.accessTokenUri}")); + } + + @Test + public void addUserInfoUri() { + EnvironmentTestUtils.addEnvironment(this.environment, + "vcap.services.sso.credentials.userInfoUri:http://example.com"); + this.listener.postProcessEnvironment(this.environment, new SpringApplication()); + assertEquals("http://example.com", this.environment + .resolvePlaceholders("${security.oauth2.resource.userInfoUri}")); + } + + @Test + public void addServiceId() { + EnvironmentTestUtils.addEnvironment(this.environment, + "vcap.services.my.credentials.tokenUri:http://example.com", + "security.oauth2.sso.serviceId:my"); + this.listener.postProcessEnvironment(this.environment, new SpringApplication()); + assertEquals("http://example.com", this.environment + .resolvePlaceholders("${security.oauth2.client.accessTokenUri}")); + } + +}