Browse Source

Fix CloudFoundry actuator auto-config in absence of RestTemplateBuilder

Previously, CloudFoundryActuatorAutoConfiguration required
RestTemplateBuilder, using it to create the RestTemplate that's used
the security interceptor. Following the modularization,
RestTemplateBuilder is only present when spring-boot-restclient is on
the classpath. In its absence, CloudFoundryActuatorAutoConfiguration
would fail.

This commit address this problem by using RestClient.Builder (and
RestClient) instead of RestTemplateBuilder (and RestTemplate). This
allows CloudFoundryActuatorAutoConfiguration to work without
spring-boot-restclient as RestClient.Builder and RestClient are
provided by spring-web that will always be there in an MVC webapp.

Fixes gh-48826
pull/48875/head
Andy Wilkinson 2 weeks ago
parent
commit
7af147d091
  1. 1
      module/spring-boot-cloudfoundry/build.gradle
  2. 17
      module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java
  3. 28
      module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityService.java
  4. 16
      module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java
  5. 19
      module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityServiceTests.java

1
module/spring-boot-cloudfoundry/build.gradle

@ -29,7 +29,6 @@ dependencies { @@ -29,7 +29,6 @@ dependencies {
api(project(":module:spring-boot-actuator-autoconfigure"))
optional(project(":module:spring-boot-health"))
optional(project(":module:spring-boot-restclient"))
optional(project(":module:spring-boot-security"))
optional(project(":module:spring-boot-webclient"))
optional(project(":module:spring-boot-webflux"))

17
module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java

@ -51,7 +51,6 @@ import org.springframework.boot.cloudfoundry.autoconfigure.actuate.endpoint.Clou @@ -51,7 +51,6 @@ import org.springframework.boot.cloudfoundry.autoconfigure.actuate.endpoint.Clou
import org.springframework.boot.health.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.health.actuate.endpoint.HealthEndpointWebExtension;
import org.springframework.boot.info.GitProperties;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -67,6 +66,7 @@ import org.springframework.security.web.SecurityFilterChain; @@ -67,6 +66,7 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.client.RestClient;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
@ -115,15 +115,15 @@ public final class CloudFoundryActuatorAutoConfiguration { @@ -115,15 +115,15 @@ public final class CloudFoundryActuatorAutoConfiguration {
@SuppressWarnings("removal")
CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping(
ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
RestTemplateBuilder restTemplateBuilder,
ObjectProvider<RestClient.Builder> restClientBuilder,
org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier servletEndpointsSupplier,
org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier,
ApplicationContext applicationContext) {
CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(applicationContext,
parameterMapper, endpointMediaTypes, null, Collections.emptyList(), Collections.emptyList(),
Collections.emptyList());
SecurityInterceptor securityInterceptor = getSecurityInterceptor(restTemplateBuilder,
applicationContext.getEnvironment());
SecurityInterceptor securityInterceptor = getSecurityInterceptor(
restClientBuilder.getIfAvailable(RestClient::builder), applicationContext.getEnvironment());
Collection<ExposableWebEndpoint> webEndpoints = discoverer.getEndpoints();
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
allEndpoints.addAll(webEndpoints);
@ -133,22 +133,21 @@ public final class CloudFoundryActuatorAutoConfiguration { @@ -133,22 +133,21 @@ public final class CloudFoundryActuatorAutoConfiguration {
endpointMediaTypes, getCorsConfiguration(), securityInterceptor, allEndpoints);
}
private SecurityInterceptor getSecurityInterceptor(RestTemplateBuilder restTemplateBuilder,
Environment environment) {
SecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(restTemplateBuilder, environment);
private SecurityInterceptor getSecurityInterceptor(RestClient.Builder restClientBuilder, Environment environment) {
SecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(restClientBuilder, environment);
TokenValidator tokenValidator = (cloudfoundrySecurityService != null)
? new TokenValidator(cloudfoundrySecurityService) : null;
return new SecurityInterceptor(tokenValidator, cloudfoundrySecurityService,
environment.getProperty("vcap.application.application_id"));
}
private @Nullable SecurityService getCloudFoundrySecurityService(RestTemplateBuilder restTemplateBuilder,
private @Nullable SecurityService getCloudFoundrySecurityService(RestClient.Builder restClientBuilder,
Environment environment) {
String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
boolean skipSslValidation = environment.getProperty("management.cloudfoundry.skip-ssl-validation",
Boolean.class, false);
return (cloudControllerUrl != null)
? new SecurityService(restTemplateBuilder, cloudControllerUrl, skipSslValidation) : null;
? new SecurityService(restClientBuilder, cloudControllerUrl, skipSslValidation) : null;
}
private CorsConfiguration getCorsConfiguration() {

28
module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityService.java

@ -27,14 +27,12 @@ import org.jspecify.annotations.Nullable; @@ -27,14 +27,12 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.cloudfoundry.autoconfigure.actuate.endpoint.AccessLevel;
import org.springframework.boot.cloudfoundry.autoconfigure.actuate.endpoint.CloudFoundryAuthorizationException;
import org.springframework.boot.cloudfoundry.autoconfigure.actuate.endpoint.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClient;
/**
* Cloud Foundry security service to handle REST calls to the cloud controller and UAA.
@ -43,19 +41,19 @@ import org.springframework.web.client.RestTemplate; @@ -43,19 +41,19 @@ import org.springframework.web.client.RestTemplate;
*/
class SecurityService {
private final RestTemplate restTemplate;
private final RestClient restClient;
private final String cloudControllerUrl;
private @Nullable String uaaUrl;
SecurityService(RestTemplateBuilder restTemplateBuilder, String cloudControllerUrl, boolean skipSslValidation) {
Assert.notNull(restTemplateBuilder, "'restTemplateBuilder' must not be null");
SecurityService(RestClient.Builder restClientBuilder, String cloudControllerUrl, boolean skipSslValidation) {
Assert.notNull(restClientBuilder, "'restClientBuilder' must not be null");
Assert.notNull(cloudControllerUrl, "'cloudControllerUrl' must not be null");
if (skipSslValidation) {
restTemplateBuilder = restTemplateBuilder.requestFactory(SkipSslVerificationHttpRequestFactory.class);
restClientBuilder = restClientBuilder.requestFactory(new SkipSslVerificationHttpRequestFactory());
}
this.restTemplate = restTemplateBuilder.build();
this.restClient = restClientBuilder.build();
this.cloudControllerUrl = cloudControllerUrl;
}
@ -69,8 +67,11 @@ class SecurityService { @@ -69,8 +67,11 @@ class SecurityService {
AccessLevel getAccessLevel(String token, String applicationId) throws CloudFoundryAuthorizationException {
try {
URI uri = getPermissionsUri(applicationId);
RequestEntity<?> request = RequestEntity.get(uri).header("Authorization", "bearer " + token).build();
Map<?, ?> body = this.restTemplate.exchange(request, Map.class).getBody();
Map<?, ?> body = this.restClient.get()
.uri(uri)
.header("Authorization", "bearer " + token)
.retrieve()
.body(Map.class);
if (body != null && Boolean.TRUE.equals(body.get("read_sensitive_data"))) {
return AccessLevel.FULL;
}
@ -102,7 +103,7 @@ class SecurityService { @@ -102,7 +103,7 @@ class SecurityService {
*/
Map<String, String> fetchTokenKeys() {
try {
Map<?, ?> response = this.restTemplate.getForObject(getUaaUrl() + "/token_keys", Map.class);
Map<?, ?> response = this.restClient.get().uri(getUaaUrl() + "/token_keys").retrieve().body(Map.class);
Assert.state(response != null, "'response' must not be null");
return extractTokenKeys(response);
}
@ -129,7 +130,10 @@ class SecurityService { @@ -129,7 +130,10 @@ class SecurityService {
String getUaaUrl() {
if (this.uaaUrl == null) {
try {
Map<?, ?> response = this.restTemplate.getForObject(this.cloudControllerUrl + "/info", Map.class);
Map<?, ?> response = this.restClient.get()
.uri(this.cloudControllerUrl + "/info")
.retrieve()
.body(Map.class);
Assert.state(response != null, "'response' must not be null");
String tokenEndpoint = (String) response.get("token_endpoint");
Assert.state(tokenEndpoint != null, "'tokenEndpoint' must not be null");

16
module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java

@ -42,7 +42,6 @@ import org.springframework.boot.health.autoconfigure.contributor.HealthContribut @@ -42,7 +42,6 @@ import org.springframework.boot.health.autoconfigure.contributor.HealthContribut
import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.restclient.autoconfigure.RestTemplateAutoConfiguration;
import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration;
import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration;
import org.springframework.boot.servlet.autoconfigure.actuate.web.ServletManagementContextAutoConfiguration;
@ -61,7 +60,6 @@ import org.springframework.test.util.ReflectionTestUtils; @@ -61,7 +60,6 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.filter.CompositeFilter;
@ -87,7 +85,7 @@ class CloudFoundryActuatorAutoConfigurationTests { @@ -87,7 +85,7 @@ class CloudFoundryActuatorAutoConfigurationTests {
ServletWebSecurityAutoConfiguration.class, WebMvcAutoConfiguration.class,
JacksonAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
RestTemplateAutoConfiguration.class, ManagementContextAutoConfiguration.class,
/* RestTemplateAutoConfiguration.class, */ ManagementContextAutoConfiguration.class,
ServletManagementContextAutoConfiguration.class, EndpointAutoConfiguration.class,
WebEndpointAutoConfiguration.class, CloudFoundryActuatorAutoConfiguration.class));
@ -164,15 +162,9 @@ class CloudFoundryActuatorAutoConfigurationTests { @@ -164,15 +162,9 @@ class CloudFoundryActuatorAutoConfigurationTests {
"management.cloudfoundry.skip-ssl-validation:true")
.run((context) -> {
CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping(context);
Object interceptor = ReflectionTestUtils.getField(handlerMapping, "securityInterceptor");
assertThat(interceptor).isNotNull();
Object interceptorSecurityService = ReflectionTestUtils.getField(interceptor,
"cloudFoundrySecurityService");
assertThat(interceptorSecurityService).isNotNull();
RestTemplate restTemplate = (RestTemplate) ReflectionTestUtils.getField(interceptorSecurityService,
"restTemplate");
assertThat(restTemplate).isNotNull();
assertThat(restTemplate.getRequestFactory()).isInstanceOf(SkipSslVerificationHttpRequestFactory.class);
assertThat(handlerMapping)
.extracting("securityInterceptor.cloudFoundrySecurityService.restClient.clientRequestFactory")
.isInstanceOf(SkipSslVerificationHttpRequestFactory.class);
});
}

19
module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityServiceTests.java

@ -29,9 +29,8 @@ import org.springframework.boot.restclient.RestTemplateBuilder; @@ -29,9 +29,8 @@ import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.restclient.test.MockServerRestTemplateCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ -63,26 +62,24 @@ class SecurityServiceTests { @@ -63,26 +62,24 @@ class SecurityServiceTests {
void setup() {
MockServerRestTemplateCustomizer mockServerCustomizer = new MockServerRestTemplateCustomizer();
RestTemplateBuilder builder = new RestTemplateBuilder(mockServerCustomizer);
this.securityService = new SecurityService(builder, CLOUD_CONTROLLER, false);
this.securityService = new SecurityService(RestClient.builder(builder.build()), CLOUD_CONTROLLER, false);
this.server = mockServerCustomizer.getServer();
}
@Test
void skipSslValidationWhenTrue() {
RestTemplateBuilder builder = new RestTemplateBuilder();
this.securityService = new SecurityService(builder, CLOUD_CONTROLLER, true);
RestTemplate restTemplate = (RestTemplate) ReflectionTestUtils.getField(this.securityService, "restTemplate");
assertThat(restTemplate).isNotNull();
assertThat(restTemplate.getRequestFactory()).isInstanceOf(SkipSslVerificationHttpRequestFactory.class);
this.securityService = new SecurityService(RestClient.builder(builder.build()), CLOUD_CONTROLLER, true);
assertThat(this.securityService).extracting("restClient.clientRequestFactory")
.isInstanceOf(SkipSslVerificationHttpRequestFactory.class);
}
@Test
void doNotSkipSslValidationWhenFalse() {
RestTemplateBuilder builder = new RestTemplateBuilder();
this.securityService = new SecurityService(builder, CLOUD_CONTROLLER, false);
RestTemplate restTemplate = (RestTemplate) ReflectionTestUtils.getField(this.securityService, "restTemplate");
assertThat(restTemplate).isNotNull();
assertThat(restTemplate.getRequestFactory()).isNotInstanceOf(SkipSslVerificationHttpRequestFactory.class);
this.securityService = new SecurityService(RestClient.builder(builder.build()), CLOUD_CONTROLLER, false);
assertThat(this.securityService).extracting("restClient.clientRequestFactory")
.isNotInstanceOf(SkipSslVerificationHttpRequestFactory.class);
}
@Test

Loading…
Cancel
Save