Merge branch '4.0.x'

Closes gh-48827
This commit is contained in:
Andy Wilkinson
2026-01-14 15:48:58 +00:00
5 changed files with 36 additions and 45 deletions
@@ -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"))
@@ -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<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 {
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() {
@@ -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<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 {
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");
@@ -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);
});
}
@@ -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