diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointProvider.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointProvider.java index 6f2a13b4dd..91d295d653 100644 --- a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointProvider.java +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointProvider.java @@ -33,7 +33,7 @@ import org.springframework.core.env.Environment; * @author Stephane Nicoll * @since 2.0.0 */ -public final class EndpointProvider { +public class EndpointProvider { private final EndpointDiscoverer discoverer; diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProvider.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProvider.java new file mode 100644 index 0000000000..6880dd9b0e --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProvider.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2017 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.actuate.autoconfigure.endpoint.web; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointProvider; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; +import org.springframework.boot.actuate.endpoint.EndpointInfo; +import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; +import org.springframework.util.Assert; + +/** + * Default {@link EndpointPathProvider} implementation. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public class DefaultEndpointPathProvider implements EndpointPathProvider { + + private final Collection> endpoints; + + private final String contextPath; + + public DefaultEndpointPathProvider(EndpointProvider provider, + ManagementServerProperties managementServerProperties) { + this.endpoints = provider.getEndpoints(); + this.contextPath = managementServerProperties.getContextPath(); + } + + @Override + public List getPaths() { + return this.endpoints.stream().map(this::getPath).collect(Collectors.toList()); + } + + @Override + public String getPath(String id) { + Assert.notNull(id, "ID must not be null"); + return this.endpoints.stream().filter((info) -> id.equals(info.getId())) + .findFirst().map(this::getPath).orElse(null); + } + + private String getPath(EndpointInfo endpointInfo) { + return this.contextPath + "/" + endpointInfo.getId(); + } + +} diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/EndpointPathProvider.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/EndpointPathProvider.java new file mode 100644 index 0000000000..f47149c4e8 --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/EndpointPathProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2017 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.actuate.autoconfigure.endpoint.web; + +import java.util.List; + +/** + * Interface that provides path information for web mapped endpints. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public interface EndpointPathProvider { + + /** + * Return all mapped endpoint paths. + * @return all paths + */ + List getPaths(); + + /** + * Return the path for the endpoint with the specified ID. + * @param id the endpoint ID + * @return the path of the endpoint or {@code null} + */ + String getPath(String id); + +} diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index af0b27ee14..a12126a245 100644 --- a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -21,6 +21,8 @@ import java.util.HashSet; import org.glassfish.jersey.server.ResourceConfig; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.DefaultEndpointPathProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -60,4 +62,12 @@ class JerseyWebEndpointManagementContextConfiguration { provider.getEndpoints()))); } + @Bean + @ConditionalOnMissingBean + public EndpointPathProvider endpointPathProvider( + EndpointProvider provider, + ManagementServerProperties managementServerProperties) { + return new DefaultEndpointPathProvider(provider, managementServerProperties); + } + } diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index 2d120fc2af..4d23de49b5 100644 --- a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.DefaultEndpointPathProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -49,4 +51,12 @@ public class WebFluxEndpointManagementContextConfiguration { provider.getEndpoints()); } + @Bean + @ConditionalOnMissingBean + public EndpointPathProvider endpointPathProvider( + EndpointProvider provider, + ManagementServerProperties managementServerProperties) { + return new DefaultEndpointPathProvider(provider, managementServerProperties); + } + } diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index 0b31ef1844..8338903fe0 100644 --- a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.DefaultEndpointPathProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -61,6 +63,14 @@ public class WebMvcEndpointManagementContextConfiguration { return handlerMapping; } + @Bean + @ConditionalOnMissingBean + public EndpointPathProvider endpointPathProvider( + EndpointProvider provider, + ManagementServerProperties managementServerProperties) { + return new DefaultEndpointPathProvider(provider, managementServerProperties); + } + private CorsConfiguration getCorsConfiguration(CorsEndpointProperties properties) { if (CollectionUtils.isEmpty(properties.getAllowedOrigins())) { return null; diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/EndpointRequest.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/EndpointRequest.java new file mode 100644 index 0000000000..42ef8d0946 --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/EndpointRequest.java @@ -0,0 +1,193 @@ +/* + * Copyright 2012-2017 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.actuate.autoconfigure.security; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.security.ApplicationContextRequestMatcher; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * Factory that can be used to create a {@link RequestMatcher} for actuator endpoint + * locations. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.0.0 + */ +public final class EndpointRequest { + + private EndpointRequest() { + } + + /** + * Returns a matcher that includes all {@link Endpoint actuator endpoints}. The + * {@link EndpointRequestMatcher#excluding(Class...) excluding} method can be used to + * further remove specific endpoints if required. For example:
+	 * EndpointRequestMatcher.toAnyEndpoint().excluding(ShutdownEndpoint.class)
+	 * 
+ * @return the configured {@link RequestMatcher} + */ + public static EndpointRequestMatcher toAnyEndpoint() { + return new EndpointRequestMatcher(); + } + + /** + * Returns a matcher that includes the specified {@link Endpoint actuator endpoints}. + * For example:
+	 * EndpointRequestMatcher.to(ShutdownEndpoint.class, HealthEndpoint.class)
+	 * 
+ * @param endpoints the endpoints to include + * @return the configured {@link RequestMatcher} + */ + public static EndpointRequestMatcher to(Class... endpoints) { + return new EndpointRequestMatcher(endpoints); + } + + /** + * Returns a matcher that includes the specified {@link Endpoint actuator endpoints}. + * For example:
+	 * EndpointRequestMatcher.to("shutdown", "health")
+	 * 
+ * @param endpoints the endpoints to include + * @return the configured {@link RequestMatcher} + */ + public static EndpointRequestMatcher to(String... endpoints) { + return new EndpointRequestMatcher(endpoints); + } + + /** + * The request matcher used to match against {@link Endpoint actuator endpoints}. + */ + public final static class EndpointRequestMatcher + extends ApplicationContextRequestMatcher { + + private final List includes; + + private final List excludes; + + private RequestMatcher delegate; + + private EndpointRequestMatcher() { + super(EndpointPathProvider.class); + this.includes = Collections.emptyList(); + this.excludes = Collections.emptyList(); + } + + private EndpointRequestMatcher(Class[] endpoints) { + super(EndpointPathProvider.class); + this.includes = Arrays.asList((Object[]) endpoints); + this.excludes = Collections.emptyList(); + } + + private EndpointRequestMatcher(String[] endpoints) { + super(EndpointPathProvider.class); + this.includes = Arrays.asList((Object[]) endpoints); + this.excludes = Collections.emptyList(); + } + + private EndpointRequestMatcher(List includes, List excludes) { + super(EndpointPathProvider.class); + this.includes = includes; + this.excludes = excludes; + } + + EndpointRequestMatcher excluding(Class... endpoints) { + List excludes = new ArrayList<>(this.excludes); + excludes.addAll(Arrays.asList((Object[]) endpoints)); + return new EndpointRequestMatcher(this.includes, excludes); + } + + EndpointRequestMatcher excluding(String... endpoints) { + List excludes = new ArrayList<>(this.excludes); + excludes.addAll(Arrays.asList((Object[]) endpoints)); + return new EndpointRequestMatcher(this.includes, excludes); + } + + @Override + protected void initialized(EndpointPathProvider endpointPathProvider) { + Set paths = new LinkedHashSet<>(this.includes.isEmpty() + ? endpointPathProvider.getPaths() : Collections.emptyList()); + streamPaths(this.includes, endpointPathProvider).forEach(paths::add); + streamPaths(this.excludes, endpointPathProvider).forEach(paths::remove); + this.delegate = new OrRequestMatcher(getDelegateMatchers(paths)); + } + + private Stream streamPaths(List source, + EndpointPathProvider endpointPathProvider) { + return source.stream().filter(Objects::nonNull).map(this::getPathId) + .map(endpointPathProvider::getPath); + } + + private String getPathId(Object source) { + if (source instanceof String) { + return (String) source; + } + if (source instanceof Class) { + return getPathId((Class) source); + } + throw new IllegalStateException("Unsupported source " + source); + } + + private String getPathId(Class source) { + Endpoint annotation = AnnotationUtils.findAnnotation(source, Endpoint.class); + Assert.state(annotation != null, + "Class " + source + " is not annotated with @Endpoint"); + return annotation.id(); + } + + private List getDelegateMatchers(Set paths) { + return paths.stream().map((path) -> new AntPathRequestMatcher(path + "/**")) + .collect(Collectors.toList()); + } + + @Override + public boolean matches(HttpServletRequest request) { + try { + return super.matches(request); + } + catch (BeanCreationException ex) { + return false; + } + } + + @Override + protected boolean matches(HttpServletRequest request, + EndpointPathProvider context) { + return this.delegate.matches(request); + } + + } + +} diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/package-info.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/package-info.java new file mode 100644 index 0000000000..807bc71171 --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Auto-configuration for security. + */ +package org.springframework.boot.actuate.autoconfigure.security; diff --git a/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProviderTests.java b/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProviderTests.java new file mode 100644 index 0000000000..14ab4d1ed1 --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProviderTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2017 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.actuate.autoconfigure.endpoint.web; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointProvider; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; +import org.springframework.boot.actuate.endpoint.EndpointInfo; +import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * Tests for {@link DefaultEndpointPathProvider}. + * + * @author Phillip Webb + */ +public class DefaultEndpointPathProviderTests { + + @Mock + private EndpointProvider endpointProvider; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void getPathsShouldReturnAllPaths() throws Exception { + DefaultEndpointPathProvider provider = createProvider(""); + assertThat(provider.getPaths()).containsOnly("/foo", "/bar"); + } + + @Test + public void getPathsWhenHasContextPathShouldReturnAllPathsWithContext() + throws Exception { + DefaultEndpointPathProvider provider = createProvider("/application"); + assertThat(provider.getPaths()).containsOnly("/application/foo", + "/application/bar"); + } + + @Test + public void getPathWhenEndpointIdIsKnownShouldReturnPath() throws Exception { + DefaultEndpointPathProvider provider = createProvider(""); + assertThat(provider.getPath("foo")).isEqualTo("/foo"); + } + + @Test + public void getPathWhenEndpointIdIsUnknownShouldReturnNull() throws Exception { + DefaultEndpointPathProvider provider = createProvider(""); + assertThat(provider.getPath("baz")).isNull(); + } + + @Test + public void getPathWhenHasContextPathReturnPath() throws Exception { + DefaultEndpointPathProvider provider = createProvider("/application"); + assertThat(provider.getPath("foo")).isEqualTo("/application/foo"); + } + + private DefaultEndpointPathProvider createProvider(String contextPath) { + Collection> endpoints = new ArrayList<>(); + endpoints.add(new EndpointInfo<>("foo", true, Collections.emptyList())); + endpoints.add(new EndpointInfo<>("bar", true, Collections.emptyList())); + given(this.endpointProvider.getEndpoints()).willReturn(endpoints); + ManagementServerProperties managementServerProperties = new ManagementServerProperties(); + managementServerProperties.setContextPath(contextPath); + return new DefaultEndpointPathProvider(this.endpointProvider, + managementServerProperties); + } + +} diff --git a/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/EndpointRequestTests.java b/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/EndpointRequestTests.java new file mode 100644 index 0000000000..b1ead9cdbe --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/EndpointRequestTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2012-2017 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.actuate.autoconfigure.security; + +import java.util.Arrays; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.assertj.core.api.AssertDelegateTarget; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EndpointRequest}. + * + * @author Phillip Webb + */ +public class EndpointRequestTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void toAnyEndpointShouldMatchEndpointPath() throws Exception { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); + assertMatcher(matcher).matches("/application/foo"); + assertMatcher(matcher).matches("/application/bar"); + } + + @Test + public void toAnyEndpointShouldNotMatchOtherPath() throws Exception { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); + assertMatcher(matcher).doesNotMatch("/application/baz"); + } + + @Test + public void toEndpointClassShouldMatchEndpointPath() throws Exception { + RequestMatcher matcher = EndpointRequest.to(FooEndpoint.class); + assertMatcher(matcher).matches("/application/foo"); + } + + @Test + public void toEndpointClassShouldNotMatchOtherPath() throws Exception { + RequestMatcher matcher = EndpointRequest.to(FooEndpoint.class); + assertMatcher(matcher).doesNotMatch("/application/bar"); + } + + @Test + public void toEndpointIdShouldMatchEndpointPath() throws Exception { + RequestMatcher matcher = EndpointRequest.to("foo"); + assertMatcher(matcher).matches("/application/foo"); + } + + @Test + public void toEndpointIdShouldNotMatchOtherPath() throws Exception { + RequestMatcher matcher = EndpointRequest.to("foo"); + assertMatcher(matcher).doesNotMatch("/application/bar"); + } + + @Test + public void excludeByClassShouldNotMatchExcluded() throws Exception { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint() + .excluding(FooEndpoint.class); + assertMatcher(matcher).doesNotMatch("/application/foo"); + assertMatcher(matcher).matches("/application/bar"); + } + + @Test + public void excludeByIdShouldNotMatchExcluded() throws Exception { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("foo"); + assertMatcher(matcher).doesNotMatch("/application/foo"); + assertMatcher(matcher).matches("/application/bar"); + } + + private RequestMatcherAssert assertMatcher(RequestMatcher matcher) { + return assertMatcher(matcher, new MockEndpointPathProvider()); + } + + private RequestMatcherAssert assertMatcher(RequestMatcher matcher, + EndpointPathProvider endpointPathProvider) { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerBean(EndpointPathProvider.class, () -> endpointPathProvider); + return assertThat(new RequestMatcherAssert(context, matcher)); + } + + private static class RequestMatcherAssert implements AssertDelegateTarget { + + private final WebApplicationContext context; + + private final RequestMatcher matcher; + + RequestMatcherAssert(WebApplicationContext context, RequestMatcher matcher) { + this.context = context; + this.matcher = matcher; + } + + public void matches(String path) { + matches(mockRequest(path)); + } + + private void matches(HttpServletRequest request) { + assertThat(this.matcher.matches(request)) + .as("Matches " + getRequestPath(request)).isTrue(); + } + + public void doesNotMatch(String path) { + doesNotMatch(mockRequest(path)); + } + + private void doesNotMatch(HttpServletRequest request) { + assertThat(this.matcher.matches(request)) + .as("Does not match " + getRequestPath(request)).isFalse(); + } + + private MockHttpServletRequest mockRequest(String path) { + return mockRequest(null, path); + } + + private MockHttpServletRequest mockRequest(String servletPath, String path) { + MockServletContext servletContext = new MockServletContext(); + servletContext.setAttribute( + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, + this.context); + MockHttpServletRequest request = new MockHttpServletRequest(servletContext); + if (servletPath != null) { + request.setServletPath(servletPath); + } + request.setPathInfo(path); + return request; + } + + private String getRequestPath(HttpServletRequest request) { + String url = request.getServletPath(); + if (request.getPathInfo() != null) { + url += request.getPathInfo(); + } + return url; + } + + } + + private static class MockEndpointPathProvider implements EndpointPathProvider { + + @Override + public List getPaths() { + return Arrays.asList("/application/foo", "/application/bar"); + } + + @Override + public String getPath(String id) { + if ("foo".equals(id)) { + return "/application/foo"; + } + if ("bar".equals(id)) { + return "/application/bar"; + } + return null; + } + + } + + @Endpoint(id = "foo") + private static class FooEndpoint { + + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/StaticResourceRequest.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/StaticResourceRequest.java new file mode 100644 index 0000000000..c77fa88928 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/StaticResourceRequest.java @@ -0,0 +1,189 @@ +/* + * Copyright 2012-2017 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; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.security.ApplicationContextRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * Factory that can be used to create a {@link RequestMatcher} for static resources in + * commonly used locations. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.0.0 + */ +public final class StaticResourceRequest { + + private StaticResourceRequest() { + } + + /** + * Returns a matcher that includes all commonly used {@link Location Locations}. The + * {@link StaticResourceRequestMatcher#excluding(Location, Location...) excluding} method + * can be used to remove specific locations if required. For example: + *
+	 * StaticResourceRequest.toCommonLocations().excluding(Location.CSS)
+	 * 
+ * @return the configured {@link RequestMatcher} + */ + public static StaticResourceRequestMatcher toCommonLocations() { + return to(EnumSet.allOf(Location.class)); + } + + /** + * Returns a matcher that includes the specified {@link Location Locations}. For + * example:
+	 * StaticResourceRequest.to(Location.CSS, Location.JAVA_SCRIPT)
+	 * 
+ * @param first the first location to include + * @param rest additional locations to include + * @return the configured {@link RequestMatcher} + */ + public static StaticResourceRequestMatcher to(Location first, Location... rest) { + return to(EnumSet.of(first, rest)); + } + + /** + * Returns a matcher that includes the specified {@link Location Locations}. For + * example:
+	 * StaticResourceRequest.to(locations)
+	 * 
+ * @param locations the locations to include + * @return the configured {@link RequestMatcher} + */ + public static StaticResourceRequestMatcher to(Set locations) { + Assert.notNull(locations, "Locations must not be null"); + return new StaticResourceRequestMatcher(new LinkedHashSet<>(locations)); + } + + public enum Location { + + /** + * Resources under {@code "/css"}. + */ + CSS("/css/**"), + + /** + * Resources under {@code "/js"}. + */ + JAVA_SCRIPT("/js/**"), + + /** + * Resources under {@code "/images"}. + */ + IMAGES("/images/**"), + + /** + * Resources under {@code "/webjars"}. + */ + WEB_JARS("/webjars/**"), + + /** + * The {@code "favicon.ico"} resource. + */ + FAVICON("/**/favicon.ico"); + + private String[] patterns; + + Location(String... patterns) { + this.patterns = patterns; + } + + Stream getPatterns() { + return Arrays.stream(this.patterns); + } + + } + + /** + * The request matcher used to match against resource {@link Location Locations}. + */ + public final static class StaticResourceRequestMatcher + extends ApplicationContextRequestMatcher { + + private final Set locations; + + private RequestMatcher delegate; + + private StaticResourceRequestMatcher(Set locations) { + super(ServerProperties.class); + this.locations = locations; + } + + /** + * Return a new {@link StaticResourceRequestMatcher} based on this one but excluding the + * specified locations. + * @param first the first location to exclude + * @param rest additional locations to exclude + * @return a new {@link StaticResourceRequestMatcher} + */ + public StaticResourceRequestMatcher excluding(Location first, Location... rest) { + return excluding(EnumSet.of(first, rest)); + } + + /** + * Return a new {@link StaticResourceRequestMatcher} based on this one but excluding the + * specified locations. + * @param locations the locations to exclude + * @return a new {@link StaticResourceRequestMatcher} + */ + public StaticResourceRequestMatcher excluding(Set locations) { + Assert.notNull(locations, "Locations must not be null"); + Set subset = new LinkedHashSet<>(this.locations); + subset.removeAll(locations); + return new StaticResourceRequestMatcher(subset); + } + + @Override + protected void initialized(ServerProperties serverProperties) { + this.delegate = new OrRequestMatcher(getDelegateMatchers(serverProperties)); + } + + private List getDelegateMatchers( + ServerProperties serverProperties) { + return getPatterns(serverProperties).map(AntPathRequestMatcher::new) + .collect(Collectors.toList()); + } + + private Stream getPatterns(ServerProperties serverProperties) { + return this.locations.stream().flatMap(Location::getPatterns) + .map(serverProperties.getServlet()::getPath); + } + + @Override + protected boolean matches(HttpServletRequest request, ServerProperties context) { + return this.delegate.matches(request); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/StaticResourceRequestTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/StaticResourceRequestTests.java new file mode 100644 index 0000000000..b21eec8d82 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/StaticResourceRequestTests.java @@ -0,0 +1,175 @@ +/* + * Copyright 2012-2017 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; + +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.assertj.core.api.AssertDelegateTarget; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.autoconfigure.security.StaticResourceRequest.Location; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StaticResourceRequest}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +public class StaticResourceRequestTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void toCommonLocationsShouldMatchCommonLocations() throws Exception { + RequestMatcher matcher = StaticResourceRequest.toCommonLocations(); + assertMatcher(matcher).matches("/css/file.css"); + assertMatcher(matcher).matches("/js/file.js"); + assertMatcher(matcher).matches("/images/file.css"); + assertMatcher(matcher).matches("/webjars/file.css"); + assertMatcher(matcher).matches("/foo/favicon.ico"); + assertMatcher(matcher).doesNotMatch("/bar"); + } + + @Test + public void toCommonLocationsWithExcludeShouldNotMatchExcluded() throws Exception { + RequestMatcher matcher = StaticResourceRequest.toCommonLocations() + .excluding(Location.CSS); + assertMatcher(matcher).doesNotMatch("/css/file.css"); + assertMatcher(matcher).matches("/js/file.js"); + } + + @Test + public void toLocationShouldMatchLocation() throws Exception { + RequestMatcher matcher = StaticResourceRequest.to(Location.CSS); + assertMatcher(matcher).matches("/css/file.css"); + assertMatcher(matcher).doesNotMatch("/js/file.js"); + } + + @Test + public void toLocationWhenHasServletPathShouldMatchLocation() throws Exception { + ServerProperties serverProperties = new ServerProperties(); + serverProperties.getServlet().setPath("/foo"); + RequestMatcher matcher = StaticResourceRequest.to(Location.CSS); + assertMatcher(matcher, serverProperties).matches("/foo", "/css/file.css"); + assertMatcher(matcher, serverProperties).doesNotMatch("/foo", "/js/file.js"); + } + + @Test + public void toLocationsFromSetWhenSetIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Locations must not be null"); + StaticResourceRequest.to((Set) null); + } + + @Test + public void excludeFromSetWhenSetIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Locations must not be null"); + StaticResourceRequest.toCommonLocations().excluding((Set) null); + } + + private RequestMatcherAssert assertMatcher(RequestMatcher matcher) { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerBean(ServerProperties.class); + return assertThat(new RequestMatcherAssert(context, matcher)); + } + + private RequestMatcherAssert assertMatcher(RequestMatcher matcher, + ServerProperties serverProperties) { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerBean(ServerProperties.class, () -> serverProperties); + return assertThat(new RequestMatcherAssert(context, matcher)); + } + + private static class RequestMatcherAssert implements AssertDelegateTarget { + + private final WebApplicationContext context; + + private final RequestMatcher matcher; + + RequestMatcherAssert(WebApplicationContext context, RequestMatcher matcher) { + this.context = context; + this.matcher = matcher; + } + + public void matches(String path) { + matches(mockRequest(path)); + } + + public void matches(String servletPath, String path) { + matches(mockRequest(servletPath, path)); + } + + private void matches(HttpServletRequest request) { + assertThat(this.matcher.matches(request)) + .as("Matches " + getRequestPath(request)).isTrue(); + } + + public void doesNotMatch(String path) { + doesNotMatch(mockRequest(path)); + } + + public void doesNotMatch(String servletPath, String path) { + doesNotMatch(mockRequest(servletPath, path)); + } + + private void doesNotMatch(HttpServletRequest request) { + assertThat(this.matcher.matches(request)) + .as("Does not match " + getRequestPath(request)).isFalse(); + } + + private MockHttpServletRequest mockRequest(String path) { + return mockRequest(null, path); + } + + private MockHttpServletRequest mockRequest(String servletPath, String path) { + MockServletContext servletContext = new MockServletContext(); + servletContext.setAttribute( + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, + this.context); + MockHttpServletRequest request = new MockHttpServletRequest(servletContext); + if (servletPath != null) { + request.setServletPath(servletPath); + } + request.setPathInfo(path); + return request; + } + + private String getRequestPath(HttpServletRequest request) { + String url = request.getServletPath(); + if (request.getPathInfo() != null) { + url += request.getPathInfo(); + } + return url; + } + + } + +} diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java index a93bf58802..7e6e94c16b 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java @@ -1,5 +1,7 @@ package sample.actuator.customsecurity; +import org.springframework.boot.actuate.autoconfigure.security.EndpointRequest; +import org.springframework.boot.autoconfigure.security.StaticResourceRequest; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -17,18 +19,17 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { - // FIXME // @formatter:off -// http.authorizeRequests() -// .requestMatchers(endpointIds("status", "info")).permitAll() -// .requestMatchers(endpointIds(SpringBootSecurity.ALL_ENDPOINTS)).hasRole("ACTUATOR") -// .requestMatchers(staticResources()).permitAll() -// .antMatchers("/foo").permitAll() -// .antMatchers("/**").hasRole("USER") -// .and() -// .cors() -// .and() -// .httpBasic(); + http.authorizeRequests() + .requestMatchers(EndpointRequest.to("status", "info")).permitAll() + .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR") + .requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll() + .antMatchers("/foo").permitAll() + .antMatchers("/**").hasRole("USER") + .and() + .cors() + .and() + .httpBasic(); // @formatter:on } diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/ActuatorSecurityConfiguration.java b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/ActuatorSecurityConfiguration.java index 81fea894ef..d2354d49ce 100644 --- a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/ActuatorSecurityConfiguration.java +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/ActuatorSecurityConfiguration.java @@ -1,5 +1,6 @@ package sample.secure.oauth2.actuator; +import org.springframework.boot.actuate.autoconfigure.security.EndpointRequest; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -16,12 +17,11 @@ public class ActuatorSecurityConfiguration extends WebSecurityConfigurerAdapter @Override protected void configure(HttpSecurity http) throws Exception { - // FIXME // @formatter:off -// http.requestMatcher(ALL_ENDPOINTS).authorizeRequests() -// .antMatchers("/**").authenticated() -// .and() -// .httpBasic(); + http.requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests() + .antMatchers("/**").authenticated() + .and() + .httpBasic(); // @formatter:on } diff --git a/spring-boot-samples/spring-boot-sample-web-method-security/src/main/java/sample/security/method/SampleMethodSecurityApplication.java b/spring-boot-samples/spring-boot-sample-web-method-security/src/main/java/sample/security/method/SampleMethodSecurityApplication.java index 3a7085f505..6535288091 100644 --- a/spring-boot-samples/spring-boot-sample-web-method-security/src/main/java/sample/security/method/SampleMethodSecurityApplication.java +++ b/spring-boot-samples/spring-boot-sample-web-method-security/src/main/java/sample/security/method/SampleMethodSecurityApplication.java @@ -19,6 +19,7 @@ package sample.security.method; import java.util.Date; import java.util.Map; +import org.springframework.boot.actuate.autoconfigure.security.EndpointRequest; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Configuration; @@ -104,9 +105,12 @@ public class SampleMethodSecurityApplication implements WebMvcConfigurer { @Override protected void configure(HttpSecurity http) throws Exception { - // FIXME - // http.requestMatcher(ALL_ENDPOINTS) - // .authorizeRequests().anyRequest().authenticated().and().httpBasic(); + // @formatter:off + http.requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests() + .anyRequest().authenticated() + .and() + .httpBasic(); + // @formatter:on } } diff --git a/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/web/secure/SampleWebSecureApplication.java b/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/web/secure/SampleWebSecureApplication.java index 0a0449116a..443547c03c 100644 --- a/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/web/secure/SampleWebSecureApplication.java +++ b/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/web/secure/SampleWebSecureApplication.java @@ -20,6 +20,7 @@ import java.util.Date; import java.util.Map; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.StaticResourceRequest; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -62,15 +63,14 @@ public class SampleWebSecureApplication implements WebMvcConfigurer { @Override protected void configure(HttpSecurity http) throws Exception { - // FIXME // @formatter:off -// http.authorizeRequests() -// .requestMatchers(staticResources()).permitAll() -// .anyRequest().fullyAuthenticated() -// .and() -// .formLogin().loginPage("/login").failureUrl("/login?error").permitAll() -// .and() -// .logout().permitAll(); + http.authorizeRequests() + .requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll() + .anyRequest().fullyAuthenticated() + .and() + .formLogin().loginPage("/login").failureUrl("/login?error").permitAll() + .and() + .logout().permitAll(); // @formatter:on } diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml index 9a9fa2fee0..d79aac4d44 100644 --- a/spring-boot/pom.xml +++ b/spring-boot/pom.xml @@ -259,6 +259,11 @@ spring-webmvc true + + org.springframework.security + spring-security-web + true + org.yaml snakeyaml @@ -372,4 +377,4 @@ test - \ No newline at end of file + diff --git a/spring-boot/src/main/java/org/springframework/boot/security/ApplicationContextRequestMatcher.java b/spring-boot/src/main/java/org/springframework/boot/security/ApplicationContextRequestMatcher.java new file mode 100644 index 0000000000..a0b3351c95 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/security/ApplicationContextRequestMatcher.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2017 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.security; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * {@link ApplicationContext} backed {@link RequestMatcher}. Can work directly with the + * {@link ApplicationContext}, obtain an existing bean or + * {@link AutowireCapableBeanFactory#createBean(Class, int, boolean) create a new bean} + * that is autowired in the usual way. + * + * @param The type of the context that the match method actually needs to use. Can be + * an {@link ApplicationContext}, a class of an {@link ApplicationContext#getBean(Class) + * existing bean} or a custom type that will be + * {@link AutowireCapableBeanFactory#createBean(Class, int, boolean) created} on demand. + * @author Phillip Webb + * @since 2.0.0 + */ +public abstract class ApplicationContextRequestMatcher implements RequestMatcher { + + private final Class contextClass; + + private volatile C context; + + private Object contextLock = new Object(); + + public ApplicationContextRequestMatcher(Class contextClass) { + Assert.notNull(contextClass, "Context class must not be null"); + this.contextClass = contextClass; + } + + @Override + public boolean matches(HttpServletRequest request) { + return matches(request, getContext(request)); + } + + /** + * Decides whether the rule implemented by the strategy matches the supplied request. + * @param request the source request + * @param context the context instance + * @return if the request matches + */ + protected abstract boolean matches(HttpServletRequest request, C context); + + private C getContext(HttpServletRequest request) { + if (this.context == null) { + synchronized (this.contextLock) { + this.context = createContext(request); + initialized(this.context); + } + } + return this.context; + } + + /** + * Called once the context has been initialized. + * @param context the initialized context + */ + protected void initialized(C context) { + } + + @SuppressWarnings("unchecked") + private C createContext(HttpServletRequest request) { + WebApplicationContext context = WebApplicationContextUtils + .getRequiredWebApplicationContext(request.getServletContext()); + if (this.contextClass.isInstance(context)) { + return (C) context; + } + try { + return context.getBean(this.contextClass); + } + catch (NoSuchBeanDefinitionException ex) { + return (C) context.getAutowireCapableBeanFactory().createBean( + this.contextClass, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, + false); + } + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/security/package-info.java b/spring-boot/src/main/java/org/springframework/boot/security/package-info.java new file mode 100644 index 0000000000..d3fb34ad48 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/security/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Classes and utilities for Spring Security. + */ +package org.springframework.boot.security; diff --git a/spring-boot/src/test/java/org/springframework/boot/security/ApplicationContextRequestMatcherTests.java b/spring-boot/src/test/java/org/springframework/boot/security/ApplicationContextRequestMatcherTests.java new file mode 100644 index 0000000000..56778b504e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/security/ApplicationContextRequestMatcherTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2017 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.security; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.context.ApplicationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationContextRequestMatcher}. + * + * @author Phillip Webb + */ +public class ApplicationContextRequestMatcherTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenContextClassIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Context class must not be null"); + new TestApplicationContextRequestMatcher<>(null); + } + + @Test + public void matchesWhenContextClassIsApplicationContextShouldProvideContext() + throws Exception { + StaticWebApplicationContext context = createWebApplicationContext(); + assertThat(new TestApplicationContextRequestMatcher<>(ApplicationContext.class) + .callMatchesAndReturnProvidedContext(context)).isEqualTo(context); + } + + @Test + public void matchesWhenContextClassIsExistingBeanShouldProvideBean() + throws Exception { + StaticWebApplicationContext context = createWebApplicationContext(); + context.registerSingleton("existingBean", ExistingBean.class); + assertThat(new TestApplicationContextRequestMatcher<>(ExistingBean.class) + .callMatchesAndReturnProvidedContext(context)) + .isEqualTo(context.getBean(ExistingBean.class)); + } + + @Test + public void matchesWhenContextClassIsNewBeanShouldProvideBean() throws Exception { + StaticWebApplicationContext context = createWebApplicationContext(); + context.registerSingleton("existingBean", ExistingBean.class); + assertThat(new TestApplicationContextRequestMatcher<>(NewBean.class) + .callMatchesAndReturnProvidedContext(context).getBean()) + .isEqualTo(context.getBean(ExistingBean.class)); + } + + private StaticWebApplicationContext createWebApplicationContext() { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + MockServletContext servletContext = new MockServletContext(); + context.setServletContext(servletContext); + servletContext.setAttribute( + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); + return context; + } + + static class ExistingBean { + + } + + static class NewBean { + + private final ExistingBean bean; + + NewBean(ExistingBean bean) { + this.bean = bean; + } + + public ExistingBean getBean() { + return this.bean; + } + + } + + static class TestApplicationContextRequestMatcher + extends ApplicationContextRequestMatcher { + + private C provdedContext; + + TestApplicationContextRequestMatcher(Class context) { + super(context); + } + + public C callMatchesAndReturnProvidedContext(WebApplicationContext context) { + return callMatchesAndReturnProvidedContext( + new MockHttpServletRequest(context.getServletContext())); + } + + public C callMatchesAndReturnProvidedContext(HttpServletRequest request) { + matches(request); + return getProvdedContext(); + } + + @Override + protected boolean matches(HttpServletRequest request, C context) { + this.provdedContext = context; + return false; + } + + public C getProvdedContext() { + return this.provdedContext; + } + + } + +}