Use endpoint mappings in CloudFoundry integration

Closes gh-35411
2.5.x
Madhura Bhave 2 years ago committed by Phillip Webb
parent 585286e472
commit 307f3c3399

@ -28,6 +28,7 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -56,12 +57,15 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH
private final EndpointLinksResolver linksResolver;
private final Collection<ExposableEndpoint<?>> allEndpoints;
CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
CorsConfiguration corsConfiguration, CloudFoundrySecurityInterceptor securityInterceptor,
EndpointLinksResolver linksResolver) {
Collection<ExposableEndpoint<?>> allEndpoints) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, true);
this.linksResolver = linksResolver;
this.linksResolver = new EndpointLinksResolver(allEndpoints);
this.allEndpoints = allEndpoints;
this.securityInterceptor = securityInterceptor;
}
@ -76,6 +80,10 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH
return new CloudFoundryLinksHandler();
}
Collection<ExposableEndpoint<?>> getAllEndpoints() {
return this.allEndpoints;
}
class CloudFoundryLinksHandler implements LinksHandler {
@Override

@ -33,10 +33,10 @@ import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoC
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
@ -82,6 +82,8 @@ import org.springframework.web.server.WebFilter;
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
public class ReactiveCloudFoundryActuatorAutoConfiguration {
private static final String BASE_PATH = "/cloudfoundryapplication";
@Bean
@ConditionalOnMissingBean
@ConditionalOnAvailableEndpoint
@ -117,9 +119,8 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
return new CloudFoundryWebFluxEndpointHandlerMapping(new EndpointMapping("/cloudfoundryapplication"),
webEndpoints, endpointMediaTypes, getCorsConfiguration(), securityInterceptor,
new EndpointLinksResolver(allEndpoints));
return new CloudFoundryWebFluxEndpointHandlerMapping(new EndpointMapping(BASE_PATH), webEndpoints,
endpointMediaTypes, getCorsConfiguration(), securityInterceptor, allEndpoints);
}
private CloudFoundrySecurityInterceptor getSecurityInterceptor(WebClient.Builder webClientBuilder,
@ -155,25 +156,33 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
static class IgnoredPathsSecurityConfiguration {
@Bean
WebFilterChainPostProcessor webFilterChainPostProcessor() {
return new WebFilterChainPostProcessor();
WebFilterChainPostProcessor webFilterChainPostProcessor(
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping) {
return new WebFilterChainPostProcessor(handlerMapping);
}
}
private static class WebFilterChainPostProcessor implements BeanPostProcessor {
private final PathMappedEndpoints pathMappedEndpoints;
WebFilterChainPostProcessor(CloudFoundryWebFluxEndpointHandlerMapping handlerMapping) {
this.pathMappedEndpoints = new PathMappedEndpoints(BASE_PATH, handlerMapping::getAllEndpoints);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebFilterChainProxy) {
return postProcess((WebFilterChainProxy) bean);
return postProcess((WebFilterChainProxy) bean, this.pathMappedEndpoints);
}
return bean;
}
private WebFilterChainProxy postProcess(WebFilterChainProxy existing) {
private WebFilterChainProxy postProcess(WebFilterChainProxy existing, PathMappedEndpoints pathMappedEndpoints) {
List<String> paths = getPaths(pathMappedEndpoints);
ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers
.pathMatchers("/cloudfoundryapplication/**");
.pathMatchers(paths.toArray(new String[] {}));
WebFilter noOpFilter = (exchange, chain) -> chain.filter(exchange);
MatcherSecurityWebFilterChain ignoredRequestFilterChain = new MatcherSecurityWebFilterChain(
cloudFoundryRequestMatcher, Collections.singletonList(noOpFilter));
@ -182,6 +191,14 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
return new WebFilterChainProxy(ignoredRequestFilterChain, allRequestsFilterChain);
}
private static List<String> getPaths(PathMappedEndpoints pathMappedEndpoints) {
List<String> paths = new ArrayList<>();
pathMappedEndpoints.getAllPaths().forEach((path) -> paths.add(path + "/**"));
paths.add(BASE_PATH);
paths.add(BASE_PATH + "/");
return paths;
}
}
}

@ -31,10 +31,10 @@ import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfi
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.health.HealthEndpoint;
@ -66,6 +66,9 @@ import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
@ -86,6 +89,8 @@ import org.springframework.web.servlet.DispatcherServlet;
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
public class CloudFoundryActuatorAutoConfiguration {
private static final String BASE_PATH = "/cloudfoundryapplication";
@Bean
@ConditionalOnMissingBean
@ConditionalOnAvailableEndpoint
@ -123,8 +128,7 @@ public class CloudFoundryActuatorAutoConfiguration {
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
return new CloudFoundryWebEndpointServletHandlerMapping(new EndpointMapping("/cloudfoundryapplication"),
webEndpoints, endpointMediaTypes, getCorsConfiguration(), securityInterceptor,
new EndpointLinksResolver(allEndpoints));
webEndpoints, endpointMediaTypes, getCorsConfiguration(), securityInterceptor, allEndpoints);
}
private CloudFoundrySecurityInterceptor getSecurityInterceptor(RestTemplateBuilder restTemplateBuilder,
@ -164,8 +168,9 @@ public class CloudFoundryActuatorAutoConfiguration {
public static class IgnoredCloudFoundryPathsWebSecurityConfiguration {
@Bean
IgnoredCloudFoundryPathsWebSecurityCustomizer ignoreCloudFoundryPathsWebSecurityCustomizer() {
return new IgnoredCloudFoundryPathsWebSecurityCustomizer();
IgnoredCloudFoundryPathsWebSecurityCustomizer ignoreCloudFoundryPathsWebSecurityCustomizer(
CloudFoundryWebEndpointServletHandlerMapping handlerMapping) {
return new IgnoredCloudFoundryPathsWebSecurityCustomizer(handlerMapping);
}
}
@ -173,9 +178,22 @@ public class CloudFoundryActuatorAutoConfiguration {
@Order(SecurityProperties.IGNORED_ORDER)
static class IgnoredCloudFoundryPathsWebSecurityCustomizer implements WebSecurityCustomizer {
private final PathMappedEndpoints pathMappedEndpoints;
IgnoredCloudFoundryPathsWebSecurityCustomizer(CloudFoundryWebEndpointServletHandlerMapping handlerMapping) {
this.pathMappedEndpoints = new PathMappedEndpoints(BASE_PATH, handlerMapping::getAllEndpoints);
}
@Override
public void customize(WebSecurity web) {
web.ignoring().requestMatchers(new AntPathRequestMatcher("/cloudfoundryapplication/**"));
List<RequestMatcher> requestMatchers = new ArrayList<>();
this.pathMappedEndpoints.getAllPaths()
.forEach((path) -> requestMatchers.add(new AntPathRequestMatcher(path + "/**")));
requestMatchers.add(new AntPathRequestMatcher(BASE_PATH));
requestMatchers.add(new AntPathRequestMatcher(BASE_PATH + "/"));
if (!CollectionUtils.isEmpty(requestMatchers)) {
web.ignoring().requestMatchers(new OrRequestMatcher(requestMatchers));
}
}
}

@ -31,6 +31,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -60,13 +61,16 @@ class CloudFoundryWebEndpointServletHandlerMapping extends AbstractWebMvcEndpoin
private final EndpointLinksResolver linksResolver;
private final Collection<ExposableEndpoint<?>> allEndpoints;
CloudFoundryWebEndpointServletHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
CorsConfiguration corsConfiguration, CloudFoundrySecurityInterceptor securityInterceptor,
EndpointLinksResolver linksResolver) {
Collection<ExposableEndpoint<?>> allEndpoints) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, true);
this.securityInterceptor = securityInterceptor;
this.linksResolver = linksResolver;
this.linksResolver = new EndpointLinksResolver(allEndpoints);
this.allEndpoints = allEndpoints;
}
@Override
@ -80,6 +84,10 @@ class CloudFoundryWebEndpointServletHandlerMapping extends AbstractWebMvcEndpoin
return new CloudFoundryLinksHandler();
}
Collection<ExposableEndpoint<?>> getAllEndpoints() {
return this.allEndpoints;
}
class CloudFoundryLinksHandler implements LinksHandler {
@Override

@ -17,8 +17,11 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@ -28,15 +31,16 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
@ -184,9 +188,10 @@ class CloudFoundryWebFluxEndpointIntegrationTests {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("https://example.com"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new CloudFoundryWebFluxEndpointHandlerMapping(new EndpointMapping("/cfApplication"),
webEndpointDiscoverer.getEndpoints(), endpointMediaTypes, corsConfiguration, interceptor,
new EndpointLinksResolver(webEndpointDiscoverer.getEndpoints()));
Collection<ExposableWebEndpoint> webEndpoints = webEndpointDiscoverer.getEndpoints();
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>(webEndpoints);
return new CloudFoundryWebFluxEndpointHandlerMapping(new EndpointMapping("/cfApplication"), webEndpoints,
endpointMediaTypes, corsConfiguration, interceptor, allEndpoints);
}
@Bean

@ -95,6 +95,8 @@ class ReactiveCloudFoundryActuatorAutoConfigurationTests {
InfoContributorAutoConfiguration.class, InfoEndpointAutoConfiguration.class,
ProjectInfoAutoConfiguration.class, ReactiveCloudFoundryActuatorAutoConfiguration.class));
private static final String BASE_PATH = "/cloudfoundryapplication";
@AfterEach
void close() {
HttpResources.reset();
@ -170,26 +172,36 @@ class ReactiveCloudFoundryActuatorAutoConfigurationTests {
@Test
@SuppressWarnings("unchecked")
void cloudFoundryPathsIgnoredBySpringSecurity() {
this.contextRunner.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:https://my-cloud-controller.com").run((context) -> {
this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new).withPropertyValues("VCAP_APPLICATION:---",
"vcap.application.application_id:my-app-id", "vcap.application.cf_api:https://my-cloud-controller.com")
.run((context) -> {
WebFilterChainProxy chainProxy = context.getBean(WebFilterChainProxy.class);
List<SecurityWebFilterChain> filters = (List<SecurityWebFilterChain>) ReflectionTestUtils
.getField(chainProxy, "filters");
Boolean cfRequestMatches = filters.get(0)
.matches(MockServerWebExchange
.from(MockServerHttpRequest.get("/cloudfoundryapplication/my-path").build()))
.block(Duration.ofSeconds(30));
Boolean otherRequestMatches = filters.get(0)
.matches(MockServerWebExchange.from(MockServerHttpRequest.get("/some-other-path").build()))
.block(Duration.ofSeconds(30));
Boolean cfBaseRequestMatches = getMatches(filters, BASE_PATH);
Boolean cfBaseWithTrailingSlashRequestMatches = getMatches(filters, BASE_PATH + "/");
Boolean cfRequestMatches = getMatches(filters, BASE_PATH + "/test");
Boolean cfRequestWithAdditionalPathMatches = getMatches(filters, BASE_PATH + "/test/a");
Boolean otherCfRequestMatches = getMatches(filters, BASE_PATH + "/other-path");
Boolean otherRequestMatches = getMatches(filters, "/some-other-path");
assertThat(cfBaseRequestMatches).isTrue();
assertThat(cfBaseWithTrailingSlashRequestMatches).isTrue();
assertThat(cfRequestMatches).isTrue();
assertThat(cfRequestWithAdditionalPathMatches).isTrue();
assertThat(otherCfRequestMatches).isFalse();
assertThat(otherRequestMatches).isFalse();
otherRequestMatches = filters.get(1)
.matches(MockServerWebExchange.from(MockServerHttpRequest.get("/some-other-path").build()))
.block(Duration.ofSeconds(30));
assertThat(otherRequestMatches).isTrue();
});
}
private static Boolean getMatches(List<SecurityWebFilterChain> filters, String urlTemplate) {
Boolean cfBaseRequestMatches = filters.get(0)
.matches(MockServerWebExchange.from(MockServerHttpRequest.get(urlTemplate).build()))
.block(Duration.ofSeconds(30));
return cfBaseRequestMatches;
}
@Test

@ -77,6 +77,8 @@ class CloudFoundryActuatorAutoConfigurationTests {
ServletManagementContextAutoConfiguration.class, EndpointAutoConfiguration.class,
WebEndpointAutoConfiguration.class, CloudFoundryActuatorAutoConfiguration.class));
private static String BASE_PATH = "/cloudfoundryapplication";
@Test
void cloudFoundryPlatformActive() {
this.contextRunner.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
@ -160,20 +162,31 @@ class CloudFoundryActuatorAutoConfigurationTests {
@Test
void cloudFoundryPathsIgnoredBySpringSecurity() {
this.contextRunner.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id")
this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new)
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id")
.run((context) -> {
FilterChainProxy securityFilterChain = (FilterChainProxy) context
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
SecurityFilterChain chain = securityFilterChain.getFilterChains().get(0);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setServletPath("/cloudfoundryapplication/my-path");
assertThat(chain.getFilters()).isEmpty();
assertThat(chain.matches(request)).isTrue();
MockHttpServletRequest request = new MockHttpServletRequest();
testCloudFoundrySecurity(request, BASE_PATH, chain);
testCloudFoundrySecurity(request, BASE_PATH + "/", chain);
testCloudFoundrySecurity(request, BASE_PATH + "/test", chain);
testCloudFoundrySecurity(request, BASE_PATH + "/test/a", chain);
request.setServletPath(BASE_PATH + "/other-path");
assertThat(chain.matches(request)).isFalse();
request.setServletPath("/some-other-path");
assertThat(chain.matches(request)).isFalse();
});
}
private static void testCloudFoundrySecurity(MockHttpServletRequest request, String basePath,
SecurityFilterChain chain) {
request.setServletPath(basePath);
assertThat(chain.matches(request)).isTrue();
}
@Test
void cloudFoundryPlatformInactive() {
this.contextRunner.withPropertyValues()

@ -17,8 +17,11 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@ -28,15 +31,16 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
@ -180,9 +184,10 @@ class CloudFoundryMvcWebEndpointIntegrationTests {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("https://example.com"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new CloudFoundryWebEndpointServletHandlerMapping(new EndpointMapping("/cfApplication"),
webEndpointDiscoverer.getEndpoints(), endpointMediaTypes, corsConfiguration, interceptor,
new EndpointLinksResolver(webEndpointDiscoverer.getEndpoints()));
Collection<ExposableWebEndpoint> webEndpoints = webEndpointDiscoverer.getEndpoints();
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>(webEndpoints);
return new CloudFoundryWebEndpointServletHandlerMapping(new EndpointMapping("/cfApplication"), webEndpoints,
endpointMediaTypes, corsConfiguration, interceptor, allEndpoints);
}
@Bean

Loading…
Cancel
Save