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 65% 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 004faff6b1..1e23642303 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 +@ManagementContextConfiguration @ConditionalOnClass({ RequestMatcher.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -public class SecurityRequestMatcherProviderAutoConfiguration { +public class SecurityRequestMatchersManagementContextConfiguration { @Configuration @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 cdfd9396c3..97d1fcc3a9 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 @@ -94,6 +94,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 871a2d1d4e..bc7c864169 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.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; */ public abstract class AbstractEndpointRequestIntegrationTests { - protected abstract WebApplicationContextRunner getContextRunner(); - @Test public void toEndpointShouldMatch() { getContextRunner().run((context) -> { @@ -79,6 +78,17 @@ public 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 @@ public abstract class AbstractEndpointRequestIntegrationTests { } @Bean - public 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; + public TestServletEndpoint servletEndpoint() { + return new TestServletEndpoint(); } } @@ -155,7 +154,13 @@ public 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 d2d6539091..2a0ec1ca18 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,37 +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.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; -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; @@ -57,18 +37,6 @@ import org.springframework.test.web.reactive.server.WebTestClient; */ public 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 public void toLinksWhenApplicationPathSetShouldMatch() { getContextRunner().withPropertyValues("spring.jersey.application-path=/admin").run((context) -> { @@ -98,16 +66,47 @@ public class JerseyEndpointRequestIntegrationTests extends AbstractEndpointReque }); } + @Test + public 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 + public 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 public TomcatServletWebServerFactory tomcat() { return new TomcatServletWebServerFactory(0); @@ -118,24 +117,6 @@ public class JerseyEndpointRequestIntegrationTests extends AbstractEndpointReque return new ResourceConfig(); } - @Bean - public 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((id) -> id.toString()), Collections.emptyList(), Collections.emptyList()); - Collection resources = new JerseyEndpointResourceFactory().createEndpointResources( - new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes, - new EndpointLinksResolver(discoverer.getEndpoints())); - 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 331a4cf9a8..5508c7aa6f 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,38 +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.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; -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. @@ -84,43 +66,52 @@ public class MvcEndpointRequestIntegrationTests extends AbstractEndpointRequestI }); } + @Test + public 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 + public 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 @EnableConfigurationProperties(WebEndpointProperties.class) static class WebMvcEndpointConfiguration { - private final ApplicationContext applicationContext; - - WebMvcEndpointConfiguration(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - @Bean public TomcatServletWebServerFactory tomcat() { return new TomcatServletWebServerFactory(0); } - @Bean - public 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((id) -> id.toString()), Collections.emptyList(), Collections.emptyList()); - return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(), - endpointMediaTypes, new CorsConfiguration(), new EndpointLinksResolver(discoverer.getEndpoints())); - } - } } 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 3d5d3332aa..ba98187ec6 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.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 */ -public class SecurityRequestMatcherProviderAutoConfigurationTests { +public class SecurityRequestMatchersManagementContextConfigurationTests { private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SecurityRequestMatcherProviderAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class)); @Test public 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 @@ public class SecurityRequestMatcherProviderAutoConfigurationTests { } @Test - public void registersMvcRequestMatcherProviderIfMvcPresent() { - this.contextRunner.withUserConfiguration(TestMvcConfiguration.class).run((context) -> assertThat(context) - .getBean(RequestMatcherProvider.class).isInstanceOf(MvcRequestMatcherProvider.class)); + public 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 public 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 public 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 + public void mvcRequestMatcherProviderConditionalOnDispatcherServletPathBean() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); } @Test public void jerseyRequestMatcherProviderConditionalOnResourceConfigClass() { this.contextRunner.withClassLoader(new FilteredClassLoader("org.glassfish.jersey.server.ResourceConfig")) - .run((context) -> assertThat(context).doesNotHaveBean(JerseyRequestMatcherProvider.class)); - } - - @Test - public void mvcRequestMatcherProviderConditionalOnHandlerMappingIntrospectorBean() { - new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SecurityRequestMatcherProviderAutoConfiguration.class)) - .run((context) -> assertThat(context).doesNotHaveBean(MvcRequestMatcherProvider.class)); + .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); } @Test public 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 static class TestMvcConfiguration { @Bean - public HandlerMappingIntrospector introspector() { - return new HandlerMappingIntrospector(); + public 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 46977b703f..2f6e9c0d7e 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 9be169f625..c25210b6cc 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 03dd3e4421..abbe0fa813 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 @@ -100,7 +100,6 @@ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\ org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\ org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\ 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-samples/pom.xml b/spring-boot-samples/pom.xml index 7c2b077849..a28f6cb81c 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -69,6 +69,7 @@ spring-boot-sample-reactive-oauth2-client spring-boot-sample-reactive-oauth2-resource-server spring-boot-sample-secure + spring-boot-sample-secure-jersey spring-boot-sample-secure-webflux spring-boot-sample-servlet spring-boot-sample-session diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java new file mode 100644 index 0000000000..5032899215 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/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 sample.actuator.customsecurity; + +import java.util.Map; + +import org.junit.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 + */ +public abstract class AbstractSampleActuatorCustomSecurityTests { + + abstract String getPath(); + + abstract String getManagementPath(); + + abstract Environment getEnvironment(); + + @Test + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public void actuatorCustomMvcSecureEndpointWithAnonymous() { + ResponseEntity entity = restTemplate() + .getForEntity(getManagementPath() + "/actuator/example/echo?text={t}", String.class, "test"); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void actuatorCustomMvcSecureEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate() + .getForEntity(getManagementPath() + "/actuator/example/echo?text={t}", String.class, "test"); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + public 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 + public 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-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/CustomServletPathSampleActuatorTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/CustomServletPathSampleActuatorTests.java new file mode 100644 index 0000000000..de4978a748 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/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 sample.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") + +public 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-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java index 1c3b16c576..c951fa2fb6 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java @@ -19,11 +19,13 @@ package sample.actuator.customsecurity; import org.junit.Test; import org.junit.runner.RunWith; +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 org.springframework.test.context.junit4.SpringRunner; @@ -31,7 +33,8 @@ import org.springframework.test.context.junit4.SpringRunner; 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 @@ -39,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "management.server.port=0", "management.server.servlet.context-path=/management" }) -public class ManagementPortAndPathSampleActuatorApplicationTests { +public class ManagementPortAndPathSampleActuatorApplicationTests extends AbstractSampleActuatorCustomSecurityTests { @LocalServerPort private int port; @@ -47,35 +50,8 @@ public class ManagementPortAndPathSampleActuatorApplicationTests { @LocalManagementPort private int managementPort; - @Test - public 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 - public void actuatorPathOnMainPortShouldNotMatch() { - ResponseEntity entity = new TestRestTemplate() - .getForEntity("http://localhost:" + this.port + "/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - - @Test - public void testSecureActuator() { - ResponseEntity entity = new TestRestTemplate() - .getForEntity("http://localhost:" + this.managementPort + "/management/actuator/env", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - - @Test - public 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 public void testMissing() { @@ -85,4 +61,19 @@ public 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-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortCustomServletPathSampleActuatorTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortCustomServletPathSampleActuatorTests.java new file mode 100644 index 0000000000..f5550d1c1b --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortCustomServletPathSampleActuatorTests.java @@ -0,0 +1,76 @@ +/* + * 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 sample.actuator.customsecurity; + +import org.junit.Test; +import org.junit.runner.RunWith; + +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 org.springframework.test.context.junit4.SpringRunner; + +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 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "spring.mvc.servlet.path=/example" }) +public class ManagementPortCustomServletPathSampleActuatorTests extends AbstractSampleActuatorCustomSecurityTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Autowired + private Environment environment; + + @Test + public 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-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java index 18cd36b953..beaccde6af 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java @@ -23,8 +23,7 @@ import org.junit.runner.RunWith; 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; @@ -33,133 +32,53 @@ import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; /** + * Integration tests for actuator endpoints with custom security configuration. + * * @author Madhura Bhave * @author Stephane Nicoll */ @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class SampleActuatorCustomSecurityApplicationTests { +public class SampleActuatorCustomSecurityApplicationTests extends AbstractSampleActuatorCustomSecurityTests { + + @LocalServerPort + private int port; @Autowired private Environment environment; - @Test - public 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 public 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 - public void testInsecureStaticResources() { - ResponseEntity entity = restTemplate().getForEntity("/css/bootstrap.min.css", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("body"); - } - - @Test - public void actuatorInsecureEndpoint() { - ResponseEntity entity = restTemplate().getForEntity("/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); - } - - @Test - public 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 - public void actuatorSecureEndpointWithAnonymous() { - ResponseEntity entity = restTemplate().getForEntity("/actuator/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - - @Test - public void actuatorSecureEndpointWithUnauthorizedUser() { - ResponseEntity entity = userRestTemplate().getForEntity("/actuator/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - } - - @Test - public void actuatorSecureEndpointWithAuthorizedUser() { - ResponseEntity entity = adminRestTemplate().getForEntity("/actuator/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - @Test - public void actuatorCustomMvcSecureEndpointWithAnonymous() { - ResponseEntity entity = restTemplate().getForEntity("/actuator/example/echo?text={t}", String.class, - "test"); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - - @Test - public void actuatorCustomMvcSecureEndpointWithUnauthorizedUser() { - ResponseEntity entity = userRestTemplate().getForEntity("/actuator/example/echo?text={t}", String.class, - "test"); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - } - - @Test - public 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 - public void actuatorExcludedFromEndpointRequestMatcher() { - ResponseEntity entity = userRestTemplate().getForEntity("/actuator/mappings", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - } - @Test public 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-samples/spring-boot-sample-secure-jersey/pom.xml b/spring-boot-samples/spring-boot-sample-secure-jersey/pom.xml new file mode 100644 index 0000000000..191ddacdf0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + ${revision} + + spring-boot-sample-secure-jersey + jar + Spring Boot Sample Secure Jersey + Spring Boot Sample Secure Jersey + + ${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-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/Endpoint.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/Endpoint.java new file mode 100644 index 0000000000..d215e09872 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/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 sample.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-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/JerseyConfig.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/JerseyConfig.java new file mode 100644 index 0000000000..62398849de --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/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 sample.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-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/ReverseEndpoint.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/ReverseEndpoint.java new file mode 100644 index 0000000000..42bee8303e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/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 sample.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-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/SampleSecureJerseyApplication.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/SampleSecureJerseyApplication.java new file mode 100644 index 0000000000..6f089b12a0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/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 sample.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-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/SecurityConfiguration.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/SecurityConfiguration.java new file mode 100644 index 0000000000..fc2cf00ad0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/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 sample.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-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/Service.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/Service.java new file mode 100644 index 0000000000..64e79b0dcf --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/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 sample.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-samples/spring-boot-sample-secure-jersey/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-secure-jersey/src/main/resources/application.properties new file mode 100644 index 0000000000..5d894fac2c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-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-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/AbstractJerseySecureTests.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/AbstractJerseySecureTests.java new file mode 100644 index 0000000000..46dcd4859d --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/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 sample.secure.jersey; + +import org.junit.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 + */ +public abstract class AbstractJerseySecureTests { + + abstract String getPath(); + + abstract String getManagementPath(); + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + public void helloEndpointIsSecure() { + ResponseEntity entity = restTemplate().getForEntity(getPath() + "/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/CustomApplicationPathActuatorTests.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/CustomApplicationPathActuatorTests.java new file mode 100644 index 0000000000..5b8394dc19 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/CustomApplicationPathActuatorTests.java @@ -0,0 +1,49 @@ +/* + * 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 sample.secure.jersey; + +import org.junit.runner.RunWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for actuator endpoints with custom application path. + * + * @author Madhura Bhave + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.jersey.application-path=/example") + +public 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-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/JerseySecureApplicationTests.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/JerseySecureApplicationTests.java new file mode 100644 index 0000000000..568ae57c45 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/JerseySecureApplicationTests.java @@ -0,0 +1,48 @@ +/* + * 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 sample.secure.jersey; + +import org.junit.runner.RunWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for actuator endpoints with custom security configuration. + * + * @author Madhura Bhave + * @author Stephane Nicoll + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public 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-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java new file mode 100644 index 0000000000..7f1c5ac25b --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java @@ -0,0 +1,68 @@ +/* + * 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 sample.secure.jersey; + +import org.junit.Test; +import org.junit.runner.RunWith; + +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 org.springframework.test.context.junit4.SpringRunner; + +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 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "management.server.servlet.context-path=/management" }) +public class ManagementPortAndPathJerseyApplicationTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Test + public 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-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java new file mode 100644 index 0000000000..155bd2c6aa --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java @@ -0,0 +1,66 @@ +/* + * 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 sample.secure.jersey; + +import org.junit.Test; +import org.junit.runner.RunWith; + +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 org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports with custom + * application path. + * + * @author Madhura Bhave + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "spring.jersey.application-path=/example" }) +public class ManagementPortCustomApplicationPathJerseyTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Test + public 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; + } + +}