Skip to content

Commit 9b2e13a

Browse files
Hatef Palizgarphilwebb
authored andcommitted
Allow remote devtools access with Spring Security
Update `ManagementWebSecurityAutoConfiguration` so that the `managementSecurityFilterChain` bean has an explicit order. Prior to this commit, the `managementSecurityFilterChain` would override the `securityFilterChain` in `RemoteDevtoolsSecurityConfiguration` which would prevent the remote devtools endpoint from being accessed. See gh-25868
1 parent c45bb2b commit 9b2e13a

File tree

2 files changed

+95
-1
lines changed

2 files changed

+95
-1
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@
2626
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2727
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
2828
import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity;
29+
import org.springframework.boot.autoconfigure.security.SecurityProperties;
2930
import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration;
3031
import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration;
3132
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration;
3233
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
3334
import org.springframework.context.annotation.Bean;
3435
import org.springframework.context.annotation.Configuration;
36+
import org.springframework.core.annotation.Order;
3537
import org.springframework.security.config.Customizer;
3638
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3739
import org.springframework.security.web.SecurityFilterChain;
@@ -58,6 +60,7 @@
5860
public class ManagementWebSecurityAutoConfiguration {
5961

6062
@Bean
63+
@Order(SecurityProperties.BASIC_AUTH_ORDER)
6164
SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) throws Exception {
6265
http.authorizeRequests((requests) -> {
6366
requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll();

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,41 @@
1717
package org.springframework.boot.actuate.autoconfigure.security.servlet;
1818

1919
import java.io.IOException;
20+
import java.util.Arrays;
21+
import java.util.Comparator;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
import java.util.concurrent.ConcurrentHashMap;
26+
import java.util.stream.Collectors;
2027

28+
import org.jetbrains.annotations.NotNull;
2129
import org.junit.jupiter.api.Test;
2230

31+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
32+
import org.springframework.beans.factory.config.BeanDefinitionHolder;
2333
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
2434
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
2535
import org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration;
2636
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
2737
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
2838
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
2939
import org.springframework.boot.autoconfigure.AutoConfigurations;
40+
import org.springframework.boot.autoconfigure.security.SecurityProperties;
3041
import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration;
3142
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration;
3243
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
3344
import org.springframework.boot.test.context.FilteredClassLoader;
3445
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
3546
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
47+
import org.springframework.context.ConfigurableApplicationContext;
3648
import org.springframework.context.annotation.Bean;
3749
import org.springframework.context.annotation.Configuration;
50+
import org.springframework.core.Ordered;
51+
import org.springframework.core.ResolvableType;
52+
import org.springframework.core.annotation.AnnotationAttributes;
53+
import org.springframework.core.annotation.AnnotationUtils;
54+
import org.springframework.core.annotation.Order;
3855
import org.springframework.http.HttpStatus;
3956
import org.springframework.mock.web.MockFilterChain;
4057
import org.springframework.mock.web.MockHttpServletRequest;
@@ -45,6 +62,7 @@
4562
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
4663
import org.springframework.security.web.FilterChainProxy;
4764
import org.springframework.security.web.SecurityFilterChain;
65+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
4866
import org.springframework.web.context.WebApplicationContext;
4967

5068
import static org.assertj.core.api.Assertions.assertThat;
@@ -121,7 +139,7 @@ void backOffIfCustomSecurityIsAdded() {
121139
@Test
122140
void backsOffIfSecurityFilterChainBeanIsPresent() {
123141
this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class).run((context) -> {
124-
assertThat(context.getBeansOfType(SecurityFilterChain.class).size()).isEqualTo(1);
142+
assertThat(context.getBeansOfType(SecurityFilterChain.class)).isNotEmpty();
125143
assertThat(context.containsBean("testSecurityFilterChain")).isTrue();
126144
});
127145
}
@@ -146,6 +164,28 @@ void backOffIfSaml2RelyingPartyAutoConfigurationPresent() {
146164
.doesNotHaveBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN));
147165
}
148166

167+
@Test
168+
void backOffIfRemoteDevToolsSecurityFilterChainIsPresent() {
169+
this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class).run((context) -> {
170+
List<String> beanNames = getOrderedBeanNames(context);
171+
172+
assertThat(beanNames).containsExactly("testRemoteDevToolsSecurityFilterChain", "testSecurityFilterChain");
173+
assertThat(context.getBeansOfType(SecurityFilterChain.class).size()).isEqualTo(2);
174+
assertThat(context).doesNotHaveBean(ManagementWebSecurityAutoConfiguration.class);
175+
assertThat(context.containsBean("testRemoteDevToolsSecurityFilterChain")).isTrue();
176+
});
177+
}
178+
179+
@NotNull
180+
private List<String> getOrderedBeanNames(AssertableWebApplicationContext context) {
181+
return Arrays.stream(context.getBeanNamesForType(SecurityFilterChain.class))
182+
.map((beanName) -> Optional.of(context).map(ConfigurableApplicationContext::getBeanFactory)
183+
.map((beanFactory) -> beanFactory.getBeanDefinition(beanName))
184+
.map((beanDefinition) -> new BeanDefinitionHolder(beanDefinition, beanName)).orElse(null))
185+
.sorted(OrderAnnotatedBeanDefinitionComparator.INSTANCE).map(BeanDefinitionHolder::getBeanName)
186+
.collect(Collectors.toList());
187+
}
188+
149189
private HttpStatus getResponseStatus(AssertableWebApplicationContext context, String path)
150190
throws IOException, javax.servlet.ServletException {
151191
FilterChainProxy filterChainProxy = context.getBean(FilterChainProxy.class);
@@ -183,6 +223,57 @@ SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception
183223
.build();
184224
}
185225

226+
@Bean
227+
@Order(SecurityProperties.BASIC_AUTH_ORDER - 1)
228+
SecurityFilterChain testRemoteDevToolsSecurityFilterChain(HttpSecurity http) throws Exception {
229+
return http.requestMatcher(new AntPathRequestMatcher("/**")).authorizeRequests().anyRequest().anonymous()
230+
.and().csrf().disable().build();
231+
}
232+
233+
}
234+
235+
static class OrderAnnotatedBeanDefinitionComparator implements Comparator<BeanDefinitionHolder> {
236+
237+
static final OrderAnnotatedBeanDefinitionComparator INSTANCE = new OrderAnnotatedBeanDefinitionComparator();
238+
239+
private final Map<String, Integer> beanNameToOrder = new ConcurrentHashMap<>();
240+
241+
@Override
242+
public int compare(BeanDefinitionHolder beanOne, BeanDefinitionHolder beanTwo) {
243+
return getOrder(beanOne).compareTo(getOrder(beanTwo));
244+
}
245+
246+
private Integer getOrder(BeanDefinitionHolder bean) {
247+
return this.beanNameToOrder.computeIfAbsent(bean.getBeanName(),
248+
(beanName) -> Optional.of(bean).map(BeanDefinitionHolder::getBeanDefinition)
249+
.filter(AnnotatedBeanDefinition.class::isInstance).map(AnnotatedBeanDefinition.class::cast)
250+
.map(this::getOrderAnnotationAttributesFromFactoryMethod).map(this::getOrder)
251+
.orElse(Ordered.LOWEST_PRECEDENCE));
252+
}
253+
254+
private Integer getOrder(AnnotationAttributes annotationAttributes) {
255+
return Optional.ofNullable(annotationAttributes)
256+
.map((it) -> it.getOrDefault("value", Ordered.LOWEST_PRECEDENCE)).map(Integer.class::cast)
257+
.orElse(Ordered.LOWEST_PRECEDENCE);
258+
}
259+
260+
private AnnotationAttributes getOrderAnnotationAttributesFromFactoryMethod(
261+
AnnotatedBeanDefinition beanDefinition) {
262+
return Optional.of(beanDefinition).map(AnnotatedBeanDefinition::getFactoryMethodMetadata)
263+
.filter((methodMetadata) -> methodMetadata.isAnnotated(Order.class.getName()))
264+
.map((methodMetadata) -> methodMetadata.getAnnotationAttributes(Order.class.getName()))
265+
.map(AnnotationAttributes::fromMap)
266+
.orElseGet(() -> getOrderAnnotationAttributesFromBeanClass(beanDefinition));
267+
}
268+
269+
private AnnotationAttributes getOrderAnnotationAttributesFromBeanClass(AnnotatedBeanDefinition beanDefinition) {
270+
return Optional.of(beanDefinition).map(AnnotatedBeanDefinition::getResolvableType)
271+
.map(ResolvableType::resolve).filter((beanType) -> beanType.isAnnotationPresent(Order.class))
272+
.map((beanType) -> beanType.getAnnotation(Order.class))
273+
.map(AnnotationUtils::getAnnotationAttributes).map(AnnotationAttributes::fromMap).orElse(null);
274+
275+
}
276+
186277
}
187278

188279
}

0 commit comments

Comments
 (0)