diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java index 4f933ef598..ee1294d75a 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java @@ -65,7 +65,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @ContextConfiguration(classes = SpringBootHypermediaApplication.class, loader = SpringBootContextLoader.class) @WebAppConfiguration @TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", - "endpoints.health.sensitive=true", "endpoints.actuator.enabled=false" }) + "endpoints.health.sensitive=true", "endpoints.actuator.enabled=false", + "management.security.enabled=false" }) @DirtiesContext @AutoConfigureRestDocs(EndpointDocumentation.RESTDOCS_OUTPUT_DIR) @AutoConfigureMockMvc(print = MockMvcPrint.NONE) diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HealthEndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HealthEndpointDocumentation.java index 7847aa4427..d52386d588 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HealthEndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HealthEndpointDocumentation.java @@ -39,7 +39,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @ContextConfiguration(classes = SpringBootHypermediaApplication.class, loader = SpringBootContextLoader.class) @WebAppConfiguration @TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", - "endpoints.health.sensitive=false" }) + "endpoints.health.sensitive=false", "management.security.enabled=false" }) @DirtiesContext @AutoConfigureMockMvc @AutoConfigureRestDocs("target/generated-snippets") diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java index 00b6e7852d..a94ba09a0e 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java @@ -40,7 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @ContextConfiguration(classes = SpringBootHypermediaApplication.class, loader = SpringBootContextLoader.class) @WebAppConfiguration @TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", - "endpoints.hypermedia.enabled=true" }) + "endpoints.hypermedia.enabled=true", "management.security.enabled=false" }) @DirtiesContext @AutoConfigureMockMvc @AutoConfigureRestDocs("target/generated-snippets") diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java index 356a10c5d8..687408b606 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.boot.actuate.endpoint.mvc.LogFileMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpointSecurityInterceptor; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint; import org.springframework.boot.autoconfigure.condition.ConditionMessage; @@ -99,6 +100,10 @@ public class EndpointWebMvcManagementContextConfiguration { EndpointHandlerMapping mapping = new EndpointHandlerMapping(endpoints, corsConfiguration); mapping.setPrefix(this.managementServerProperties.getContextPath()); + MvcEndpointSecurityInterceptor securityInterceptor = new MvcEndpointSecurityInterceptor( + this.managementServerProperties.getSecurity().isEnabled(), + this.managementServerProperties.getSecurity().getRoles()); + mapping.setSecurityInterceptor(securityInterceptor); for (EndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) { customizer.customize(mapping); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementServerProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementServerProperties.java index 75a14db3af..f32b5a4b7e 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementServerProperties.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementServerProperties.java @@ -168,7 +168,7 @@ public class ManagementServerProperties implements SecurityPrerequisite { /** * Comma-separated list of roles that can access the management endpoint. */ - private List roles = Arrays.asList("ADMIN"); + private List roles = Arrays.asList("ACTUATOR"); /** * Session creating policy for security use (always, never, if_required, diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java index 85b5de2dd5..77a21cb902 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java @@ -26,7 +26,6 @@ import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; @@ -42,11 +41,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.security.AuthenticationManagerConfiguration; import org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.IgnoredRequestCustomizer; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityPrerequisite; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SpringBootWebSecurityConfiguration; -import org.springframework.boot.autoconfigure.web.ErrorController; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; @@ -56,9 +55,7 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; @@ -72,7 +69,6 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -102,9 +98,34 @@ public class ManagementWebSecurityAutoConfiguration { AnyRequestMatcher.INSTANCE); @Bean - @ConditionalOnMissingBean({ IgnoredPathsWebSecurityConfigurerAdapter.class }) - public IgnoredPathsWebSecurityConfigurerAdapter ignoredPathsWebSecurityConfigurerAdapter() { - return new IgnoredPathsWebSecurityConfigurerAdapter(); + public IgnoredRequestCustomizer managementIgnoredRequestCustomizer( + ManagementServerProperties management, + ObjectProvider contextResolverProvider) { + return new ManagementIgnoredRequestCustomizer(management, + contextResolverProvider.getIfAvailable()); + } + + private class ManagementIgnoredRequestCustomizer implements IgnoredRequestCustomizer { + + private final ManagementServerProperties management; + + private final ManagementContextResolver contextResolver; + + ManagementIgnoredRequestCustomizer(ManagementServerProperties management, + ManagementContextResolver contextResolver) { + this.management = management; + this.contextResolver = contextResolver; + } + + @Override + public void customize(IgnoredRequestConfigurer configurer) { + if (!this.management.getSecurity().isEnabled()) { + RequestMatcher requestMatcher = LazyEndpointPathRequestMatcher + .getRequestMatcher(this.contextResolver); + configurer.requestMatchers(requestMatcher); + } + + } } @Configuration @@ -132,80 +153,6 @@ public class ManagementWebSecurityAutoConfiguration { } - // Get the ignored paths in early - @Order(SecurityProperties.IGNORED_ORDER + 1) - private static class IgnoredPathsWebSecurityConfigurerAdapter - implements WebSecurityConfigurer { - - @Autowired(required = false) - private ErrorController errorController; - - @Autowired - private SecurityProperties security; - - @Autowired - private ManagementServerProperties management; - - @Autowired(required = false) - private ManagementContextResolver contextResolver; - - @Autowired(required = false) - private ServerProperties server; - - @Override - public void configure(WebSecurity builder) throws Exception { - } - - @Override - public void init(WebSecurity builder) throws Exception { - if (this.server == null) { - return; - } - IgnoredRequestConfigurer ignoring = builder.ignoring(); - // The ignores are not cumulative, so to prevent overwriting the defaults - // we add them back. - Set ignored = new LinkedHashSet( - SpringBootWebSecurityConfiguration.getIgnored(this.security)); - if (ignored.contains("none")) { - ignored.remove("none"); - } - if (this.errorController != null) { - ignored.add(normalizePath(this.errorController.getErrorPath())); - } - RequestMatcher requestMatcher = getRequestMatcher(); - String[] paths = this.server.getPathsArray(ignored); - if (!ObjectUtils.isEmpty(paths)) { - List matchers = new ArrayList(); - for (String pattern : paths) { - matchers.add(new AntPathRequestMatcher(pattern, null)); - } - if (requestMatcher != null) { - matchers.add(requestMatcher); - } - requestMatcher = new OrRequestMatcher(matchers); - } - if (requestMatcher != null) { - ignoring.requestMatchers(requestMatcher); - } - } - - private RequestMatcher getRequestMatcher() { - if (this.management.getSecurity().isEnabled()) { - return null; - } - return LazyEndpointPathRequestMatcher.getRequestMatcher(this.contextResolver); - } - - private String normalizePath(String errorPath) { - String result = StringUtils.cleanPath(errorPath); - if (!result.startsWith("/")) { - result = "/" + result; - } - return result; - } - - } - @Configuration @ConditionalOnMissingBean(WebSecurityConfiguration.class) @Conditional(WebSecurityEnablerCondition.class) @@ -310,9 +257,7 @@ public class ManagementWebSecurityAutoConfiguration { // Permit access to the non-sensitive endpoints requests.requestMatchers(new LazyEndpointPathRequestMatcher( this.contextResolver, EndpointPaths.NON_SENSITIVE)).permitAll(); - // Restrict the rest to the configured roles - List roles = this.management.getSecurity().getRoles(); - requests.anyRequest().hasAnyRole(roles.toArray(new String[roles.size()])); + requests.anyRequest().authenticated(); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java index e3d299224b..68e51c8258 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java @@ -28,12 +28,15 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.IgnoredRequestCustomizer; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerInterceptor; @@ -95,4 +98,20 @@ public class CloudFoundryActuatorAutoConfiguration { return corsConfiguration; } + @Bean + public IgnoredRequestCustomizer cloudFoundryIgnoredRequestCustomizer() { + return new CloudFoundryIgnoredRequestCustomizer(); + } + + private class CloudFoundryIgnoredRequestCustomizer + implements IgnoredRequestCustomizer { + + @Override + public void customize(WebSecurity.IgnoredRequestConfigurer configurer) { + configurer.requestMatchers( + new AntPathRequestMatcher("/cloudfoundryapplication/**")); + } + + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java index 8a2ecf1984..3145b5f055 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java @@ -16,15 +16,9 @@ package org.springframework.boot.actuate.cloudfoundry; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Iterator; -import java.util.List; import java.util.Set; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint; @@ -32,10 +26,8 @@ import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint; import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; /** * {@link HandlerMapping} to map {@link Endpoint}s to Cloud Foundry specific URLs. @@ -45,15 +37,10 @@ import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; class CloudFoundryEndpointHandlerMapping extends AbstractEndpointHandlerMapping { - private final HandlerInterceptor securityInterceptor; - - private final CorsConfiguration corsConfiguration; - CloudFoundryEndpointHandlerMapping(Set endpoints, CorsConfiguration corsConfiguration, HandlerInterceptor securityInterceptor) { super(endpoints, corsConfiguration); - this.securityInterceptor = securityInterceptor; - this.corsConfiguration = corsConfiguration; + setSecurityInterceptor(securityInterceptor); } @Override @@ -91,42 +78,4 @@ class CloudFoundryEndpointHandlerMapping return super.getPath(endpoint); } - @Override - protected HandlerExecutionChain getHandlerExecutionChain(Object handler, - HttpServletRequest request) { - HandlerExecutionChain chain = super.getHandlerExecutionChain(handler, request); - HandlerInterceptor[] interceptors = addSecurityInterceptor( - chain.getInterceptors()); - return new HandlerExecutionChain(chain.getHandler(), interceptors); - } - - private HandlerInterceptor[] addSecurityInterceptor(HandlerInterceptor[] existing) { - List interceptors = new ArrayList(); - interceptors.add(new CorsInterceptor(this.corsConfiguration)); - interceptors.add(this.securityInterceptor); - if (existing != null) { - interceptors.addAll(Arrays.asList(existing)); - } - return interceptors.toArray(new HandlerInterceptor[interceptors.size()]); - } - - /** - * {@link HandlerInterceptor} that processes the response for CORS. - */ - class CorsInterceptor extends HandlerInterceptorAdapter { - - private final CorsConfiguration config; - - CorsInterceptor(CorsConfiguration config) { - this.config = config; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, - Object handler) throws Exception { - return getCorsProcessor().processRequest(this.config, request, response); - } - - } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java index 33591f2564..c0025913fe 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java @@ -18,18 +18,24 @@ package org.springframework.boot.actuate.endpoint.mvc; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import javax.servlet.http.HttpServletRequest; + import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; @@ -57,6 +63,8 @@ public abstract class AbstractEndpointHandlerMapping private final Set endpoints; + private HandlerInterceptor securityInterceptor; + private final CorsConfiguration corsConfiguration; private String prefix = ""; @@ -175,6 +183,45 @@ public abstract class AbstractEndpointHandlerMapping mapping.getCustomCondition()); } + @Override + protected HandlerExecutionChain getHandlerExecutionChain(Object handler, + HttpServletRequest request) { + HandlerExecutionChain chain = super.getHandlerExecutionChain(handler, request); + if (this.securityInterceptor == null || CorsUtils.isCorsRequest(request)) { + return chain; + } + return addSecurityInterceptor(chain); + } + + @Override + protected HandlerExecutionChain getCorsHandlerExecutionChain( + HttpServletRequest request, HandlerExecutionChain chain, + CorsConfiguration config) { + chain = super.getCorsHandlerExecutionChain(request, chain, config); + if (this.securityInterceptor == null) { + return chain; + } + return addSecurityInterceptor(chain); + } + + private HandlerExecutionChain addSecurityInterceptor(HandlerExecutionChain chain) { + List interceptors = new ArrayList(); + if (chain.getInterceptors() != null) { + interceptors.addAll(Arrays.asList(chain.getInterceptors())); + } + interceptors.add(this.securityInterceptor); + return new HandlerExecutionChain(chain.getHandler(), + interceptors.toArray(new HandlerInterceptor[interceptors.size()])); + } + + /** + * Set the handler interceptor that will be used for security. + * @param securityInterceptor the security handler interceptor + */ + public void setSecurityInterceptor(HandlerInterceptor securityInterceptor) { + this.securityInterceptor = securityInterceptor; + } + /** * Set the prefix used in mappings. * @param prefix the prefix diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java index a44df8fc43..53bd7eded7 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java @@ -194,7 +194,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter roles = Arrays.asList(StringUtils .trimArrayElements(StringUtils.commaDelimitedListToStringArray( - this.roleResolver.getProperty("roles", "ROLE_ADMIN")))); + this.roleResolver.getProperty("roles", "ROLE_ACTUATOR")))); for (GrantedAuthority authority : authentication.getAuthorities()) { String name = authority.getAuthority(); for (String role : roles) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointSecurityInterceptor.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointSecurityInterceptor.java new file mode 100644 index 0000000000..08d4dba5c0 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointSecurityInterceptor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2016 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.endpoint.mvc; + +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +/** + * Security interceptor for MvcEndpoints. + * + * @author Madhura Bhave + * @since 1.5.0 + */ +public class MvcEndpointSecurityInterceptor extends HandlerInterceptorAdapter { + + private final boolean secure; + + private final List roles; + + public MvcEndpointSecurityInterceptor(boolean secure, List roles) { + this.secure = secure; + this.roles = roles; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + if (CorsUtils.isPreFlightRequest(request) || !this.secure) { + return true; + } + HandlerMethod handlerMethod = (HandlerMethod) handler; + MvcEndpoint mvcEndpoint = (MvcEndpoint) handlerMethod.getBean(); + if (!mvcEndpoint.isSensitive()) { + return true; + } + for (String role : this.roles) { + if (request.isUserInRole(role)) { + return true; + } + } + setFailureResponseStatus(request, response); + return false; + } + + private void setFailureResponseStatus(HttpServletRequest request, + HttpServletResponse response) { + if (request.getUserPrincipal() != null) { + response.setStatus(HttpStatus.FORBIDDEN.value()); + } + else { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + } + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java index 1bd3b5cde0..b6e2d3dc74 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java @@ -56,6 +56,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -73,6 +74,7 @@ import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext +@TestPropertySource(properties = "management.security.enabled=false") public class EndpointMvcIntegrationTests { @LocalServerPort @@ -82,7 +84,7 @@ public class EndpointMvcIntegrationTests { private TestInterceptor interceptor; @Test - public void envEndpointHidden() throws InterruptedException { + public void envEndpointNotHidden() throws InterruptedException { String body = new TestRestTemplate().getForObject( "http://localhost:" + this.port + "/env/user.dir", String.class); assertThat(body).isNotNull().contains("spring-boot-actuator"); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index e0ffed25b4..ce8cfe0cdc 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -128,6 +128,7 @@ public class EndpointWebMvcAutoConfigurationTests { @Before public void defaultContextPath() { management.setContextPath(""); + management.getSecurity().setEnabled(false); server.setContextPath(""); } @@ -147,6 +148,8 @@ public class EndpointWebMvcAutoConfigurationTests { @Test public void onSamePort() throws Exception { + EnvironmentTestUtils.addEnvironment(this.applicationContext, + "management.security.enabled:false"); this.applicationContext.register(RootConfig.class, EndpointConfig.class, BaseConfiguration.class, ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); @@ -318,7 +321,8 @@ public class EndpointWebMvcAutoConfigurationTests { public void specificPortsViaProperties() throws Exception { EnvironmentTestUtils.addEnvironment(this.applicationContext, "server.port:" + ports.get().server, - "management.port:" + ports.get().management); + "management.port:" + ports.get().management, + "management.security.enabled:false"); this.applicationContext.register(RootConfig.class, EndpointConfig.class, BaseConfiguration.class, EndpointWebMvcAutoConfiguration.class, ErrorMvcAutoConfiguration.class); @@ -352,7 +356,7 @@ public class EndpointWebMvcAutoConfigurationTests { @Test public void contextPath() throws Exception { EnvironmentTestUtils.addEnvironment(this.applicationContext, - "management.contextPath:/test"); + "management.contextPath:/test", "management.security.enabled:false"); this.applicationContext.register(RootConfig.class, EndpointConfig.class, ServerPortConfig.class, PropertyPlaceholderAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, @@ -884,6 +888,7 @@ public class EndpointWebMvcAutoConfigurationTests { public ManagementServerProperties managementServerProperties() { ManagementServerProperties properties = new ManagementServerProperties(); properties.setPort(0); + properties.getSecurity().setEnabled(false); return properties; } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfigurationTests.java new file mode 100644 index 0000000000..92ef72ccaf --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfigurationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2016 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; + +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpointSecurityInterceptor; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EndpointWebMvcManagementContextConfiguration}. + * + * @author Madhura Bhave + */ +public class EndpointWebMvcManagementContextConfigurationTests { + + private AnnotationConfigWebApplicationContext context; + + @Before + public void setup() { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setServletContext(new MockServletContext()); + this.context.register(SecurityAutoConfiguration.class, + WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + WebClientAutoConfiguration.class, + EndpointWebMvcManagementContextConfiguration.class); + } + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void endpointHandlerMapping() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "management.security.enabled=false", + "management.security.roles=my-role,your-role"); + this.context.refresh(); + EndpointHandlerMapping mapping = this.context.getBean("endpointHandlerMapping", + EndpointHandlerMapping.class); + assertThat(mapping.getPrefix()).isEmpty(); + MvcEndpointSecurityInterceptor securityInterceptor = (MvcEndpointSecurityInterceptor) ReflectionTestUtils + .getField(mapping, "securityInterceptor"); + Object secure = ReflectionTestUtils.getField(securityInterceptor, "secure"); + List roles = getRoles(securityInterceptor); + assertThat(secure).isEqualTo(false); + assertThat(roles).containsExactly("my-role", "your-role"); + } + + @SuppressWarnings("unchecked") + private List getRoles(MvcEndpointSecurityInterceptor securityInterceptor) { + return (List) ReflectionTestUtils.getField(securityInterceptor, "roles"); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/JolokiaAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/JolokiaAutoConfigurationTests.java index 471d4d9259..0613caaba4 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/JolokiaAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/JolokiaAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure; import java.util.Collection; +import java.util.Collections; import org.hamcrest.Matchers; import org.junit.After; @@ -25,6 +26,7 @@ import org.junit.Test; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.JolokiaMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpointSecurityInterceptor; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; @@ -141,7 +143,10 @@ public class JolokiaAutoConfigurationTests { @Bean public EndpointHandlerMapping endpointHandlerMapping( Collection endpoints) { - return new EndpointHandlerMapping(endpoints); + EndpointHandlerMapping mapping = new EndpointHandlerMapping(endpoints); + mapping.setSecurityInterceptor( + new MvcEndpointSecurityInterceptor(false, Collections.EMPTY_LIST)); + return mapping; } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java index 731e3bd81b..b9f6d8b10f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java @@ -120,7 +120,7 @@ public class ManagementWebSecurityAutoConfigurationTests { ArrayList authorities = new ArrayList( user.getAuthorities()); assertThat(authorities).containsAll(AuthorityUtils - .commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_ADMIN")); + .commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_ACTUATOR")); } private UserDetails getUser() { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java index 53830fd342..5de230a353 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java @@ -21,6 +21,7 @@ import java.util.Arrays; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; @@ -29,18 +30,24 @@ import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties import org.springframework.boot.actuate.autoconfigure.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.security.IgnoredRequestCustomizer; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockServletContext; +import org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.cors.CorsConfiguration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link CloudFoundryActuatorAutoConfiguration}. @@ -116,10 +123,9 @@ public class CloudFoundryActuatorAutoConfigurationTests { EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id"); this.context.refresh(); - CloudFoundryEndpointHandlerMapping handlerMapping1 = this.context.getBean( + CloudFoundryEndpointHandlerMapping handlerMapping = this.context.getBean( "cloudFoundryEndpointHandlerMapping", CloudFoundryEndpointHandlerMapping.class); - CloudFoundryEndpointHandlerMapping handlerMapping = handlerMapping1; Object securityInterceptor = ReflectionTestUtils.getField(handlerMapping, "securityInterceptor"); Object interceptorSecurityService = ReflectionTestUtils @@ -127,13 +133,24 @@ public class CloudFoundryActuatorAutoConfigurationTests { assertThat(interceptorSecurityService).isNull(); } - private CloudFoundryEndpointHandlerMapping getHandlerMapping() { + @Test + public void cloudFoundryPathsIgnoredBySpringSecurity() throws Exception { EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION:---", - "vcap.application.application_id:my-app-id", - "vcap.application.cf_api:http://my-cloud-controller.com"); + "vcap.application.application_id:my-app-id"); this.context.refresh(); - return this.context.getBean("cloudFoundryEndpointHandlerMapping", - CloudFoundryEndpointHandlerMapping.class); + IgnoredRequestCustomizer customizer = (IgnoredRequestCustomizer) this.context + .getBean("cloudFoundryIgnoredRequestCustomizer"); + IgnoredRequestConfigurer configurer = mock(IgnoredRequestConfigurer.class); + customizer.customize(configurer); + ArgumentCaptor requestMatcher = ArgumentCaptor + .forClass(RequestMatcher.class); + verify(configurer).requestMatchers(requestMatcher.capture()); + RequestMatcher matcher = requestMatcher.getValue(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServletPath("/cloudfoundryapplication/my-path"); + assertThat(matcher.matches(request)).isTrue(); + request.setServletPath("/some-other-path"); + assertThat(matcher.matches(request)).isFalse(); } @Test @@ -152,4 +169,13 @@ public class CloudFoundryActuatorAutoConfigurationTests { .isFalse(); } + private CloudFoundryEndpointHandlerMapping getHandlerMapping() { + EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION:---", + "vcap.application.application_id:my-app-id", + "vcap.application.cf_api:http://my-cloud-controller.com"); + this.context.refresh(); + return this.context.getBean("cloudFoundryEndpointHandlerMapping", + CloudFoundryEndpointHandlerMapping.class); + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java index b3d13d326f..77f61c24c9 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java @@ -19,7 +19,6 @@ package org.springframework.boot.actuate.cloudfoundry; import java.util.Collections; import org.junit.Test; -import org.mockito.Mockito; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; import org.springframework.boot.actuate.endpoint.HealthEndpoint; @@ -33,16 +32,10 @@ import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.context.support.StaticApplicationContext; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsProcessor; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerExecutionChain; -import org.springframework.web.servlet.HandlerInterceptor; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link CloudFoundryEndpointHandlerMapping}. @@ -52,37 +45,6 @@ import static org.mockito.Mockito.verify; public class CloudFoundryEndpointHandlerMappingTests extends AbstractEndpointHandlerMappingTests { - @Test - public void corsInterceptorShouldBeFirstAndCallCorsProcessor() throws Exception { - TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a")); - CorsConfiguration corsConfiguration = new CorsConfiguration(); - CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping( - Collections.singleton(endpoint), corsConfiguration, null); - CorsProcessor corsProcessor = mock(CorsProcessor.class); - handlerMapping.setCorsProcessor(corsProcessor); - MockHttpServletRequest request = new MockHttpServletRequest(); - HandlerExecutionChain handlerExecutionChain = handlerMapping - .getHandlerExecutionChain(endpoint, request); - HandlerInterceptor[] interceptors = handlerExecutionChain.getInterceptors(); - CloudFoundryEndpointHandlerMapping.CorsInterceptor corsInterceptor = (CloudFoundryEndpointHandlerMapping.CorsInterceptor) interceptors[0]; - MockHttpServletResponse response = new MockHttpServletResponse(); - corsInterceptor.preHandle(request, response, new Object()); - verify(corsProcessor).processRequest(corsConfiguration, request, response); - } - - @Test - public void getHandlerExecutionChainShouldHaveSecurityInterceptor() throws Exception { - CloudFoundrySecurityInterceptor securityInterceptor = Mockito - .mock(CloudFoundrySecurityInterceptor.class); - TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a")); - CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping( - Collections.singleton(endpoint), null, securityInterceptor); - HandlerExecutionChain handlerExecutionChain = handlerMapping - .getHandlerExecutionChain(endpoint, new MockHttpServletRequest()); - HandlerInterceptor[] interceptors = handlerExecutionChain.getInterceptors(); - assertThat(interceptors[1]).isEqualTo(securityInterceptor); - } - @Test public void getHandlerExecutionChainWhenEndpointHasPathShouldMapAgainstName() throws Exception { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java index 72a9daac08..0d70e8aba6 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.mvc; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import org.junit.Test; @@ -25,8 +26,10 @@ import org.springframework.boot.actuate.endpoint.AbstractEndpoint; import org.springframework.context.support.StaticApplicationContext; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.servlet.HandlerInterceptor; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link AbstractEndpointHandlerMapping}. @@ -37,6 +40,46 @@ public abstract class AbstractEndpointHandlerMappingTests { private final StaticApplicationContext context = new StaticApplicationContext(); + @Test + public void securityInterceptorShouldBePresentForNonCorsRequest() throws Exception { + HandlerInterceptor securityInterceptor = mock(HandlerInterceptor.class); + TestActionEndpoint endpoint = new TestActionEndpoint(new TestEndpoint("a")); + AbstractEndpointHandlerMapping mapping = new TestEndpointHandlerMapping( + Collections.singletonList(endpoint)); + mapping.setApplicationContext(this.context); + mapping.setSecurityInterceptor(securityInterceptor); + mapping.afterPropertiesSet(); + assertThat(mapping.getHandler(request("POST", "/a")).getInterceptors()) + .contains(securityInterceptor); + } + + @Test + public void securityInterceptorIfNullShouldNotBeAdded() throws Exception { + TestActionEndpoint endpoint = new TestActionEndpoint(new TestEndpoint("a")); + AbstractEndpointHandlerMapping mapping = new TestEndpointHandlerMapping( + Collections.singletonList(endpoint)); + mapping.setApplicationContext(this.context); + mapping.afterPropertiesSet(); + assertThat(mapping.getHandler(request("POST", "/a")).getInterceptors()).isNull(); + } + + @Test + public void securityInterceptorShouldBePresentAfterCorsInterceptorForCorsRequest() + throws Exception { + HandlerInterceptor securityInterceptor = mock(HandlerInterceptor.class); + TestActionEndpoint endpoint = new TestActionEndpoint(new TestEndpoint("a")); + AbstractEndpointHandlerMapping mapping = new TestEndpointHandlerMapping( + Collections.singletonList(endpoint)); + mapping.setApplicationContext(this.context); + mapping.setSecurityInterceptor(securityInterceptor); + mapping.afterPropertiesSet(); + MockHttpServletRequest request = request("POST", "/a"); + request.addHeader("Origin", "http://example.com"); + assertThat(mapping.getHandler(request).getInterceptors().length).isEqualTo(2); + assertThat(mapping.getHandler(request).getInterceptors()[1]) + .isEqualTo(securityInterceptor); + } + @Test public void pathNotMappedWhenGetPathReturnsNull() throws Exception { TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a")); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java index 5b91e61886..1f24b65302 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java @@ -39,6 +39,7 @@ import org.springframework.context.annotation.Import; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -57,6 +58,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @RunWith(SpringRunner.class) @SpringBootTest +@TestPropertySource(properties = "management.security.enabled=false") @DirtiesContext public class EnvironmentMvcEndpointTests { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java index e7772532b9..351c9d8a50 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java @@ -28,6 +28,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -46,6 +47,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @RunWith(SpringRunner.class) @SpringBootTest +@TestPropertySource(properties = "management.security.enabled=false") @DirtiesContext public class HalBrowserMvcEndpointDisabledIntegrationTests { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointManagementContextPathIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointManagementContextPathIntegrationTests.java index ea29faa176..e36e685da1 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointManagementContextPathIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointManagementContextPathIntegrationTests.java @@ -52,7 +52,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @RunWith(SpringRunner.class) @SpringBootTest -@TestPropertySource(properties = "management.contextPath:/admin") +@TestPropertySource(properties = { "management.contextPath:/admin", + "management.security.enabled=false" }) @DirtiesContext public class HalBrowserMvcEndpointManagementContextPathIntegrationTests { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java index 049c8dfbc3..a7ecfb7f58 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java @@ -51,7 +51,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @RunWith(SpringRunner.class) @SpringBootTest @DirtiesContext -@TestPropertySource(properties = "endpoints.hypermedia.enabled=true") +@TestPropertySource(properties = { "endpoints.hypermedia.enabled=true", + "management.security.enabled=false" }) public class HalBrowserMvcEndpointVanillaIntegrationTests { @Autowired diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java index 42157bee1d..03df244c4c 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java @@ -37,6 +37,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -55,6 +56,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @RunWith(SpringRunner.class) @SpringBootTest +@TestPropertySource(properties = "management.security.enabled=false") public class HeapdumpMvcEndpointTests { @Autowired diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java index a0a5e8c22e..403c30cbdc 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java @@ -37,6 +37,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -56,6 +57,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @RunWith(SpringRunner.class) @SpringBootTest +@TestPropertySource(properties = "management.security.enabled=false") @ContextConfiguration(classes = { Config.class }, initializers = ContextPathListener.class) @DirtiesContext diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java index 2eed780f5e..7c6be1e97f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java @@ -35,6 +35,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -56,6 +57,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @RunWith(SpringRunner.class) @DirtiesContext @SpringBootTest +@TestPropertySource(properties = "management.security.enabled=false") public class JolokiaMvcEndpointTests { @Autowired diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java index c43b902d11..9ce1f92de7 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java @@ -40,6 +40,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; @@ -64,6 +65,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @RunWith(SpringRunner.class) @SpringBootTest +@TestPropertySource(properties = "management.security.enabled=false") public class LoggersMvcEndpointTests { @Autowired diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java index 2bf65e88b5..416ab12112 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java @@ -38,6 +38,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -58,6 +59,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @RunWith(SpringRunner.class) @DirtiesContext @SpringBootTest +@TestPropertySource(properties = "management.security.enabled=false") public class MetricsMvcEndpointTests { @Autowired diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java index 68fe00cf18..7a0f72e0e6 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java @@ -67,15 +67,17 @@ public class MvcEndpointIntegrationTests { @Test public void defaultJsonResponseIsNotIndented() throws Exception { + TestSecurityContextHolder.getContext().setAuthentication( + new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR")); this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(DefaultConfiguration.class); - MockMvc mockMvc = createMockMvc(); + this.context.register(SecureConfiguration.class); + MockMvc mockMvc = createSecureMockMvc(); mockMvc.perform(get("/mappings")).andExpect(content().string(startsWith("{\""))); } @Test public void jsonResponsesCanBeIndented() throws Exception { - assertIndentedJsonResponse(DefaultConfiguration.class); + assertIndentedJsonResponse(SecureConfiguration.class); } @Test @@ -100,9 +102,11 @@ public class MvcEndpointIntegrationTests { @Test public void jsonExtensionProvided() throws Exception { + TestSecurityContextHolder.getContext().setAuthentication( + new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR")); this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(DefaultConfiguration.class); - MockMvc mockMvc = createMockMvc(); + this.context.register(SecureConfiguration.class); + MockMvc mockMvc = createSecureMockMvc(); mockMvc.perform(get("/beans.json")).andExpect(status().isOk()); } @@ -147,7 +151,7 @@ public class MvcEndpointIntegrationTests { } @Test - public void sensitiveEndpointsAreSecureWithNonAdminRoleWithCustomContextPath() + public void sensitiveEndpointsAreSecureWithNonActuatorRoleWithCustomContextPath() throws Exception { TestSecurityContextHolder.getContext().setAuthentication( new TestingAuthenticationToken("user", "N/A", "ROLE_USER")); @@ -160,10 +164,10 @@ public class MvcEndpointIntegrationTests { } @Test - public void sensitiveEndpointsAreSecureWithAdminRoleWithCustomContextPath() + public void sensitiveEndpointsAreSecureWithActuatorRoleWithCustomContextPath() throws Exception { TestSecurityContextHolder.getContext().setAuthentication( - new TestingAuthenticationToken("user", "N/A", "ROLE_ADMIN")); + new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR")); this.context = new AnnotationConfigWebApplicationContext(); this.context.register(SecureConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, @@ -194,11 +198,13 @@ public class MvcEndpointIntegrationTests { } private void assertIndentedJsonResponse(Class configuration) throws Exception { + TestSecurityContextHolder.getContext().setAuthentication( + new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR")); this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configuration); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.serialization.indent-output:true"); - MockMvc mockMvc = createMockMvc(); + MockMvc mockMvc = createSecureMockMvc(); mockMvc.perform(get("/mappings")) .andExpect(content().string(startsWith("{" + LINE_SEPARATOR))); } @@ -230,21 +236,15 @@ public class MvcEndpointIntegrationTests { } - @ImportAutoConfiguration({ HypermediaAutoConfiguration.class, - JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class }) + @Import(SecureConfiguration.class) + @ImportAutoConfiguration({ HypermediaAutoConfiguration.class }) static class SpringHateoasConfiguration { } + @Import(SecureConfiguration.class) @ImportAutoConfiguration({ HypermediaAutoConfiguration.class, - RepositoryRestMvcAutoConfiguration.class, JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class }) + RepositoryRestMvcAutoConfiguration.class }) static class SpringDataRestConfiguration { } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointSecurityInterceptorTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointSecurityInterceptorTests.java new file mode 100644 index 0000000000..8586bdd7f7 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointSecurityInterceptorTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2016 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.endpoint.mvc; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.method.HandlerMethod; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MvcEndpointSecurityInterceptor}. + * + * @author Madhura Bhave + */ +public class MvcEndpointSecurityInterceptorTests { + + private MvcEndpointSecurityInterceptor securityInterceptor; + + private TestMvcEndpoint mvcEndpoint; + + private TestEndpoint endpoint; + + private HandlerMethod handlerMethod; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private MockServletContext servletContext; + + private List roles; + + @Before + public void setup() throws Exception { + this.roles = Arrays.asList("SUPER_HERO"); + this.securityInterceptor = new MvcEndpointSecurityInterceptor(true, this.roles); + this.endpoint = new TestEndpoint("a"); + this.mvcEndpoint = new TestMvcEndpoint(this.endpoint); + this.handlerMethod = new HandlerMethod(this.mvcEndpoint, "invoke"); + this.servletContext = new MockServletContext(); + this.request = new MockHttpServletRequest(this.servletContext); + this.response = new MockHttpServletResponse(); + } + + @Test + public void securityDisabledShouldAllowAccess() throws Exception { + this.securityInterceptor = new MvcEndpointSecurityInterceptor(false, this.roles); + assertThat(this.securityInterceptor.preHandle(this.request, this.response, + this.handlerMethod)).isTrue(); + } + + @Test + public void endpointNotSensitiveShouldAllowAccess() throws Exception { + this.endpoint.setSensitive(false); + assertThat(this.securityInterceptor.preHandle(this.request, this.response, + this.handlerMethod)).isTrue(); + } + + @Test + public void sensitiveEndpointIfRoleIsPresentShouldAllowAccess() throws Exception { + this.servletContext.declareRoles("SUPER_HERO"); + assertThat(this.securityInterceptor.preHandle(this.request, this.response, + this.handlerMethod)).isTrue(); + } + + @Test + public void sensitiveEndpointIfRoleIsNotPresentShouldNotAllowAccess() + throws Exception { + this.servletContext.declareRoles("HERO"); + assertThat(this.securityInterceptor.preHandle(this.request, this.response, + this.handlerMethod)).isFalse(); + } + + private static class TestEndpoint extends AbstractEndpoint { + + TestEndpoint(String id) { + super(id); + } + + @Override + public Object invoke() { + return null; + } + + } + + private static class TestMvcEndpoint extends EndpointMvcAdapter { + + TestMvcEndpoint(TestEndpoint delegate) { + super(delegate); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/IgnoredRequestCustomizer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/IgnoredRequestCustomizer.java new file mode 100644 index 0000000000..bdba36efae --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/IgnoredRequestCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2016 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 org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer; + +/** + * Customizer that can be implemented by beans to configure paths that need to be ignored + * by Spring Boot's default Spring Security configuration. + * + * @author Madhura Bhave + * @since 1.5.0 + */ +public interface IgnoredRequestCustomizer { + + /** + * Customize the provided {@link IgnoredRequestConfigurer}. + * @param configurer the configurer to customize + */ + void customize(IgnoredRequestConfigurer configurer); + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java index 0197ca2ee7..ed904894bd 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java @@ -22,7 +22,7 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -41,6 +41,7 @@ import org.springframework.security.config.annotation.authentication.builders.Au import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -48,7 +49,9 @@ import org.springframework.security.config.annotation.web.configurers.HeadersCon import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.header.writers.HstsHeaderWriter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -94,8 +97,17 @@ public class SpringBootWebSecurityConfiguration { @Bean @ConditionalOnMissingBean({ IgnoredPathsWebSecurityConfigurerAdapter.class }) - public IgnoredPathsWebSecurityConfigurerAdapter ignoredPathsWebSecurityConfigurerAdapter() { - return new IgnoredPathsWebSecurityConfigurerAdapter(); + public IgnoredPathsWebSecurityConfigurerAdapter ignoredPathsWebSecurityConfigurerAdapter( + List customizers) { + return new IgnoredPathsWebSecurityConfigurerAdapter(customizers); + } + + @Bean + public IgnoredRequestCustomizer defaultIgnoredRequestsCustomizer( + ServerProperties server, SecurityProperties security, + ObjectProvider errorController) { + return new DefaultIgnoredRequestCustomizer(server, security, + errorController.getIfAvailable()); } public static void configureHeaders(HeadersConfigurer configurer, @@ -146,14 +158,12 @@ public class SpringBootWebSecurityConfiguration { private static class IgnoredPathsWebSecurityConfigurerAdapter implements WebSecurityConfigurer { - @Autowired(required = false) - private ErrorController errorController; + private final List customizers; - @Autowired - private SecurityProperties security; - - @Autowired - private ServerProperties server; + IgnoredPathsWebSecurityConfigurerAdapter( + List customizers) { + this.customizers = customizers; + } @Override public void configure(WebSecurity builder) throws Exception { @@ -161,14 +171,55 @@ public class SpringBootWebSecurityConfiguration { @Override public void init(WebSecurity builder) throws Exception { + for (IgnoredRequestCustomizer customizer : this.customizers) { + customizer.customize(builder.ignoring()); + } + } + + } + + private class DefaultIgnoredRequestCustomizer implements IgnoredRequestCustomizer { + + private final ServerProperties server; + + private final SecurityProperties security; + + private final ErrorController errorController; + + DefaultIgnoredRequestCustomizer(ServerProperties server, + SecurityProperties security, ErrorController errorController) { + this.server = server; + this.security = security; + this.errorController = errorController; + } + + @Override + public void customize(IgnoredRequestConfigurer configurer) { List ignored = getIgnored(this.security); if (this.errorController != null) { ignored.add(normalizePath(this.errorController.getErrorPath())); } String[] paths = this.server.getPathsArray(ignored); + List matchers = new ArrayList(); if (!ObjectUtils.isEmpty(paths)) { - builder.ignoring().antMatchers(paths); + for (String pattern : paths) { + matchers.add(new AntPathRequestMatcher(pattern, null)); + } } + if (!matchers.isEmpty()) { + configurer.requestMatchers(new OrRequestMatcher(matchers)); + } + } + + private List getIgnored(SecurityProperties security) { + List ignored = new ArrayList(security.getIgnored()); + if (ignored.isEmpty()) { + ignored.addAll(DEFAULT_IGNORED); + } + else if (ignored.contains("none")) { + ignored.remove("none"); + } + return ignored; } private String normalizePath(String errorPath) { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java index 8c4f534ff3..2ea6929ac0 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java @@ -90,9 +90,9 @@ public class SecurityAutoConfigurationTests { PropertyPlaceholderAutoConfiguration.class); this.context.refresh(); assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull(); - // 5 for static resources and one for the rest + // 1 for static resources and one for the rest assertThat(this.context.getBean(FilterChainProxy.class).getFilterChains()) - .hasSize(6); + .hasSize(2); } @Test diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java index 78bd58c424..95e6f3787d 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java @@ -21,7 +21,6 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.List; import javax.servlet.Filter; @@ -84,13 +83,6 @@ public class SpringBootWebSecurityConfigurationTests { } } - @Test - public void testDefaultIgnores() { - List ignored = SpringBootWebSecurityConfiguration - .getIgnored(new SecurityProperties()); - assertThat(ignored).contains("/css/**"); - } - @Test public void testWebConfigurationOverrideGlobalAuthentication() throws Exception { this.context = SpringApplication.run(TestWebConfiguration.class, diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/sample/hypermedia/gson/SampleHypermediaGsonApplicationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/sample/hypermedia/gson/SampleHypermediaGsonApplicationTests.java index ef4702e13b..40b1680e8f 100644 --- a/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/sample/hypermedia/gson/SampleHypermediaGsonApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/sample/hypermedia/gson/SampleHypermediaGsonApplicationTests.java @@ -35,7 +35,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @RunWith(SpringRunner.class) @SpringBootTest -@TestPropertySource(properties = "endpoints.hypermedia.enabled: true") +@TestPropertySource(properties = {"endpoints.hypermedia.enabled: true", "management.security.enabled: false"}) public class SampleHypermediaGsonApplicationTests { @Autowired 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 93847c0cf9..3dfbeffcff 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 @@ -73,7 +73,7 @@ public class SampleMethodSecurityApplication extends WebMvcConfigurerAdapter { @Override public void init(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("admin").password("admin") - .roles("ADMIN", "USER").and().withUser("user").password("user") + .roles("ADMIN", "USER", "ACTUATOR").and().withUser("user").password("user") .roles("USER"); }