diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java similarity index 66% rename from spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfiguration.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java index 9535eb1310a..661d1780611 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java @@ -13,41 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.boot.autoconfigure.security.servlet; + +package org.springframework.boot.actuate.autoconfigure.security.servlet; import org.glassfish.jersey.server.ResourceConfig; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider; +import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; /** - * Auto-configuration for {@link RequestMatcherProvider}. + * {@link ManagementContextConfiguration} that configures the appropriate + * {@link RequestMatcherProvider}. * * @author Madhura Bhave - * @since 2.0.5 + * @since 2.1.8 */ -@Configuration(proxyBeanMethods = false) +@ManagementContextConfiguration @ConditionalOnClass({ RequestMatcher.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -public class SecurityRequestMatcherProviderAutoConfiguration { +public class SecurityRequestMatchersManagementContextConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(DispatcherServlet.class) - @ConditionalOnBean(HandlerMappingIntrospector.class) + @ConditionalOnBean(DispatcherServletPath.class) public static class MvcRequestMatcherConfiguration { @Bean + @ConditionalOnMissingBean @ConditionalOnClass(DispatcherServlet.class) - public RequestMatcherProvider requestMatcherProvider(HandlerMappingIntrospector introspector) { - return new MvcRequestMatcherProvider(introspector); + public RequestMatcherProvider requestMatcherProvider(DispatcherServletPath servletPath) { + return new AntPathRequestMatcherProvider(servletPath::getRelativePath); } } @@ -60,7 +67,7 @@ public class SecurityRequestMatcherProviderAutoConfiguration { @Bean public RequestMatcherProvider requestMatcherProvider(JerseyApplicationPath applicationPath) { - return new JerseyRequestMatcherProvider(applicationPath); + return new AntPathRequestMatcherProvider(applicationPath::getRelativePath); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index 0052355cde6..bfa6e20e8cd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -95,6 +95,7 @@ org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManag org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration,\ +org.springframework.boot.actuate.autoconfigure.security.servlet.SecurityRequestMatchersManagementContextConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.jersey.JerseyChildManagementContextConfiguration,\ org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementChildContextConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java index d44202f5812..f048926e1df 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java @@ -15,20 +15,24 @@ */ package org.springframework.boot.actuate.autoconfigure.security.servlet; -import java.util.ArrayList; import java.util.Base64; -import java.util.List; +import java.util.function.Supplier; +import org.jolokia.http.AgentServlet; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; -import org.springframework.boot.actuate.endpoint.Operation; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint; -import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; +import org.springframework.boot.actuate.endpoint.web.EndpointServlet; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -39,9 +43,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - /** * Abstract base class for {@link EndpointRequest} tests. * @@ -49,8 +50,6 @@ import static org.mockito.Mockito.mock; */ abstract class AbstractEndpointRequestIntegrationTests { - protected abstract WebApplicationContextRunner getContextRunner(); - @Test void toEndpointShouldMatch() { getContextRunner().run((context) -> { @@ -79,6 +78,17 @@ abstract class AbstractEndpointRequestIntegrationTests { }); } + protected final WebApplicationContextRunner getContextRunner() { + return createContextRunner().withPropertyValues("management.endpoints.web.exposure.include=*") + .withUserConfiguration(BaseConfiguration.class, SecurityConfiguration.class).withConfiguration( + AutoConfigurations.of(JacksonAutoConfiguration.class, SecurityAutoConfiguration.class, + UserDetailsServiceAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class)); + + } + + protected abstract WebApplicationContextRunner createContextRunner(); + protected WebTestClient getWebTestClient(AssertableWebApplicationContext context) { int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) .getWebServer().getPort(); @@ -108,19 +118,8 @@ abstract class AbstractEndpointRequestIntegrationTests { } @Bean - PathMappedEndpoints pathMappedEndpoints() { - List> endpoints = new ArrayList<>(); - endpoints.add(mockEndpoint("e1")); - endpoints.add(mockEndpoint("e2")); - endpoints.add(mockEndpoint("e3")); - return new PathMappedEndpoints("/actuator", () -> endpoints); - } - - private TestPathMappedEndpoint mockEndpoint(String id) { - TestPathMappedEndpoint endpoint = mock(TestPathMappedEndpoint.class); - given(endpoint.getEndpointId()).willReturn(EndpointId.of(id)); - given(endpoint.getRootPath()).willReturn(id); - return endpoint; + TestServletEndpoint servletEndpoint() { + return new TestServletEndpoint(); } } @@ -155,7 +154,13 @@ abstract class AbstractEndpointRequestIntegrationTests { } - public interface TestPathMappedEndpoint extends ExposableEndpoint, PathMappedEndpoint { + @ServletEndpoint(id = "se1") + static class TestServletEndpoint implements Supplier { + + @Override + public EndpointServlet get() { + return new EndpointServlet(AgentServlet.class); + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java index c613fc3e512..c8eee1e2700 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java @@ -15,38 +15,17 @@ */ package org.springframework.boot.actuate.autoconfigure.security.servlet; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; - import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.server.model.Resource; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; -import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; -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.annotation.WebEndpointDiscoverer; -import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; -import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherProviderAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.web.reactive.server.WebTestClient; @@ -58,18 +37,6 @@ import org.springframework.test.web.reactive.server.WebTestClient; */ class JerseyEndpointRequestIntegrationTests extends AbstractEndpointRequestIntegrationTests { - @Override - protected WebApplicationContextRunner getContextRunner() { - return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) - .withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) - .withUserConfiguration(JerseyEndpointConfiguration.class, SecurityConfiguration.class, - BaseConfiguration.class) - .withConfiguration(AutoConfigurations.of(SecurityAutoConfiguration.class, - UserDetailsServiceAutoConfiguration.class, - SecurityRequestMatcherProviderAutoConfiguration.class, JacksonAutoConfiguration.class, - JerseyAutoConfiguration.class)); - } - @Test void toLinksWhenApplicationPathSetShouldMatch() { getContextRunner().withPropertyValues("spring.jersey.application-path=/admin").run((context) -> { @@ -99,16 +66,47 @@ class JerseyEndpointRequestIntegrationTests extends AbstractEndpointRequestInteg }); } - @Configuration(proxyBeanMethods = false) + @Test + void toAnyEndpointShouldMatchServletEndpoint() { + getContextRunner().withPropertyValues("spring.security.user.password=password", + "management.endpoints.web.exposure.include=se1").run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/actuator/se1").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/actuator/se1").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + webTestClient.get().uri("/actuator/se1/list").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/actuator/se1/list").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + }); + } + + @Test + void toAnyEndpointWhenApplicationPathSetShouldMatchServletEndpoint() { + getContextRunner().withPropertyValues("spring.jersey.application-path=/admin", + "spring.security.user.password=password", "management.endpoints.web.exposure.include=se1") + .run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/admin/actuator/se1").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/admin/actuator/se1").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + webTestClient.get().uri("/admin/actuator/se1/list").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/admin/actuator/se1/list").header("Authorization", getBasicAuth()) + .exchange().expectStatus().isOk(); + }); + } + + @Override + protected WebApplicationContextRunner createContextRunner() { + return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) + .withUserConfiguration(JerseyEndpointConfiguration.class) + .withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class)); + } + + @Configuration @EnableConfigurationProperties(WebEndpointProperties.class) static class JerseyEndpointConfiguration { - private final ApplicationContext applicationContext; - - JerseyEndpointConfiguration(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - @Bean TomcatServletWebServerFactory tomcat() { return new TomcatServletWebServerFactory(0); @@ -119,24 +117,6 @@ class JerseyEndpointRequestIntegrationTests extends AbstractEndpointRequestInteg return new ResourceConfig(); } - @Bean - ResourceConfigCustomizer webEndpointRegistrar() { - return this::customize; - } - - private void customize(ResourceConfig config) { - List mediaTypes = Arrays.asList(javax.ws.rs.core.MediaType.APPLICATION_JSON, - ActuatorMediaType.V2_JSON); - EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes); - WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, - new ConversionServiceParameterValueMapper(), endpointMediaTypes, - Arrays.asList(EndpointId::toString), Collections.emptyList(), Collections.emptyList()); - Collection resources = new JerseyEndpointResourceFactory().createEndpointResources( - new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes, - new EndpointLinksResolver(discoverer.getEndpoints()), true); - config.registerResources(new HashSet<>(resources)); - } - } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/MvcEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/MvcEndpointRequestIntegrationTests.java index 0df5dc2f40c..ce93ec195e0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/MvcEndpointRequestIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/MvcEndpointRequestIntegrationTests.java @@ -15,39 +15,20 @@ */ package org.springframework.boot.actuate.autoconfigure.security.servlet; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; -import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; -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.annotation.WebEndpointDiscoverer; -import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherProviderAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.cors.CorsConfiguration; /** * Integration tests for {@link EndpointRequest} with Spring MVC. @@ -85,44 +66,52 @@ class MvcEndpointRequestIntegrationTests extends AbstractEndpointRequestIntegrat }); } + @Test + void toAnyEndpointShouldMatchServletEndpoint() { + getContextRunner().withPropertyValues("spring.security.user.password=password", + "management.endpoints.web.exposure.include=se1").run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/actuator/se1").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/actuator/se1").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + webTestClient.get().uri("/actuator/se1/list").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/actuator/se1/list").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + }); + } + + @Test + void toAnyEndpointWhenServletPathSetShouldMatchServletEndpoint() { + getContextRunner().withPropertyValues("spring.mvc.servlet.path=/admin", + "spring.security.user.password=password", "management.endpoints.web.exposure.include=se1") + .run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/admin/actuator/se1").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/admin/actuator/se1").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + webTestClient.get().uri("/admin/actuator/se1/list").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/admin/actuator/se1/list").header("Authorization", getBasicAuth()) + .exchange().expectStatus().isOk(); + }); + } + @Override - protected WebApplicationContextRunner getContextRunner() { + protected WebApplicationContextRunner createContextRunner() { return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) - .withUserConfiguration(WebMvcEndpointConfiguration.class, SecurityConfiguration.class, - BaseConfiguration.class) - .withConfiguration(AutoConfigurations.of(SecurityAutoConfiguration.class, - UserDetailsServiceAutoConfiguration.class, WebMvcAutoConfiguration.class, - SecurityRequestMatcherProviderAutoConfiguration.class, JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, DispatcherServletAutoConfiguration.class)); + .withUserConfiguration(WebMvcEndpointConfiguration.class) + .withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class)); } @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(WebEndpointProperties.class) static class WebMvcEndpointConfiguration { - private final ApplicationContext applicationContext; - - WebMvcEndpointConfiguration(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - @Bean TomcatServletWebServerFactory tomcat() { return new TomcatServletWebServerFactory(0); } - @Bean - WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() { - List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON); - EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes); - WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, - new ConversionServiceParameterValueMapper(), endpointMediaTypes, - Arrays.asList(EndpointId::toString), Collections.emptyList(), Collections.emptyList()); - return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(), - endpointMediaTypes, new CorsConfiguration(), new EndpointLinksResolver(discoverer.getEndpoints()), - true); - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java similarity index 62% rename from spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfigurationTests.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java index de85df5f8f2..1c08e3239ca 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java @@ -13,35 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.boot.autoconfigure.security.servlet; + +package org.springframework.boot.actuate.autoconfigure.security.servlet; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider; +import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link SecurityRequestMatcherProviderAutoConfiguration}. + * Tests for {@link SecurityRequestMatchersManagementContextConfiguration}. * * @author Madhura Bhave */ -class SecurityRequestMatcherProviderAutoConfigurationTests { +class SecurityRequestMatchersManagementContextConfigurationTests { private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SecurityRequestMatcherProviderAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class)); @Test void configurationConditionalOnWebApplication() { new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SecurityRequestMatcherProviderAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class)) .withUserConfiguration(TestMvcConfiguration.class) .run((context) -> assertThat(context).doesNotHaveBean(RequestMatcherProvider.class)); } @@ -55,51 +60,58 @@ class SecurityRequestMatcherProviderAutoConfigurationTests { } @Test - void registersMvcRequestMatcherProviderIfMvcPresent() { - this.contextRunner.withUserConfiguration(TestMvcConfiguration.class).run((context) -> assertThat(context) - .getBean(RequestMatcherProvider.class).isInstanceOf(MvcRequestMatcherProvider.class)); + void registersRequestMatcherProviderIfMvcPresent() { + this.contextRunner.withUserConfiguration(TestMvcConfiguration.class).run((context) -> { + AntPathRequestMatcherProvider matcherProvider = context.getBean(AntPathRequestMatcherProvider.class); + RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example"); + assertThat(ReflectionTestUtils.getField(requestMatcher, "pattern")).isEqualTo("/custom/example"); + }); } @Test void registersRequestMatcherForJerseyProviderIfJerseyPresentAndMvcAbsent() { this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) - .withUserConfiguration(TestJerseyConfiguration.class).run((context) -> assertThat(context) - .getBean(RequestMatcherProvider.class).isInstanceOf(JerseyRequestMatcherProvider.class)); + .withUserConfiguration(TestJerseyConfiguration.class).run((context) -> { + AntPathRequestMatcherProvider matcherProvider = context + .getBean(AntPathRequestMatcherProvider.class); + RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example"); + assertThat(ReflectionTestUtils.getField(requestMatcher, "pattern")).isEqualTo("/admin/example"); + }); } @Test void mvcRequestMatcherProviderConditionalOnDispatcherServletClass() { this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) - .run((context) -> assertThat(context).doesNotHaveBean(MvcRequestMatcherProvider.class)); + .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); + } + + @Test + void mvcRequestMatcherProviderConditionalOnDispatcherServletPathBean() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); } @Test void jerseyRequestMatcherProviderConditionalOnResourceConfigClass() { this.contextRunner.withClassLoader(new FilteredClassLoader("org.glassfish.jersey.server.ResourceConfig")) - .run((context) -> assertThat(context).doesNotHaveBean(JerseyRequestMatcherProvider.class)); - } - - @Test - void mvcRequestMatcherProviderConditionalOnHandlerMappingIntrospectorBean() { - new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SecurityRequestMatcherProviderAutoConfiguration.class)) - .run((context) -> assertThat(context).doesNotHaveBean(MvcRequestMatcherProvider.class)); + .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); } @Test void jerseyRequestMatcherProviderConditionalOnJerseyApplicationPathBean() { new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SecurityRequestMatcherProviderAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class)) .withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) - .run((context) -> assertThat(context).doesNotHaveBean(JerseyRequestMatcherProvider.class)); + .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); } @Configuration(proxyBeanMethods = false) static class TestMvcConfiguration { @Bean - HandlerMappingIntrospector introspector() { - return new HandlerMappingIntrospector(); + DispatcherServletPath dispatcherServletPath() { + return () -> "/custom"; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/MvcRequestMatcherProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/AntPathRequestMatcherProvider.java similarity index 59% rename from spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/MvcRequestMatcherProvider.java rename to spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/AntPathRequestMatcherProvider.java index 46977b703f8..2f6e9c0d7ed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/MvcRequestMatcherProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/AntPathRequestMatcherProvider.java @@ -13,30 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.servlet; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import java.util.function.Function; + +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; /** - * {@link RequestMatcherProvider} that provides an {@link MvcRequestMatcher} that can be - * used for Spring MVC applications. + * {@link RequestMatcherProvider} that provides an {@link AntPathRequestMatcher}. * * @author Madhura Bhave - * @since 2.0.5 + * @since 2.1.8 */ -public class MvcRequestMatcherProvider implements RequestMatcherProvider { +public class AntPathRequestMatcherProvider implements RequestMatcherProvider { - private final HandlerMappingIntrospector introspector; + private final Function pathFactory; - public MvcRequestMatcherProvider(HandlerMappingIntrospector introspector) { - this.introspector = introspector; + public AntPathRequestMatcherProvider(Function pathFactory) { + this.pathFactory = pathFactory; } @Override public RequestMatcher getRequestMatcher(String pattern) { - return new MvcRequestMatcher(this.introspector, pattern); + return new AntPathRequestMatcher(this.pathFactory.apply(pattern)); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/JerseyRequestMatcherProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/JerseyRequestMatcherProvider.java index 9be169f6256..c25210b6cc0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/JerseyRequestMatcherProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/JerseyRequestMatcherProvider.java @@ -25,7 +25,9 @@ import org.springframework.security.web.util.matcher.RequestMatcher; * * @author Madhura Bhave * @since 2.0.7 + * @deprecated since 2.1.8 in favor of {@link AntPathRequestMatcher} */ +@Deprecated public class JerseyRequestMatcherProvider implements RequestMatcherProvider { private final JerseyApplicationPath jerseyApplicationPath; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index e791c007e06..eb889e83103 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -105,7 +105,6 @@ org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\ -org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherProviderAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\ org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\ diff --git a/spring-boot-tests/spring-boot-smoke-tests/pom.xml b/spring-boot-tests/spring-boot-smoke-tests/pom.xml index d6cf48412cb..da356cdd123 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/pom.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/pom.xml @@ -71,6 +71,7 @@ spring-boot-smoke-test-reactive-oauth2-client spring-boot-smoke-test-reactive-oauth2-resource-server spring-boot-smoke-test-secure + spring-boot-smoke-test-secure-jersey spring-boot-smoke-test-secure-webflux spring-boot-smoke-test-servlet spring-boot-smoke-test-session diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java new file mode 100644 index 00000000000..2e957a906c7 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.actuator.customsecurity; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Abstract base class for actuator tests with custom security. + * + * @author Madhura Bhave + */ +abstract class AbstractSampleActuatorCustomSecurityTests { + + abstract String getPath(); + + abstract String getManagementPath(); + + abstract Environment getEnvironment(); + + @Test + void homeIsSecure() { + @SuppressWarnings("rawtypes") + ResponseEntity entity = restTemplate().getForEntity(getPath() + "/", Map.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + @SuppressWarnings("unchecked") + Map body = entity.getBody(); + assertThat(body.get("error")).isEqualTo("Unauthorized"); + assertThat(entity.getHeaders()).doesNotContainKey("Set-Cookie"); + } + + @Test + void testInsecureStaticResources() { + ResponseEntity entity = restTemplate().getForEntity(getPath() + "/css/bootstrap.min.css", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("body"); + } + + @Test + void actuatorInsecureEndpoint() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator/health", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/health/diskSpace", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + } + + @Test + void actuatorLinksWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator", Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/", Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorLinksWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/", Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void actuatorLinksWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + adminRestTemplate().getForEntity(getManagementPath() + "/actuator/", Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void actuatorSecureEndpointWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator/env", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorSecureEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/env", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void actuatorSecureEndpointWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/env", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + entity = adminRestTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void secureServletEndpointWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity("/actuator/jolokia", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/jolokia/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void secureServletEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/jolokia", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/jolokia/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void secureServletEndpointWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/jolokia", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/jolokia/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void actuatorCustomMvcSecureEndpointWithAnonymous() { + ResponseEntity entity = restTemplate() + .getForEntity(getManagementPath() + "/actuator/example/echo?text={t}", String.class, "test"); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorCustomMvcSecureEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate() + .getForEntity(getManagementPath() + "/actuator/example/echo?text={t}", String.class, "test"); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void actuatorCustomMvcSecureEndpointWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate() + .getForEntity(getManagementPath() + "/actuator/example/echo?text={t}", String.class, "test"); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("test"); + assertThat(entity.getHeaders().getFirst("echo")).isEqualTo("test"); + } + + @Test + void actuatorExcludedFromEndpointRequestMatcher() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/mappings", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + TestRestTemplate restTemplate() { + return configure(new TestRestTemplate()); + } + + TestRestTemplate adminRestTemplate() { + return configure(new TestRestTemplate("admin", "admin")); + } + + TestRestTemplate userRestTemplate() { + return configure(new TestRestTemplate("user", "password")); + } + + TestRestTemplate beansRestTemplate() { + return configure(new TestRestTemplate("beans", "beans")); + } + + private TestRestTemplate configure(TestRestTemplate restTemplate) { + restTemplate.setUriTemplateHandler(new LocalHostUriTemplateHandler(getEnvironment())); + return restTemplate; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CustomServletPathSampleActuatorTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CustomServletPathSampleActuatorTests.java new file mode 100644 index 00000000000..e55e11e07d7 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CustomServletPathSampleActuatorTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.actuator.customsecurity; + +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for actuator endpoints with custom dispatcher servlet path. + * + * @author Madhura Bhave + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.mvc.servlet.path=/example") + +class CustomServletPathSampleActuatorTests extends AbstractSampleActuatorCustomSecurityTests { + + @LocalServerPort + private int port; + + @Autowired + private Environment environment; + + @Override + String getPath() { + return "http://localhost:" + this.port + "/example"; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.port + "/example"; + } + + @Override + Environment getEnvironment() { + return this.environment; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java index 412c8a82b25..bf875b44b29 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java @@ -18,25 +18,28 @@ package smoketest.actuator.customsecurity; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.web.server.LocalManagementPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for separate management and main service ports. + * Integration tests for separate management and main service ports with custom management + * context path. * * @author Dave Syer * @author Madhura Bhave */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "management.server.port=0", "management.server.servlet.context-path=/management" }) -class ManagementPortAndPathSampleActuatorApplicationTests { +class ManagementPortAndPathSampleActuatorApplicationTests extends AbstractSampleActuatorCustomSecurityTests { @LocalServerPort private int port; @@ -44,35 +47,8 @@ class ManagementPortAndPathSampleActuatorApplicationTests { @LocalManagementPort private int managementPort; - @Test - void testHome() { - ResponseEntity entity = new TestRestTemplate("user", "password") - .getForEntity("http://localhost:" + this.port, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("Hello World"); - } - - @Test - void actuatorPathOnMainPortShouldNotMatch() { - ResponseEntity entity = new TestRestTemplate() - .getForEntity("http://localhost:" + this.port + "/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - - @Test - void testSecureActuator() { - ResponseEntity entity = new TestRestTemplate() - .getForEntity("http://localhost:" + this.managementPort + "/management/actuator/env", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - - @Test - void testInsecureActuator() { - ResponseEntity entity = new TestRestTemplate() - .getForEntity("http://localhost:" + this.managementPort + "/management/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); - } + @Autowired + private Environment environment; @Test void testMissing() { @@ -82,4 +58,19 @@ class ManagementPortAndPathSampleActuatorApplicationTests { assertThat(entity.getBody()).contains("\"status\":404"); } + @Override + String getPath() { + return "http://localhost:" + this.port; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.managementPort + "/management"; + } + + @Override + Environment getEnvironment() { + return this.environment; + } + } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementPortCustomServletPathSampleActuatorTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementPortCustomServletPathSampleActuatorTests.java new file mode 100644 index 00000000000..d8fb11a4eed --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementPortCustomServletPathSampleActuatorTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.actuator.customsecurity; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.web.server.LocalManagementPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports with custom dispatcher + * servlet path. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "spring.mvc.servlet.path=/example" }) +class ManagementPortCustomServletPathSampleActuatorTests extends AbstractSampleActuatorCustomSecurityTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Autowired + private Environment environment; + + @Test + void actuatorPathOnMainPortShouldNotMatch() { + ResponseEntity entity = new TestRestTemplate() + .getForEntity("http://localhost:" + this.port + "/example/actuator/health", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Override + String getPath() { + return "http://localhost:" + this.port + "/example"; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.managementPort; + } + + @Override + Environment getEnvironment() { + return this.environment; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java index a27eda943d9..2fbfdf25461 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java @@ -22,8 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler; -import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -31,132 +30,52 @@ import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; /** + * Integration tests for actuator endpoints with custom security configuration. + * * @author Madhura Bhave * @author Stephane Nicoll */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class SampleActuatorCustomSecurityApplicationTests { +class SampleActuatorCustomSecurityApplicationTests extends AbstractSampleActuatorCustomSecurityTests { + + @LocalServerPort + private int port; @Autowired private Environment environment; - @Test - void homeIsSecure() { - @SuppressWarnings("rawtypes") - ResponseEntity entity = restTemplate().getForEntity("/", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - @SuppressWarnings("unchecked") - Map body = entity.getBody(); - assertThat(body.get("error")).isEqualTo("Unauthorized"); - assertThat(entity.getHeaders()).doesNotContainKey("Set-Cookie"); + @Override + String getPath() { + return "http://localhost:" + this.port; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.port; + } + + @Override + Environment getEnvironment() { + return this.environment; } @Test void testInsecureApplicationPath() { @SuppressWarnings("rawtypes") - ResponseEntity entity = restTemplate().getForEntity("/foo", Map.class); + ResponseEntity entity = restTemplate().getForEntity(getPath() + "/foo", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); @SuppressWarnings("unchecked") Map body = entity.getBody(); assertThat((String) body.get("message")).contains("Expected exception in controller"); } - @Test - void testInsecureStaticResources() { - ResponseEntity entity = restTemplate().getForEntity("/css/bootstrap.min.css", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("body"); - } - - @Test - void actuatorInsecureEndpoint() { - ResponseEntity entity = restTemplate().getForEntity("/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); - } - - @Test - void actuatorLinksIsSecure() { - ResponseEntity entity = restTemplate().getForEntity("/actuator", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = adminRestTemplate().getForEntity("/actuator", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - @Test - void actuatorSecureEndpointWithAnonymous() { - ResponseEntity entity = restTemplate().getForEntity("/actuator/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - - @Test - void actuatorSecureEndpointWithUnauthorizedUser() { - ResponseEntity entity = userRestTemplate().getForEntity("/actuator/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - } - - @Test - void actuatorSecureEndpointWithAuthorizedUser() { - ResponseEntity entity = adminRestTemplate().getForEntity("/actuator/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - @Test - void actuatorCustomMvcSecureEndpointWithAnonymous() { - ResponseEntity entity = restTemplate().getForEntity("/actuator/example/echo?text={t}", String.class, - "test"); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - - @Test - void actuatorCustomMvcSecureEndpointWithUnauthorizedUser() { - ResponseEntity entity = userRestTemplate().getForEntity("/actuator/example/echo?text={t}", String.class, - "test"); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - } - - @Test - void actuatorCustomMvcSecureEndpointWithAuthorizedUser() { - ResponseEntity entity = adminRestTemplate().getForEntity("/actuator/example/echo?text={t}", - String.class, "test"); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("test"); - assertThat(entity.getHeaders().getFirst("echo")).isEqualTo("test"); - } - - @Test - void actuatorExcludedFromEndpointRequestMatcher() { - ResponseEntity entity = userRestTemplate().getForEntity("/actuator/mappings", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - } - @Test void mvcMatchersCanBeUsedToSecureActuators() { - ResponseEntity entity = beansRestTemplate().getForEntity("/actuator/beans", Object.class); + ResponseEntity entity = beansRestTemplate().getForEntity(getManagementPath() + "/actuator/beans", + Object.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - entity = beansRestTemplate().getForEntity("/actuator/beans/", Object.class); + entity = beansRestTemplate().getForEntity(getManagementPath() + "/actuator/beans/", Object.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } - private TestRestTemplate restTemplate() { - return configure(new TestRestTemplate()); - } - - private TestRestTemplate adminRestTemplate() { - return configure(new TestRestTemplate("admin", "admin")); - } - - private TestRestTemplate userRestTemplate() { - return configure(new TestRestTemplate("user", "password")); - } - - private TestRestTemplate beansRestTemplate() { - return configure(new TestRestTemplate("beans", "beans")); - } - - private TestRestTemplate configure(TestRestTemplate restTemplate) { - restTemplate.setUriTemplateHandler(new LocalHostUriTemplateHandler(this.environment)); - return restTemplate; - } - } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/pom.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/pom.xml new file mode 100644 index 00000000000..5ad8da0f5d6 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-smoke-tests + ${revision} + + spring-boot-smoke-test-secure-jersey + jar + Spring Boot Secure Jersey Smoke Test + Spring Boot Secure Jersey Smoke Test + + ${basedir}/../../.. + + + + + org.springframework.boot + spring-boot-starter-jersey + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-security + + + org.jolokia + jolokia-core + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + generate build info + + build-info + + + + + + + + + java9+ + + [9,) + + + + javax.xml.bind + jaxb-api + + + + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java new file mode 100644 index 00000000000..74d2cc339d0 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.springframework.stereotype.Component; + +@Component +@Path("/hello") +public class Endpoint { + + private final Service service; + + public Endpoint(Service service) { + this.service = service; + } + + @GET + public String message() { + return "Hello " + this.service.message(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java new file mode 100644 index 00000000000..72c1e421713 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.stereotype.Component; + +@Component +public class JerseyConfig extends ResourceConfig { + + public JerseyConfig() { + register(Endpoint.class); + register(ReverseEndpoint.class); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java new file mode 100644 index 00000000000..21604d5813c --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import javax.validation.constraints.NotNull; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +import org.springframework.stereotype.Component; + +@Component +@Path("/reverse") +public class ReverseEndpoint { + + @GET + public String reverse(@QueryParam("input") @NotNull String input) { + return new StringBuilder(input).reverse().toString(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java new file mode 100644 index 00000000000..4c2c3079772 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleSecureJerseyApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleSecureJerseyApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java new file mode 100644 index 00000000000..c9942d8cfa3 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; +import org.springframework.boot.actuate.web.mappings.MappingsEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +@Configuration +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @SuppressWarnings("deprecation") + @Bean + public InMemoryUserDetailsManager inMemoryUserDetailsManager() { + return new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder().username("user").password("password").authorities("ROLE_USER") + .build(), + User.withDefaultPasswordEncoder().username("admin").password("admin") + .authorities("ROLE_ACTUATOR", "ROLE_USER").build()); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests() + .requestMatchers(EndpointRequest.to("health", "info")).permitAll() + .requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)).hasRole("ACTUATOR") + .antMatchers("/**").hasRole("USER") + .and() + .httpBasic(); + // @formatter:on + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java new file mode 100644 index 00000000000..f3059aea246 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class Service { + + @Value("${message:World}") + private String msg; + + public String message() { + return this.msg; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties new file mode 100644 index 00000000000..5d894fac2c8 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties @@ -0,0 +1,4 @@ +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java new file mode 100644 index 00000000000..e4a0199bc42 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Abstract base class for actuator tests with custom security. + * + * @author Madhura Bhave + */ +abstract class AbstractJerseySecureTests { + + abstract String getPath(); + + abstract String getManagementPath(); + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void helloEndpointIsSecure() { + ResponseEntity entity = restTemplate().getForEntity(getPath() + "/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorInsecureEndpoint() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator/health", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/health/diskSpace", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + } + + @Test + void actuatorLinksWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorLinksWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void actuatorLinksWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + adminRestTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void actuatorSecureEndpointWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator/env", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorSecureEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/env", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void actuatorSecureEndpointWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/env", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + entity = adminRestTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void secureServletEndpointWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator/jolokia", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/jolokia/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void secureServletEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/jolokia", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/jolokia/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void secureServletEndpointWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/jolokia", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/jolokia/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void actuatorExcludedFromEndpointRequestMatcher() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/mappings", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + TestRestTemplate restTemplate() { + return this.testRestTemplate; + } + + TestRestTemplate adminRestTemplate() { + return this.testRestTemplate.withBasicAuth("admin", "admin"); + } + + TestRestTemplate userRestTemplate() { + return this.testRestTemplate.withBasicAuth("user", "password"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java new file mode 100644 index 00000000000..cc95ef506bb --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; + +/** + * Integration tests for actuator endpoints with custom application path. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.jersey.application-path=/example") + +class CustomApplicationPathActuatorTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @Override + String getPath() { + return "http://localhost:" + this.port + "/example"; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.port + "/example"; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java new file mode 100644 index 00000000000..45cbdca9c50 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; + +/** + * Integration tests for actuator endpoints with custom security configuration. + * + * @author Madhura Bhave + * @author Stephane Nicoll + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class JerseySecureApplicationTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @Override + String getPath() { + return "http://localhost:" + this.port; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.port; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java new file mode 100644 index 00000000000..bb58b713d2c --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.web.server.LocalManagementPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports with custom management + * context path. + * + * @author Dave Syer + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "management.server.servlet.context-path=/management" }) +class ManagementPortAndPathJerseyApplicationTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Test + void testMissing() { + ResponseEntity entity = new TestRestTemplate("admin", "admin") + .getForEntity("http://localhost:" + this.managementPort + "/management/actuator/missing", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Override + String getPath() { + return "http://localhost:" + this.port; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.managementPort + "/management"; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java new file mode 100644 index 00000000000..3a4cf816575 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.web.server.LocalManagementPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports with custom + * application path. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "spring.jersey.application-path=/example" }) +class ManagementPortCustomApplicationPathJerseyTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Test + void actuatorPathOnMainPortShouldNotMatch() { + ResponseEntity entity = new TestRestTemplate() + .getForEntity("http://localhost:" + this.port + "/example/actuator/health", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Override + String getPath() { + return "http://localhost:" + this.port + "/example"; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.managementPort; + } + +}