Account for application path for Jersey servlet endpoints

Closes gh-14895
This commit is contained in:
Madhura Bhave 2018-10-29 13:20:21 -07:00
parent 8560010bd4
commit decaacddce
9 changed files with 387 additions and 43 deletions

View File

@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClas
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -82,11 +83,21 @@ public class ServletEndpointManagementContextConfiguration {
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
public static class JerseyServletEndpointManagementContextConfiguration {
private final ApplicationContext context;
public JerseyServletEndpointManagementContextConfiguration(
ApplicationContext context) {
this.context = context;
}
@Bean
public ServletEndpointRegistrar servletEndpointRegistrar(
WebEndpointProperties properties,
ServletEndpointsSupplier servletEndpointsSupplier) {
return new ServletEndpointRegistrar(properties.getBasePath(),
JerseyApplicationPath jerseyApplicationPath = this.context
.getBean(JerseyApplicationPath.class);
return new ServletEndpointRegistrar(
jerseyApplicationPath.getRelativePath(properties.getBasePath()),
servletEndpointsSupplier.getEndpoints());
}

View File

@ -156,7 +156,7 @@ public abstract class AbstractEndpointRequestIntegrationTests {
}
interface TestPathMappedEndpoint
public interface TestPathMappedEndpoint
extends ExposableEndpoint<Operation>, PathMappedEndpoint {
}

View File

@ -23,6 +23,7 @@ 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;
@ -41,12 +42,14 @@ import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfi
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;
/**
* Integration tests for {@link EndpointRequest} with Jersey.
@ -60,6 +63,8 @@ public class JerseyEndpointRequestIntegrationTests
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(
@ -70,6 +75,41 @@ public class JerseyEndpointRequestIntegrationTests
JerseyAutoConfiguration.class));
}
@Test
public void toLinksWhenApplicationPathSetShouldMatch() {
getContextRunner().withPropertyValues("spring.jersey.application-path=/admin")
.run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/admin/actuator/").exchange().expectStatus()
.isOk();
webTestClient.get().uri("/admin/actuator").exchange().expectStatus()
.isOk();
});
}
@Test
public void toEndpointWhenApplicationPathSetShouldMatch() {
getContextRunner().withPropertyValues("spring.jersey.application-path=/admin")
.run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/admin/actuator/e1").exchange()
.expectStatus().isOk();
});
}
@Test
public void toAnyEndpointWhenApplicationPathSetShouldMatch() {
getContextRunner().withPropertyValues("spring.jersey.application-path=/admin",
"spring.security.user.password=password").run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/admin/actuator/e2").exchange()
.expectStatus().isUnauthorized();
webTestClient.get().uri("/admin/actuator/e2")
.header("Authorization", getBasicAuth()).exchange()
.expectStatus().isOk();
});
}
@Configuration
@EnableConfigurationProperties(WebEndpointProperties.class)
static class JerseyEndpointConfiguration {

View File

@ -16,7 +16,7 @@
package org.springframework.boot.autoconfigure.jersey;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
@ -53,6 +53,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.DynamicRegistrationBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@ -96,8 +97,6 @@ public class JerseyAutoConfiguration implements ServletContextAware {
private final List<ResourceConfigCustomizer> customizers;
private String path;
public JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config,
ObjectProvider<List<ResourceConfigCustomizer>> customizers) {
this.jersey = jersey;
@ -107,16 +106,15 @@ public class JerseyAutoConfiguration implements ServletContextAware {
@PostConstruct
public void path() {
resolveApplicationPath();
customize();
}
private void resolveApplicationPath() {
private String resolveApplicationPath() {
if (StringUtils.hasLength(this.jersey.getApplicationPath())) {
this.path = parseApplicationPath(this.jersey.getApplicationPath());
return this.jersey.getApplicationPath();
}
else {
this.path = findApplicationPath(AnnotationUtils.findAnnotation(
return findApplicationPath(AnnotationUtils.findAnnotation(
this.config.getApplication().getClass(), ApplicationPath.class));
}
}
@ -130,6 +128,12 @@ public class JerseyAutoConfiguration implements ServletContextAware {
}
}
@Bean
@ConditionalOnMissingBean
public JerseyApplicationPath jerseyApplicationPath() {
return this::resolveApplicationPath;
}
@Bean
@ConditionalOnMissingBean
public FilterRegistrationBean<RequestContextFilter> requestContextFilter() {
@ -143,13 +147,15 @@ public class JerseyAutoConfiguration implements ServletContextAware {
@Bean
@ConditionalOnMissingBean(name = "jerseyFilterRegistration")
@ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "filter")
public FilterRegistrationBean<ServletContainer> jerseyFilterRegistration() {
public FilterRegistrationBean<ServletContainer> jerseyFilterRegistration(
JerseyApplicationPath applicationPath) {
FilterRegistrationBean<ServletContainer> registration = new FilterRegistrationBean<>();
registration.setFilter(new ServletContainer(this.config));
registration.setUrlPatterns(Arrays.asList(this.path));
registration.setUrlPatterns(
Collections.singletonList(applicationPath.getUrlMapping()));
registration.setOrder(this.jersey.getFilter().getOrder());
registration.addInitParameter(ServletProperties.FILTER_CONTEXT_PATH,
stripPattern(this.path));
stripPattern(applicationPath.getPath()));
addInitParameters(registration);
registration.setName("jerseyFilter");
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
@ -166,9 +172,10 @@ public class JerseyAutoConfiguration implements ServletContextAware {
@Bean
@ConditionalOnMissingBean(name = "jerseyServletRegistration")
@ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "servlet", matchIfMissing = true)
public ServletRegistrationBean<ServletContainer> jerseyServletRegistration() {
public ServletRegistrationBean<ServletContainer> jerseyServletRegistration(
JerseyApplicationPath applicationPath) {
ServletRegistrationBean<ServletContainer> registration = new ServletRegistrationBean<>(
new ServletContainer(this.config), this.path);
new ServletContainer(this.config), applicationPath.getUrlMapping());
addInitParameters(registration);
registration.setName(getServletRegistrationName());
registration.setLoadOnStartup(this.jersey.getServlet().getLoadOnStartup());
@ -188,14 +195,7 @@ public class JerseyAutoConfiguration implements ServletContextAware {
if (annotation == null) {
return "/*";
}
return parseApplicationPath(annotation.value());
}
private static String parseApplicationPath(String applicationPath) {
if (!applicationPath.startsWith("/")) {
applicationPath = "/" + applicationPath;
}
return applicationPath.equals("/") ? "/*" : applicationPath + "/*";
return annotation.value();
}
@Override

View File

@ -0,0 +1,43 @@
/*
* Copyright 2012-2018 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
*
* http://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 org.springframework.boot.autoconfigure.security.servlet;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* {@link RequestMatcherProvider} that provides an {@link AntPathRequestMatcher} that can
* be used for Jersey applications.
*
* @author Madhura Bhave
* @since 2.0.7
*/
public class JerseyRequestMatcherProvider implements RequestMatcherProvider {
private final JerseyApplicationPath jerseyApplicationPath;
public JerseyRequestMatcherProvider(JerseyApplicationPath jerseyApplicationPath) {
this.jerseyApplicationPath = jerseyApplicationPath;
}
@Override
public RequestMatcher getRequestMatcher(String pattern) {
return new AntPathRequestMatcher(
this.jerseyApplicationPath.getRelativePath(pattern));
}
}

View File

@ -15,9 +15,13 @@
*/
package org.springframework.boot.autoconfigure.security.servlet;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
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;
@ -31,15 +35,36 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
* @since 2.0.5
*/
@Configuration
@ConditionalOnClass({ RequestMatcher.class, DispatcherServlet.class })
@ConditionalOnClass({ RequestMatcher.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnBean(HandlerMappingIntrospector.class)
public class SecurityRequestMatcherProviderAutoConfiguration {
@Bean
public RequestMatcherProvider requestMatcherProvider(
HandlerMappingIntrospector introspector) {
return new MvcRequestMatcherProvider(introspector);
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
@ConditionalOnBean(HandlerMappingIntrospector.class)
public static class MvcRequestMatcherConfiguration {
@Bean
@ConditionalOnClass(DispatcherServlet.class)
public RequestMatcherProvider requestMatcherProvider(
HandlerMappingIntrospector introspector) {
return new MvcRequestMatcherProvider(introspector);
}
}
@Configuration
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
@ConditionalOnBean(JerseyApplicationPath.class)
public static class JerseyRequestMatcherConfiguration {
@Bean
public RequestMatcherProvider requestMatcherProvider(
JerseyApplicationPath applicationPath) {
return new JerseyRequestMatcherProvider(applicationPath);
}
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright 2012-2018 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
*
* http://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 org.springframework.boot.autoconfigure.web.servlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
/**
* Interface that can be used by auto-configurations that need path details Jersey's
* application path that serves as the base URI for the application.
*
* @author Madhura Bhave
* @since 2.0.7
*/
@FunctionalInterface
public interface JerseyApplicationPath {
/**
* Returns the configured path of the application.
* @return the configured path
*/
String getPath();
/**
* Return a form of the given path that's relative to the Jersey application path.
* @param path the path to make relative
* @return the relative path
*/
default String getRelativePath(String path) {
String prefix = getPrefix();
if (!path.startsWith("/")) {
path = "/" + path;
}
return prefix + path;
}
/**
* Return a cleaned up version of the path that can be used as a prefix for URLs. The
* resulting path will have path will not have a trailing slash.
* @return the prefix
* @see #getRelativePath(String)
*/
default String getPrefix() {
String result = getPath();
int index = result.indexOf('*');
if (index != -1) {
result = result.substring(0, index);
}
if (result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
/**
* Return a URL mapping pattern that can be used with a
* {@link ServletRegistrationBean} to map Jersey's servlet.
* @return the path as a servlet URL mapping
*/
default String getUrlMapping() {
String path = getPath();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (path.equals("/")) {
return "/*";
}
if (path.contains("*")) {
return path;
}
if (path.endsWith("/")) {
return path + "*";
}
return path + "/*";
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.security.servlet;
import org.junit.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
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;
@ -36,23 +37,44 @@ public class SecurityRequestMatcherProviderAutoConfigurationTests {
private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations
.of(SecurityRequestMatcherProviderAutoConfiguration.class))
.withUserConfiguration(TestConfiguration.class);
.of(SecurityRequestMatcherProviderAutoConfiguration.class));
@Test
public void registersMvcRequestMatcherProviderIfMvcPresent() {
this.contextRunner.run((context) -> assertThat(context)
.hasSingleBean(MvcRequestMatcherProvider.class));
}
@Test
public void mvcRequestMatcherProviderConditionalOnWebApplication() {
public void configurationConditionalOnWebApplication() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations
.of(SecurityRequestMatcherProviderAutoConfiguration.class))
.withUserConfiguration(TestConfiguration.class)
.withUserConfiguration(TestMvcConfiguration.class)
.run((context) -> assertThat(context)
.doesNotHaveBean(MvcRequestMatcherProvider.class));
.doesNotHaveBean(RequestMatcherProvider.class));
}
@Test
public void configurationConditionalOnRequestMatcherClass() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(
"org.springframework.security.web.util.matcher.RequestMatcher"))
.run((context) -> assertThat(context)
.doesNotHaveBean(RequestMatcherProvider.class));
}
@Test
public void registersMvcRequestMatcherProviderIfMvcPresent() {
this.contextRunner.withUserConfiguration(TestMvcConfiguration.class)
.run((context) -> assertThat(context)
.getBean(RequestMatcherProvider.class)
.isInstanceOf(MvcRequestMatcherProvider.class));
}
@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));
}
@Test
@ -65,12 +87,12 @@ public class SecurityRequestMatcherProviderAutoConfigurationTests {
}
@Test
public void mvcRequestMatcherProviderConditionalOnRequestMatcherClass() {
public void jerseyRequestMatcherProviderConditionalOnResourceConfigClass() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(
"org.springframework.security.web.util.matcher.RequestMatcher"))
"org.glassfish.jersey.server.ResourceConfig"))
.run((context) -> assertThat(context)
.doesNotHaveBean(MvcRequestMatcherProvider.class));
.doesNotHaveBean(JerseyRequestMatcherProvider.class));
}
@Test
@ -82,8 +104,19 @@ public class SecurityRequestMatcherProviderAutoConfigurationTests {
.doesNotHaveBean(MvcRequestMatcherProvider.class));
}
@Test
public void jerseyRequestMatcherProviderConditionalOnJerseyApplicationPathBean() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations
.of(SecurityRequestMatcherProviderAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(
"org.springframework.web.servlet.DispatcherServlet"))
.run((context) -> assertThat(context)
.doesNotHaveBean(JerseyRequestMatcherProvider.class));
}
@Configuration
static class TestConfiguration {
static class TestMvcConfiguration {
@Bean
public HandlerMappingIntrospector introspector() {
@ -92,4 +125,14 @@ public class SecurityRequestMatcherProviderAutoConfigurationTests {
}
@Configuration
static class TestJerseyConfiguration {
@Bean
public JerseyApplicationPath jerseyApplicationPath() {
return () -> "/admin";
}
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2012-2018 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
*
* http://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 org.springframework.boot.autoconfigure.web.servlet;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyApplicationPath}.
*
* @author Madhura Bhave
*/
public class JerseyApplicationPathTests {
@Test
public void getRelativePathReturnsRelativePath() {
assertThat(((JerseyApplicationPath) () -> "spring").getRelativePath("boot"))
.isEqualTo("spring/boot");
assertThat(((JerseyApplicationPath) () -> "spring/").getRelativePath("boot"))
.isEqualTo("spring/boot");
assertThat(((JerseyApplicationPath) () -> "spring").getRelativePath("/boot"))
.isEqualTo("spring/boot");
assertThat(((JerseyApplicationPath) () -> "spring/*").getRelativePath("/boot"))
.isEqualTo("spring/boot");
}
@Test
public void getPrefixWhenHasSimplePathReturnPath() {
assertThat(((JerseyApplicationPath) () -> "spring").getPrefix())
.isEqualTo("spring");
}
@Test
public void getPrefixWhenHasPatternRemovesPattern() {
assertThat(((JerseyApplicationPath) () -> "spring/*.do").getPrefix())
.isEqualTo("spring");
}
@Test
public void getPrefixWhenPathEndsWithSlashRemovesSlash() {
assertThat(((JerseyApplicationPath) () -> "spring/").getPrefix())
.isEqualTo("spring");
}
@Test
public void getUrlMappingWhenPathIsEmptyReturnsSlash() {
assertThat(((JerseyApplicationPath) () -> "").getUrlMapping()).isEqualTo("/*");
}
@Test
public void getUrlMappingWhenPathIsSlashReturnsSlash() {
assertThat(((JerseyApplicationPath) () -> "/").getUrlMapping()).isEqualTo("/*");
}
@Test
public void getUrlMappingWhenPathContainsStarReturnsPath() {
assertThat(((JerseyApplicationPath) () -> "/spring/*.do").getUrlMapping())
.isEqualTo("/spring/*.do");
}
@Test
public void getUrlMappingWhenHasPathNotEndingSlashReturnsSlashStarPattern() {
assertThat(((JerseyApplicationPath) () -> "/spring/boot").getUrlMapping())
.isEqualTo("/spring/boot/*");
}
@Test
public void getUrlMappingWhenHasPathDoesNotStartWithSlashPrependsSlash() {
assertThat(((JerseyApplicationPath) () -> "spring/boot").getUrlMapping())
.isEqualTo("/spring/boot/*");
}
@Test
public void getUrlMappingWhenHasPathEndingWithSlashReturnsSlashStarPattern() {
assertThat(((JerseyApplicationPath) () -> "/spring/boot/").getUrlMapping())
.isEqualTo("/spring/boot/*");
}
}