From 674f2f5a6c010a6fb0249f06e6307b5d99a7bddb Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 20 Aug 2019 22:38:20 -0700 Subject: [PATCH] EndpointRequest should match @ServletEndpoint This commit also changes the request matcher for MVC endpoints to use an AntPathRequestMatcher instead of an MvcRequestMatcher. The endpoint is always available under the mapped endpoint path and this way the same matcher can be used for both MVC and Jersey. Fixes gh-17912 Co-authored-by: Phillip Webb --- ...tchersManagementContextConfiguration.java} | 27 ++- .../main/resources/META-INF/spring.factories | 1 + ...stractEndpointRequestIntegrationTests.java | 57 ++--- ...JerseyEndpointRequestIntegrationTests.java | 93 ++++---- .../MvcEndpointRequestIntegrationTests.java | 75 +++---- ...sManagementContextConfigurationTests.java} | 60 +++-- ...ava => AntPathRequestMatcherProvider.java} | 21 +- .../servlet/JerseyRequestMatcherProvider.java | 2 + .../main/resources/META-INF/spring.factories | 1 - spring-boot-samples/pom.xml | 1 + ...ractSampleActuatorCustomSecurityTests.java | 206 ++++++++++++++++++ .../CustomServletPathSampleActuatorTests.java | 59 +++++ ...AndPathSampleActuatorApplicationTests.java | 53 ++--- ...tCustomServletPathSampleActuatorTests.java | 76 +++++++ ...ctuatorCustomSecurityApplicationTests.java | 129 ++--------- .../spring-boot-sample-secure-jersey/pom.xml | 78 +++++++ .../java/sample/secure/jersey/Endpoint.java | 39 ++++ .../sample/secure/jersey/JerseyConfig.java | 31 +++ .../sample/secure/jersey/ReverseEndpoint.java | 35 +++ .../jersey/SampleSecureJerseyApplication.java | 29 +++ .../secure/jersey/SecurityConfiguration.java | 53 +++++ .../java/sample/secure/jersey/Service.java | 32 +++ .../src/main/resources/application.properties | 4 + .../jersey/AbstractJerseySecureTests.java | 161 ++++++++++++++ .../CustomApplicationPathActuatorTests.java | 49 +++++ .../jersey/JerseySecureApplicationTests.java | 48 ++++ ...mentPortAndPathJerseyApplicationTests.java | 68 ++++++ ...tPortCustomApplicationPathJerseyTests.java | 66 ++++++ 28 files changed, 1249 insertions(+), 305 deletions(-) rename spring-boot-project/{spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfiguration.java => spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java} (65%) rename spring-boot-project/{spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfigurationTests.java => spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java} (62%) rename spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/{MvcRequestMatcherProvider.java => AntPathRequestMatcherProvider.java} (59%) create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/CustomServletPathSampleActuatorTests.java create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortCustomServletPathSampleActuatorTests.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/pom.xml create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/Endpoint.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/JerseyConfig.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/ReverseEndpoint.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/SampleSecureJerseyApplication.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/SecurityConfiguration.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/main/java/sample/secure/jersey/Service.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/main/resources/application.properties create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/AbstractJerseySecureTests.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/CustomApplicationPathActuatorTests.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/JerseySecureApplicationTests.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-jersey/src/test/java/sample/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java 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 004faff6b1f..1e23642303d 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 cdfd9396c3e..97d1fcc3a9a 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 871a2d1d4e5..bc7c8641692 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 d2d65390911..2a0ec1ca186 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 331a4cf9a8c..5508c7aa6f5 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 3d5d3332aa3..ba98187ec60 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 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 03dd3e44216..abbe0fa8130 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 7c2b0778494..a28f6cb81c2 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 00000000000..50328992159 --- /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 00000000000..de4978a7485 --- /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 1c3b16c576d..c951fa2fb64 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 00000000000..f5550d1c1bb --- /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 18cd36b953a..beaccde6afd 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 00000000000..191ddacdf09 --- /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 00000000000..d215e09872a --- /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 00000000000..62398849de6 --- /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 00000000000..42bee8303ef --- /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 00000000000..6f089b12a0c --- /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 00000000000..fc2cf00ad0b --- /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 00000000000..64e79b0dcff --- /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 00000000000..5d894fac2c8 --- /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 00000000000..46dcd4859d1 --- /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 00000000000..5b8394dc19d --- /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 00000000000..568ae57c450 --- /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 00000000000..7f1c5ac25b8 --- /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 00000000000..155bd2c6aaa --- /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; + } + +}