Permit write and delete operations on Cloud Foundry with Spring Security

Update `CloudFoundryActuatorAutoConfiguration` so that CSRF protection
is ignored for Cloud Foundry endpoint requests. This aligns with the
behavior of Spring Boot 3.4 where `ignoring()` was used for security
configuration. With the migration to `permitAll()` we now need
additional CSRF configuration.

Fixes gh-45848
This commit is contained in:
Phillip Webb
2025-06-09 10:45:52 +01:00
parent 139436ea23
commit 564b65bc78
2 changed files with 28 additions and 2 deletions

View File

@@ -174,6 +174,7 @@ public class CloudFoundryActuatorAutoConfiguration {
SecurityFilterChain cloudFoundrySecurityFilterChain(HttpSecurity http,
CloudFoundryWebEndpointServletHandlerMapping handlerMapping) throws Exception {
RequestMatcher cloudFoundryRequest = getRequestMatcher(handlerMapping);
http.csrf((csrf) -> csrf.ignoringRequestMatchers(cloudFoundryRequest));
http.securityMatchers((matches) -> matches.requestMatchers(cloudFoundryRequest))
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll());
return http.build();

View File

@@ -33,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.ApiVersion;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
@@ -49,17 +50,24 @@ import org.springframework.boot.test.context.assertj.AssertableWebApplicationCon
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.config.BeanIds;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
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;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link CloudFoundryActuatorAutoConfiguration}.
@@ -170,7 +178,7 @@ class CloudFoundryActuatorAutoConfigurationTests {
}
@Test
void cloudFoundryPathsIgnoredBySpringSecurity() {
void cloudFoundryPathsPermittedBySpringSecurity() {
this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new)
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id")
.run((context) -> {
@@ -189,6 +197,19 @@ class CloudFoundryActuatorAutoConfigurationTests {
});
}
@Test
void cloudFoundryPathsPermittedWithCsrfBySpringSecurity() {
this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new)
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id")
.run((context) -> {
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
mvc.perform(post(BASE_PATH + "/test?name=test").contentType(MediaType.APPLICATION_JSON)
.with(csrf().useInvalidToken())).andExpect(status().isServiceUnavailable());
// If CSRF fails we'll get a 403, if it works we get service unavailable
// because of "Cloud controller URL is not available"
});
}
private SecurityFilterChain getSecurityFilterChain(AssertableWebApplicationContext context) {
Filter springSecurityFilterChain = context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class);
FilterChainProxy filterChainProxy = getFilterChainProxy(springSecurityFilterChain);
@@ -258,7 +279,7 @@ class CloudFoundryActuatorAutoConfigurationTests {
.findFirst()
.get();
Collection<WebOperation> operations = endpoint.getOperations();
assertThat(operations).hasSize(1);
assertThat(operations).hasSize(2);
assertThat(operations.iterator().next().getRequestPredicate().getPath()).isEqualTo("test");
});
}
@@ -307,6 +328,10 @@ class CloudFoundryActuatorAutoConfigurationTests {
return "hello world";
}
@WriteOperation
void update(String name) {
}
}
}