Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@

import org.jspecify.annotations.Nullable;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
Expand Down Expand Up @@ -56,22 +54,24 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.WebFilter;

/**
* {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
* Cloud Foundry to use in a reactive environment.
*
* @author Madhura Bhave
* @author Aashikant Kumar
* @since 4.0.0
*/
@AutoConfiguration(after = InfoEndpointAutoConfiguration.class,
Expand Down Expand Up @@ -133,7 +133,7 @@ private SecurityInterceptor getSecurityInterceptor(WebClient.Builder webClientBu
? new SecurityService(webClientBuilder, cloudControllerUrl, skipSslValidation) : null;
}

private CorsConfiguration getCorsConfiguration() {
private static CorsConfiguration getCorsConfiguration() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
corsConfiguration.setAllowedMethods(Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name()));
Expand All @@ -158,38 +158,21 @@ CloudFoundryReactiveHealthEndpointWebExtension cloudFoundryReactiveHealthEndpoin
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MatcherSecurityWebFilterChain.class)
@ConditionalOnClass({ ServerHttpSecurity.class, SecurityWebFilterChain.class, WebFilterChainProxy.class })
static class IgnoredPathsSecurityConfiguration {

@Bean
static WebFilterChainPostProcessor webFilterChainPostProcessor() {
return new WebFilterChainPostProcessor();
}

}

static class WebFilterChainPostProcessor implements BeanPostProcessor {
private static final int FILTER_CHAIN_ORDER = -1;

WebFilterChainPostProcessor() {
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebFilterChainProxy webFilterChainProxy) {
return postProcess(webFilterChainProxy);
}
return bean;
}

private WebFilterChainProxy postProcess(WebFilterChainProxy existing) {
ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers
.pathMatchers(BASE_PATH + "/**");
WebFilter noOpFilter = (exchange, chain) -> chain.filter(exchange);
MatcherSecurityWebFilterChain ignoredRequestFilterChain = new MatcherSecurityWebFilterChain(
cloudFoundryRequestMatcher, Collections.singletonList(noOpFilter));
MatcherSecurityWebFilterChain allRequestsFilterChain = new MatcherSecurityWebFilterChain(
ServerWebExchangeMatchers.anyExchange(), Collections.singletonList(existing));
return new WebFilterChainProxy(ignoredRequestFilterChain, allRequestsFilterChain);
@Bean
@Order(FILTER_CHAIN_ORDER)
SecurityWebFilterChain cloudFoundrySecurityWebFilterChain(ServerHttpSecurity http) {
ServerWebExchangeMatcher cloudFoundryRequest = ServerWebExchangeMatchers.pathMatchers(BASE_PATH + "/**");
http.securityMatcher(cloudFoundryRequest);
http.authorizeExchange((exchanges) -> exchanges.anyExchange().permitAll());
http.csrf((csrf) -> csrf.disable());
CorsConfiguration corsConfiguration = getCorsConfiguration();
http.cors((cors) -> cors.configurationSource((exchange) -> corsConfiguration));
return http.build();
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.netty.http.HttpResources;

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration;
Expand Down Expand Up @@ -75,18 +77,24 @@
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.function.client.WebClient;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;

/**
* Tests for {@link CloudFoundryReactiveActuatorAutoConfiguration}.
*
* @author Madhura Bhave
* @author Moritz Halbritter
* @author Aashikant Kumar
*/
class CloudFoundryReactiveActuatorAutoConfigurationTests {

Expand Down Expand Up @@ -206,10 +214,23 @@ void cloudFoundryPathsIgnoredBySpringSecurity() {
assertThat(cfRequestWithAdditionalPathMatches).isTrue();
assertThat(otherCfRequestMatches).isTrue();
assertThat(otherRequestMatches).isFalse();
otherRequestMatches = filters.get(1)
.matches(MockServerWebExchange.from(MockServerHttpRequest.get("/some-other-path").build()))
.block(Duration.ofSeconds(30));
assertThat(otherRequestMatches).isTrue();
});
});
}

@Test
void userSecurityWebFilterChainIsPreserved() {
SecurityWebFilterChain userChain = mock(SecurityWebFilterChain.class);
this.contextRunner.withBean(SecurityWebFilterChain.class, () -> userChain)
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:https://my-cloud-controller.com")
.run((context) -> {
assertThat(context.getBean(WebFilterChainProxy.class))
.extracting("filters", InstanceOfAssertFactories.list(SecurityWebFilterChain.class))
.hasSize(2)
.satisfies((filters) -> {
assertThat(getMatches(filters, BASE_PATH)).isTrue();
assertThat(filters.get(1)).isSameAs(userChain);
});
});
}
Expand All @@ -220,6 +241,55 @@ void cloudFoundryPathsIgnoredBySpringSecurity() {
.block(Duration.ofSeconds(30));
}

@Test
void customFirewallBeanIsPreserved() {
ServerWebExchangeFirewall customFirewall = mock(ServerWebExchangeFirewall.class);
this.contextRunner.withBean(ServerWebExchangeFirewall.class, () -> customFirewall)
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:https://my-cloud-controller.com")
.run((context) -> {
WebFilterChainProxy proxy = context.getBean(WebFilterChainProxy.class);
assertThat(proxy).extracting("firewall").isSameAs(customFirewall);
});
}

@Test
void directlyConfiguredFirewallIsPreserved() {
ServerWebExchangeFirewall customFirewall = mock(ServerWebExchangeFirewall.class);
this.contextRunner.withBean(BeanPostProcessor.class, () -> new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof WebFilterChainProxy proxy) {
proxy.setFirewall(customFirewall);
}
return bean;
}
})
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:https://my-cloud-controller.com")
.run((context) -> {
WebFilterChainProxy proxy = context.getBean(WebFilterChainProxy.class);
assertThat(proxy).extracting("firewall").isSameAs(customFirewall);
});
}

@Test
void customFirewallIsInvokedOnRequests() {
ServerWebExchangeFirewall customFirewall = mock(ServerWebExchangeFirewall.class);
given(customFirewall.getFirewalledExchange(any()))
.willAnswer((invocation) -> Mono.just(invocation.getArgument(0)));
this.contextRunner.withBean(ServerWebExchangeFirewall.class, () -> customFirewall)
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:https://my-cloud-controller.com")
.run((context) -> {
WebFilterChainProxy proxy = context.getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/some-other-path").build());
proxy.filter(exchange, (ex) -> Mono.empty()).block(Duration.ofSeconds(30));
then(customFirewall).should().getFirewalledExchange(any());
});
}

@Test
void cloudFoundryPlatformInactive() {
this.contextRunner
Expand Down