Skip to content

Commit

Permalink
Use endpoint mappings in CloudFoundry integration
Browse files Browse the repository at this point in the history
Closes gh-35085
  • Loading branch information
mbhave authored and wilkinsona committed Apr 20, 2023
1 parent da10c4e commit 3522714
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -76,6 +80,10 @@ protected LinksHandler getLinksHandler() {
return new CloudFoundryLinksHandler();
}

Collection<ExposableEndpoint<?>> getAllEndpoints() {
return this.allEndpoints;
}

class CloudFoundryLinksHandler implements LinksHandler {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
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;
Expand Down Expand Up @@ -81,6 +81,8 @@
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
public class ReactiveCloudFoundryActuatorAutoConfiguration {

private static final String BASE_PATH = "/cloudfoundryapplication";

@Bean
@ConditionalOnMissingBean
@ConditionalOnAvailableEndpoint
Expand Down Expand Up @@ -116,9 +118,8 @@ public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHand
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,
Expand Down Expand Up @@ -154,25 +155,33 @@ private CorsConfiguration getCorsConfiguration() {
static class IgnoredPathsSecurityConfiguration {

@Bean
WebFilterChainPostProcessor webFilterChainPostProcessor() {
return new WebFilterChainPostProcessor();
WebFilterChainPostProcessor webFilterChainPostProcessor(
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping) {
return new WebFilterChainPostProcessor(handlerMapping);
}

}

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));
Expand All @@ -181,6 +190,14 @@ private WebFilterChainProxy postProcess(WebFilterChainProxy existing) {
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;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@
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;
Expand Down Expand Up @@ -66,6 +66,9 @@
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;

Expand All @@ -85,6 +88,8 @@
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
public class CloudFoundryActuatorAutoConfiguration {

private static final String BASE_PATH = "/cloudfoundryapplication";

@Bean
@ConditionalOnMissingBean
@ConditionalOnAvailableEndpoint
Expand Down Expand Up @@ -122,8 +127,7 @@ public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServl
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,
Expand Down Expand Up @@ -163,18 +167,32 @@ private CorsConfiguration getCorsConfiguration() {
public static class IgnoredCloudFoundryPathsWebSecurityConfiguration {

@Bean
IgnoredCloudFoundryPathsWebSecurityCustomizer ignoreCloudFoundryPathsWebSecurityCustomizer() {
return new IgnoredCloudFoundryPathsWebSecurityCustomizer();
IgnoredCloudFoundryPathsWebSecurityCustomizer ignoreCloudFoundryPathsWebSecurityCustomizer(
CloudFoundryWebEndpointServletHandlerMapping handlerMapping) {
return new IgnoredCloudFoundryPathsWebSecurityCustomizer(handlerMapping);
}

}

@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));
}
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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;
Expand Down Expand Up @@ -61,14 +62,17 @@ 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,
WebMvcAutoConfiguration.pathPatternParser);
this.securityInterceptor = securityInterceptor;
this.linksResolver = linksResolver;
this.linksResolver = new EndpointLinksResolver(allEndpoints);
this.allEndpoints = allEndpoints;
}

@Override
Expand All @@ -82,6 +86,10 @@ protected LinksHandler getLinksHandler() {
return new CloudFoundryLinksHandler();
}

Collection<ExposableEndpoint<?>> getAllEndpoints() {
return this.allEndpoints;
}

class CloudFoundryLinksHandler implements LinksHandler {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,15 +31,16 @@
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;
Expand Down Expand Up @@ -245,9 +249,10 @@ CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebEndpointServletHandlerM
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -176,21 +178,24 @@ void cloudFoundryPlatformActiveAndCloudControllerUrlNotPresent() {
@Test
@SuppressWarnings("unchecked")
void cloudFoundryPathsIgnoredBySpringSecurity() {
this.contextRunner
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()))
Expand All @@ -200,6 +205,13 @@ void cloudFoundryPathsIgnoredBySpringSecurity() {

}

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
void cloudFoundryPlatformInactive() {
this.contextRunner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,20 +170,31 @@ void cloudFoundryPlatformActiveAndCloudControllerUrlNotPresent() {

@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()
Expand Down

0 comments on commit 3522714

Please sign in to comment.