Commit 0699fdcc authored by Phillip Webb's avatar Phillip Webb

Polish 'Allow remote devtools access with Spring Security'

See gh-25868
parent 9b2e13aa
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -48,6 +48,7 @@ import org.springframework.security.web.SecurityFilterChain;
* of the custom security configuration.
*
* @author Madhura Bhave
* @author Hatef Palizgar
* @since 2.1.0
*/
@Configuration(proxyBeanMethods = false)
......
......@@ -17,19 +17,11 @@
package org.springframework.boot.actuate.autoconfigure.security.servlet;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration;
......@@ -44,13 +36,8 @@ import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfi
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockFilterChain;
......@@ -71,6 +58,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link ManagementWebSecurityAutoConfiguration}.
*
* @author Madhura Bhave
* @author Hatef Palizgar
*/
class ManagementWebSecurityAutoConfigurationTests {
......@@ -139,7 +127,7 @@ class ManagementWebSecurityAutoConfigurationTests {
@Test
void backsOffIfSecurityFilterChainBeanIsPresent() {
this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class).run((context) -> {
assertThat(context.getBeansOfType(SecurityFilterChain.class)).isNotEmpty();
assertThat(context.getBeansOfType(SecurityFilterChain.class)).hasSize(1);
assertThat(context.containsBean("testSecurityFilterChain")).isTrue();
});
}
......@@ -166,26 +154,19 @@ class ManagementWebSecurityAutoConfigurationTests {
@Test
void backOffIfRemoteDevToolsSecurityFilterChainIsPresent() {
this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class).run((context) -> {
List<String> beanNames = getOrderedBeanNames(context);
assertThat(beanNames).containsExactly("testRemoteDevToolsSecurityFilterChain", "testSecurityFilterChain");
assertThat(context.getBeansOfType(SecurityFilterChain.class).size()).isEqualTo(2);
this.contextRunner.withUserConfiguration(TestRemoteDevToolsSecurityFilterChainConfig.class).run((context) -> {
SecurityFilterChain testSecurityFilterChain = context.getBean("testSecurityFilterChain",
SecurityFilterChain.class);
SecurityFilterChain testRemoteDevToolsSecurityFilterChain = context
.getBean("testRemoteDevToolsSecurityFilterChain", SecurityFilterChain.class);
List<SecurityFilterChain> orderedSecurityFilterChains = context.getBeanProvider(SecurityFilterChain.class)
.orderedStream().collect(Collectors.toList());
assertThat(orderedSecurityFilterChains).containsExactly(testRemoteDevToolsSecurityFilterChain,
testSecurityFilterChain);
assertThat(context).doesNotHaveBean(ManagementWebSecurityAutoConfiguration.class);
assertThat(context.containsBean("testRemoteDevToolsSecurityFilterChain")).isTrue();
});
}
@NotNull
private List<String> getOrderedBeanNames(AssertableWebApplicationContext context) {
return Arrays.stream(context.getBeanNamesForType(SecurityFilterChain.class))
.map((beanName) -> Optional.of(context).map(ConfigurableApplicationContext::getBeanFactory)
.map((beanFactory) -> beanFactory.getBeanDefinition(beanName))
.map((beanDefinition) -> new BeanDefinitionHolder(beanDefinition, beanName)).orElse(null))
.sorted(OrderAnnotatedBeanDefinitionComparator.INSTANCE).map(BeanDefinitionHolder::getBeanName)
.collect(Collectors.toList());
}
private HttpStatus getResponseStatus(AssertableWebApplicationContext context, String path)
throws IOException, javax.servlet.ServletException {
FilterChainProxy filterChainProxy = context.getBean(FilterChainProxy.class);
......@@ -223,6 +204,11 @@ class ManagementWebSecurityAutoConfigurationTests {
.build();
}
}
@Configuration(proxyBeanMethods = false)
static class TestRemoteDevToolsSecurityFilterChainConfig extends TestSecurityFilterChainConfig {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER - 1)
SecurityFilterChain testRemoteDevToolsSecurityFilterChain(HttpSecurity http) throws Exception {
......@@ -232,48 +218,4 @@ class ManagementWebSecurityAutoConfigurationTests {
}
static class OrderAnnotatedBeanDefinitionComparator implements Comparator<BeanDefinitionHolder> {
static final OrderAnnotatedBeanDefinitionComparator INSTANCE = new OrderAnnotatedBeanDefinitionComparator();
private final Map<String, Integer> beanNameToOrder = new ConcurrentHashMap<>();
@Override
public int compare(BeanDefinitionHolder beanOne, BeanDefinitionHolder beanTwo) {
return getOrder(beanOne).compareTo(getOrder(beanTwo));
}
private Integer getOrder(BeanDefinitionHolder bean) {
return this.beanNameToOrder.computeIfAbsent(bean.getBeanName(),
(beanName) -> Optional.of(bean).map(BeanDefinitionHolder::getBeanDefinition)
.filter(AnnotatedBeanDefinition.class::isInstance).map(AnnotatedBeanDefinition.class::cast)
.map(this::getOrderAnnotationAttributesFromFactoryMethod).map(this::getOrder)
.orElse(Ordered.LOWEST_PRECEDENCE));
}
private Integer getOrder(AnnotationAttributes annotationAttributes) {
return Optional.ofNullable(annotationAttributes)
.map((it) -> it.getOrDefault("value", Ordered.LOWEST_PRECEDENCE)).map(Integer.class::cast)
.orElse(Ordered.LOWEST_PRECEDENCE);
}
private AnnotationAttributes getOrderAnnotationAttributesFromFactoryMethod(
AnnotatedBeanDefinition beanDefinition) {
return Optional.of(beanDefinition).map(AnnotatedBeanDefinition::getFactoryMethodMetadata)
.filter((methodMetadata) -> methodMetadata.isAnnotated(Order.class.getName()))
.map((methodMetadata) -> methodMetadata.getAnnotationAttributes(Order.class.getName()))
.map(AnnotationAttributes::fromMap)
.orElseGet(() -> getOrderAnnotationAttributesFromBeanClass(beanDefinition));
}
private AnnotationAttributes getOrderAnnotationAttributesFromBeanClass(AnnotatedBeanDefinition beanDefinition) {
return Optional.of(beanDefinition).map(AnnotatedBeanDefinition::getResolvableType)
.map(ResolvableType::resolve).filter((beanType) -> beanType.isAnnotationPresent(Order.class))
.map((beanType) -> beanType.getAnnotation(Order.class))
.map(AnnotationUtils::getAnnotationAttributes).map(AnnotationAttributes::fromMap).orElse(null);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment