From 7af147d09196554a3386a463abb18f0eb90c3398 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Jan 2026 15:45:08 +0000 Subject: [PATCH] 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 --- module/spring-boot-cloudfoundry/build.gradle | 1 - ...CloudFoundryActuatorAutoConfiguration.java | 17 ++++++----- .../endpoint/servlet/SecurityService.java | 28 +++++++++++-------- ...FoundryActuatorAutoConfigurationTests.java | 16 +++-------- .../servlet/SecurityServiceTests.java | 19 ++++++------- 5 files changed, 36 insertions(+), 45 deletions(-) diff --git a/module/spring-boot-cloudfoundry/build.gradle b/module/spring-boot-cloudfoundry/build.gradle index 5482e1a7ca0..2be858c4e4b 100644 --- a/module/spring-boot-cloudfoundry/build.gradle +++ b/module/spring-boot-cloudfoundry/build.gradle @@ -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")) diff --git a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java index aebc8276b5a..705d011ef80 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java +++ b/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 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; 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 { @SuppressWarnings("removal") CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping( ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, - RestTemplateBuilder restTemplateBuilder, + ObjectProvider 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 webEndpoints = discoverer.getEndpoints(); List> allEndpoints = new ArrayList<>(); allEndpoints.addAll(webEndpoints); @@ -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() { diff --git a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityService.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityService.java index 132aa7f7768..9078796dbef 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityService.java +++ b/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; 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; */ 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 { 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 { */ Map 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 { 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"); diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java index 521fb969a68..b9f15a5a36e 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/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 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; 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 { 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 { "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); }); } diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityServiceTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityServiceTests.java index 3eae46a36b2..b1c8c250bda 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/SecurityServiceTests.java +++ b/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; 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 { 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