Skip to content

Commit

Permalink
Uses a ConfigDataLocationResolver to configure authorization intercep…
Browse files Browse the repository at this point in the history
…tor (#399)
  • Loading branch information
kvmw committed Jan 2, 2024
1 parent 512c459 commit f9c734a
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 457 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,16 @@
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;
import org.springframework.core.Ordered;
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
* <code>spring.config.import</code> property before the
Expand All @@ -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<CfService> 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<String, Object>();
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<String, Object> 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<String, Object> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
* <code>RestTemplate</code> that calls config server.
* <p>
* 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 <code>RestTemplate</code> for
* calling config server.
* <p>
* Finally, it registers the <code>RestTemplate</code> bean to be consumed by
* {@link ConfigResourceClientAutoConfiguration} and
* {@link VaultTokenRenewalAutoConfiguration} after application startup.
*/
public class ConfigClientOAuth2ConfigDataLocationResolver
implements ConfigDataLocationResolver<ConfigServerConfigDataResource>, 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<ConfigServerConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location)
throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException {
throw new IllegalStateException("Unexpected call. This resolver should not resolve any location");
}

@Override
public List<ConfigServerConfigDataResource> 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;
}

}

}
Loading

0 comments on commit f9c734a

Please sign in to comment.