diff --git a/spring-cloud-services-config-client-autoconfigure/build.gradle b/spring-cloud-services-config-client-autoconfigure/build.gradle index 8a74e856..de7a79bc 100644 --- a/spring-cloud-services-config-client-autoconfigure/build.gradle +++ b/spring-cloud-services-config-client-autoconfigure/build.gradle @@ -27,8 +27,8 @@ dependencies { testImplementation('org.junit.jupiter:junit-jupiter-api') testImplementation('org.junit.vintage:junit-vintage-engine') testImplementation("org.springframework.cloud:spring-cloud-config-server") - testImplementation("org.awaitility:awaitility:4.2.0") testImplementation("org.wiremock:wiremock-standalone:${wireMockVersion}") + testImplementation("org.awaitility:awaitility:4.2.0") } publishing { diff --git a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientEnvironmentPostProcessor.java b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientEnvironmentPostProcessor.java index 63b471e0..57c3f89a 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientEnvironmentPostProcessor.java +++ b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientEnvironmentPostProcessor.java @@ -16,12 +16,7 @@ package io.pivotal.spring.cloud.config.client; import java.util.HashMap; -import java.util.List; -import java.util.Map; -import io.pivotal.cfenv.core.CfCredentials; -import io.pivotal.cfenv.core.CfEnv; -import io.pivotal.cfenv.core.CfService; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; import org.springframework.boot.env.EnvironmentPostProcessor; @@ -29,6 +24,8 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; +import io.pivotal.cfenv.core.CfEnv; + /** * Using {@link CfEnv} directly here as we need to set the * spring.config.import property before the @@ -40,42 +37,32 @@ */ public class ConfigClientEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { - private static final String SPRING_CLOUD_SERVICES_CONFIG_IMPORT = "springCloudServicesConfigImport"; + private static final String PROPERTY_SOURCE_NAME = ConfigClientEnvironmentPostProcessor.class.getSimpleName(); @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - CfEnv cfEnv = new CfEnv(); - List configServices = cfEnv.findServicesByTag("configuration"); - if (configServices.size() != 1) + var configServices = new CfEnv().findServicesByTag("configuration"); + if (configServices.size() != 1) { return; - CfCredentials credentials = configServices.stream().findFirst().get().getCredentials(); - environment.getPropertySources().addFirst(oauth2PropertySource(credentials)); - environment.getPropertySources().addFirst(configImportPropertySource(credentials)); - } + } - @Override - public int getOrder() { - return ConfigDataEnvironmentPostProcessor.ORDER - 1; - } + var credentials = configServices.get(0).getCredentials(); + + var map = new HashMap(); + map.put(ConfigClientOAuth2Properties.PREFIX + ".client-id", credentials.getString("client_id")); + map.put(ConfigClientOAuth2Properties.PREFIX + ".client-secret", credentials.getString("client_secret")); + map.put(ConfigClientOAuth2Properties.PREFIX + ".access-token-uri", credentials.getString("access_token_uri")); + map.put(ConfigClientOAuth2Properties.PREFIX + ".scope", ""); - private MapPropertySource configImportPropertySource(CfCredentials credentials) { - Map map = new HashMap<>(); map.put("spring.config.import", "optional:configserver:" + credentials.getUri()); - map.put("spring.cloud.refresh.additional-property-sources-to-retain", SPRING_CLOUD_SERVICES_CONFIG_IMPORT); - return new MapPropertySource(SPRING_CLOUD_SERVICES_CONFIG_IMPORT, map); - } + map.put("spring.cloud.refresh.additional-property-sources-to-retain", PROPERTY_SOURCE_NAME); - /** - * This method can be removed once java-cfenv supports config-client. - */ - private MapPropertySource oauth2PropertySource(CfCredentials credentials) { - Map map = new HashMap<>(); - map.put("spring.cloud.config.uri", credentials.getUri()); - map.put("spring.cloud.config.client.oauth2.client-id", credentials.getString("client_id")); - map.put("spring.cloud.config.client.oauth2.client-secret", credentials.getString("client_secret")); - map.put("spring.cloud.config.client.oauth2.access-token-uri", credentials.getString("access_token_uri")); + environment.getPropertySources().addFirst(new MapPropertySource(PROPERTY_SOURCE_NAME, map)); + } - return new MapPropertySource("CfConfigClientProcessor", map); + @Override + public int getOrder() { + return ConfigDataEnvironmentPostProcessor.ORDER - 1; } } diff --git a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2BootstrapConfiguration.java b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2BootstrapConfiguration.java index 89a5bc82..bb469744 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2BootstrapConfiguration.java +++ b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2BootstrapConfiguration.java @@ -16,42 +16,27 @@ package io.pivotal.spring.cloud.config.client; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.config.client.ConfigClientProperties; import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.web.client.RestTemplate; @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ ConfigClientProperties.class }) -@EnableConfigurationProperties(ConfigClientOAuth2Properties.class) public class ConfigClientOAuth2BootstrapConfiguration { @Bean @ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class) - @ConditionalOnProperty(prefix = "spring.cloud.config.client.oauth2", - name = { "client-id", "client-secret", "access-token-uri" }) - public ConfigServicePropertySourceLocator configServicePropertySourceLocator( - ConfigClientProperties configClientProperties, ConfigClientOAuth2Properties configClientOAuth2Properties) { - ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("config-client") - .clientId(configClientOAuth2Properties.getClientId()) - .clientSecret(configClientOAuth2Properties.getClientSecret()) - .scope(configClientOAuth2Properties.getScope()) - .tokenUri(configClientOAuth2Properties.getAccessTokenUri()) - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .build(); - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add(new OAuth2AuthorizedClientHttpRequestInterceptor(clientRegistration)); + @ConditionalOnBean(value = RestTemplate.class, name = "configClientRestTemplate") + public ConfigServicePropertySourceLocator configServicePropertySourceLocator(RestTemplate configClientRestTemplate, + ConfigClientProperties configClientProperties) { - ConfigServicePropertySourceLocator configServicePropertySourceLocator = new ConfigServicePropertySourceLocator( - configClientProperties); - configServicePropertySourceLocator.setRestTemplate(restTemplate); + var configServicePropertySourceLocator = new ConfigServicePropertySourceLocator(configClientProperties); + configServicePropertySourceLocator.setRestTemplate(configClientRestTemplate); return configServicePropertySourceLocator; } diff --git a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2BootstrapRegistryInitializer.java b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2BootstrapRegistryInitializer.java deleted file mode 100644 index 1e7557fa..00000000 --- a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2BootstrapRegistryInitializer.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2021-2024 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 io.pivotal.spring.cloud.config.client; - -import java.util.List; - -import org.springframework.boot.BootstrapRegistry; -import org.springframework.boot.BootstrapRegistryInitializer; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.util.ClassUtils; -import org.springframework.web.client.RestTemplate; - -import io.pivotal.cfenv.core.CfCredentials; -import io.pivotal.cfenv.core.CfEnv; -import io.pivotal.cfenv.core.CfService; - -/** - * Using {@link CfEnv} directly as a {@link BootstrapRegistryInitializer} is required to - * setup the RestTemplate that calls config-server. There's presently no earlier extension - * point that java-cfenv library can use to setup the properties before this is called. - * - * @author Dylan Roberts - */ -public class ConfigClientOAuth2BootstrapRegistryInitializer implements BootstrapRegistryInitializer { - - private static final boolean CONFIG_CLIENT_IS_PRESENT = ClassUtils - .isPresent("org.springframework.cloud.config.client.ConfigServerConfigDataLoader", null); - - private static final boolean OAUTH2_CLIENT_IS_PRESENT = ClassUtils - .isPresent("org.springframework.security.oauth2.client.registration.ClientRegistration", null); - - private static final boolean JAVA_CFENV_IS_PRESENT = ClassUtils.isPresent("io.pivotal.cfenv.core.CfEnv", null); - - @Override - public void initialize(BootstrapRegistry registry) { - if (!CONFIG_CLIENT_IS_PRESENT || !OAUTH2_CLIENT_IS_PRESENT || !JAVA_CFENV_IS_PRESENT) - return; - - CfEnv cfEnv = new CfEnv(); - List configServices = cfEnv.findServicesByTag("configuration"); - if (configServices.size() != 1) - return; - CfCredentials credentials = configServices.stream().findFirst().get().getCredentials(); - - registry.register(RestTemplate.class, context -> { - String clientId = credentials.getString("client_id"); - String clientSecret = credentials.getString("client_secret"); - String accessTokenUri = credentials.getString("access_token_uri"); - RestTemplate restTemplate = new RestTemplate(); - ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("config-client") - .clientId(clientId) - .clientSecret(clientSecret) - .tokenUri(accessTokenUri) - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .build(); - restTemplate.getInterceptors().add(new OAuth2AuthorizedClientHttpRequestInterceptor(clientRegistration)); - return restTemplate; - }); - } - -} diff --git a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2ConfigDataLocationResolver.java b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2ConfigDataLocationResolver.java new file mode 100644 index 00000000..d24cd785 --- /dev/null +++ b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2ConfigDataLocationResolver.java @@ -0,0 +1,149 @@ +/* + * Copyright 2023-2024 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 io.pivotal.spring.cloud.config.client; + +import org.apache.commons.logging.Log; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.context.config.*; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.cloud.config.client.ConfigClientRequestTemplateFactory; +import org.springframework.cloud.config.client.ConfigServerConfigDataLocationResolver; +import org.springframework.cloud.config.client.ConfigServerConfigDataResource; +import org.springframework.core.Ordered; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +/** + * Using oauth2 properties to configure an authorization interceptor for the + * RestTemplate that calls config server. + *

+ * Note: Despite implementing {@link ConfigDataLocationResolver}, this class does not + * resolve any location. It only configures and registers the + * {@link ConfigClientRequestTemplateFactory} which later will be used by + * {@link ConfigServerConfigDataLocationResolver} to create RestTemplate for + * calling config server. + *

+ * Finally, it registers the RestTemplate bean to be consumed by + * {@link ConfigResourceClientAutoConfiguration} and + * {@link VaultTokenRenewalAutoConfiguration} after application startup. + */ +public class ConfigClientOAuth2ConfigDataLocationResolver + implements ConfigDataLocationResolver, Ordered { + + private final Log log; + + public ConfigClientOAuth2ConfigDataLocationResolver(DeferredLogFactory factory) { + this.log = factory.getLog(ConfigClientOAuth2ConfigDataLocationResolver.class); + } + + @Override + public boolean isResolvable(ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location) { + if (!location.hasPrefix(ConfigServerConfigDataLocationResolver.PREFIX)) { + return false; + } + + var binder = resolverContext.getBinder(); + var isConfigEnabled = binder.bind(ConfigClientProperties.PREFIX + ".enabled", Boolean.class).orElse(true); + if (!isConfigEnabled) { + return false; + } + + var oAuth2Properties = binder.bind(ConfigClientOAuth2Properties.PREFIX, ConfigClientOAuth2Properties.class) + .orElse(null); + if (oAuth2Properties == null) { + log.warn("Config Client oauth2 properties are missing. Skipping the auth interceptor configuration"); + return false; + } + + var bootstrapContext = resolverContext.getBootstrapContext(); + + // Register the oauth2 properties + bootstrapContext.registerIfAbsent(ConfigClientOAuth2Properties.class, + BootstrapRegistry.InstanceSupplier.of(oAuth2Properties).withScope(BootstrapRegistry.Scope.PROTOTYPE)); + + // Register the custom factory with oauth2 interceptor. + bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, + context -> new ConfigClientOAuth2RequestTemplateFactory(this.log, + context.get(ConfigClientProperties.class), oAuth2Properties)); + + bootstrapContext.addCloseListener(event -> { + var beanFactory = event.getApplicationContext().getBeanFactory(); + + // Add the RestTemplate as bean, once the startup is finished. + beanFactory.registerSingleton("configClientRestTemplate", + event.getBootstrapContext().get(RestTemplate.class)); + + // Add the OAuth2 Properties as bean, once the startup is finished. + beanFactory.registerSingleton("configClientOAuth2Properties", + event.getBootstrapContext().get(ConfigClientOAuth2Properties.class)); + }); + + return false; + } + + @Override + public List resolve(ConfigDataLocationResolverContext context, + ConfigDataLocation location) + throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException { + throw new IllegalStateException("Unexpected call. This resolver should not resolve any location"); + } + + @Override + public List resolveProfileSpecific(ConfigDataLocationResolverContext context, + ConfigDataLocation location, Profiles profiles) throws ConfigDataLocationNotFoundException { + throw new IllegalStateException("Unexpected call. This resolver should not resolve any location"); + } + + /** + * It should be registered before {@link ConfigServerConfigDataLocationResolver}. See + * {@link ConfigServerConfigDataLocationResolver#getOrder()} + */ + @Override + public int getOrder() { + return -2; + } + + private static class ConfigClientOAuth2RequestTemplateFactory extends ConfigClientRequestTemplateFactory { + + private final ClientRegistration clientRegistration; + + public ConfigClientOAuth2RequestTemplateFactory(Log log, ConfigClientProperties clientProperties, + ConfigClientOAuth2Properties oAuth2Properties) { + super(log, clientProperties); + + this.clientRegistration = ClientRegistration.withRegistrationId("config-client") + .clientId(oAuth2Properties.getClientId()) + .clientSecret(oAuth2Properties.getClientSecret()) + .tokenUri(oAuth2Properties.getAccessTokenUri()) + .scope(oAuth2Properties.getScope()) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build(); + } + + @Override + public RestTemplate create() { + var restTemplate = super.create(); + restTemplate.getInterceptors().add(new OAuth2AuthorizedClientHttpRequestInterceptor(clientRegistration)); + return restTemplate; + } + + } + +} diff --git a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfiguration.java b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfiguration.java index 1c991a87..32cc5b9e 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfiguration.java +++ b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfiguration.java @@ -17,15 +17,12 @@ package io.pivotal.spring.cloud.config.client; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.config.client.ConfigClientAutoConfiguration; import org.springframework.cloud.config.client.ConfigClientProperties; import org.springframework.context.annotation.Bean; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.web.client.RestTemplate; /** @@ -35,25 +32,14 @@ */ @AutoConfiguration(after = ConfigClientAutoConfiguration.class) @ConditionalOnClass({ ConfigClientProperties.class }) -@EnableConfigurationProperties(ConfigClientOAuth2Properties.class) public class ConfigResourceClientAutoConfiguration { @Bean @ConditionalOnMissingBean(ConfigResourceClient.class) - @ConditionalOnProperty(prefix = "spring.cloud.config.client.oauth2", - name = { "client-id", "client-secret", "access-token-uri" }) - public ConfigResourceClient configResourceClient(ConfigClientProperties configClientProperties, - ConfigClientOAuth2Properties configClientOAuth2Properties) { - ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("config-client") - .clientId(configClientOAuth2Properties.getClientId()) - .clientSecret(configClientOAuth2Properties.getClientSecret()) - .scope(configClientOAuth2Properties.getScope()) - .tokenUri(configClientOAuth2Properties.getAccessTokenUri()) - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .build(); - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add(new OAuth2AuthorizedClientHttpRequestInterceptor(clientRegistration)); - return new OAuth2ConfigResourceClient(restTemplate, configClientProperties); + @ConditionalOnBean(value = RestTemplate.class, name = "configClientRestTemplate") + public ConfigResourceClient configResourceClient(RestTemplate configClientRestTemplate, + ConfigClientProperties configClientProperties) { + return new DefaultConfigResourceClient(configClientRestTemplate, configClientProperties); } } diff --git a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/OAuth2ConfigResourceClient.java b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/DefaultConfigResourceClient.java similarity index 96% rename from spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/OAuth2ConfigResourceClient.java rename to spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/DefaultConfigResourceClient.java index 4b30c464..01456b4a 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/OAuth2ConfigResourceClient.java +++ b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/DefaultConfigResourceClient.java @@ -38,7 +38,7 @@ * @author Daniel Lavoie * @author Anshul Mehra */ -class OAuth2ConfigResourceClient implements ConfigResourceClient { +class DefaultConfigResourceClient implements ConfigResourceClient { private enum ResourceType { @@ -50,7 +50,7 @@ private enum ResourceType { private final RestTemplate restTemplate; - protected OAuth2ConfigResourceClient(RestTemplate restTemplate, + protected DefaultConfigResourceClient(RestTemplate restTemplate, final ConfigClientProperties configClientProperties) { this.restTemplate = restTemplate; this.configClientProperties = configClientProperties; diff --git a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/VaultTokenRenewalAutoConfiguration.java b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/VaultTokenRenewalAutoConfiguration.java index 0577ac3a..ae6c1745 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/VaultTokenRenewalAutoConfiguration.java +++ b/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/VaultTokenRenewalAutoConfiguration.java @@ -20,7 +20,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -34,85 +33,69 @@ import org.springframework.http.MediaType; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; /** * Configuration for a periodic Vault token renewer. Conditionally configured if there is * a {@link ConfigClientProperties} bean and if there is a `spring.cloud.config.token` - * property set. - * - * By default, the token is renewed every 60 seconds and is renewed with a 5 minute - * time-to-live. The renew rate can be configured by setting `vault.token.renew.rate` to - * some value that is the renewal rate in milliseconds. The renewal time-to-live can be - * specified with by setting `vault.token.ttl` to some value indicating the time-to-live - * in milliseconds. + * property set. By default, the token is renewed every 60 seconds and is renewed with a 5 + * minute time-to-live. The renewal rate can be configured by setting + * `vault.token.renew.rate` to some value that is the renewal rate in milliseconds. The + * renewal time-to-live can be specified with by setting `vault.token.ttl` to some value + * indicating the time-to-live in milliseconds. * * @author cwalls */ @AutoConfiguration(after = ConfigClientAutoConfiguration.class) @ConditionalOnBean(ConfigClientProperties.class) -@ConditionalOnProperty(prefix = "spring.cloud.config", - name = { "token", "client.oauth2.client-id", "client.oauth2.client-secret", "client.oauth2.access-token-uri" }) +@ConditionalOnProperty(name = "spring.cloud.config.token") @EnableConfigurationProperties(ConfigClientOAuth2Properties.class) @EnableScheduling public class VaultTokenRenewalAutoConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(VaultTokenRenewalAutoConfiguration.class); - @Bean - public VaultTokenRefresher vaultTokenRefresher(ConfigClientProperties configClientProperties, - ConfigClientOAuth2Properties configClientOAuth2Properties, - @Qualifier("vaultTokenRenewal") RestTemplate restTemplate, - @Value("${spring.cloud.config.token}") String vaultToken, - // Default to a 300 second (5 minute) TTL - @Value("${vault.token.ttl:300000}") long renewTtl) { - - var clientRegistration = ClientRegistration.withRegistrationId("config-client") - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .clientId(configClientOAuth2Properties.getClientId()) - .clientSecret(configClientOAuth2Properties.getClientSecret()) - .scope(configClientOAuth2Properties.getScope()) - .tokenUri(configClientOAuth2Properties.getAccessTokenUri()) - .build(); - - restTemplate.getInterceptors().add(new OAuth2AuthorizedClientHttpRequestInterceptor(clientRegistration)); - // convert to seconds, since that's what Vault wants - var request = buildTokenRenewRequest(vaultToken, renewTtl / 1000); + private static final String VAULT_TOKEN_HEADER = "X-Vault-Token"; - return new VaultTokenRefresher(restTemplate, obscuredToken(vaultToken), renewTtl, - refreshUri(configClientProperties), request); - } + private static final String REFRESH_PATH = "/vault/v1/auth/token/renew-self"; - @Bean("vaultTokenRenewal") - public RestTemplate restTemplate() { - return new RestTemplate(); - } + @Value("${spring.cloud.config.token}") + private String vaultToken; - private String refreshUri(ConfigClientProperties configClientProperties) { - return configClientProperties.getUri()[0] + "/vault/v1/auth/token/renew-self"; - } + // Default to a 300 second (5 minute) TTL + @Value("${vault.token.ttl:300000}") + long ttl; + + @Bean + @ConditionalOnBean(value = RestTemplate.class, name = "configClientRestTemplate") + public VaultTokenRefresher vaultTokenRefresher(RestTemplate configClientRestTemplate, + ConfigClientProperties configClientProperties) { - private String obscuredToken(String vaultToken) { - return vaultToken.substring(0, 4) + "[*]" + vaultToken.substring(vaultToken.length() - 4); + var refreshUri = configClientProperties.getUri()[0] + REFRESH_PATH; + var obscuredToken = this.vaultToken.substring(0, 4) + "[*]" + + this.vaultToken.substring(this.vaultToken.length() - 4); + + return new VaultTokenRefresher(configClientRestTemplate, obscuredToken, ttl, refreshUri, + buildTokenRenewRequest()); } - private HttpEntity> buildTokenRenewRequest(String vaultToken, long renewTtlInMs) { - var requestBody = Map.of("increment", renewTtlInMs); + private HttpEntity> buildTokenRenewRequest() { + // convert to seconds, since that's what Vault wants + var ttlInSeconds = this.ttl / 1000; + var requestBody = Map.of("increment", ttlInSeconds); var headers = new HttpHeaders(); - headers.set("X-Vault-Token", vaultToken); + headers.set(VAULT_TOKEN_HEADER, this.vaultToken); headers.setContentType(MediaType.APPLICATION_JSON); return new HttpEntity<>(requestBody, headers); } - static class VaultTokenRefresher { + public static class VaultTokenRefresher { private final String obscuredToken; - private final long renewTTL; + private final long ttl; private final String refreshUri; @@ -120,11 +103,11 @@ static class VaultTokenRefresher { private final RestTemplate restTemplate; - VaultTokenRefresher(RestTemplate restTemplate, String obscuredToken, long renewTTL, String refreshUri, + VaultTokenRefresher(RestTemplate restTemplate, String obscuredToken, long ttl, String refreshUri, HttpEntity> request) { this.restTemplate = restTemplate; this.obscuredToken = obscuredToken; - this.renewTTL = renewTTL; + this.ttl = ttl; this.refreshUri = refreshUri; this.request = request; } @@ -133,7 +116,7 @@ static class VaultTokenRefresher { @Scheduled(fixedRateString = "${vault.token.renew.rate:60000}") public void refreshVaultToken() { try { - LOGGER.debug("Renewing Vault token " + obscuredToken + " for " + renewTTL + " milliseconds."); + LOGGER.debug("Renewing Vault token " + obscuredToken + " for " + ttl + " milliseconds."); restTemplate.postForObject(refreshUri, request, String.class); } catch (RestClientException e) { diff --git a/spring-cloud-services-config-client-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-services-config-client-autoconfigure/src/main/resources/META-INF/spring.factories index ea7e55b0..41bd3bea 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-services-config-client-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,5 +1,5 @@ -org.springframework.boot.BootstrapRegistryInitializer=\ -io.pivotal.spring.cloud.config.client.ConfigClientOAuth2BootstrapRegistryInitializer +org.springframework.boot.context.config.ConfigDataLocationResolver=\ +io.pivotal.spring.cloud.config.client.ConfigClientOAuth2ConfigDataLocationResolver org.springframework.cloud.bootstrap.BootstrapConfiguration=\ io.pivotal.spring.cloud.config.client.ConfigClientOAuth2BootstrapConfiguration diff --git a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2InterceptorTest.java b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2InterceptorTest.java new file mode 100644 index 00000000..7a24f82c --- /dev/null +++ b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigClientOAuth2InterceptorTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017-2024 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 io.pivotal.spring.cloud.config.client; + +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Base64; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; + +@SpringBootTest(classes = ConfigClientOAuth2InterceptorTest.TestApplication.class, + properties = { "spring.config.import=optional:configserver:", "spring.cloud.config.enabled=true", + "spring.cloud.config.label=main", "spring.cloud.config.uri=http://localhost:8888", + "spring.cloud.config.client.oauth2.client-id=id", + "spring.cloud.config.client.oauth2.client-secret=secret", + "spring.cloud.config.client.oauth2.scope=profile,email", + "spring.cloud.config.client.oauth2.access-token-uri=http://localhost:9999/token/uri" }) +public class ConfigClientOAuth2InterceptorTest { + + private static WireMockServer uaaServer; + + private static WireMockServer configServer; + + @BeforeAll + static void setup() { + uaaServer = new WireMockServer(wireMockConfig().port(9999)); + uaaServer.start(); + uaaServer.stubFor(post("/token/uri") + .willReturn(aResponse().withHeader("Content-Type", "application/json;charset=UTF-8").withBody(""" + { + "access_token" : "access-token", + "token_type" : "bearer" + }"""))); + + configServer = new WireMockServer(wireMockConfig().port(8888)); + configServer.start(); + configServer.stubFor(get(urlMatching("/application/.*")) + .willReturn(aResponse().withHeader("Content-Type", "application/json;charset=UTF-8").withBody(""" + { + "name" : "remote-env" + }"""))); + } + + @AfterAll + static void cleanup() { + configServer.stop(); + uaaServer.stop(); + } + + @Test + void configurationIsLoadedUsingAccessToken() { + configServer.verify(getRequestedFor(urlEqualTo("/application/default/main")).withHeader("Authorization", + equalTo("Bearer access-token"))); + configServer.verify(getRequestedFor(urlEqualTo("/application/native/main")).withHeader("Authorization", + equalTo("Bearer access-token"))); + } + + @Test + void accessTokenIsRequestedUsingClientOAuthProperties() { + String base64Credentials = Base64.getEncoder().encodeToString(("id:secret").getBytes()); + + uaaServer.verify(postRequestedFor(urlEqualTo("/token/uri")) + .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded;charset=UTF-8")) + .withHeader("Authorization", equalTo("Basic " + base64Credentials)) + .withRequestBody(containing("grant_type=client_credentials"))); + } + + @Test + void optionalScopePropertyShouldBeIncludedInTokenRequest() { + String base64Credentials = Base64.getEncoder().encodeToString(("id:secret").getBytes()); + + uaaServer.verify(postRequestedFor(urlEqualTo("/token/uri")) + .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded;charset=UTF-8")) + .withHeader("Authorization", equalTo("Basic " + base64Credentials)) + .withRequestBody(containing("grant_type=client_credentials")) + .withRequestBody(containing("scope=profile+email"))); + } + + @SpringBootApplication + static class TestApplication { + + } + +} diff --git a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfigurationTest.java b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfigurationTest.java index d0cb91be..ffb22153 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfigurationTest.java +++ b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfigurationTest.java @@ -19,117 +19,28 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.cloud.config.client.ConfigClientAutoConfiguration; +import org.springframework.web.client.RestTemplate; -import java.util.Base64; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.containing; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static org.assertj.core.api.Assertions.assertThat; @WireMockTest(proxyMode = true) public class ConfigResourceClientAutoConfigurationTest { - private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .withAllowBeanDefinitionOverriding(true) - .withConfiguration(AutoConfigurations.of(ConfigResourceClientAutoConfiguration.class, - ConfigClientAutoConfiguration.class)); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( + AutoConfigurations.of(ConfigResourceClientAutoConfiguration.class, ConfigClientAutoConfiguration.class)); @Test - void configurationIsNotEnabledWhenOAuth2PropertiesAreMissing() { + void shouldNotCreateConfigResourceClientWhenRestTemplateIsMissing() { contextRunner.run(context -> assertThat(context).doesNotHaveBean(ConfigResourceClient.class)); } @Test - void configurationIsEnabledWhenOAuth2PropertiesArePresent() { - var pairs = applicationProperties("::id::", "::secret::"); - - contextRunner.withPropertyValues(pairs) + void shouldCreateConfigResourceClientWhenRestTemplateIsPresent() { + contextRunner.withBean("configClientRestTemplate", RestTemplate.class) .run(context -> assertThat(context).hasSingleBean(ConfigResourceClient.class)); } - @Test - void authInterceptorIsConfiguredWhenOAuth2PropertiesArePresent() { - var pairs = applicationProperties("id", "secret"); - String base64Credentials = Base64.getEncoder().encodeToString(("id:secret").getBytes()); - - stubEndpoints(); - - contextRunner.withPropertyValues(pairs).run(context -> { - assertThat(context).hasSingleBean(ConfigResourceClient.class); - - tryFetchingAnyResource(context); - - verify(postRequestedFor(urlEqualTo("/token/uri")) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded;charset=UTF-8")) - .withHeader("Authorization", equalTo("Basic " + base64Credentials)) - .withRequestBody(containing("grant_type=client_credentials"))); - - verify(getRequestedFor(urlEqualTo("/application/profile/label/path")).withHeader("Authorization", - equalTo("Bearer access-token"))); - }); - } - - @Test - void optionalScopePropertyShouldBeIncludedInTokenRequest() { - var pairs = applicationProperties("id", "secret", "profile,email"); - String base64Credentials = Base64.getEncoder().encodeToString(("id:secret").getBytes()); - - stubEndpoints(); - - contextRunner.withPropertyValues(pairs).run(context -> { - assertThat(context).hasSingleBean(ConfigResourceClient.class); - - tryFetchingAnyResource(context); - - verify(postRequestedFor(urlEqualTo("/token/uri")) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded;charset=UTF-8")) - .withHeader("Authorization", equalTo("Basic " + base64Credentials)) - .withRequestBody(containing("grant_type=client_credentials")) - .withRequestBody(containing("scope=profile+email"))); - - verify(getRequestedFor(urlEqualTo("/application/profile/label/path")).withHeader("Authorization", - equalTo("Bearer access-token"))); - }); - } - - private void stubEndpoints() { - stubFor(post("/token/uri").withHost(equalTo("uaa.local")) - .willReturn(aResponse().withHeader("Content-Type", "application/json;charset=UTF-8").withBody(""" - { - "access_token" : "access-token", - "token_type" : "bearer" - }"""))); - - stubFor(get("/application/profile/label/path").withHost(equalTo("server.local")) - .willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("::content::"))); - } - - private void tryFetchingAnyResource(BeanFactory factory) { - factory.getBean(ConfigResourceClient.class).getPlainTextResource("profile", "label", "path"); - } - - private String[] applicationProperties(String clientId, String clientSecret) { - return applicationProperties(clientId, clientSecret, ""); - } - - private String[] applicationProperties(String clientId, String clientSecret, String scope) { - return new String[] { "spring.cloud.config.uri=http://server.local", - "spring.cloud.config.client.oauth2.access-token-uri=http://uaa.local/token/uri", - String.format("spring.cloud.config.client.oauth2.client-id=%s", clientId), - String.format("spring.cloud.config.client.oauth2.client-secret=%s", clientSecret), - String.format("spring.cloud.config.client.oauth2.scope=%s", scope), }; - } - } diff --git a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientTests.java b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientTests.java index 4c88e460..b85a5187 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientTests.java +++ b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientTests.java @@ -16,50 +16,74 @@ package io.pivotal.spring.cloud.config.client; -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.config.client.ConfigClientProperties; -import org.springframework.context.annotation.Import; import java.io.IOException; import java.nio.charset.StandardCharsets; -import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.not; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Daniel Lavoie */ -@Import(ConfigResourceClientAutoConfiguration.class) -@SpringBootTest(classes = ConfigClientProperties.class, - properties = { "spring.cloud.config.label=main", "spring.cloud.config.uri=http://server.local", +@SpringBootTest(classes = ConfigResourceClientAutoConfiguration.class, + properties = { "spring.config.import=optional:configserver:", "spring.cloud.config.enabled=true", + "spring.cloud.config.label=main", "spring.cloud.config.uri=http://localhost:8888", "spring.cloud.config.client.oauth2.client-id=id", "spring.cloud.config.client.oauth2.client-secret=secret", - "spring.cloud.config.client.oauth2.access-token-uri=http://uaa.local/token/uri" }) -@WireMockTest(proxyMode = true) + "spring.cloud.config.client.oauth2.access-token-uri=http://localhost:9999/token/uri" }) public class ConfigResourceClientTests { - @Autowired - private ConfigResourceClient configResourceClient; + private static WireMockServer uaaServer; - @BeforeEach - void setup() { - stubFor(post("/token/uri").withHost(equalTo("uaa.local")) + private static WireMockServer configServer; + + @BeforeAll + static void setup() { + uaaServer = new WireMockServer(wireMockConfig().port(9999)); + uaaServer.start(); + uaaServer.stubFor(post("/token/uri") .willReturn(aResponse().withHeader("Content-Type", "application/json;charset=UTF-8").withBody(""" { "access_token" : "access-token", "token_type" : "bearer" }"""))); + + configServer = new WireMockServer(wireMockConfig().port(8888)); + configServer.start(); + configServer.stubFor(get(urlMatching("/application/.*")) + .willReturn(aResponse().withHeader("Content-Type", "application/json;charset=UTF-8").withBody(""" + { + "name" : "remote-env" + }"""))); } + @AfterAll + static void cleanup() { + configServer.stop(); + uaaServer.stop(); + } + + @Autowired + private ConfigResourceClient configResourceClient; + @Test public void shouldLoadPlainText() throws IOException { - stubFor(get("/application/development/dev/path").withHost(equalTo("server.local")) - .withHeader("Accept", not(equalTo("application/octet-stream"))) - .willReturn(aResponse().withHeader("Content-Type", "plain/text").withBody("::text::"))); + configServer.stubFor( + get("/application/development/dev/path").withHeader("Accept", not(equalTo("application/octet-stream"))) + .willReturn(aResponse().withHeader("Content-Type", "plain/text").withBody("::text::"))); var resource = configResourceClient.getPlainTextResource("development", "dev", "path"); @@ -68,9 +92,9 @@ public void shouldLoadPlainText() throws IOException { @Test public void shouldLoadBinaryResource() throws IOException { - stubFor(get("/application/production/prd/path").withHost(equalTo("server.local")) - .withHeader("Accept", equalTo("application/octet-stream")) - .willReturn(aResponse().withHeader("Content-Type", "application/octet-stream").withBody("::binary::"))); + configServer + .stubFor(get("/application/production/prd/path").withHeader("Accept", equalTo("application/octet-stream")) + .willReturn(aResponse().withHeader("Content-Type", "application/octet-stream").withBody("::binary::"))); var resource = configResourceClient.getBinaryResource("production", "prd", "path"); @@ -79,7 +103,7 @@ public void shouldLoadBinaryResource() throws IOException { @Test public void shouldLoadPlainTextWithDefaultProfileAndLabel() throws IOException { - stubFor(get("/application/default/main/path").withHost(equalTo("server.local")) + configServer.stubFor(get("/application/default/main/path") .withHeader("Accept", not(equalTo("application/octet-stream"))) .willReturn( aResponse().withHeader("Content-Type", "application/octet-stream").withBody("::default text::"))); @@ -90,7 +114,7 @@ public void shouldLoadPlainTextWithDefaultProfileAndLabel() throws IOException { @Test public void shouldLoadBinaryResourceWithDefaultProfileAndLabel() throws IOException { - stubFor(get("/application/default/main/path").withHost(equalTo("server.local")) + configServer.stubFor(get("/application/default/main/path") .withHeader("Accept", equalTo("application/octet-stream")) .willReturn( aResponse().withHeader("Content-Type", "application/octet-stream").withBody("::default binary::"))); diff --git a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigServerTestApplication.java b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigServerTestApplication.java deleted file mode 100644 index 845ec365..00000000 --- a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/ConfigServerTestApplication.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2019-2024 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 io.pivotal.spring.cloud.config.client; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.config.server.EnableConfigServer; - -/** - * @author Daniel Lavoie - * @author Dylan Roberts - */ -@EnableConfigServer -@SpringBootApplication -public class ConfigServerTestApplication { - - public static void main(String[] args) { - SpringApplication.run(ConfigServerTestApplication.class); - } - -} diff --git a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/OAuth2ConfigResourceClientTest.java b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/DefaultConfigResourceClientTest.java similarity index 60% rename from spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/OAuth2ConfigResourceClientTest.java rename to spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/DefaultConfigResourceClientTest.java index 72b514e6..0ae3ef8d 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/OAuth2ConfigResourceClientTest.java +++ b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/DefaultConfigResourceClientTest.java @@ -23,14 +23,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.cloud.config.server.EnableConfigServer; import org.springframework.core.io.Resource; import org.springframework.mock.env.MockEnvironment; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import static io.pivotal.spring.cloud.config.client.DefaultConfigResourceClientTest.ConfigServerTestApplication; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -40,7 +44,7 @@ */ @SpringBootTest(classes = ConfigServerTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { "spring.cloud.config.enabled=true", "spring.config.import=optional:configserver:" }) -public class OAuth2ConfigResourceClientTest { +public class DefaultConfigResourceClientTest { // @formatter:off private static final String NGINX_CONFIG = """ @@ -73,38 +77,64 @@ public class OAuth2ConfigResourceClientTest { public void setup() { configClientProperties = new ConfigClientProperties(new MockEnvironment()); configClientProperties.setName("app"); - configClientProperties.setProfile(null); + configClientProperties.setProfile("default"); + configClientProperties.setLabel("main"); configClientProperties.setUri(new String[] { "http://localhost:" + port }); - configClient = new OAuth2ConfigResourceClient(new RestTemplate(), configClientProperties); + configClient = new DefaultConfigResourceClient(new RestTemplate(), configClientProperties); } @Test - public void shouldFindSimplePlainFile() { + public void shouldFindResourcesUsingDefaultProfile() { assertThat(read(configClient.getPlainTextResource(null, "main", "nginx.conf"))).isEqualTo(NGINX_CONFIG); + assertThat(read(configClient.getBinaryResource(null, "main", "nginx.conf"))).isEqualTo(NGINX_CONFIG); + } - assertThat(read(configClient.getPlainTextResource("dev", "main", "nginx.conf"))).isEqualTo(DEV_NGINX_CONFIG); + @Test + public void shouldFindResourcesUsingDefaultLabel() { + assertThat(read(configClient.getPlainTextResource("dev", null, "nginx.conf"))).isEqualTo(DEV_NGINX_CONFIG); + assertThat(read(configClient.getBinaryResource("dev", null, "nginx.conf"))).isEqualTo(DEV_NGINX_CONFIG); + } + + @Test + public void shouldFindResourcesUsingDefaultProfileAndLabel() { + assertThat(read(configClient.getPlainTextResource("nginx.conf"))).isEqualTo(NGINX_CONFIG); + assertThat(read(configClient.getBinaryResource("nginx.conf"))).isEqualTo(NGINX_CONFIG); + } - configClientProperties.setProfile("test"); - assertThat(read(configClient.getPlainTextResource(null, "main", "nginx.conf"))).isEqualTo(TEST_NGINX_CONFIG); + @Test + public void shouldFindResourceWithGivenProfileAndLabel() { + assertThat(read(configClient.getPlainTextResource("test", "main", "nginx.conf"))).isEqualTo(TEST_NGINX_CONFIG); + assertThat(read(configClient.getBinaryResource("test", "main", "nginx.conf"))).isEqualTo(TEST_NGINX_CONFIG); } @Test - public void missingConfigFileShouldReturnHttpError() { - assertThatThrownBy(() -> configClient.getPlainTextResource(null, "master", "missing-config.xml")) + public void missingResourceShouldReturnHttpError() { + assertThatThrownBy(() -> configClient.getPlainTextResource(null, "main", "missing-config.xml")) + .isInstanceOf(HttpClientErrorException.class); + + assertThatThrownBy(() -> configClient.getBinaryResource(null, "main", "missing-config.bin")) .isInstanceOf(HttpClientErrorException.class); } @Test public void missingApplicationNameShouldCrash() { configClientProperties.setName(""); - assertThatThrownBy(() -> configClient.getPlainTextResource(null, "master", "nginx.conf")) + + assertThatThrownBy(() -> configClient.getPlainTextResource(null, "main", "nginx.conf")) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> configClient.getBinaryResource(null, "main", "nginx.conf")) .isInstanceOf(IllegalArgumentException.class); } @Test public void missingConfigServerUrlShouldCrash() { configClientProperties.setUri(new String[] { "" }); - assertThatThrownBy(() -> configClient.getPlainTextResource(null, "master", "nginx.conf")) + + assertThatThrownBy(() -> configClient.getPlainTextResource(null, "main", "nginx.conf")) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> configClient.getBinaryResource(null, "main", "nginx.conf")) .isInstanceOf(IllegalArgumentException.class); } @@ -117,4 +147,14 @@ private String read(Resource resource) { } } + @EnableConfigServer + @SpringBootApplication + static class ConfigServerTestApplication { + + public static void main(String[] args) { + SpringApplication.run(ConfigServerTestApplication.class); + } + + } + } diff --git a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/PropertyMaskingEnvironmentPostProcessorTest.java b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/PropertyMaskingEnvironmentPostProcessorTest.java index e488405f..ade30c36 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/PropertyMaskingEnvironmentPostProcessorTest.java +++ b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/PropertyMaskingEnvironmentPostProcessorTest.java @@ -74,11 +74,11 @@ public void gitPropertyIsNotIncludedInSanitizeEndpoints() { } @SpringBootApplication - public static class TestVaultApplication { + static class TestVaultApplication { } - public static class VaultPropertySourceContextLoader extends SpringBootContextLoader { + static class VaultPropertySourceContextLoader extends SpringBootContextLoader { @Override protected ConfigurableEnvironment getEnvironment() { diff --git a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/VaultTokenRenewalAutoConfigurationTest.java b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/VaultTokenRenewalAutoConfigurationTest.java index 53b0e586..824faeb5 100644 --- a/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/VaultTokenRenewalAutoConfigurationTest.java +++ b/spring-cloud-services-config-client-autoconfigure/src/test/java/io/pivotal/spring/cloud/config/client/VaultTokenRenewalAutoConfigurationTest.java @@ -16,26 +16,17 @@ package io.pivotal.spring.cloud.config.client; -import java.util.Base64; import java.util.concurrent.TimeUnit; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.cloud.config.client.ConfigClientAutoConfiguration; +import org.springframework.web.client.RestTemplate; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.containing; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.moreThan; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.client.WireMock.*; import static io.pivotal.spring.cloud.config.client.VaultTokenRenewalAutoConfiguration.VaultTokenRefresher; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -47,108 +38,43 @@ @WireMockTest(proxyMode = true) public class VaultTokenRenewalAutoConfigurationTest { - private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( - AutoConfigurations.of(ConfigClientAutoConfiguration.class, VaultTokenRenewalAutoConfiguration.class)); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(ConfigClientAutoConfiguration.class, VaultTokenRenewalAutoConfiguration.class)) + .withPropertyValues(applicationProperties()) + .withBean("configClientRestTemplate", RestTemplate.class); @Test - void configurationIsNotEnabledWhenOAuth2PropertiesAreMissing() { + void configurationIsNotEnabledWhenTokenIsMissing() { contextRunner.run(context -> assertThat(context).doesNotHaveBean(VaultTokenRefresher.class)); } @Test - void configurationIsEnabledWhenOAuth2PropertiesArePresent() { - var pairs = applicationProperties("::id::", "::secret::"); - - contextRunner.withPropertyValues(pairs) + void configurationIsEnabledWhenTokenIsPresent() { + contextRunner.withPropertyValues("spring.cloud.config.token=vault-token") .run(context -> assertThat(context).hasSingleBean(VaultTokenRefresher.class)); } - @Test - void authInterceptorIsConfiguredWhenOAuth2PropertiesArePresent() { - var pairs = applicationProperties("id", "secret"); - String base64Credentials = Base64.getEncoder().encodeToString(("id:secret").getBytes()); - - stubTokenEndpoints(); - - contextRunner.withPropertyValues(pairs).run(context -> { - assertThat(context).hasSingleBean(VaultTokenRefresher.class); - - tryRefreshingToken(context); - - verify(postRequestedFor(urlEqualTo("/token/uri")) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded;charset=UTF-8")) - .withHeader("Authorization", equalTo("Basic " + base64Credentials)) - .withRequestBody(containing("grant_type=client_credentials"))); - }); - } - - @Test - void optionalScopePropertyShouldBeIncludedInTokenRequest() { - var pairs = applicationProperties("id", "secret", "profile,email"); - String base64Credentials = Base64.getEncoder().encodeToString(("id:secret").getBytes()); - - stubTokenEndpoints(); - - contextRunner.withPropertyValues(pairs).run(context -> { - assertThat(context).hasSingleBean(VaultTokenRefresher.class); - - tryRefreshingToken(context); - - verify(postRequestedFor(urlEqualTo("/token/uri")) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded;charset=UTF-8")) - .withHeader("Authorization", equalTo("Basic " + base64Credentials)) - .withRequestBody(containing("grant_type=client_credentials")) - .withRequestBody(containing("scope=profile+email"))); - }); - } - @Test void schedulesVaultTokenRefresh() { - var pairs = applicationProperties("id", "secret", "profile,email"); - - stubTokenEndpoints(); + stubFor(post("/vault/v1/auth/token/renew-self").withHost(equalTo("server.local")) + .willReturn(aResponse().withHeader("Content-Type", "plain/text").withBody("new-token"))); - contextRunner.withPropertyValues(pairs).run(context -> { + contextRunner.withPropertyValues("spring.cloud.config.token=vault-token").run(context -> { assertThat(context).hasSingleBean(VaultTokenRefresher.class); await().atMost(3L, TimeUnit.SECONDS).untilAsserted(() -> { verify(moreThan(2), postRequestedFor(urlEqualTo("/vault/v1/auth/token/renew-self")) .withHeader("Content-Type", equalTo("application/json")) - .withHeader("Authorization", equalTo("Bearer access-token")) .withHeader("X-Vault-Token", equalTo("vault-token")) .withRequestBody(containing("{\"increment\":300}"))); }); }); } - private void stubTokenEndpoints() { - stubFor(post("/token/uri").withHost(equalTo("uaa.local")) - .willReturn(aResponse().withHeader("Content-Type", "application/json;charset=UTF-8").withBody(""" - { - "access_token" : "access-token", - "token_type" : "bearer" - }"""))); - - stubFor(post("/vault/v1/auth/token/renew-self").withHost(equalTo("server.local")) - .willReturn(aResponse().withHeader("Content-Type", "plain/text").withBody("new-token"))); - } - - private void tryRefreshingToken(BeanFactory factory) { - factory.getBean(VaultTokenRefresher.class).refreshVaultToken(); - } - - private String[] applicationProperties(String clientId, String clientSecret) { - return applicationProperties(clientId, clientSecret, ""); - } - - private String[] applicationProperties(String clientId, String clientSecret, String scope) { - return new String[] { "vault.token.renew.rate=1000", "spring.cloud.config.token=vault-token", - "spring.cloud.config.uri=http://server.local", - "spring.cloud.config.client.oauth2.access-token-uri=http://uaa.local/token/uri", - String.format("spring.cloud.config.client.oauth2.client-id=%s", clientId), - String.format("spring.cloud.config.client.oauth2.client-secret=%s", clientSecret), - String.format("spring.cloud.config.client.oauth2.scope=%s", scope), }; + private String[] applicationProperties() { + return new String[] { "vault.token.renew.rate=1000", "spring.cloud.config.uri=http://server.local" }; } }