Rework security request matchers

Update the security request matchers so that a bean is no longer needed
when the matcher is used. Matchers can now be build by starting from
the `EndpointRequest` or `StaticResourceRequest` classes. For example:

http.authorizeRequests()
  .requestMatchers(EndpointRequest.to("status", "info")).permitAll()
  .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR")
  .requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll()

Closes gh-7958
This commit is contained in:
Phillip Webb 2017-08-31 16:10:12 -07:00
parent 2e51b48cd9
commit 46dfe38b60
20 changed files with 1294 additions and 29 deletions

View File

@ -33,7 +33,7 @@ import org.springframework.core.env.Environment;
* @author Stephane Nicoll
* @since 2.0.0
*/
public final class EndpointProvider<T extends Operation> {
public class EndpointProvider<T extends Operation> {
private final EndpointDiscoverer<T> discoverer;

View File

@ -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<EndpointInfo<WebEndpointOperation>> endpoints;
private final String contextPath;
public DefaultEndpointPathProvider(EndpointProvider<WebEndpointOperation> provider,
ManagementServerProperties managementServerProperties) {
this.endpoints = provider.getEndpoints();
this.contextPath = managementServerProperties.getContextPath();
}
@Override
public List<String> 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<WebEndpointOperation> endpointInfo) {
return this.contextPath + "/" + endpointInfo.getId();
}
}

View File

@ -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<String> 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);
}

View File

@ -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<WebEndpointOperation> provider,
ManagementServerProperties managementServerProperties) {
return new DefaultEndpointPathProvider(provider, managementServerProperties);
}
}

View File

@ -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<WebEndpointOperation> provider,
ManagementServerProperties managementServerProperties) {
return new DefaultEndpointPathProvider(provider, managementServerProperties);
}
}

View File

@ -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<WebEndpointOperation> provider,
ManagementServerProperties managementServerProperties) {
return new DefaultEndpointPathProvider(provider, managementServerProperties);
}
private CorsConfiguration getCorsConfiguration(CorsEndpointProperties properties) {
if (CollectionUtils.isEmpty(properties.getAllowedOrigins())) {
return null;

View File

@ -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: <pre class="code">
* EndpointRequestMatcher.toAnyEndpoint().excluding(ShutdownEndpoint.class)
* </pre>
* @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: <pre class="code">
* EndpointRequestMatcher.to(ShutdownEndpoint.class, HealthEndpoint.class)
* </pre>
* @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: <pre class="code">
* EndpointRequestMatcher.to("shutdown", "health")
* </pre>
* @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<EndpointPathProvider> {
private final List<Object> includes;
private final List<Object> 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<Object> includes, List<Object> excludes) {
super(EndpointPathProvider.class);
this.includes = includes;
this.excludes = excludes;
}
EndpointRequestMatcher excluding(Class<?>... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints));
return new EndpointRequestMatcher(this.includes, excludes);
}
EndpointRequestMatcher excluding(String... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints));
return new EndpointRequestMatcher(this.includes, excludes);
}
@Override
protected void initialized(EndpointPathProvider endpointPathProvider) {
Set<String> 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<String> streamPaths(List<Object> 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<RequestMatcher> getDelegateMatchers(Set<String> 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);
}
}
}

View File

@ -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;

View File

@ -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<WebEndpointOperation> 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<EndpointInfo<WebEndpointOperation>> 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);
}
}

View File

@ -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<String> 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 {
}
}

View File

@ -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:
* <pre class="code">
* StaticResourceRequest.toCommonLocations().excluding(Location.CSS)
* </pre>
* @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: <pre class="code">
* StaticResourceRequest.to(Location.CSS, Location.JAVA_SCRIPT)
* </pre>
* @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: <pre class="code">
* StaticResourceRequest.to(locations)
* </pre>
* @param locations the locations to include
* @return the configured {@link RequestMatcher}
*/
public static StaticResourceRequestMatcher to(Set<Location> 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<String> getPatterns() {
return Arrays.stream(this.patterns);
}
}
/**
* The request matcher used to match against resource {@link Location Locations}.
*/
public final static class StaticResourceRequestMatcher
extends ApplicationContextRequestMatcher<ServerProperties> {
private final Set<Location> locations;
private RequestMatcher delegate;
private StaticResourceRequestMatcher(Set<Location> 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<Location> locations) {
Assert.notNull(locations, "Locations must not be null");
Set<Location> 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<RequestMatcher> getDelegateMatchers(
ServerProperties serverProperties) {
return getPatterns(serverProperties).map(AntPathRequestMatcher::new)
.collect(Collectors.toList());
}
private Stream<String> 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);
}
}
}

View File

@ -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<Location>) null);
}
@Test
public void excludeFromSetWhenSetIsNullShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Locations must not be null");
StaticResourceRequest.toCommonLocations().excluding((Set<Location>) 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;
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -259,6 +259,11 @@
<artifactId>spring-webmvc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
@ -372,4 +377,4 @@
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>

View File

@ -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 <C> 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<C> implements RequestMatcher {
private final Class<? extends C> contextClass;
private volatile C context;
private Object contextLock = new Object();
public ApplicationContextRequestMatcher(Class<? extends C> 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);
}
}
}

View File

@ -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;

View File

@ -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<C>
extends ApplicationContextRequestMatcher<C> {
private C provdedContext;
TestApplicationContextRequestMatcher(Class<? extends C> 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;
}
}
}