mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-03 15:59:31 +00:00
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
This commit is contained in:
@@ -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"))
|
||||
|
||||
+8
-9
@@ -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() {
|
||||
|
||||
+16
-12
@@ -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");
|
||||
|
||||
+4
-12
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+8
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user