From e08ddbf838a54e589c07e2be36153a3f330f9550 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Sun, 27 Aug 2017 21:19:34 -0700 Subject: [PATCH] Rework security autoconfiguration This commit combines security autoconfigurations for management endpoints and the rest of the application. By default, if Spring Security is on the classpath, it turns on @EnableWebSecurity. In the presence of another WebSecurityConfigurerAdapter this backs off completely. A default AuthenticationManager is also provided with a user and generated password. This can be turned off by specifying a bean of type AuthenticationManager, AuthenticationProvider or UserDetailsService. Closes gh-7958 --- .../ManagementEndpointPathResolver.java | 41 +++ ...anagementWebSecurityAutoConfiguration.java | 326 +---------------- .../ManagementEndpointPathResolverTests.java | 54 +++ ...mentWebSecurityAutoConfigurationTests.java | 272 -------------- .../mvc/MvcEndpointCorsIntegrationTests.java | 11 + .../mvc/MvcEndpointIntegrationTests.java | 33 +- ...rityHealthMvcEndpointIntegrationTests.java | 114 ------ .../h2/H2ConsoleAutoConfiguration.java | 14 +- .../AuthenticationManagerConfiguration.java | 29 +- ...BootGlobalAuthenticationConfiguration.java | 83 ----- .../security/SecurityAutoConfiguration.java | 19 +- .../security/SecurityProperties.java | 275 -------------- .../security/SpringBootSecurity.java | 138 +++++++ .../SpringBootWebSecurityConfiguration.java | 267 +------------- ...a => WebSecurityEnablerConfiguration.java} | 22 +- .../main/resources/META-INF/spring.factories | 1 - ...soleAutoConfigurationIntegrationTests.java | 13 +- .../SecurityAutoConfigurationTests.java | 32 +- ...ConfigurationEarlyInitializationTests.java | 13 +- .../security/SecurityPropertiesTests.java | 62 +--- .../security/SpringBootSecurityTests.java | 170 +++++++++ ...ringBootWebSecurityConfigurationTests.java | 343 ------------------ .../CustomOAuth2SsoConfigurationTests.java | 7 - ...nticationEntryPointConfigurationTests.java | 8 - .../sample/HelloWebSecurityApplication.java | 14 +- .../HelloWebSecurityApplicationTests.java | 2 + spring-boot-samples/pom.xml | 1 + .../pom.xml | 60 +++ .../customsecurity/ExampleController.java | 26 ++ ...mpleActuatorCustomSecurityApplication.java | 29 ++ .../customsecurity/SecurityConfiguration.java | 40 ++ .../src/main/resources/application.properties | 1 + .../resources/static/css/bootstrap.min.css | 11 + .../src/main/resources/templates/error.ftl | 32 ++ .../src/main/resources/templates/home.ftl | 26 ++ .../CorsSampleActuatorApplicationTests.java | 10 +- ...AndPathSampleActuatorApplicationTests.java | 42 +-- ...ctuatorCustomSecurityApplicationTests.java | 75 ++++ .../resources/application-cors.properties | 0 .../SampleActuatorLog4J2Application.java | 11 + .../src/main/resources/application.properties | 3 +- .../SampleActuatorLog4J2ApplicationTests.java | 11 +- .../ui/SampleActuatorUiApplication.java | 11 + .../src/main/resources/application.properties | 4 +- .../SampleActuatorUiApplicationPortTests.java | 7 +- .../ui/SampleActuatorUiApplicationTests.java | 10 +- .../actuator/SampleActuatorApplication.java | 11 + .../src/main/resources/application.properties | 1 + ...pertiesSampleActuatorApplicationTests.java | 6 +- ...agementSampleActuatorApplicationTests.java | 80 ---- ...nsecureSampleActuatorApplicationTests.java | 61 ---- ...gementAddressActuatorApplicationTests.java | 7 +- ...entPathSampleActuatorApplicationTests.java | 6 +- ...AndPathSampleActuatorApplicationTests.java | 11 +- ...entPortSampleActuatorApplicationTests.java | 11 +- ...agementSampleActuatorApplicationTests.java | 6 +- .../SampleActuatorApplicationTests.java | 9 +- ...nsecureSampleActuatorApplicationTests.java | 70 ---- ...letPathSampleActuatorApplicationTests.java | 9 +- ...hutdownSampleActuatorApplicationTests.java | 6 +- .../application-endpoints.properties | 3 +- .../ActuatorSecurityConfiguration.java | 29 ++ ...SampleSecureOAuth2ActuatorApplication.java | 11 + .../src/main/resources/application.properties | 3 +- .../oauth2/AuthenticationConfiguration.java | 17 + .../src/main/resources/application.properties | 2 - .../secure/SampleSecureApplication.java | 11 + .../secure/SampleSecureApplicationTests.java | 20 +- .../src/test/resources/test.properties | 1 - .../servlet/SampleServletApplication.java | 10 + .../SampleServletApplicationTests.java | 16 +- .../SampleMethodSecurityApplication.java | 24 +- .../src/main/resources/application.properties | 3 +- .../SampleMethodSecurityApplicationTests.java | 4 +- .../SampleWebSecureCustomApplication.java | 3 - .../jdbc/SampleWebSecureJdbcApplication.java | 3 - .../secure/SampleWebSecureApplication.java | 14 +- .../src/main/resources/application.properties | 1 - ...rityAutoConfigurationIntegrationTests.java | 6 +- .../security/SecurityTestApplication.java | 11 + .../endpoint/DefaultEndpointPathResolver.java | 33 ++ .../boot/endpoint/EndpointPathResolver.java | 22 +- .../DefaultEndpointPathResolverTests.java | 40 ++ 83 files changed, 1144 insertions(+), 2210 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ManagementEndpointPathResolver.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ManagementEndpointPathResolverTests.java delete mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/web/ManagementWebSecurityAutoConfigurationTests.java delete mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NoSpringSecurityHealthMvcEndpointIntegrationTests.java delete mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/BootGlobalAuthenticationConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootSecurity.java rename spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/{FallbackWebSecurityAutoConfiguration.java => WebSecurityEnablerConfiguration.java} (63%) create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootSecurityTests.java delete mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/pom.xml create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/ExampleController.java create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplication.java create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/application.properties create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/static/css/bootstrap.min.css create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/templates/error.ftl create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/templates/home.ftl rename spring-boot-samples/{spring-boot-sample-actuator/src/test/java/sample/actuator => spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity}/CorsSampleActuatorApplicationTests.java (86%) rename spring-boot-samples/{spring-boot-sample-actuator/src/test/java/sample/actuator => spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity}/InsecureManagementPortAndPathSampleActuatorApplicationTests.java (69%) create mode 100644 spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java rename spring-boot-samples/{spring-boot-sample-actuator => spring-boot-sample-actuator-custom-security}/src/test/resources/application-cors.properties (100%) delete mode 100644 spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureManagementSampleActuatorApplicationTests.java delete mode 100644 spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureSampleActuatorApplicationTests.java delete mode 100644 spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathInsecureSampleActuatorApplicationTests.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/ActuatorSecurityConfiguration.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/secure/oauth2/AuthenticationConfiguration.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/endpoint/DefaultEndpointPathResolver.java rename spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/IgnoredRequestCustomizer.java => spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointPathResolver.java (54%) create mode 100644 spring-boot/src/test/java/org/springframework/boot/endpoint/DefaultEndpointPathResolverTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ManagementEndpointPathResolver.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ManagementEndpointPathResolver.java new file mode 100644 index 0000000000..8356ddef7d --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ManagementEndpointPathResolver.java @@ -0,0 +1,41 @@ +/* + * 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; + +import org.springframework.boot.actuate.autoconfigure.web.ManagementServerProperties; +import org.springframework.boot.endpoint.EndpointPathResolver; + +/** + * {@link EndpointPathResolver} implementation for resolving + * actuator endpoint paths based on the endpoint id and management.context-path. + * + * @author Madhura Bhave + */ +public class ManagementEndpointPathResolver implements EndpointPathResolver { + + private final String contextPath; + + public ManagementEndpointPathResolver(ManagementServerProperties properties) { + this.contextPath = properties.getContextPath(); + } + + @Override + public String resolvePath(String endpointId) { + return this.contextPath + "/" + endpointId; + } +} + diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/security/ManagementWebSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/security/ManagementWebSecurityAutoConfiguration.java index c4e312b8eb..f214879b15 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/security/ManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/security/ManagementWebSecurityAutoConfiguration.java @@ -16,340 +16,32 @@ package org.springframework.boot.actuate.autoconfigure.security; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import javax.annotation.PostConstruct; -import javax.servlet.http.HttpServletRequest; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.endpoint.ManagementContextResolver; +import org.springframework.boot.actuate.autoconfigure.endpoint.ManagementEndpointPathResolver; import org.springframework.boot.actuate.autoconfigure.web.ManagementServerProperties; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionMessage; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; -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.ServerProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.endpoint.EndpointInfo; -import org.springframework.boot.endpoint.web.WebEndpointOperation; -import org.springframework.boot.endpoint.web.mvc.WebEndpointServletHandlerMapping; -import org.springframework.context.ApplicationContext; +import org.springframework.boot.endpoint.EndpointPathResolver; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ConditionContext; -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.builders.HttpSecurity; -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; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -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.StringUtils; /** - * {@link EnableAutoConfiguration Auto-configuration} for security of framework endpoints. - * Many aspects of the behavior can be controller with {@link ManagementServerProperties} - * via externalized application properties (or via an bean definition of that type to set - * the defaults).. + * Security configuration for management endpoints. * - * @author Dave Syer - * @author Andy Wilkinson - * @since 2.0.0 + * @author Madhura Bhave */ @Configuration -@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass({ EnableWebSecurity.class }) -@AutoConfigureAfter(SecurityAutoConfiguration.class) -@AutoConfigureBefore(FallbackWebSecurityAutoConfiguration.class) -@EnableConfigurationProperties(ManagementServerProperties.class) +@AutoConfigureBefore(SecurityAutoConfiguration.class) public class ManagementWebSecurityAutoConfiguration { - private static final String[] NO_PATHS = new String[0]; - - private static final RequestMatcher MATCH_NONE = new NegatedRequestMatcher( - AnyRequestMatcher.INSTANCE); - @Bean - public IgnoredRequestCustomizer managementIgnoredRequestCustomizer( - ManagementServerProperties management, - ObjectProvider contextResolver) { - return new ManagementIgnoredRequestCustomizer(management, - contextResolver.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 - protected static class ManagementSecurityPropertiesConfiguration - implements SecurityPrerequisite { - - private final SecurityProperties securityProperties; - - private final ManagementServerProperties managementServerProperties; - - public ManagementSecurityPropertiesConfiguration( - ObjectProvider securityProperties, - ObjectProvider managementServerProperties) { - this.securityProperties = securityProperties.getIfAvailable(); - this.managementServerProperties = managementServerProperties.getIfAvailable(); - } - - @PostConstruct - public void init() { - if (this.managementServerProperties != null - && this.securityProperties != null) { - this.securityProperties.getUser().getRole() - .addAll(this.managementServerProperties.getSecurity().getRoles()); - } - } - - } - - @Configuration - @ConditionalOnMissingBean(WebSecurityConfiguration.class) - @Conditional(WebSecurityEnablerCondition.class) - @EnableWebSecurity - protected static class WebSecurityEnabler extends AuthenticationManagerConfiguration { - - } - - /** - * WebSecurityEnabler condition. - */ - static class WebSecurityEnablerCondition extends SpringBootCondition { - - @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, - AnnotatedTypeMetadata metadata) { - String managementEnabled = context.getEnvironment() - .getProperty("management.security.enabled", "true"); - String basicEnabled = context.getEnvironment() - .getProperty("security.basic.enabled", "true"); - ConditionMessage.Builder message = ConditionMessage - .forCondition("WebSecurityEnabled"); - if ("true".equalsIgnoreCase(managementEnabled) - && !"true".equalsIgnoreCase(basicEnabled)) { - return ConditionOutcome.match(message.because("security enabled")); - } - return ConditionOutcome.noMatch(message.because("security disabled")); - } - - } - - @Configuration - @ConditionalOnMissingBean({ ManagementWebSecurityConfigurerAdapter.class }) - @ConditionalOnProperty(prefix = "management.security", name = "enabled", matchIfMissing = true) - @EnableConfigurationProperties(SecurityProperties.class) - @Order(ManagementServerProperties.BASIC_AUTH_ORDER) - protected static class ManagementWebSecurityConfigurerAdapter - extends WebSecurityConfigurerAdapter { - - private final SecurityProperties security; - - private final ManagementServerProperties management; - - private final ManagementContextResolver contextResolver; - - public ManagementWebSecurityConfigurerAdapter(SecurityProperties security, - ManagementServerProperties management, - ObjectProvider contextResolver) { - this.security = security; - this.management = management; - this.contextResolver = contextResolver.getIfAvailable(); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - // secure endpoints - RequestMatcher matcher = getRequestMatcher(); - if (matcher != null) { - // Always protect them if present - if (this.security.isRequireSsl()) { - http.requiresChannel().anyRequest().requiresSecure(); - } - AuthenticationEntryPoint entryPoint = entryPoint(); - http.exceptionHandling().authenticationEntryPoint(entryPoint); - // Match all the requests for actuator endpoints ... - http.requestMatcher(matcher).authorizeRequests().anyRequest() - .authenticated(); - http.httpBasic().authenticationEntryPoint(entryPoint).and().cors(); - // No cookies for management endpoints by default - http.csrf().disable(); - http.sessionManagement() - .sessionCreationPolicy(asSpringSecuritySessionCreationPolicy( - this.management.getSecurity().getSessions())); - SpringBootWebSecurityConfiguration.configureHeaders(http.headers(), - this.security.getHeaders()); - } - } - - private SessionCreationPolicy asSpringSecuritySessionCreationPolicy( - Enum value) { - if (value == null) { - return SessionCreationPolicy.STATELESS; - } - return SessionCreationPolicy.valueOf(value.name()); - } - - private RequestMatcher getRequestMatcher() { - if (this.management.getSecurity().isEnabled()) { - return LazyEndpointPathRequestMatcher - .getRequestMatcher(this.contextResolver); - } - return null; - } - - private AuthenticationEntryPoint entryPoint() { - BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint(); - entryPoint.setRealmName(this.security.getBasic().getRealm()); - return entryPoint; - } - - } - - private static class EndpointPaths { - - public String[] getPaths( - WebEndpointServletHandlerMapping endpointHandlerMapping) { - if (endpointHandlerMapping == null) { - return NO_PATHS; - } - Collection> endpoints = endpointHandlerMapping - .getEndpoints(); - Set paths = new LinkedHashSet<>(endpoints.size()); - for (EndpointInfo endpoint : endpoints) { - String path = endpointHandlerMapping.getEndpointPath() + "/" - + endpoint.getId(); - paths.add(path); - paths.add(path + "/**"); - // Add Spring MVC-generated additional paths - paths.add(path + ".*"); - paths.add(path + "/"); - } - return paths.toArray(new String[paths.size()]); - } - - } - - private static class LazyEndpointPathRequestMatcher implements RequestMatcher { - - private final EndpointPaths endpointPaths; - - private final ManagementContextResolver contextResolver; - - private RequestMatcher delegate; - - public static RequestMatcher getRequestMatcher( - ManagementContextResolver contextResolver) { - if (contextResolver == null) { - return null; - } - ManagementServerProperties management = contextResolver - .getApplicationContext().getBean(ManagementServerProperties.class); - ServerProperties server = contextResolver.getApplicationContext() - .getBean(ServerProperties.class); - String path = management.getContextPath(); - if (StringUtils.hasText(path)) { - AntPathRequestMatcher matcher = new AntPathRequestMatcher( - server.getServlet().getPath(path) + "/**"); - return matcher; - } - // Match all endpoint paths - return new LazyEndpointPathRequestMatcher(contextResolver, - new EndpointPaths()); - } - - LazyEndpointPathRequestMatcher(ManagementContextResolver contextResolver, - EndpointPaths endpointPaths) { - this.contextResolver = contextResolver; - this.endpointPaths = endpointPaths; - } - - @Override - public boolean matches(HttpServletRequest request) { - if (this.delegate == null) { - this.delegate = createDelegate(); - } - return this.delegate.matches(request); - } - - private RequestMatcher createDelegate() { - ServerProperties server = this.contextResolver.getApplicationContext() - .getBean(ServerProperties.class); - List matchers = new ArrayList<>(); - WebEndpointServletHandlerMapping endpointHandlerMapping = getRequiredEndpointHandlerMapping(); - for (String path : this.endpointPaths.getPaths(endpointHandlerMapping)) { - matchers.add( - new AntPathRequestMatcher(server.getServlet().getPath(path))); - } - return (matchers.isEmpty() ? MATCH_NONE : new OrRequestMatcher(matchers)); - } - - private WebEndpointServletHandlerMapping getRequiredEndpointHandlerMapping() { - WebEndpointServletHandlerMapping endpointHandlerMapping = null; - ApplicationContext context = this.contextResolver.getApplicationContext(); - if (context.getBeanNamesForType( - WebEndpointServletHandlerMapping.class).length > 0) { - endpointHandlerMapping = context - .getBean(WebEndpointServletHandlerMapping.class); - } - if (endpointHandlerMapping == null) { - // Maybe there are actually no endpoints (e.g. management.port=-1) - endpointHandlerMapping = new WebEndpointServletHandlerMapping("", - Collections.emptySet()); - } - return endpointHandlerMapping; - } - + public EndpointPathResolver managementEndpointPathResolver(ManagementServerProperties properties) { + return new ManagementEndpointPathResolver(properties); } } + diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ManagementEndpointPathResolverTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ManagementEndpointPathResolverTests.java new file mode 100644 index 0000000000..3d8213c039 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ManagementEndpointPathResolverTests.java @@ -0,0 +1,54 @@ +/* + * 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; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.actuate.autoconfigure.web.ManagementServerProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ManagementEndpointPathResolver}. + * + * @author Madhura Bhave + */ +public class ManagementEndpointPathResolverTests { + + private ManagementEndpointPathResolver resolver; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + ManagementServerProperties properties = new ManagementServerProperties(); + properties.setContextPath("/test"); + this.resolver = new ManagementEndpointPathResolver(properties); + + } + + @Test + public void resolveShouldReturnPathBasedOnContextPath() throws Exception { + String path = this.resolver.resolvePath("my-id"); + assertThat(path.equals("/test/my-id")); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/web/ManagementWebSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/web/ManagementWebSecurityAutoConfigurationTests.java deleted file mode 100644 index fe1f46169a..0000000000 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/web/ManagementWebSecurityAutoConfigurationTests.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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.web; - -import java.util.ArrayList; - -import javax.servlet.Filter; - -import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.infrastructure.ServletEndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.security.ManagementWebSecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.mock.web.MockServletContext; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.ProviderManager; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.FilterChainProxy; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultMatcher; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.util.StringUtils; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ManagementWebSecurityAutoConfiguration}. - * - * @author Dave Syer - * @author Andy Wilkinson - */ -public class ManagementWebSecurityAutoConfigurationTests { - - private AnnotationConfigWebApplicationContext context; - - @After - public void close() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - public void testWebConfiguration() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(SecurityAutoConfiguration.class, - WebMvcAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class, - JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, - EndpointAutoConfiguration.class, ServletEndpointAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class); - TestPropertyValues.of("security.basic.enabled:false").applyTo(this.context); - this.context.refresh(); - assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull(); - FilterChainProxy filterChainProxy = this.context.getBean(FilterChainProxy.class); - // 1 for static resources, one for management endpoints and one for the rest - assertThat(filterChainProxy.getFilterChains()).hasSize(3); - assertThat(filterChainProxy.getFilters("/application/beans")).isNotEmpty(); - assertThat(filterChainProxy.getFilters("/application/beans/")).isNotEmpty(); - assertThat(filterChainProxy.getFilters("/application/beans.foo")).isNotEmpty(); - assertThat(filterChainProxy.getFilters("/application/beans/foo/bar")) - .isNotEmpty(); - } - - @Test - public void testPathNormalization() throws Exception { - String path = "admin/./error"; - assertThat(StringUtils.cleanPath(path)).isEqualTo("admin/error"); - } - - @Test - public void testWebConfigurationWithExtraRole() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(WebConfiguration.class); - this.context.refresh(); - UserDetails user = getUser(); - ArrayList authorities = new ArrayList<>(user.getAuthorities()); - assertThat(authorities).containsAll(AuthorityUtils - .commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_ACTUATOR")); - } - - private UserDetails getUser() { - ProviderManager parent = (ProviderManager) this.context - .getBean(AuthenticationManager.class); - DaoAuthenticationProvider provider = (DaoAuthenticationProvider) parent - .getProviders().get(0); - UserDetailsService service = (UserDetailsService) ReflectionTestUtils - .getField(provider, "userDetailsService"); - UserDetails user = service.loadUserByUsername("user"); - return user; - } - - @Test - public void testDisableIgnoredStaticApplicationPaths() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(SecurityAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class, - EndpointAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); - TestPropertyValues.of("security.ignored:none").applyTo(this.context); - this.context.refresh(); - // Just the application and management endpoints now - assertThat(this.context.getBean(FilterChainProxy.class).getFilterChains()) - .hasSize(2); - } - - @Test - public void testDisableBasicAuthOnApplicationPaths() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(WebConfiguration.class); - TestPropertyValues.of("security.basic.enabled:false").applyTo(this.context); - this.context.refresh(); - // Just the management endpoints (one filter) and ignores now plus the backup - // filter on app endpoints - assertThat(this.context.getBean(FilterChainProxy.class).getFilterChains()) - .hasSize(3); - } - - @Test - public void testOverrideAuthenticationManager() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(TestConfiguration.class, SecurityAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class, - EndpointAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBean(AuthenticationManager.class)).isEqualTo( - this.context.getBean(TestConfiguration.class).authenticationManager); - } - - @Test - public void testSecurityPropertiesNotAvailable() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(TestConfiguration.class, SecurityAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class, - EndpointAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBean(AuthenticationManager.class)).isEqualTo( - this.context.getBean(TestConfiguration.class).authenticationManager); - } - - // gh-2466 - @Test - public void realmSameForManagement() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(AuthenticationConfig.class, SecurityAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class, - JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, - EndpointAutoConfiguration.class, ServletEndpointAutoConfiguration.class, - WebMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, - AuditAutoConfiguration.class); - this.context.refresh(); - - Filter filter = this.context.getBean("springSecurityFilterChain", Filter.class); - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .addFilters(filter).build(); - - // no user (Main) - mockMvc.perform(MockMvcRequestBuilders.get("/home")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(springAuthenticateRealmHeader()); - - // invalid user (Main) - mockMvc.perform( - MockMvcRequestBuilders.get("/home").header("authorization", "Basic xxx")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(springAuthenticateRealmHeader()); - - // no user (Management) - mockMvc.perform(MockMvcRequestBuilders.get("/beans")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(springAuthenticateRealmHeader()); - - // invalid user (Management) - mockMvc.perform( - MockMvcRequestBuilders.get("/beans").header("authorization", "Basic xxx")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(springAuthenticateRealmHeader()); - } - - private ResultMatcher springAuthenticateRealmHeader() { - return MockMvcResultMatchers.header().string("www-authenticate", - Matchers.containsString("realm=\"Spring\"")); - } - - @Configuration - @ImportAutoConfiguration({ SecurityAutoConfiguration.class, - WebMvcAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - EndpointAutoConfiguration.class, ServletEndpointAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class, - FallbackWebSecurityAutoConfiguration.class }) - static class WebConfiguration { - - } - - @EnableGlobalAuthentication - @Configuration - static class AuthenticationConfig { - - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication().withUser("user").password("password") - .roles("USER"); - } - - } - - @Configuration - protected static class TestConfiguration { - - private AuthenticationManager authenticationManager; - - @Bean - public AuthenticationManager myAuthenticationManager() { - this.authenticationManager = ( - authentication) -> new TestingAuthenticationToken("foo", "bar"); - return this.authenticationManager; - } - - } - -} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java index a4ad66e6e5..03361f4d4e 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java @@ -35,6 +35,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockServletContext; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -194,4 +196,13 @@ public class MvcEndpointCorsIntegrationTests { } + @Configuration + static class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.cors(); + } + } + } 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 4e4aad1fe0..ceb5db9b7a 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 @@ -24,7 +24,6 @@ import org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfigurati import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.infrastructure.EndpointInfrastructureAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.infrastructure.ServletEndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.security.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration; @@ -36,6 +35,7 @@ import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoC import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockServletContext; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.test.context.TestSecurityContextHolder; @@ -69,7 +69,8 @@ public class MvcEndpointIntegrationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(SecureConfiguration.class); MockMvc mockMvc = createSecureMockMvc(); - mockMvc.perform(get("/application/beans")).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/application/beans") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized()); } @Test @@ -79,7 +80,8 @@ public class MvcEndpointIntegrationTests { TestPropertyValues.of("management.context-path:/management") .applyTo(this.context); MockMvc mockMvc = createSecureMockMvc(); - mockMvc.perform(get("/management/beans")).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/management/beans") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized()); } @Test @@ -89,31 +91,13 @@ public class MvcEndpointIntegrationTests { new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR")); this.context = new AnnotationConfigWebApplicationContext(); this.context.register(SecureConfiguration.class); - TestPropertyValues.of("management.context-path:/management") - .applyTo(this.context); - MockMvc mockMvc = createSecureMockMvc(); - mockMvc.perform(get("/management/beans")).andExpect(status().isOk()); - } - - @Test - public void endpointSecurityCanBeDisabledWithCustomContextPath() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(SecureConfiguration.class); TestPropertyValues.of("management.context-path:/management", - "management.security.enabled:false").applyTo(this.context); + "endpoints.all.web.enabled=true") + .applyTo(this.context); MockMvc mockMvc = createSecureMockMvc(); mockMvc.perform(get("/management/beans")).andExpect(status().isOk()); } - @Test - public void endpointSecurityCanBeDisabled() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(SecureConfiguration.class); - TestPropertyValues.of("management.security.enabled:false").applyTo(this.context); - MockMvc mockMvc = createSecureMockMvc(); - mockMvc.perform(get("/application/beans")).andExpect(status().isOk()); - } - private MockMvc createSecureMockMvc() { return doCreateMockMvc(springSecurity()); } @@ -153,8 +137,7 @@ public class MvcEndpointIntegrationTests { } @Import(DefaultConfiguration.class) - @ImportAutoConfiguration({ SecurityAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class }) + @ImportAutoConfiguration({ SecurityAutoConfiguration.class}) static class SecureConfiguration { } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NoSpringSecurityHealthMvcEndpointIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NoSpringSecurityHealthMvcEndpointIntegrationTests.java deleted file mode 100644 index 150f727275..0000000000 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NoSpringSecurityHealthMvcEndpointIntegrationTests.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.endpoint.mvc; - -import java.util.Map; - -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.boot.actuate.autoconfigure.ManagementContextAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.infrastructure.EndpointInfrastructureAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.infrastructure.ServletEndpointAutoConfiguration; -import org.springframework.boot.actuate.endpoint.HealthEndpoint; -import org.springframework.boot.actuate.endpoint.web.HealthWebEndpointExtension; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.actuate.health.HealthIndicatorFactory; -import org.springframework.boot.actuate.health.HealthStatusHttpMapper; -import org.springframework.boot.actuate.health.OrderedHealthAggregator; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.boot.testsupport.runner.classpath.ClassPathExclusions; -import org.springframework.boot.testsupport.runner.classpath.ModifiedClassPathRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.mock.web.MockServletContext; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Integration tests for the health endpoint when Spring Security is not available. - * - * @author Andy Wilkinson - * @author Madhura Bhave - */ -@RunWith(ModifiedClassPathRunner.class) -@ClassPathExclusions("spring-security-*.jar") -public class NoSpringSecurityHealthMvcEndpointIntegrationTests { - - private AnnotationConfigWebApplicationContext context; - - @After - public void closeContext() { - this.context.close(); - } - - @Test - public void healthDetailPresent() throws Exception { - this.context = new AnnotationConfigWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(TestConfiguration.class); - TestPropertyValues.of("management.security.enabled:false").applyTo(this.context); - this.context.refresh(); - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); - mockMvc.perform(get("/application/health")).andExpect(status().isOk()) - .andExpect(content().string(containsString( - "\"status\":\"UP\",\"test\":{\"status\":\"UP\",\"hello\":\"world\"}"))); - } - - @ImportAutoConfiguration({ JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, - EndpointInfrastructureAutoConfiguration.class, - ManagementContextAutoConfiguration.class, - ServletEndpointAutoConfiguration.class }) - @Configuration - static class TestConfiguration { - - @Bean - public HealthIndicator testHealthIndicator() { - return () -> Health.up().withDetail("hello", "world").build(); - } - - @Bean - public HealthEndpoint healthEndpoint( - Map healthIndicators) { - return new HealthEndpoint(new HealthIndicatorFactory().createHealthIndicator( - new OrderedHealthAggregator(), healthIndicators)); - } - - @Bean - public HealthWebEndpointExtension healthWebEndpointExtension( - HealthEndpoint delegate) { - return new HealthWebEndpointExtension(delegate, new HealthStatusHttpMapper()); - } - - } - -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java index 4a3cb9dd92..2c91a95faa 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java @@ -26,7 +26,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; -import org.springframework.boot.autoconfigure.security.SecurityAuthorizeMode; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -79,7 +78,6 @@ public class H2ConsoleAutoConfiguration { @Configuration @ConditionalOnClass(WebSecurityConfigurerAdapter.class) @ConditionalOnBean(ObjectPostProcessor.class) - @EnableConfigurationProperties(SecurityProperties.class) @ConditionalOnProperty(prefix = "security.basic", name = "enabled", matchIfMissing = true) static class H2ConsoleSecurityConfiguration { @@ -95,9 +93,6 @@ public class H2ConsoleAutoConfiguration { @Autowired private H2ConsoleProperties console; - @Autowired - private SecurityProperties security; - @Override public void configure(HttpSecurity http) throws Exception { String path = this.console.getPath(); @@ -106,14 +101,7 @@ public class H2ConsoleAutoConfiguration { h2Console.csrf().disable(); h2Console.httpBasic(); h2Console.headers().frameOptions().sameOrigin(); - String[] roles = this.security.getUser().getRole().toArray(new String[0]); - SecurityAuthorizeMode mode = this.security.getBasic().getAuthorizeMode(); - if (mode == null || mode == SecurityAuthorizeMode.ROLE) { - http.authorizeRequests().anyRequest().hasAnyRole(roles); - } - else if (mode == SecurityAuthorizeMode.AUTHENTICATED) { - http.authorizeRequests().anyRequest().authenticated(); - } + http.authorizeRequests().anyRequest().authenticated(); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java index a718d695ff..c11e1c2e0a 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java @@ -17,9 +17,8 @@ package org.springframework.boot.autoconfigure.security; import java.lang.reflect.Field; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; +import java.util.UUID; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -29,7 +28,6 @@ import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.security.SecurityProperties.User; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; @@ -47,13 +45,13 @@ import org.springframework.security.config.annotation.authentication.builders.Au import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.util.ReflectionUtils; /** * Configuration for a Spring Security in-memory {@link AuthenticationManager}. Can be - * disabled by providing a bean of type AuthenticationManager, or by autowiring an - * {@link AuthenticationManagerBuilder} into a method in one of your configuration - * classes. The value provided by this configuration will become the "global" + * disabled by providing a bean of type {@link AuthenticationManager}, {@link AuthenticationProvider} + * or {@link UserDetailsService}. The value provided by this configuration will become the "global" * authentication manager (from Spring Security), or the parent of the global instance. * Thus it acts as a fallback when no others are provided, is used by method security if * enabled, and as a parent authentication manager for "local" authentication managers in @@ -61,10 +59,12 @@ import org.springframework.util.ReflectionUtils; * * @author Dave Syer * @author Rob Winch + * @author Madhura Bhave */ @Configuration @ConditionalOnBean(ObjectPostProcessor.class) -@ConditionalOnMissingBean({ AuthenticationManager.class }) +@ConditionalOnMissingBean({ AuthenticationManager.class, + AuthenticationProvider.class, UserDetailsService.class}) @Order(0) public class AuthenticationManagerConfiguration { @@ -102,7 +102,7 @@ public class AuthenticationManagerConfiguration { * {@link GlobalAuthenticationConfigurerAdapter#init(AuthenticationManagerBuilder)} * exists that adds a {@link SecurityConfigurer} to the * {@link AuthenticationManagerBuilder}. - *
  • {@link AuthenticationManagerConfiguration#init(AuthenticationManagerBuilder)} + *
  • {@link AuthenticationManagerConfiguration} * adds {@link SpringBootAuthenticationConfigurerAdapter} so it is after the * {@link SecurityConfigurer} in the first step.
  • *
  • We then can default an {@link AuthenticationProvider} if necessary. Note we can @@ -168,14 +168,11 @@ public class AuthenticationManagerConfiguration { if (auth.isConfigured()) { return; } - User user = this.securityProperties.getUser(); - if (user.isDefaultPassword()) { - logger.info(String.format("%n%nUsing default security password: %s%n", - user.getPassword())); - } - Set roles = new LinkedHashSet<>(user.getRole()); - withUser(user.getName()).password(user.getPassword()) - .roles(roles.toArray(new String[roles.size()])); + String password = UUID.randomUUID().toString(); + logger.info(String.format("%n%nUsing default security password: %s%n", + password)); + withUser("user").password(password) + .roles(); setField(auth, "defaultUserDetailsService", getUserDetailsService()); super.configure(auth); } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/BootGlobalAuthenticationConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/BootGlobalAuthenticationConfiguration.java deleted file mode 100644 index e41a20885a..0000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/BootGlobalAuthenticationConfiguration.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2015 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.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter; - -/** - * {@link GlobalAuthenticationConfigurerAdapter} to trigger early initialization of - * {@code @EnableAutoConfiguration} beans. This configuration is imported from - * {@link AuthenticationConfiguration} to ensure that users are able to configure the - * {@link AuthenticationManagerBuilder} from their {@code @EnableAutoConfiguration} or - * {@code @SpringBootApplication} configuration class: - * - *
    - * @Autowired
    - * public void configureGlobal(AuthenticationManagerBuilder auth) {
    - *     ...
    - * }
    - * 
    - * - * @author Rob Winch - * @since 1.1.11 - */ -@Configuration -@ConditionalOnClass(GlobalAuthenticationConfigurerAdapter.class) -public class BootGlobalAuthenticationConfiguration { - - @Bean - public static BootGlobalAuthenticationConfigurationAdapter bootGlobalAuthenticationConfigurationAdapter( - ApplicationContext context) { - return new BootGlobalAuthenticationConfigurationAdapter(context); - } - - private static class BootGlobalAuthenticationConfigurationAdapter - extends GlobalAuthenticationConfigurerAdapter { - - private static final Log logger = LogFactory - .getLog(BootGlobalAuthenticationConfiguration.class); - - private final ApplicationContext context; - - BootGlobalAuthenticationConfigurationAdapter(ApplicationContext context) { - this.context = context; - } - - @Override - public void init(AuthenticationManagerBuilder auth) { - Map beansWithAnnotation = this.context - .getBeansWithAnnotation(EnableAutoConfiguration.class); - if (logger.isDebugEnabled()) { - logger.debug("Eagerly initializing " + beansWithAnnotation); - } - } - - } - -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java index 95a2d4d244..ae1c4002e2 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java @@ -16,10 +16,14 @@ package org.springframework.boot.autoconfigure.security; +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; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorController; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.endpoint.DefaultEndpointPathResolver; +import org.springframework.boot.endpoint.EndpointPathResolver; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -49,10 +53,23 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur GlobalAuthenticationConfigurerAdapter.class }) @EnableConfigurationProperties(SecurityProperties.class) @Import({ SpringBootWebSecurityConfiguration.class, + WebSecurityEnablerConfiguration.class, AuthenticationManagerConfiguration.class, - BootGlobalAuthenticationConfiguration.class, SecurityDataConfiguration.class }) + SecurityDataConfiguration.class }) public class SecurityAutoConfiguration { + @Bean + @ConditionalOnMissingBean + public EndpointPathResolver endpointPathResolver() { + return new DefaultEndpointPathResolver(); + } + + @Bean + public SpringBootSecurity springBootSecurity(EndpointPathResolver endpointPathResolver, + ObjectProvider errorController) { + return new SpringBootSecurity(endpointPathResolver, errorController.getIfAvailable()); + } + @Bean @ConditionalOnMissingBean(AuthenticationEventPublisher.class) public DefaultAuthenticationEventPublisher authenticationEventPublisher( diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java index 7deecdb97a..f6cbc2941c 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java @@ -16,20 +16,14 @@ package org.springframework.boot.autoconfigure.security; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Set; -import java.util.UUID; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.DispatcherType; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.core.Ordered; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.util.StringUtils; /** * Properties for the security aspects of an application. @@ -70,33 +64,8 @@ public class SecurityProperties implements SecurityPrerequisite { public static final int DEFAULT_FILTER_ORDER = FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100; - /** - * Enable secure channel for all requests. - */ - private boolean requireSsl; - - /** - * Enable Cross Site Request Forgery support. - */ - // Flip this when session creation is disabled by default - private boolean enableCsrf = false; - private Basic basic = new Basic(); - private final Headers headers = new Headers(); - - /** - * Session creation policy (always, never, if_required, stateless). - */ - private SessionCreationPolicy sessions = SessionCreationPolicy.STATELESS; - - /** - * Comma-separated list of paths to exclude from the default secured paths. - */ - private List ignored = new ArrayList<>(); - - private final User user = new User(); - /** * Security filter chain order. */ @@ -108,22 +77,6 @@ public class SecurityProperties implements SecurityPrerequisite { private Set filterDispatcherTypes = new HashSet<>(Arrays.asList( DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST)); - public Headers getHeaders() { - return this.headers; - } - - public User getUser() { - return this.user; - } - - public SessionCreationPolicy getSessions() { - return this.sessions; - } - - public void setSessions(SessionCreationPolicy sessions) { - this.sessions = sessions; - } - public Basic getBasic() { return this.basic; } @@ -132,30 +85,6 @@ public class SecurityProperties implements SecurityPrerequisite { this.basic = basic; } - public boolean isRequireSsl() { - return this.requireSsl; - } - - public void setRequireSsl(boolean requireSsl) { - this.requireSsl = requireSsl; - } - - public boolean isEnableCsrf() { - return this.enableCsrf; - } - - public void setEnableCsrf(boolean enableCsrf) { - this.enableCsrf = enableCsrf; - } - - public void setIgnored(List ignored) { - this.ignored = new ArrayList<>(ignored); - } - - public List getIgnored() { - return this.ignored; - } - public int getFilterOrder() { return this.filterOrder; } @@ -172,122 +101,6 @@ public class SecurityProperties implements SecurityPrerequisite { this.filterDispatcherTypes = filterDispatcherTypes; } - public static class Headers { - - public enum HSTS { - - NONE, DOMAIN, ALL - - } - - public enum ContentSecurityPolicyMode { - - /** - * Use the 'Content-Security-Policy' header. - */ - DEFAULT, - - /** - * Use the 'Content-Security-Policy-Report-Only' header. - */ - REPORT_ONLY - - } - - /** - * Enable cross site scripting (XSS) protection. - */ - private boolean xss = true; - - /** - * Enable cache control HTTP headers. - */ - private boolean cache = true; - - /** - * Enable "X-Frame-Options" header. - */ - private boolean frame = true; - - /** - * Enable "X-Content-Type-Options" header. - */ - private boolean contentType = true; - - /** - * Value for content security policy header. - */ - private String contentSecurityPolicy; - - /** - * Content security policy mode. - */ - private ContentSecurityPolicyMode contentSecurityPolicyMode = ContentSecurityPolicyMode.DEFAULT; - - /** - * HTTP Strict Transport Security (HSTS) mode (none, domain, all). - */ - private HSTS hsts = HSTS.ALL; - - public boolean isXss() { - return this.xss; - } - - public void setXss(boolean xss) { - this.xss = xss; - } - - public boolean isCache() { - return this.cache; - } - - public void setCache(boolean cache) { - this.cache = cache; - } - - public boolean isFrame() { - return this.frame; - } - - public void setFrame(boolean frame) { - this.frame = frame; - } - - public boolean isContentType() { - return this.contentType; - } - - public void setContentType(boolean contentType) { - this.contentType = contentType; - } - - public String getContentSecurityPolicy() { - return this.contentSecurityPolicy; - } - - public void setContentSecurityPolicy(String contentSecurityPolicy) { - this.contentSecurityPolicy = contentSecurityPolicy; - } - - public ContentSecurityPolicyMode getContentSecurityPolicyMode() { - return this.contentSecurityPolicyMode; - } - - public void setContentSecurityPolicyMode( - ContentSecurityPolicyMode contentSecurityPolicyMode) { - this.contentSecurityPolicyMode = contentSecurityPolicyMode; - } - - public HSTS getHsts() { - return this.hsts; - } - - public void setHsts(HSTS hsts) { - this.hsts = hsts; - } - - } - public static class Basic { /** @@ -295,16 +108,6 @@ public class SecurityProperties implements SecurityPrerequisite { */ private boolean enabled = true; - /** - * HTTP basic realm name. - */ - private String realm = "Spring"; - - /** - * Comma-separated list of paths to secure. - */ - private String[] path = new String[] { "/**" }; - /** * Security authorize mode to apply. */ @@ -318,84 +121,6 @@ public class SecurityProperties implements SecurityPrerequisite { this.enabled = enabled; } - public String getRealm() { - return this.realm; - } - - public void setRealm(String realm) { - this.realm = realm; - } - - public String[] getPath() { - return this.path; - } - - public void setPath(String... paths) { - this.path = paths; - } - - public SecurityAuthorizeMode getAuthorizeMode() { - return this.authorizeMode; - } - - public void setAuthorizeMode(SecurityAuthorizeMode authorizeMode) { - this.authorizeMode = authorizeMode; - } - - } - - public static class User { - - /** - * Default user name. - */ - private String name = "user"; - - /** - * Password for the default user name. - */ - private String password = UUID.randomUUID().toString(); - - /** - * Granted roles for the default user name. - */ - private List role = new ArrayList<>(Collections.singletonList("USER")); - - private boolean defaultPassword = true; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - if (password.startsWith("${") && password.endsWith("}") - || !StringUtils.hasLength(password)) { - return; - } - this.defaultPassword = false; - this.password = password; - } - - public List getRole() { - return this.role; - } - - public void setRole(List role) { - this.role = new ArrayList<>(role); - } - - public boolean isDefaultPassword() { - return this.defaultPassword; - } - } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootSecurity.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootSecurity.java new file mode 100644 index 0000000000..16da7a6c80 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootSecurity.java @@ -0,0 +1,138 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorController; +import org.springframework.boot.endpoint.Endpoint; +import org.springframework.boot.endpoint.EndpointPathResolver; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +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; +import org.springframework.util.StringUtils; + + +/** + * Provides request matchers that can be used to + * configure security for static resources and the error controller path in a custom + * {@link WebSecurityConfigurerAdapter}. + * + * @author Madhura Bhave + */ +public final class SpringBootSecurity { + + /** + * Used as a wildcard matcher for all endpoints. + */ + public final static String ALL_ENDPOINTS = "**"; + + private static String[] STATIC_RESOURCES = new String[]{"/css/**", "/js/**", + "/images/**", "/webjars/**", "/**/favicon.ico"}; + + private final EndpointPathResolver endpointPathResolver; + + private final ErrorController errorController; + + SpringBootSecurity(EndpointPathResolver endpointPathResolver, ErrorController errorController) { + this.endpointPathResolver = endpointPathResolver; + this.errorController = errorController; + } + + /** + * Returns a {@link RequestMatcher} that matches on all endpoint paths with given ids. + * @param ids the endpoint ids + * @return the request matcher + */ + public RequestMatcher endpointIds(String... ids) { + Assert.notEmpty(ids, "At least one endpoint id must be specified."); + List pathList = Arrays.asList(ids); + if (pathList.contains(ALL_ENDPOINTS)) { + return new AntPathRequestMatcher(this.endpointPathResolver.resolvePath(ALL_ENDPOINTS), null); + } + return getEndpointsRequestMatcher(pathList); + } + + /** + * Returns a {@link RequestMatcher} that matches on all endpoint paths for the given + * classes with the {@link Endpoint} annotation. + * @param endpoints the endpoint classes + * @return the request matcher + */ + public RequestMatcher endpoints(Class... endpoints) { + Assert.notEmpty(endpoints, "At least one endpoint must be specified."); + List paths = Arrays.stream(endpoints).map(e -> { + if (e.isAnnotationPresent(Endpoint.class)) { + return e.getAnnotation(Endpoint.class).id(); + } + throw new IllegalArgumentException("Only classes annotated with @Endpoint are supported."); + }).collect(Collectors.toList()); + return getEndpointsRequestMatcher(paths); + } + + /** + * Returns a {@link RequestMatcher} that matches on all static resources. + * @return the request matcher + */ + public RequestMatcher staticResources() { + return getRequestMatcher(STATIC_RESOURCES); + } + + /** + * Returns a {@link RequestMatcher} that matches on the {@link ErrorController} path, + * if present. + * @return the request matcher + */ + public RequestMatcher error() { + if (this.errorController == null) { + throw new IllegalStateException("Path for error controller could not be determined."); + } + String path = normalizePath(this.errorController.getErrorPath()); + return new AntPathRequestMatcher(path + "/**", null); + } + + private RequestMatcher getEndpointsRequestMatcher(List ids) { + List matchers = new ArrayList<>(); + for (String id : ids) { + String path = this.endpointPathResolver.resolvePath(id); + matchers.add(new AntPathRequestMatcher(path + "/**", null)); + } + return new OrRequestMatcher(matchers); + } + + private static RequestMatcher getRequestMatcher(String... paths) { + List matchers = new ArrayList<>(); + for (String path : paths) { + matchers.add(new AntPathRequestMatcher(path, null)); + } + return new OrRequestMatcher(matchers); + } + + private String normalizePath(String errorPath) { + String result = StringUtils.cleanPath(errorPath); + if (!result.startsWith("/")) { + result = "/" + result; + } + return result; + } +} + 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 fce4bd0804..5fc3cd7a0a 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 @@ -16,284 +16,41 @@ package org.springframework.boot.autoconfigure.security; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -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; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; -import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers; -import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers.ContentSecurityPolicyMode; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.servlet.error.ErrorController; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -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; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -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; /** - * Configuration for security of a web application or service. By default everything is - * secured with HTTP Basic authentication except the - * {@link SecurityProperties#getIgnored() explicitly ignored} paths (defaults to - * /css/**, /js/**, /images/**, /**/favicon.ico - * ). Many aspects of the behavior can be controller with {@link SecurityProperties} via - * externalized application properties (or via an bean definition of that type to set the - * defaults). The user details for authentication are just placeholders - * {@code (username=user, password=password)} but can easily be customized by providing a - * an {@link AuthenticationManager}. Also provides audit logging of authentication events. - *

    - * Some common simple customizations: - *

      - *
    • Switch off security completely and permanently: remove Spring Security from the - * classpath or {@link EnableAutoConfiguration#exclude() exclude} - * {@link SecurityAutoConfiguration}.
    • - *
    • Switch off security temporarily (e.g. for a dev environment): set - * {@code security.basic.enabled=false}
    • - *
    • Customize the user details: autowire an {@link AuthenticationManagerBuilder} into a - * method in one of your configuration classes or equivalently add a bean of type - * AuthenticationManager
    • - *
    • Add form login for user facing resources: add a - * {@link WebSecurityConfigurerAdapter} and use {@link HttpSecurity#formLogin()}
    • - *
    + * The default configuration for web security. It relies on Spring Security's + * content-negotiation strategy to determine what sort of authentication to use. + * If the user specifies their own {@link WebSecurityConfigurerAdapter}, this will back-off + * completely and the users should specify all the bits that they want to configure as part + * of the custom security configuration. * - * @author Dave Syer - * @author Andy Wilkinson + * @author Madhura Bhave */ -@Configuration -@EnableConfigurationProperties(ServerProperties.class) -@ConditionalOnClass({ EnableWebSecurity.class, AuthenticationEntryPoint.class }) -@ConditionalOnMissingBean(WebSecurityConfiguration.class) +@ConditionalOnProperty(prefix = "security.basic", name = "enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnClass(EnableWebSecurity.class) +@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) @ConditionalOnWebApplication(type = Type.SERVLET) -@EnableWebSecurity public class SpringBootWebSecurityConfiguration { - private static List DEFAULT_IGNORED = Arrays.asList("/css/**", "/js/**", - "/images/**", "/webjars/**", "/**/favicon.ico"); - - @Bean - @ConditionalOnMissingBean({ IgnoredPathsWebSecurityConfigurerAdapter.class }) - 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, - SecurityProperties.Headers headers) throws Exception { - if (headers.getHsts() != Headers.HSTS.NONE) { - boolean includeSubDomains = headers.getHsts() == Headers.HSTS.ALL; - HstsHeaderWriter writer = new HstsHeaderWriter(includeSubDomains); - writer.setRequestMatcher(AnyRequestMatcher.INSTANCE); - configurer.addHeaderWriter(writer); - } - if (!headers.isContentType()) { - configurer.contentTypeOptions().disable(); - } - if (StringUtils.hasText(headers.getContentSecurityPolicy())) { - String policyDirectives = headers.getContentSecurityPolicy(); - ContentSecurityPolicyMode mode = headers.getContentSecurityPolicyMode(); - if (mode == ContentSecurityPolicyMode.DEFAULT) { - configurer.contentSecurityPolicy(policyDirectives); - } - else { - configurer.contentSecurityPolicy(policyDirectives).reportOnly(); - } - } - if (!headers.isXss()) { - configurer.xssProtection().disable(); - } - if (!headers.isCache()) { - configurer.cacheControl().disable(); - } - if (!headers.isFrame()) { - configurer.frameOptions().disable(); - } - } - - // Get the ignored paths in early - @Order(SecurityProperties.IGNORED_ORDER) - private static class IgnoredPathsWebSecurityConfigurerAdapter - implements WebSecurityConfigurer { - - private final List customizers; - - IgnoredPathsWebSecurityConfigurerAdapter( - List customizers) { - this.customizers = customizers; - } - - @Override - public void configure(WebSecurity builder) throws Exception { - } - - @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.getServlet().getPathsArray(ignored); - List matchers = new ArrayList<>(); - if (!ObjectUtils.isEmpty(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) { - String result = StringUtils.cleanPath(errorPath); - if (!result.startsWith("/")) { - result = "/" + result; - } - return result; - } - - } - @Configuration - @ConditionalOnProperty(prefix = "security.basic", name = "enabled", havingValue = "false") @Order(SecurityProperties.BASIC_AUTH_ORDER) - protected static class ApplicationNoWebSecurityConfigurerAdapter - extends WebSecurityConfigurerAdapter { + static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { - http.requestMatcher((request) -> false); + super.configure(http); + http.csrf().disable(); } - - } - - @Configuration - @ConditionalOnProperty(prefix = "security.basic", name = "enabled", matchIfMissing = true) - @Order(SecurityProperties.BASIC_AUTH_ORDER) - protected static class ApplicationWebSecurityConfigurerAdapter - extends WebSecurityConfigurerAdapter { - - private SecurityProperties security; - - protected ApplicationWebSecurityConfigurerAdapter(SecurityProperties security) { - this.security = security; - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - if (this.security.isRequireSsl()) { - http.requiresChannel().anyRequest().requiresSecure(); - } - if (!this.security.isEnableCsrf()) { - http.csrf().disable(); - } - // No cookies for application endpoints by default - http.sessionManagement().sessionCreationPolicy(this.security.getSessions()); - SpringBootWebSecurityConfiguration.configureHeaders(http.headers(), - this.security.getHeaders()); - String[] paths = getSecureApplicationPaths(); - if (paths.length > 0) { - AuthenticationEntryPoint entryPoint = entryPoint(); - http.exceptionHandling().authenticationEntryPoint(entryPoint); - http.httpBasic().authenticationEntryPoint(entryPoint); - http.requestMatchers().antMatchers(paths); - String[] roles = this.security.getUser().getRole().toArray(new String[0]); - SecurityAuthorizeMode mode = this.security.getBasic().getAuthorizeMode(); - if (mode == null || mode == SecurityAuthorizeMode.ROLE) { - http.authorizeRequests().anyRequest().hasAnyRole(roles); - } - else if (mode == SecurityAuthorizeMode.AUTHENTICATED) { - http.authorizeRequests().anyRequest().authenticated(); - } - } - } - - private String[] getSecureApplicationPaths() { - List list = new ArrayList<>(); - for (String path : this.security.getBasic().getPath()) { - path = (path == null ? "" : path.trim()); - if (path.equals("/**")) { - return new String[] { path }; - } - if (!path.equals("")) { - list.add(path); - } - } - return list.toArray(new String[list.size()]); - } - - private AuthenticationEntryPoint entryPoint() { - BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint(); - entryPoint.setRealmName(this.security.getBasic().getRealm()); - return entryPoint; - } - } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/FallbackWebSecurityAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/WebSecurityEnablerConfiguration.java similarity index 63% rename from spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/FallbackWebSecurityAutoConfiguration.java rename to spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/WebSecurityEnablerConfiguration.java index 00372557fc..ef0520cde3 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/FallbackWebSecurityAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/WebSecurityEnablerConfiguration.java @@ -16,33 +16,27 @@ package org.springframework.boot.autoconfigure.security; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; 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; /** - * If the user explicitly disables the basic security features and forgets to - * {@code @EnableWebSecurity}, and yet still wants a bean of type - * WebSecurityConfigurerAdapter, he is trying to use a custom security setup. The app - * would fail in a confusing way without this shim configuration, which just helpfully - * defines an empty {@code @EnableWebSecurity}. + * If there is a bean of type WebSecurityConfigurerAdapter, + * this adds the {@code @EnableWebSecurity} annotation if it is not already specified. + * This will make sure that the annotation is present with default security autoconfiguration + * and also if the user adds custom security and forgets to add the annotation. * - * @author Dave Syer + * @author Madhura Bhave */ -@ConditionalOnProperty(prefix = "security.basic", name = "enabled", havingValue = "false") @ConditionalOnBean(WebSecurityConfigurerAdapter.class) @ConditionalOnClass(EnableWebSecurity.class) @ConditionalOnMissingBean(WebSecurityConfiguration.class) -@ConditionalOnWebApplication(type = Type.SERVLET) -@AutoConfigureAfter(SecurityAutoConfiguration.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @EnableWebSecurity -public class FallbackWebSecurityAutoConfiguration { - +public class WebSecurityEnablerConfiguration { } + diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 21846af9f0..ac220d1de2 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -98,7 +98,6 @@ org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\ org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\ org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,\ -org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration,\ org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\ org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationIntegrationTests.java index c362f55cf1..9552cb83bd 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationIntegrationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationIntegrationTests.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfigurationInteg import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -60,26 +61,18 @@ public class H2ConsoleAutoConfigurationIntegrationTests { public void noPrincipal() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(springSecurity()).build(); - mockMvc.perform(get("/h2-console/")).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/h2-console/").accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized()); } @Test public void userPrincipal() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(springSecurity()).build(); - mockMvc.perform(get("/h2-console/").with(user("test").roles("USER"))) + mockMvc.perform(get("/h2-console/").accept(MediaType.APPLICATION_JSON).with(user("test").roles("USER"))) .andExpect(status().isOk()) .andExpect(header().string("X-Frame-Options", "SAMEORIGIN")); } - @Test - public void someOtherPrincipal() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(springSecurity()).build(); - mockMvc.perform(get("/h2-console/").with(user("test").roles("FOO"))) - .andExpect(status().isForbidden()); - } - @Configuration @Import({ SecurityAutoConfiguration.class, H2ConsoleAutoConfiguration.class }) @Controller 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 22024413df..3bf16f4a8a 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 @@ -21,6 +21,7 @@ import java.util.EnumSet; import javax.servlet.DispatcherType; import org.junit.After; +import org.junit.Rule; import org.junit.Test; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; @@ -28,6 +29,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoCon import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.test.City; +import org.springframework.boot.test.rule.OutputCapture; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -50,7 +52,6 @@ import org.springframework.security.config.annotation.authentication.configurers import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; @@ -72,6 +73,9 @@ public class SecurityAutoConfigurationTests { private AnnotationConfigWebApplicationContext context; + @Rule + public OutputCapture outputCapture = new OutputCapture(); + @After public void close() { if (this.context != null) { @@ -87,9 +91,8 @@ public class SecurityAutoConfigurationTests { PropertyPlaceholderAutoConfiguration.class); this.context.refresh(); assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull(); - // 1 for static resources and one for the rest assertThat(this.context.getBean(FilterChainProxy.class).getFilterChains()) - .hasSize(2); + .hasSize(1); } @Test @@ -157,7 +160,7 @@ public class SecurityAutoConfigurationTests { } @Test - public void testDisableBasicAuthOnApplicationPaths() throws Exception { + public void testDisableDefaultSecurity() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, @@ -166,7 +169,7 @@ public class SecurityAutoConfigurationTests { this.context.refresh(); // Ignores and the "matches-none" filter only assertThat(this.context.getBeanNamesForType(FilterChainProxy.class).length) - .isEqualTo(1); + .isEqualTo(0); } @Test @@ -295,33 +298,24 @@ public class SecurityAutoConfigurationTests { this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class); this.context.refresh(); - SecurityProperties security = this.context.getBean(SecurityProperties.class); + String password = this.outputCapture.toString().split("Using default security password: ")[1].split("\n")[0]; AuthenticationManager manager = this.context.getBean(AuthenticationManager.class); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - security.getUser().getName(), security.getUser().getPassword()); + "user", password); assertThat(manager.authenticate(token)).isNotNull(); } @Test - public void testCustomAuthenticationDoesNotAuthenticateWithBootSecurityUser() + public void testCustomAuthenticationDoesNotCreateDefaultUser() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(AuthenticationManagerCustomizer.class, SecurityAutoConfiguration.class); this.context.refresh(); - SecurityProperties security = this.context.getBean(SecurityProperties.class); AuthenticationManager manager = this.context.getBean(AuthenticationManager.class); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - security.getUser().getName(), security.getUser().getPassword()); - try { - manager.authenticate(token); - fail("Expected Exception"); - } - catch (AuthenticationException success) { - // Expected - } - token = new UsernamePasswordAuthenticationToken("foo", "bar"); + assertThat(this.outputCapture.toString()).doesNotContain("Using default security password: "); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("foo", "bar"); assertThat(manager.authenticate(token)).isNotNull(); } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityFilterAutoConfigurationEarlyInitializationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityFilterAutoConfigurationEarlyInitializationTests.java index 0143bba486..8307a14e52 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityFilterAutoConfigurationEarlyInitializationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityFilterAutoConfigurationEarlyInitializationTests.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; +import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -30,9 +31,9 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.security.SecurityAutoConfigurationTests.WebSecurity; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.test.rule.OutputCapture; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -54,17 +55,19 @@ import org.springframework.web.bind.annotation.RestController; */ public class SecurityFilterAutoConfigurationEarlyInitializationTests { - // gh-4154 + @Rule + public OutputCapture outputCapture = new OutputCapture(); @Test public void testSecurityFilterDoesNotCauseEarlyInitialization() throws Exception { try (AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext()) { - TestPropertyValues.of("server.port:0", "security.user.password:password") + TestPropertyValues.of("server.port:0") .applyTo(context); context.register(Config.class); context.refresh(); int port = context.getWebServer().getPort(); - new TestRestTemplate("user", "password") + String password = this.outputCapture.toString().split("Using default security password: ")[1].split("\n")[0]; + new TestRestTemplate("user", password) .getForEntity("http://localhost:" + port, Object.class); // If early initialization occurred a ConverterNotFoundException is thrown @@ -76,7 +79,7 @@ public class SecurityFilterAutoConfigurationEarlyInitializationTests { ConverterBean.class }) @ImportAutoConfiguration({ WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebSecurity.class, + DispatcherServletAutoConfiguration.class, SecurityAutoConfiguration.class, SecurityFilterAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) static class Config { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityPropertiesTests.java index d9bfea4c25..f9e7141206 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityPropertiesTests.java @@ -17,8 +17,6 @@ package org.springframework.boot.autoconfigure.security; import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; import org.junit.Test; @@ -39,63 +37,9 @@ public class SecurityPropertiesTests { private SecurityProperties security = new SecurityProperties(); @Test - public void testBindingIgnoredSingleValued() { - bind("security.ignored", "/css/**"); - assertThat(this.security.getIgnored()).hasSize(1); - } - - @Test - public void testBindingIgnoredEmpty() { - bind("security.ignored", ""); - assertThat(this.security.getIgnored()).isEmpty(); - } - - @Test - public void testBindingIgnoredDisable() { - bind("security.ignored", "none"); - assertThat(this.security.getIgnored()).hasSize(1); - } - - @Test - public void testBindingIgnoredMultiValued() { - bind("security.ignored", "/css/**,/images/**"); - assertThat(this.security.getIgnored()).hasSize(2); - } - - @Test - public void testBindingIgnoredMultiValuedList() { - Map map = new LinkedHashMap<>(); - map.put("security.ignored[0]", "/css/**"); - map.put("security.ignored[1]", "/foo/**"); - MapConfigurationPropertySource source = new MapConfigurationPropertySource(map); - bind(source); - assertThat(this.security.getIgnored()).hasSize(2); - assertThat(this.security.getIgnored().contains("/foo/**")).isTrue(); - } - - @Test - public void testDefaultPasswordAutogeneratedIfUnresolvedPlaceholder() { - bind("security.user.password", "${ADMIN_PASSWORD}"); - assertThat(this.security.getUser().isDefaultPassword()).isTrue(); - } - - @Test - public void testDefaultPasswordAutogeneratedIfEmpty() { - bind("security.user.password", ""); - assertThat(this.security.getUser().isDefaultPassword()).isTrue(); - } - - @Test - public void testRoles() { - bind("security.user.role", "USER,ADMIN"); - assertThat(this.security.getUser().getRole().toString()) - .isEqualTo("[USER, ADMIN]"); - } - - @Test - public void testRole() { - bind("security.user.role", "ADMIN"); - assertThat(this.security.getUser().getRole().toString()).isEqualTo("[ADMIN]"); + public void testBinding() { + bind("security.basic.enabled", "false"); + assertThat(this.security.getBasic().isEnabled()).isFalse(); } private void bind(String name, String value) { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootSecurityTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootSecurityTests.java new file mode 100644 index 0000000000..4e3c6a3c3c --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootSecurityTests.java @@ -0,0 +1,170 @@ +/* + * 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 org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorController; +import org.springframework.boot.endpoint.Endpoint; +import org.springframework.boot.endpoint.EndpointPathResolver; +import org.springframework.mock.web.MockHttpServletRequest; +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 static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringBootSecurity}. + * + * @author Madhura Bhave + */ +public class SpringBootSecurityTests { + + private SpringBootSecurity bootSecurity; + + private EndpointPathResolver endpointPathResolver = new TestEndpointPathResolver(); + + private ErrorController errorController = new TestErrorController(); + + private MockHttpServletRequest request = new MockHttpServletRequest(); + + private static String[] STATIC_RESOURCES = new String[]{"/css/**", "/js/**", + "/images/**", "/webjars/**", "/**/favicon.ico"}; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + this.bootSecurity = new SpringBootSecurity(this.endpointPathResolver, this.errorController); + } + + @Test + public void endpointIdsShouldThrowIfNoEndpointPaths() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("At least one endpoint id must be specified."); + this.bootSecurity.endpointIds(); + } + + @Test + public void endpointIdsShouldReturnRequestMatcherWithEndpointPaths() throws Exception { + RequestMatcher requestMatcher = this.bootSecurity.endpointIds("id-1", "id-2"); + assertThat(requestMatcher).isInstanceOf(OrRequestMatcher.class); + this.request.setServletPath("/test/id-1"); + assertThat(requestMatcher.matches(this.request)).isTrue(); + this.request.setServletPath("/test/id-2"); + assertThat(requestMatcher.matches(this.request)).isTrue(); + this.request.setServletPath("/test/other-id"); + assertThat(requestMatcher.matches(this.request)).isFalse(); + } + + @Test + public void endpointIdsShouldReturnRequestMatcherWithAllEndpointPaths() throws Exception { + RequestMatcher requestMatcher = this.bootSecurity.endpointIds(SpringBootSecurity.ALL_ENDPOINTS); + this.request.setServletPath("/test/id-1"); + assertThat(requestMatcher.matches(this.request)).isTrue(); + this.request.setServletPath("/test/id-2"); + assertThat(requestMatcher.matches(this.request)).isTrue(); + this.request.setServletPath("/test/other-id"); + assertThat(requestMatcher.matches(this.request)).isTrue(); + } + + @Test + public void endpointsShouldReturnRequestMatcherWithEndpointPaths() throws Exception { + RequestMatcher requestMatcher = this.bootSecurity.endpoints(TestEndpoint1.class); + assertThat(requestMatcher).isInstanceOf(OrRequestMatcher.class); + this.request.setServletPath("/test/id-1"); + assertThat(requestMatcher.matches(this.request)).isTrue(); + this.request.setServletPath("/test/id-2"); + assertThat(requestMatcher.matches(this.request)).isFalse(); + } + + @Test + public void endpointsShouldThrowIfNoEndpointPaths() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("At least one endpoint must be specified."); + this.bootSecurity.endpoints(); + } + + @Test + public void endpointsShouldThrowExceptionWhenClassNotEndpoint() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Only classes annotated with @Endpoint are supported."); + this.bootSecurity.endpoints(FakeEndpoint.class); + } + + @Test + public void staticResourcesShouldReturnRequestMatcherWithStaticResources() throws Exception { + RequestMatcher requestMatcher = this.bootSecurity.staticResources(); + assertThat(requestMatcher).isInstanceOf(OrRequestMatcher.class); + for (String resource : STATIC_RESOURCES) { + this.request.setServletPath(resource); + assertThat(requestMatcher.matches(this.request)).isTrue(); + } + } + + @Test + public void errorShouldReturnRequestMatcherWithErrorControllerPath() throws Exception { + RequestMatcher requestMatcher = this.bootSecurity.error(); + assertThat(requestMatcher).isInstanceOf(AntPathRequestMatcher.class); + this.request.setServletPath("/test/error"); + assertThat(requestMatcher.matches(this.request)).isTrue(); + } + + @Test + public void errorShouldThrowExceptionWhenNoErrorController() throws Exception { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Path for error controller could not be determined."); + this.bootSecurity = new SpringBootSecurity(this.endpointPathResolver, null); + this.bootSecurity.error(); + } + + static class TestEndpointPathResolver implements EndpointPathResolver { + + @Override + public String resolvePath(String endpointId) { + return "/test/" + endpointId; + } + + } + + static class TestErrorController implements ErrorController { + + @Override + public String getErrorPath() { + return "/test/error"; + } + } + + @Endpoint(id = "id-1") + static class TestEndpoint1 { + + } + + @Endpoint(id = "id-2") + static class TestEndpoint2 { + + } + + static class FakeEndpoint { + + } +} 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 deleted file mode 100644 index ce9b93a3c6..0000000000 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * 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.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import javax.servlet.Filter; - -import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -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.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.web.FilterChainProxy; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.context.WebApplicationContext; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; - -/** - * Tests for {@link SpringBootWebSecurityConfiguration}. - * - * @author Dave Syer - * @author Rob Winch - * @author Andy Wilkinson - */ -public class SpringBootWebSecurityConfigurationTests { - - private ConfigurableApplicationContext context; - - @After - public void close() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - public void testWebConfigurationOverrideGlobalAuthentication() throws Exception { - this.context = SpringApplication.run(TestWebConfiguration.class, - "--server.port=0"); - assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull(); - assertThat(this.context.getBean(AuthenticationManager.class) - .authenticate(new UsernamePasswordAuthenticationToken("dave", "secret"))) - .isNotNull(); - } - - @Test - public void testWebConfigurationFilterChainUnauthenticated() throws Exception { - this.context = SpringApplication.run(VanillaWebConfiguration.class, - "--server.port=0"); - MockMvc mockMvc = MockMvcBuilders - .webAppContextSetup((WebApplicationContext) this.context) - .addFilters( - this.context.getBean("springSecurityFilterChain", Filter.class)) - .build(); - mockMvc.perform(MockMvcRequestBuilders.get("/")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(MockMvcResultMatchers.header().string("www-authenticate", - Matchers.containsString("realm=\"Spring\""))); - } - - @Test - public void testWebConfigurationFilterChainUnauthenticatedWithAuthorizeModeNone() - throws Exception { - this.context = SpringApplication.run(VanillaWebConfiguration.class, - "--server.port=0", "--security.basic.authorize-mode=none"); - MockMvc mockMvc = MockMvcBuilders - .webAppContextSetup((WebApplicationContext) this.context) - .addFilters( - this.context.getBean("springSecurityFilterChain", Filter.class)) - .build(); - mockMvc.perform(MockMvcRequestBuilders.get("/")) - .andExpect(MockMvcResultMatchers.status().isNotFound()); - } - - @Test - public void testWebConfigurationFilterChainUnauthenticatedWithAuthorizeModeAuthenticated() - throws Exception { - this.context = SpringApplication.run(VanillaWebConfiguration.class, - "--server.port=0", "--security.basic.authorize-mode=authenticated"); - MockMvc mockMvc = MockMvcBuilders - .webAppContextSetup((WebApplicationContext) this.context) - .addFilters( - this.context.getBean("springSecurityFilterChain", Filter.class)) - .build(); - mockMvc.perform(MockMvcRequestBuilders.get("/")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(MockMvcResultMatchers.header().string("www-authenticate", - Matchers.containsString("realm=\"Spring\""))); - } - - @Test - public void testWebConfigurationFilterChainBadCredentials() throws Exception { - this.context = SpringApplication.run(VanillaWebConfiguration.class, - "--server.port=0"); - MockMvc mockMvc = MockMvcBuilders - .webAppContextSetup((WebApplicationContext) this.context) - .addFilters( - this.context.getBean("springSecurityFilterChain", Filter.class)) - .build(); - mockMvc.perform( - MockMvcRequestBuilders.get("/").header("authorization", "Basic xxx")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(MockMvcResultMatchers.header().string("www-authenticate", - Matchers.containsString("realm=\"Spring\""))); - } - - @Test - public void testWebConfigurationInjectGlobalAuthentication() throws Exception { - this.context = SpringApplication.run(TestInjectWebConfiguration.class, - "--server.port=0"); - assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull(); - assertThat(this.context.getBean(AuthenticationManager.class) - .authenticate(new UsernamePasswordAuthenticationToken("dave", "secret"))) - .isNotNull(); - } - - // gh-3447 - @Test - public void testHiddenHttpMethodFilterOrderedFirst() throws Exception { - this.context = SpringApplication.run(DenyPostRequestConfig.class, - "--server.port=0"); - int port = Integer - .parseInt(this.context.getEnvironment().getProperty("local.server.port")); - TestRestTemplate rest = new TestRestTemplate(); - - // not overriding causes forbidden - MultiValueMap form = new LinkedMultiValueMap<>(); - - ResponseEntity result = rest - .postForEntity("http://localhost:" + port + "/", form, Object.class); - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - - // override method with GET - form = new LinkedMultiValueMap<>(); - form.add("_method", "GET"); - - result = rest.postForEntity("http://localhost:" + port + "/", form, Object.class); - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - } - - @Test - public void defaultHeaderConfiguration() throws Exception { - this.context = SpringApplication.run(VanillaWebConfiguration.class, - "--server.port=0"); - MockMvc mockMvc = MockMvcBuilders - .webAppContextSetup((WebApplicationContext) this.context) - .addFilters((FilterChainProxy) this.context - .getBean("springSecurityFilterChain", Filter.class)) - .build(); - mockMvc.perform(MockMvcRequestBuilders.get("/")) - .andExpect(MockMvcResultMatchers.header().string("X-Content-Type-Options", - is(notNullValue()))) - .andExpect(MockMvcResultMatchers.header().string("X-XSS-Protection", - is(notNullValue()))) - .andExpect(MockMvcResultMatchers.header().string("Cache-Control", - is(notNullValue()))) - .andExpect(MockMvcResultMatchers.header().string("X-Frame-Options", - is(notNullValue()))) - .andExpect(MockMvcResultMatchers.header() - .doesNotExist("Content-Security-Policy")); - } - - @Test - public void securityHeadersCanBeDisabled() throws Exception { - this.context = SpringApplication.run(VanillaWebConfiguration.class, - "--server.port=0", "--security.headers.content-type=false", - "--security.headers.xss=false", "--security.headers.cache=false", - "--security.headers.frame=false"); - - MockMvc mockMvc = MockMvcBuilders - .webAppContextSetup((WebApplicationContext) this.context) - .addFilters( - this.context.getBean("springSecurityFilterChain", Filter.class)) - .build(); - mockMvc.perform(MockMvcRequestBuilders.get("/")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(MockMvcResultMatchers.header() - .doesNotExist("X-Content-Type-Options")) - .andExpect( - MockMvcResultMatchers.header().doesNotExist("X-XSS-Protection")) - .andExpect(MockMvcResultMatchers.header().doesNotExist("Cache-Control")) - .andExpect( - MockMvcResultMatchers.header().doesNotExist("X-Frame-Options")); - } - - @Test - public void contentSecurityPolicyConfiguration() throws Exception { - this.context = SpringApplication.run(VanillaWebConfiguration.class, - "--security.headers.content-security-policy=default-src 'self';", - "--server.port=0"); - MockMvc mockMvc = MockMvcBuilders - .webAppContextSetup((WebApplicationContext) this.context) - .addFilters((FilterChainProxy) this.context - .getBean("springSecurityFilterChain", Filter.class)) - .build(); - mockMvc.perform(MockMvcRequestBuilders.get("/")) - .andExpect(MockMvcResultMatchers.header() - .string("Content-Security-Policy", is("default-src 'self';"))) - .andExpect(MockMvcResultMatchers.header() - .doesNotExist("Content-Security-Policy-Report-Only")); - } - - @Test - public void contentSecurityPolicyReportOnlyConfiguration() throws Exception { - this.context = SpringApplication.run(VanillaWebConfiguration.class, - "--security.headers.content-security-policy=default-src 'self';", - "--security.headers.content-security-policy-mode=report-only", - "--server.port=0"); - MockMvc mockMvc = MockMvcBuilders - .webAppContextSetup((WebApplicationContext) this.context) - .addFilters((FilterChainProxy) this.context - .getBean("springSecurityFilterChain", Filter.class)) - .build(); - mockMvc.perform(MockMvcRequestBuilders.get("/")) - .andExpect(MockMvcResultMatchers.header().string( - "Content-Security-Policy-Report-Only", is("default-src 'self';"))) - .andExpect(MockMvcResultMatchers.header() - .doesNotExist("Content-Security-Policy")); - } - - @Configuration - @Import(TestWebConfiguration.class) - @Order(Ordered.LOWEST_PRECEDENCE) - protected static class TestInjectWebConfiguration - extends WebSecurityConfigurerAdapter { - - private final AuthenticationManagerBuilder auth; - - // It's a bad idea to inject an AuthenticationManager into a - // WebSecurityConfigurerAdapter because it can cascade early instantiation, - // unless you explicitly want the Boot default AuthenticationManager. It's - // better to inject the builder, if you want the global AuthenticationManager. It - // might even be necessary to wrap the builder in a lazy AuthenticationManager - // (that calls getOrBuild() only when the AuthenticationManager is actually - // called). - protected TestInjectWebConfiguration(AuthenticationManagerBuilder auth) { - this.auth = auth; - } - - @Override - public void init(WebSecurity web) throws Exception { - this.auth.getOrBuild(); - } - - } - - @MinimalWebConfiguration - @Import(SecurityAutoConfiguration.class) - protected static class VanillaWebConfiguration { - - } - - @MinimalWebConfiguration - @Import(SecurityAutoConfiguration.class) - @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) - protected static class TestWebConfiguration extends WebSecurityConfigurerAdapter { - - @Autowired - public void init(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication().withUser("dave").password("secret") - .roles("USER"); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests().anyRequest().denyAll(); - } - - } - - @Configuration - @Target(ElementType.TYPE) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @Import({ ServletWebServerFactoryAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class }) - protected @interface MinimalWebConfiguration { - - } - - @MinimalWebConfiguration - @Import(SecurityAutoConfiguration.class) - protected static class DenyPostRequestConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests().antMatchers(HttpMethod.POST, "/**").denyAll(); - } - - } - -} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoConfigurationTests.java index 7b5988bcf8..c87c5dcb48 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoConfigurationTests.java @@ -40,7 +40,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.WebApplicationContext; -import static org.hamcrest.Matchers.startsWith; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -76,12 +75,6 @@ public class CustomOAuth2SsoConfigurationTests { .addFilters(this.filter).build(); } - @Test - public void homePageIsBasicAuth() throws Exception { - this.mvc.perform(get("/")).andExpect(status().isUnauthorized()) - .andExpect(header().string("WWW-Authenticate", startsWith("Basic"))); - } - @Test public void uiPageIsSecure() throws Exception { this.mvc.perform(get("/ui/")).andExpect(status().isFound()) diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests.java index 91630c0c52..874ff62e9a 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests.java @@ -42,10 +42,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.WebApplicationContext; -import static org.hamcrest.Matchers.startsWith; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -78,12 +76,6 @@ public class CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests { .addFilters(this.filter).build(); } - @Test - public void homePageIsBasicAuth() throws Exception { - this.mvc.perform(get("/")).andExpect(status().isUnauthorized()) - .andExpect(header().string("WWW-Authenticate", startsWith("Basic"))); - } - @Test public void uiPageIsSecure() throws Exception { this.mvc.perform(get("/ui/")).andExpect(status().isUnauthorized()); diff --git a/spring-boot-integration-tests/spring-boot-security-tests/spring-boot-security-test-web-helloworld/src/main/java/sample/HelloWebSecurityApplication.java b/spring-boot-integration-tests/spring-boot-security-tests/spring-boot-security-test-web-helloworld/src/main/java/sample/HelloWebSecurityApplication.java index 53143ce80b..db9b633dfe 100644 --- a/spring-boot-integration-tests/spring-boot-security-tests/spring-boot-security-test-web-helloworld/src/main/java/sample/HelloWebSecurityApplication.java +++ b/spring-boot-integration-tests/spring-boot-security-tests/spring-boot-security-test-web-helloworld/src/main/java/sample/HelloWebSecurityApplication.java @@ -16,17 +16,21 @@ package sample; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; @SpringBootApplication public class HelloWebSecurityApplication { - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); + @Bean + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user").password("password").roles("USER").build()); + return manager; } public static void main(String[] args) { diff --git a/spring-boot-integration-tests/spring-boot-security-tests/spring-boot-security-test-web-helloworld/src/test/java/sample/HelloWebSecurityApplicationTests.java b/spring-boot-integration-tests/spring-boot-security-tests/spring-boot-security-test-web-helloworld/src/test/java/sample/HelloWebSecurityApplicationTests.java index ce17ae2aeb..d3e2572c81 100644 --- a/spring-boot-integration-tests/spring-boot-security-tests/spring-boot-security-test-web-helloworld/src/test/java/sample/HelloWebSecurityApplicationTests.java +++ b/spring-boot-integration-tests/spring-boot-security-tests/spring-boot-security-test-web-helloworld/src/test/java/sample/HelloWebSecurityApplicationTests.java @@ -57,6 +57,7 @@ public class HelloWebSecurityApplicationTests { @Test public void requiresAuthentication() throws Exception { + this.request.addHeader("Accept", "application/json"); this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); assertThat(this.response.getStatus()) .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); @@ -64,6 +65,7 @@ public class HelloWebSecurityApplicationTests { @Test public void userAuthenticates() throws Exception { + this.request.addHeader("Accept", "application/json"); this.request.addHeader("Authorization", "Basic " + new String( Base64.getEncoder().encode("user:password".getBytes("UTF-8")))); diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index 33d005cd06..48f9c578a7 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -27,6 +27,7 @@ spring-boot-sample-actuator-log4j2 spring-boot-sample-actuator-noweb spring-boot-sample-actuator-ui + spring-boot-sample-actuator-custom-security spring-boot-sample-amqp spring-boot-sample-aop spring-boot-sample-atmosphere diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/pom.xml b/spring-boot-samples/spring-boot-sample-actuator-custom-security/pom.xml new file mode 100644 index 0000000000..4c04cc7622 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 2.0.0.BUILD-SNAPSHOT + + spring-boot-sample-actuator-custom-security + Spring Boot Actuator Custom Security Sample + Spring Boot Actuator Custom Security Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-freemarker + + + org.springframework.boot + spring-boot-starter-security + + + org.jolokia + jolokia-core + + + + org.apache.httpcomponents + httpclient + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/ExampleController.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/ExampleController.java new file mode 100644 index 0000000000..e0caad83f8 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/ExampleController.java @@ -0,0 +1,26 @@ +package sample.actuator.customsecurity; + +import java.util.Date; +import java.util.Map; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class ExampleController { + + @GetMapping("/") + public String home(Map model) { + model.put("message", "Hello World"); + model.put("title", "Hello Home"); + model.put("date", new Date()); + return "home"; + } + + @RequestMapping("/foo") + public String foo() { + throw new RuntimeException("Expected exception in controller"); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplication.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplication.java new file mode 100644 index 0000000000..ce5afab41d --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplication.java @@ -0,0 +1,29 @@ +/* + * 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 sample.actuator.customsecurity; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleActuatorCustomSecurityApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleActuatorCustomSecurityApplication.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java new file mode 100644 index 0000000000..9ce1e55b5e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java @@ -0,0 +1,40 @@ +package sample.actuator.customsecurity; + +import org.springframework.boot.autoconfigure.security.SpringBootSecurity; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + private SpringBootSecurity bootSecurity; + + public SecurityConfiguration(SpringBootSecurity bootSecurity) { + this.bootSecurity = bootSecurity; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication() + .withUser("user").password("password").authorities("ROLE_USER").and() + .withUser("admin").password("admin").authorities("ROLE_ACTUATOR", "ROLE_USER"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .requestMatchers(this.bootSecurity.endpointIds("status", "info")).permitAll() + .requestMatchers(this.bootSecurity.endpointIds(SpringBootSecurity.ALL_ENDPOINTS)).hasRole("ACTUATOR") + .requestMatchers(this.bootSecurity.staticResources()).permitAll() + .antMatchers("/foo").permitAll() + .antMatchers("/**").hasRole("USER") + .and() + .cors() + .and() + .httpBasic(); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/application.properties new file mode 100644 index 0000000000..d7dc392adf --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/application.properties @@ -0,0 +1 @@ +endpoints.all.web.enabled=true \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/static/css/bootstrap.min.css b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/static/css/bootstrap.min.css new file mode 100644 index 0000000000..5589964e71 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/static/css/bootstrap.min.css @@ -0,0 +1,11 @@ +/*! + * Bootstrap v2.0.4 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover{color:#005580;text-decoration:underline}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:20px}.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.127659574%;*margin-left:2.0744680846382977%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:99.99999998999999%;*width:99.94680850063828%}.row-fluid .span11{width:91.489361693%;*width:91.4361702036383%}.row-fluid .span10{width:82.97872339599999%;*width:82.92553190663828%}.row-fluid .span9{width:74.468085099%;*width:74.4148936096383%}.row-fluid .span8{width:65.95744680199999%;*width:65.90425531263828%}.row-fluid .span7{width:57.446808505%;*width:57.3936170156383%}.row-fluid .span6{width:48.93617020799999%;*width:48.88297871863829%}.row-fluid .span5{width:40.425531911%;*width:40.3723404216383%}.row-fluid .span4{width:31.914893614%;*width:31.8617021246383%}.row-fluid .span3{width:23.404255317%;*width:23.3510638276383%}.row-fluid .span2{width:14.89361702%;*width:14.8404255306383%}.row-fluid .span1{width:6.382978723%;*width:6.329787233638298%}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;content:""}.container-fluid:after{clear:both}p{margin:0 0 9px}p small{font-size:11px;color:#999}.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px}h1,h2,h3,h4,h5,h6{margin:0;font-family:inherit;font-weight:bold;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999}h1{font-size:30px;line-height:36px}h1 small{font-size:18px}h2{font-size:24px;line-height:36px}h2 small{font-size:18px}h3{font-size:18px;line-height:27px}h3 small{font-size:14px}h4,h5,h6{line-height:18px}h4{font-size:14px}h4 small{font-size:12px}h5{font-size:12px}h6{font-size:11px;color:#999;text-transform:uppercase}.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eee}.page-header h1{line-height:1}ul,ol{padding:0;margin:0 0 9px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}ul{list-style:disc}ol{list-style:decimal}li{line-height:18px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}dl{margin-bottom:18px}dt,dd{line-height:18px}dt{font-weight:bold;line-height:17px}dd{margin-left:9px}.dl-horizontal dt{float:left;width:120px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:130px}hr{margin:18px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}strong{font-weight:bold}em{font-style:italic}.muted{color:#999}abbr[title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px}blockquote small{display:block;line-height:18px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:18px;font-style:normal;line-height:18px}small{font-size:100%}cite{font-style:normal}code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12.025px;line-height:18px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:18px}pre code{padding:0;color:inherit;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 18px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:13.5px;color:#999}label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555}input,textarea{width:210px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-ms-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer}input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}.uneditable-textarea{width:auto;height:auto}select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px}select{width:220px;border:1px solid #bbb}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.radio,.checkbox{min-height:18px;padding-left:18px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:930px}input.span11,textarea.span11,.uneditable-input.span11{width:850px}input.span10,textarea.span10,.uneditable-input.span10{width:770px}input.span9,textarea.span9,.uneditable-input.span9{width:690px}input.span8,textarea.span8,.uneditable-input.span8{width:610px}input.span7,textarea.span7,.uneditable-input.span7{width:530px}input.span6,textarea.span6,.uneditable-input.span6{width:450px}input.span5,textarea.span5,.uneditable-input.span5{width:370px}input.span4,textarea.span4,.uneditable-input.span4{width:290px}input.span3,textarea.span3,.uneditable-input.span3{width:210px}input.span2,textarea.span2,.uneditable-input.span2{width:130px}input.span1,textarea.span1,.uneditable-input.span1{width:50px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee;border-color:#ddd}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853}.control-group.warning .checkbox:focus,.control-group.warning .radio:focus,.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48}.control-group.error .checkbox:focus,.control-group.error .radio:focus,.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847}.control-group.success .checkbox:focus,.control-group.success .radio:focus,.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:17px 20px 18px;margin-top:18px;margin-bottom:18px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;content:""}.form-actions:after{clear:both}.uneditable-input{overflow:hidden;white-space:nowrap;cursor:not-allowed;background-color:#fff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}:-moz-placeholder{color:#999}:-ms-input-placeholder{color:#999}::-webkit-input-placeholder{color:#999}.help-block,.help-inline{color:#555}.help-block{display:block;margin-bottom:9px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-prepend,.input-append{margin-bottom:5px}.input-prepend input,.input-append input,.input-prepend select,.input-append select,.input-prepend .uneditable-input,.input-append .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:middle;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-prepend input:focus,.input-append input:focus,.input-prepend select:focus,.input-append select:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{z-index:2}.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc}.input-prepend .add-on,.input-append .add-on{display:inline-block;width:auto;height:18px;min-width:16px;padding:4px 5px;font-weight:normal;line-height:18px;text-align:center;text-shadow:0 1px 0 #fff;vertical-align:middle;background-color:#eee;border:1px solid #ccc}.input-prepend .add-on,.input-append .add-on,.input-prepend .btn,.input-append .btn{margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-append .uneditable-input{border-right-color:#ccc;border-left-color:#eee}.input-append .add-on:last-child,.input-append .btn:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:9px}legend+.control-group{margin-top:18px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:18px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:160px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:160px}.form-horizontal .help-block{margin-top:9px;margin-bottom:0}.form-horizontal .form-actions{padding-left:160px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:18px}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapsed;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9}.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5}table .span1{float:none;width:44px;margin-left:0}table .span2{float:none;width:124px;margin-left:0}table .span3{float:none;width:204px;margin-left:0}table .span4{float:none;width:284px;margin-left:0}table .span5{float:none;width:364px;margin-left:0}table .span6{float:none;width:444px;margin-left:0}table .span7{float:none;width:524px;margin-left:0}table .span8{float:none;width:604px;margin-left:0}table .span9{float:none;width:684px;margin-left:0}table .span10{float:none;width:764px;margin-left:0}table .span11{float:none;width:844px;margin-left:0}table .span12{float:none;width:924px;margin-left:0}table .span13{float:none;width:1004px;margin-left:0}table .span14{float:none;width:1084px;margin-left:0}table .span15{float:none;width:1164px;margin-left:0}table .span16{float:none;width:1244px;margin-left:0}table .span17{float:none;width:1324px;margin-left:0}table .span18{float:none;width:1404px;margin-left:0}table .span19{float:none;width:1484px;margin-left:0}table .span20{float:none;width:1564px;margin-left:0}table .span21{float:none;width:1644px;margin-left:0}table .span22{float:none;width:1724px;margin-left:0}table .span23{float:none;width:1804px;margin-left:0}table .span24{float:none;width:1884px;margin-left:0}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0}.icon-white{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{background-position:-384px -120px}.icon-folder-open{background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:"";opacity:.3;filter:alpha(opacity=30)}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown:hover .caret,.open .caret{opacity:1;filter:alpha(opacity=100)}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:4px 0;margin:1px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:8px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:18px;color:#333;white-space:nowrap}.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#fff;text-decoration:none;background-color:#08c}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:"\2191"}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0,0,0,0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-ms-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-ms-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 10px 4px;margin-bottom:0;*margin-left:.3em;font-size:13px;line-height:18px;*line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-ms-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(top,#fff,#e6e6e6);background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff',endColorstr='#e6e6e6',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover{color:#333;text-decoration:none;background-color:#e6e6e6;*background-color:#d9d9d9;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-ms-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-color:#e6e6e6;background-color:#d9d9d9 \9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-color:#e6e6e6;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.btn-large [class^="icon-"]{margin-top:1px}.btn-small{padding:5px 9px;font-size:11px;line-height:16px}.btn-small [class^="icon-"]{margin-top:-1px}.btn-mini{padding:2px 6px;font-size:11px;line-height:14px}.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn{border-color:#ccc;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.btn-primary{background-color:#0074cc;*background-color:#05c;background-image:-ms-linear-gradient(top,#08c,#05c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#05c));background-image:-webkit-linear-gradient(top,#08c,#05c);background-image:-o-linear-gradient(top,#08c,#05c);background-image:-moz-linear-gradient(top,#08c,#05c);background-image:linear-gradient(top,#08c,#05c);background-repeat:repeat-x;border-color:#05c #05c #003580;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#0088cc',endColorstr='#0055cc',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#05c;*background-color:#004ab3}.btn-primary:active,.btn-primary.active{background-color:#004099 \9}.btn-warning{background-color:#faa732;*background-color:#f89406;background-image:-ms-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(top,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450',endColorstr='#f89406',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{background-color:#da4f49;*background-color:#bd362f;background-image:-ms-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(top,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#bd362f',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{background-color:#5bb75b;*background-color:#51a351;background-image:-ms-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(top,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#62c462',endColorstr='#51a351',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{background-color:#49afcd;*background-color:#2f96b4;background-image:-ms-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(top,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de',endColorstr='#2f96b4',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{background-color:#414141;*background-color:#222;background-image:-ms-linear-gradient(top,#555,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#555),to(#222));background-image:-webkit-linear-gradient(top,#555,#222);background-image:-o-linear-gradient(top,#555,#222);background-image:-moz-linear-gradient(top,#555,#222);background-image:linear-gradient(top,#555,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#555555',endColorstr='#222222',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-group{position:relative;*margin-left:.3em;*zoom:1}.btn-group:before,.btn-group:after{display:table;content:""}.btn-group:after{clear:both}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:9px;margin-bottom:9px}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1}.btn-group>.btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.dropdown-toggle{*padding-top:4px;padding-right:8px;*padding-bottom:4px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini.dropdown-toggle{padding-right:5px;padding-left:5px}.btn-group>.btn-small.dropdown-toggle{*padding-top:4px;*padding-bottom:4px}.btn-group>.btn-large.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#05c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:7px;margin-left:0}.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100)}.btn-mini .caret{margin-top:5px}.btn-small .caret{margin-top:6px}.btn-large .caret{margin-top:6px;border-top-width:5px;border-right-width:5px;border-left-width:5px}.dropup .btn-large .caret{border-top:0;border-bottom:5px solid #000}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:.75;filter:alpha(opacity=75)}.alert{padding:8px 35px 8px 14px;margin-bottom:18px;color:#c09853;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert-heading{color:inherit}.alert .close{position:relative;top:-2px;right:-21px;line-height:18px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:18px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>.pull-right{float:right}.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:8px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:18px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.nav-tabs.nav-stacked>li>a:hover{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px}.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333;border-bottom-color:#333}.nav>.dropdown.active>a:hover{color:#000;cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.navbar{*position:relative;*z-index:2;margin-bottom:18px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top,#333,#222);background-image:-ms-linear-gradient(top,#333,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#333),to(#222));background-image:-webkit-linear-gradient(top,#333,#222);background-image:-o-linear-gradient(top,#333,#222);background-image:linear-gradient(top,#333,#222);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#333333',endColorstr='#222222',GradientType=0);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.25),inset 0 -1px 0 rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.25),inset 0 -1px 0 rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.25),inset 0 -1px 0 rgba(0,0,0,0.1)}.navbar .container{width:auto}.nav-collapse.collapse{height:auto}.navbar{color:#999}.navbar .brand:hover{text-decoration:none}.navbar .brand{display:block;float:left;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#999}.navbar .navbar-text{margin-bottom:0;line-height:40px}.navbar .navbar-link{color:#999}.navbar .navbar-link:hover{color:#fff}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn{margin:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#fff;background-color:#626262;border:1px solid #151515;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none}.navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-bottom{bottom:0}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right}.navbar .nav>li{display:block;float:left}.navbar .nav>li>a{float:none;padding:9px 10px 11px;line-height:19px;color:#999;text-decoration:none;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar .btn{display:inline-block;padding:4px 10px 4px;margin:5px 5px 6px;line-height:18px}.navbar .btn-group{padding:5px 5px 6px;margin:0}.navbar .nav>li>a:hover{color:#fff;text-decoration:none;background-color:transparent}.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#fff;text-decoration:none;background-color:#222}.navbar .divider-vertical{width:1px;height:40px;margin:0 9px;overflow:hidden;background-color:#222;border-right:1px solid #333}.navbar .nav.pull-right{margin-right:0;margin-left:10px}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;background-color:#2c2c2c;*background-color:#222;background-image:-ms-linear-gradient(top,#333,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#333),to(#222));background-image:-webkit-linear-gradient(top,#333,#222);background-image:-o-linear-gradient(top,#333,#222);background-image:linear-gradient(top,#333,#222);background-image:-moz-linear-gradient(top,#333,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#333333',endColorstr='#222222',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{background-color:#222;*background-color:#151515}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#080808 \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown .dropdown-toggle .caret,.navbar .nav li.dropdown.open .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar .nav li.dropdown.active .caret{opacity:1;filter:alpha(opacity=100)}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{background-color:transparent}.navbar .nav li.dropdown.active>.dropdown-toggle:hover{color:#fff}.navbar .pull-right .dropdown-menu,.navbar .dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right .dropdown-menu:before,.navbar .dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right .dropdown-menu:after,.navbar .dropdown-menu.pull-right:after{right:13px;left:auto}.breadcrumb{padding:7px 14px;margin:0 0 18px;list-style:none;background-color:#fbfbfb;background-image:-moz-linear-gradient(top,#fff,#f5f5f5);background-image:-ms-linear-gradient(top,#fff,#f5f5f5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#fff,#f5f5f5);background-image:-o-linear-gradient(top,#fff,#f5f5f5);background-image:linear-gradient(top,#fff,#f5f5f5);background-repeat:repeat-x;border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff',endColorstr='#f5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.breadcrumb li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb .divider{padding:0 5px;color:#999}.breadcrumb .active a{color:#333}.pagination{height:36px;margin:18px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination li{display:inline}.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0}.pagination a:hover,.pagination .active a{background-color:#f5f5f5}.pagination .active a{color:#999;cursor:default}.pagination .disabled span,.pagination .disabled a,.pagination .disabled a:hover{color:#999;cursor:default;background-color:transparent}.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pager{margin-bottom:18px;margin-left:0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;content:""}.pager:after{clear:both}.pager li{display:inline}.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager a:hover{text-decoration:none;background-color:#f5f5f5}.pager .next a{float:right}.pager .previous a{float:left}.pager .disabled a,.pager .disabled a:hover{color:#999;cursor:default;background-color:#fff}.modal-open .dropdown-menu{z-index:2050}.modal-open .dropdown.open{*z-index:2050}.modal-open .popover{z-index:2060}.modal-open .tooltip{z-index:2070}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:50%;left:50%;z-index:1050;width:560px;margin:-250px 0 0 -280px;overflow:auto;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-ms-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:50%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-body{max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.tooltip{position:absolute;z-index:1020;display:block;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{margin-top:-2px}.tooltip.right{margin-left:2px}.tooltip.bottom{margin-top:2px}.tooltip.left{margin-left:-2px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top:5px solid #000;border-right:5px solid transparent;border-left:5px solid transparent}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-right:5px solid transparent;border-bottom:5px solid #000;border-left:5px solid transparent}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-right:5px solid #000;border-bottom:5px solid transparent}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px}.popover.top{margin-top:-5px}.popover.right{margin-left:5px}.popover.bottom{margin-top:5px}.popover.left{margin-left:-5px}.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-top:5px solid #000;border-right:5px solid transparent;border-left:5px solid transparent}.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-right:5px solid #000;border-bottom:5px solid transparent}.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-right:5px solid transparent;border-bottom:5px solid #000;border-left:5px solid transparent}.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000}.popover .arrow{position:absolute;width:0;height:0}.popover-inner{width:280px;padding:3px;overflow:hidden;background:#000;background:rgba(0,0,0,0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3)}.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0}.popover-content{padding:14px;background-color:#fff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:18px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:0 1px 1px rgba(0,0,0,0.075);box-shadow:0 1px 1px rgba(0,0,0,0.075)}a.thumbnail:hover{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px}.label,.badge{font-size:10.998px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{padding:1px 4px 2px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding:1px 9px 2px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}a.label:hover,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:18px;margin-bottom:18px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-ms-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(top,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#f5f5f5',endColorstr='#f9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{width:0;height:18px;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(top,#149bdf,#0480be);background-image:-ms-linear-gradient(top,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#149bdf',endColorstr='#0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-ms-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .bar{background-color:#149bdf;background-image:-o-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-webkit-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-ms-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-ms-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(top,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#c43c35',GradientType=0)}.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-ms-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-ms-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(top,#62c462,#57a957);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#62c462',endColorstr='#57a957',GradientType=0)}.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-ms-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-ms-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(top,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de',endColorstr='#339bb9',GradientType=0)}.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-ms-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-ms-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(top,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450',endColorstr='#f89406',GradientType=0)}.progress-warning.progress-striped .bar{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-ms-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(-45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:18px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:18px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel .item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-ms-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel .item>img{display:block;line-height:1}.carousel .active,.carousel .next,.carousel .prev{display:block}.carousel .active{left:0}.carousel .next,.carousel .prev{position:absolute;top:0;width:100%}.carousel .next{left:100%}.carousel .prev{left:-100%}.carousel .next.left,.carousel .prev.right{left:0}.carousel .active.left{left:-100%}.carousel .active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:10px 15px 5px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{color:#fff}.hero-unit{padding:60px;margin-bottom:30px;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit p{font-size:18px;font-weight:200;line-height:27px;color:inherit}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden} + + input.field-error, textarea.field-error { border: 1px solid #B94A48; } \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/templates/error.ftl b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/templates/error.ftl new file mode 100644 index 0000000000..b16ae5895b --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/templates/error.ftl @@ -0,0 +1,32 @@ +<#import "/spring.ftl" as spring /> + + + +Error +<#assign home><@spring.url relativeUrl="/"/> +<#assign bootstrap><@spring.url relativeUrl="/css/bootstrap.min.css"/> + + + +
    + +

    Error Page

    +
    ${timestamp?datetime}
    +
    + There was an unexpected error (type=${error}, status=${status}). +
    +
    ${message}
    +
    + Please contact the operator with the above information. +
    +
    + + diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/templates/home.ftl b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/templates/home.ftl new file mode 100644 index 0000000000..347990e049 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/resources/templates/home.ftl @@ -0,0 +1,26 @@ +<#import "/spring.ftl" as spring /> + + + +${title} +<#assign home><@spring.url relativeUrl="/"/> +<#assign bootstrap><@spring.url relativeUrl="/css/bootstrap.min.css"/> + + + +
    + +

    ${title}

    +
    ${message}
    +
    ${date?datetime}
    +
    + + diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/CorsSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/CorsSampleActuatorApplicationTests.java similarity index 86% rename from spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/CorsSampleActuatorApplicationTests.java rename to spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/CorsSampleActuatorApplicationTests.java index aa08a14eaa..a9737905a9 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/CorsSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/CorsSampleActuatorApplicationTests.java @@ -1,4 +1,4 @@ -package sample.actuator; +package sample.actuator.customsecurity; import java.net.URI; import java.util.Map; @@ -12,10 +12,14 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; @@ -59,7 +63,7 @@ public class CorsSampleActuatorApplicationTests { @Test public void preflightRequestToEndpointShouldReturnOk() throws Exception { RequestEntity healthRequest = RequestEntity - .options(new URI("/application/health")) + .options(new URI("/application/env")) .header("Origin", "http://localhost:8080") .header("Access-Control-Request-Method", "GET").build(); ResponseEntity exchange = this.testRestTemplate.exchange(healthRequest, @@ -70,7 +74,7 @@ public class CorsSampleActuatorApplicationTests { @Test public void preflightRequestWhenCorsConfigInvalidShouldReturnForbidden() throws Exception { - RequestEntity entity = RequestEntity.options(new URI("/application/health")) + RequestEntity entity = RequestEntity.options(new URI("/application/env")) .header("Origin", "http://localhost:9095") .header("Access-Control-Request-Method", "GET").build(); ResponseEntity exchange = this.testRestTemplate.exchange(entity, diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureManagementPortAndPathSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/InsecureManagementPortAndPathSampleActuatorApplicationTests.java similarity index 69% rename from spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureManagementPortAndPathSampleActuatorApplicationTests.java rename to spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/InsecureManagementPortAndPathSampleActuatorApplicationTests.java index 9bf7583296..06dc039fce 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureManagementPortAndPathSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/InsecureManagementPortAndPathSampleActuatorApplicationTests.java @@ -14,16 +14,12 @@ * limitations under the License. */ -package sample.actuator; - -import java.util.Map; +package sample.actuator.customsecurity; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.web.server.LocalManagementPort; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -39,17 +35,14 @@ import static org.assertj.core.api.Assertions.assertThat; * Integration tests for separate management and main service ports. * * @author Dave Syer + * @author Madhura Bhave */ @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { - "management.port=0", "management.context-path=/admin", - "management.security.enabled=false" }) + "management.port=0", "management.context-path=/admin"}) @DirtiesContext public class InsecureManagementPortAndPathSampleActuatorApplicationTests { - @Autowired - private SecurityProperties security; - @LocalServerPort private int port = 9010; @@ -59,27 +52,24 @@ public class InsecureManagementPortAndPathSampleActuatorApplicationTests { @Test public void testHome() throws Exception { @SuppressWarnings("rawtypes") - ResponseEntity entity = new TestRestTemplate("user", getPassword()) - .getForEntity("http://localhost:" + this.port, Map.class); + ResponseEntity entity = new TestRestTemplate("user", "password") + .getForEntity("http://localhost:" + this.port, String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - @SuppressWarnings("unchecked") - Map body = entity.getBody(); - assertThat(body.get("message")).isEqualTo("Hello Phil"); + assertThat(entity.getBody()).contains("Hello World"); } @Test - public void testMetrics() throws Exception { - testHome(); // makes sure some requests have been made - @SuppressWarnings("rawtypes") - ResponseEntity entity = new TestRestTemplate().getForEntity( - "http://localhost:" + this.managementPort + "/admin/metrics", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + public void testSecureActuator() throws Exception { + ResponseEntity entity = new TestRestTemplate().getForEntity( + "http://localhost:" + this.managementPort + "/admin/health", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test - public void testHealth() throws Exception { + public void testInsecureActuator() throws Exception { ResponseEntity entity = new TestRestTemplate().getForEntity( - "http://localhost:" + this.managementPort + "/admin/health", + "http://localhost:" + this.managementPort + "/admin/status", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("\"status\":\"UP\""); @@ -87,15 +77,11 @@ public class InsecureManagementPortAndPathSampleActuatorApplicationTests { @Test public void testMissing() throws Exception { - ResponseEntity entity = new TestRestTemplate().getForEntity( + ResponseEntity entity = new TestRestTemplate("admin", "admin").getForEntity( "http://localhost:" + this.managementPort + "/admin/missing", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(entity.getBody()).contains("\"status\":404"); } - private String getPassword() { - return this.security.getUser().getPassword(); - } - } diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java new file mode 100644 index 0000000000..2e86ff7b63 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java @@ -0,0 +1,75 @@ +package sample.actuator.customsecurity; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Madhura Bhave + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext +public class SampleActuatorCustomSecurityApplicationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void homeIsSecure() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = this.restTemplate.getForEntity("/", Map.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + @SuppressWarnings("unchecked") + Map body = entity.getBody(); + assertThat(body.get("error")).isEqualTo("Unauthorized"); + assertThat(entity.getHeaders()).doesNotContainKey("Set-Cookie"); + } + + @Test + public void testInsecureApplicationPath() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = this.restTemplate.getForEntity("/foo", Map.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + @SuppressWarnings("unchecked") + Map body = entity.getBody(); + assertThat((String)body.get("message")).contains("Expected exception in controller"); + } + + @Test + public void testInsecureStaticResources() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = this.restTemplate.getForEntity("/css/bootstrap.min.css", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("body"); + } + + @Test + public void insecureActuator() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = this.restTemplate.getForEntity("/application/status", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + } + + @Test + public void secureActuator() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = this.restTemplate.getForEntity("/application/env", + Map.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-cors.properties b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/resources/application-cors.properties similarity index 100% rename from spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-cors.properties rename to spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/resources/application-cors.properties diff --git a/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/java/sample/actuator/log4j2/SampleActuatorLog4J2Application.java b/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/java/sample/actuator/log4j2/SampleActuatorLog4J2Application.java index 84faa97ddf..5455223c41 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/java/sample/actuator/log4j2/SampleActuatorLog4J2Application.java +++ b/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/java/sample/actuator/log4j2/SampleActuatorLog4J2Application.java @@ -18,10 +18,21 @@ package sample.actuator.log4j2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; @SpringBootApplication public class SampleActuatorLog4J2Application { + @Bean + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user").password("password").roles("USER").build()); + return manager; + } + public static void main(String[] args) throws Exception { SpringApplication.run(SampleActuatorLog4J2Application.class, args); } diff --git a/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/resources/application.properties index efe0d6cd97..2bb0991cef 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/resources/application.properties @@ -1,3 +1,2 @@ endpoints.shutdown.enabled=true - -management.security.enabled=false \ No newline at end of file +endpoints.all.web.enabled=true \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/test/java/sample/actuator/log4j2/SampleActuatorLog4J2ApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/test/java/sample/actuator/log4j2/SampleActuatorLog4J2ApplicationTests.java index fe9af5c77a..a63bdcd006 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/test/java/sample/actuator/log4j2/SampleActuatorLog4J2ApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/test/java/sample/actuator/log4j2/SampleActuatorLog4J2ApplicationTests.java @@ -16,6 +16,8 @@ package sample.actuator.log4j2; +import java.util.Base64; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.Rule; @@ -63,10 +65,17 @@ public class SampleActuatorLog4J2ApplicationTests { @Test public void validateLoggersEndpoint() throws Exception { - this.mvc.perform(get("/application/loggers/org.apache.coyote.http11.Http11NioProtocol")) + this.mvc.perform(get("/application/loggers/org.apache.coyote.http11.Http11NioProtocol") + .header("Authorization", "Basic " + getBasicAuth())) .andExpect(status().isOk()) .andExpect(content().string(equalTo("{\"configuredLevel\":\"WARN\"," + "\"effectiveLevel\":\"WARN\"}"))); } + private String getBasicAuth() { + return new String(Base64.getEncoder() + .encode(("user:password").getBytes())); + } + } + diff --git a/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/java/sample/actuator/ui/SampleActuatorUiApplication.java b/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/java/sample/actuator/ui/SampleActuatorUiApplication.java index e85d9422ef..f36399f132 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/java/sample/actuator/ui/SampleActuatorUiApplication.java +++ b/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/java/sample/actuator/ui/SampleActuatorUiApplication.java @@ -21,6 +21,10 @@ import java.util.Map; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -29,6 +33,13 @@ import org.springframework.web.bind.annotation.RequestMapping; @Controller public class SampleActuatorUiApplication { + @Bean + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user").password("password").roles("USER").build()); + return manager; + } + @GetMapping("/") public String home(Map model) { model.put("message", "Hello World"); diff --git a/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/resources/application.properties index 1c253a9f6c..ed6b8f0368 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/resources/application.properties @@ -1,4 +1,2 @@ health.diskspace.enabled=false - -# empty so home page is unsecured -security.basic.path= \ No newline at end of file +endpoints.all.web.enabled=true \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/sample/actuator/ui/SampleActuatorUiApplicationPortTests.java b/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/sample/actuator/ui/SampleActuatorUiApplicationPortTests.java index 2eeb4e8bd2..6a5815da1a 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/sample/actuator/ui/SampleActuatorUiApplicationPortTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/sample/actuator/ui/SampleActuatorUiApplicationPortTests.java @@ -21,9 +21,7 @@ import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.web.server.LocalManagementPort; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -46,9 +44,6 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class SampleActuatorUiApplicationPortTests { - @Autowired - private SecurityProperties security; - @LocalServerPort private int port = 9010; @@ -82,7 +77,7 @@ public class SampleActuatorUiApplicationPortTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/sample/actuator/ui/SampleActuatorUiApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/sample/actuator/ui/SampleActuatorUiApplicationTests.java index 63185b1071..9f373d9435 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/sample/actuator/ui/SampleActuatorUiApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/sample/actuator/ui/SampleActuatorUiApplicationTests.java @@ -54,7 +54,8 @@ public class SampleActuatorUiApplicationTests { public void testHome() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - ResponseEntity entity = this.restTemplate.exchange("/", HttpMethod.GET, + ResponseEntity entity = this.restTemplate.withBasicAuth("user", getPassword()) + .exchange("/", HttpMethod.GET, new HttpEntity(headers), String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("Hello"); @@ -80,11 +81,16 @@ public class SampleActuatorUiApplicationTests { public void testError() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - ResponseEntity<String> entity = this.restTemplate.exchange("/error", + ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", getPassword()) + .exchange("/error", HttpMethod.GET, new HttpEntity<Void>(headers), String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(entity.getBody()).contains("<html>").contains("<body>") .contains("Please contact the operator with the above information"); } + private String getPassword() { + return "password"; + } + } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/main/java/sample/actuator/SampleActuatorApplication.java b/spring-boot-samples/spring-boot-sample-actuator/src/main/java/sample/actuator/SampleActuatorApplication.java index f0a111839c..44930055de 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/main/java/sample/actuator/SampleActuatorApplication.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/main/java/sample/actuator/SampleActuatorApplication.java @@ -22,6 +22,10 @@ import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; @SpringBootApplication @EnableConfigurationProperties(ServiceProperties.class) @@ -31,6 +35,13 @@ public class SampleActuatorApplication { SpringApplication.run(SampleActuatorApplication.class, args); } + @Bean + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user").password("password").roles("USER").build()); + return manager; + } + @Bean public HealthIndicator helloHealthIndicator() { return new HealthIndicator() { diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties index a9cd72f2c9..68cf4d46ab 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties @@ -11,6 +11,7 @@ server.tomcat.accesslog.pattern=%h %t "%r" %s %b security.require-ssl=false #spring.jackson.serialization.INDENT_OUTPUT=true spring.jmx.enabled=true +endpoints.all.web.enabled=true spring.jackson.serialization.write_dates_as_timestamps=false diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java index 4a4863d454..61d358a517 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java @@ -22,7 +22,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -45,9 +44,6 @@ import static org.assertj.core.api.Assertions.assertThat; @ActiveProfiles("endpoints") public class EndpointsPropertiesSampleActuatorApplicationTests { - @Autowired - private SecurityProperties security; - @Autowired private TestRestTemplate restTemplate; @@ -74,7 +70,7 @@ public class EndpointsPropertiesSampleActuatorApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureManagementSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureManagementSampleActuatorApplicationTests.java deleted file mode 100644 index bcf7a4e65b..0000000000 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureManagementSampleActuatorApplicationTests.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 sample.actuator; - -import java.util.Map; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for insecured service endpoints (even with Spring Security on - * classpath). - * - * @author Dave Syer - */ -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { - "management.security.enabled:false" }) -@DirtiesContext -@ActiveProfiles("unsecure-management") -public class InsecureManagementSampleActuatorApplicationTests { - - @Autowired - private TestRestTemplate restTemplate; - - @Test - public void testHomeIsSecure() throws Exception { - @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = this.restTemplate.getForEntity("/", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - @SuppressWarnings("unchecked") - Map<String, Object> body = entity.getBody(); - assertThat(body.get("error")).isEqualTo("Unauthorized"); - assertThat(entity.getHeaders()).doesNotContainKey("Set-Cookie"); - } - - @Test - public void testMetrics() throws Exception { - try { - testHomeIsSecure(); // makes sure some requests have been made - } - catch (AssertionError ex) { - // ignore; - } - @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = this.restTemplate.getForEntity("/application/metrics", - Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - @SuppressWarnings("unchecked") - Map<String, Object> body = entity.getBody(); - assertThat(body).containsKey("counter.status.401.unmapped"); - } - -} diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureSampleActuatorApplicationTests.java deleted file mode 100644 index aba7cc15d4..0000000000 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/InsecureSampleActuatorApplicationTests.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 sample.actuator; - -import java.util.Map; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for insecured service endpoints (even with Spring Security on - * classpath). - * - * @author Dave Syer - */ -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { - "security.basic.enabled:false" }) -@DirtiesContext -public class InsecureSampleActuatorApplicationTests { - - @Autowired - private TestRestTemplate restTemplate; - - @Test - public void testHome() throws Exception { - @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = this.restTemplate.getForEntity("/", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - @SuppressWarnings("unchecked") - Map<String, Object> body = entity.getBody(); - assertThat(body.get("message")).isEqualTo("Hello Phil"); - assertThat(entity.getHeaders()).doesNotContainKey("Set-Cookie"); - } - -} diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementAddressActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementAddressActuatorApplicationTests.java index 23b8e4d5f2..3499b76d7b 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementAddressActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementAddressActuatorApplicationTests.java @@ -21,9 +21,7 @@ import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.web.server.LocalManagementPort; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -47,9 +45,6 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class ManagementAddressActuatorApplicationTests { - @Autowired - private SecurityProperties security; - @LocalServerPort private int port = 9010; @@ -75,7 +70,7 @@ public class ManagementAddressActuatorApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPathSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPathSampleActuatorApplicationTests.java index 76a84289ec..169f4db003 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPathSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPathSampleActuatorApplicationTests.java @@ -22,7 +22,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -47,9 +46,6 @@ public class ManagementPathSampleActuatorApplicationTests { @Autowired private TestRestTemplate restTemplate; - @Autowired - private SecurityProperties securityProperties; - @Test public void testHealth() throws Exception { ResponseEntity<String> entity = this.restTemplate @@ -71,7 +67,7 @@ public class ManagementPathSampleActuatorApplicationTests { } private String getPassword() { - return this.securityProperties.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPortAndPathSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPortAndPathSampleActuatorApplicationTests.java index 24f50444c4..67440a15bf 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPortAndPathSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPortAndPathSampleActuatorApplicationTests.java @@ -21,9 +21,7 @@ import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.web.server.LocalManagementPort; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -46,9 +44,6 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class ManagementPortAndPathSampleActuatorApplicationTests { - @Autowired - private SecurityProperties security; - @LocalServerPort private int port = 9010; @@ -98,7 +93,7 @@ public class ManagementPortAndPathSampleActuatorApplicationTests { @Test public void testErrorPage() throws Exception { @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = new TestRestTemplate() + ResponseEntity<Map> entity = new TestRestTemplate("user", getPassword()) .getForEntity("http://localhost:" + this.port + "/error", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); @SuppressWarnings("unchecked") @@ -109,7 +104,7 @@ public class ManagementPortAndPathSampleActuatorApplicationTests { @Test public void testManagementErrorPage() throws Exception { @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity( + ResponseEntity<Map> entity = new TestRestTemplate("user", getPassword()).getForEntity( "http://localhost:" + this.managementPort + "/error", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); @SuppressWarnings("unchecked") @@ -118,7 +113,7 @@ public class ManagementPortAndPathSampleActuatorApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPortSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPortSampleActuatorApplicationTests.java index 863c2d0e00..0aca403e3d 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPortSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ManagementPortSampleActuatorApplicationTests.java @@ -18,14 +18,14 @@ package sample.actuator; import java.util.Map; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.web.server.LocalManagementPort; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.rule.OutputCapture; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.http.HttpStatus; @@ -46,9 +46,6 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class ManagementPortSampleActuatorApplicationTests { - @Autowired - private SecurityProperties security; - @LocalServerPort private int port = 9010; @@ -89,7 +86,7 @@ public class ManagementPortSampleActuatorApplicationTests { @Test public void testErrorPage() throws Exception { @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity( + ResponseEntity<Map> entity = new TestRestTemplate("user", getPassword()).getForEntity( "http://localhost:" + this.managementPort + "/error", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); @SuppressWarnings("unchecked") @@ -98,7 +95,7 @@ public class ManagementPortSampleActuatorApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/NoManagementSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/NoManagementSampleActuatorApplicationTests.java index 6e63518c8f..f3b5178d4a 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/NoManagementSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/NoManagementSampleActuatorApplicationTests.java @@ -22,7 +22,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -44,9 +43,6 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class NoManagementSampleActuatorApplicationTests { - @Autowired - private SecurityProperties security; - @Autowired private TestRestTemplate restTemplate; @@ -71,7 +67,7 @@ public class NoManagementSampleActuatorApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java index 8ca1ca437a..aed5cd189f 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java @@ -25,7 +25,6 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -51,9 +50,6 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class SampleActuatorApplicationTests { - @Autowired - private SecurityProperties security; - @Autowired private TestRestTemplate restTemplate; @@ -204,7 +200,8 @@ public class SampleActuatorApplicationTests { @Test public void testErrorPageDirectAccess() throws Exception { @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = this.restTemplate.getForEntity("/error", Map.class); + ResponseEntity<Map> entity = this.restTemplate.withBasicAuth("user", getPassword()) + .getForEntity("/error", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); @SuppressWarnings("unchecked") Map<String, Object> body = entity.getBody(); @@ -239,7 +236,7 @@ public class SampleActuatorApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathInsecureSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathInsecureSampleActuatorApplicationTests.java deleted file mode 100644 index f58f3b37a9..0000000000 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathInsecureSampleActuatorApplicationTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 sample.actuator; - -import java.util.Map; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for insecured service endpoints (even with Spring Security on - * classpath). - * - * @author Dave Syer - */ -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { - "security.basic.enabled:false", "server.servlet.path:/spring" }) -@DirtiesContext -public class ServletPathInsecureSampleActuatorApplicationTests { - - @Autowired - private TestRestTemplate restTemplate; - - @Test - public void testHome() throws Exception { - @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = this.restTemplate.getForEntity("/spring/", - Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - @SuppressWarnings("unchecked") - Map<String, Object> body = entity.getBody(); - assertThat(body.get("message")).isEqualTo("Hello Phil"); - assertThat(entity.getHeaders()).doesNotContainKey("Set-Cookie"); - } - - @Test - public void testMetricsIsSecure() throws Exception { - @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = this.restTemplate - .getForEntity("/spring/application/metrics", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - } - -} diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathSampleActuatorApplicationTests.java index 8515586d94..c4ccff1ffb 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathSampleActuatorApplicationTests.java @@ -22,7 +22,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -47,13 +46,11 @@ public class ServletPathSampleActuatorApplicationTests { @Autowired private TestRestTemplate restTemplate; - @Autowired - private SecurityProperties security; - @Test public void testErrorPath() throws Exception { @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = this.restTemplate.getForEntity("/spring/error", + ResponseEntity<Map> entity = this.restTemplate.withBasicAuth("user", getPassword()) + .getForEntity("/spring/error", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); @SuppressWarnings("unchecked") @@ -84,7 +81,7 @@ public class ServletPathSampleActuatorApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ShutdownSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ShutdownSampleActuatorApplicationTests.java index aa13d9ccfb..2866ac445f 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ShutdownSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ShutdownSampleActuatorApplicationTests.java @@ -22,7 +22,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -43,9 +42,6 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class ShutdownSampleActuatorApplicationTests { - @Autowired - private SecurityProperties security; - @Autowired private TestRestTemplate restTemplate; @@ -73,7 +69,7 @@ public class ShutdownSampleActuatorApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-endpoints.properties b/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-endpoints.properties index 9803d5a610..d4a059f105 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-endpoints.properties +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-endpoints.properties @@ -1,3 +1,2 @@ server.error.path: /oops -management.context-path: /admin -endpoints.health.sensitive: false \ No newline at end of file +management.context-path: /admin \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/ActuatorSecurityConfiguration.java b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/ActuatorSecurityConfiguration.java new file mode 100644 index 0000000000..3cda2c153c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/ActuatorSecurityConfiguration.java @@ -0,0 +1,29 @@ +package sample.secure.oauth2.actuator; + +import org.springframework.boot.autoconfigure.security.SpringBootSecurity; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * Basic auth security for actuator endpoints. + * + * @author Madhura Bhave + */ +@Configuration +public class ActuatorSecurityConfiguration extends WebSecurityConfigurerAdapter { + + private final SpringBootSecurity springBootSecurity; + + public ActuatorSecurityConfiguration(SpringBootSecurity springBootSecurity) { + this.springBootSecurity = springBootSecurity; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.requestMatcher(this.springBootSecurity.endpointIds(SpringBootSecurity.ALL_ENDPOINTS)) + .authorizeRequests().antMatchers("/**").authenticated() + .and() + .httpBasic(); + } +} diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/SampleSecureOAuth2ActuatorApplication.java b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/SampleSecureOAuth2ActuatorApplication.java index 40bafd66f8..1bef7723a4 100644 --- a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/SampleSecureOAuth2ActuatorApplication.java +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/actuator/SampleSecureOAuth2ActuatorApplication.java @@ -20,7 +20,11 @@ import java.util.UUID; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -34,6 +38,13 @@ public class SampleSecureOAuth2ActuatorApplication { return new Message("Hello World"); } + @Bean + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user").password("password").roles("USER").build()); + return manager; + } + public static void main(String[] args) { SpringApplication.run(SampleSecureOAuth2ActuatorApplication.class, args); } diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/resources/application.properties index ac60cbc595..4779edfcd9 100644 --- a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/resources/application.properties @@ -1,6 +1,5 @@ server.port=8081 -security.basic.enabled=true -security.user.password=password +endpoints.all.web.enabled=true security.oauth2.resource.id=service security.oauth2.resource.userInfoUri=http://localhost:8080/user logging.level.org.springframework.security=DEBUG diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/secure/oauth2/AuthenticationConfiguration.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/secure/oauth2/AuthenticationConfiguration.java new file mode 100644 index 0000000000..81ca2cff77 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/secure/oauth2/AuthenticationConfiguration.java @@ -0,0 +1,17 @@ +package sample.secure.oauth2; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter; + +/** + * @author Madhura Bhave + */ +@Configuration +public class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter { + + @Override + public void init(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser("greg").password("turnquist").roles("read"); + } +} diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties index 6ab81af5db..517152df98 100644 --- a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties @@ -1,7 +1,5 @@ spring.datasource.platform=h2 -security.user.name=greg -security.user.password=turnquist security.oauth2.client.client-id=foo security.oauth2.client.client-secret=bar security.oauth2.authorization.checkTokenAccess=isAuthenticated() diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java b/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java index 7f2c3129ef..a901c22c0a 100644 --- a/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java +++ b/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java @@ -20,11 +20,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; @EnableAutoConfiguration @ComponentScan @@ -34,6 +38,13 @@ public class SampleSecureApplication implements CommandLineRunner { @Autowired private SampleService service; + @Bean + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user").password("password").roles("USER").build()); + return manager; + } + @Override public void run(String... args) throws Exception { SecurityContextHolder.getContext() diff --git a/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java b/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java index cdb339f609..c8d2b481b0 100644 --- a/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java @@ -20,15 +20,10 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import sample.secure.SampleSecureApplicationTests.TestConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -43,23 +38,17 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Dave Syer */ @RunWith(SpringRunner.class) -@SpringBootTest(classes = { SampleSecureApplication.class, TestConfiguration.class }) +@SpringBootTest(classes = {SampleSecureApplication.class}) public class SampleSecureApplicationTests { @Autowired private SampleService service; - @Autowired - private ApplicationContext context; - private Authentication authentication; @Before public void init() { - AuthenticationManager authenticationManager = this.context - .getBean(AuthenticationManager.class); - this.authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken("user", "password")); + this.authentication = new UsernamePasswordAuthenticationToken("user", "password"); } @After @@ -90,9 +79,4 @@ public class SampleSecureApplicationTests { assertThat("Goodbye World").isEqualTo(this.service.denied()); } - @PropertySource("classpath:test.properties") - @Configuration - protected static class TestConfiguration { - } - } diff --git a/spring-boot-samples/spring-boot-sample-secure/src/test/resources/test.properties b/spring-boot-samples/spring-boot-sample-secure/src/test/resources/test.properties index 2eef63c566..e69de29bb2 100644 --- a/spring-boot-samples/spring-boot-sample-secure/src/test/resources/test.properties +++ b/spring-boot-samples/spring-boot-sample-secure/src/test/resources/test.properties @@ -1 +0,0 @@ -security.user.password=password \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-servlet/src/main/java/sample/servlet/SampleServletApplication.java b/spring-boot-samples/spring-boot-sample-servlet/src/main/java/sample/servlet/SampleServletApplication.java index 0a68c83801..8dbbcfb59c 100644 --- a/spring-boot-samples/spring-boot-sample-servlet/src/main/java/sample/servlet/SampleServletApplication.java +++ b/spring-boot-samples/spring-boot-sample-servlet/src/main/java/sample/servlet/SampleServletApplication.java @@ -30,11 +30,21 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.annotation.Bean; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; @SpringBootConfiguration @EnableAutoConfiguration public class SampleServletApplication extends SpringBootServletInitializer { + @Bean + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user").password("password").roles("USER").build()); + return manager; + } + @SuppressWarnings("serial") @Bean public Servlet dispatcherServlet() { diff --git a/spring-boot-samples/spring-boot-sample-servlet/src/test/java/sample/servlet/SampleServletApplicationTests.java b/spring-boot-samples/spring-boot-sample-servlet/src/test/java/sample/servlet/SampleServletApplicationTests.java index b36fc2653a..6861e118de 100644 --- a/spring-boot-samples/spring-boot-sample-servlet/src/test/java/sample/servlet/SampleServletApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-servlet/src/test/java/sample/servlet/SampleServletApplicationTests.java @@ -16,15 +16,20 @@ package sample.servlet; +import java.util.Collections; + import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; @@ -44,12 +49,11 @@ public class SampleServletApplicationTests { @Autowired private TestRestTemplate restTemplate; - @Autowired - private SecurityProperties security; - @Test public void testHomeIsSecure() throws Exception { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity<String> entity = this.restTemplate.exchange("/", HttpMethod.GET, new HttpEntity<Void>(headers), String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @@ -62,6 +66,6 @@ public class SampleServletApplicationTests { } private String getPassword() { - return this.security.getUser().getPassword(); + return "password"; } } 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 613dd7692a..5a459bdc1f 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 @@ -20,7 +20,7 @@ import java.util.Date; import java.util.Map; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.SpringBootSecurity; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -80,7 +80,6 @@ public class SampleMethodSecurityApplication implements WebMvcConfigurer { } @Configuration - @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) protected static class ApplicationSecurity extends WebSecurityConfigurerAdapter { @Override @@ -94,4 +93,25 @@ public class SampleMethodSecurityApplication implements WebMvcConfigurer { } + @Configuration + @Order(1) + protected static class ActuatorSecurity extends WebSecurityConfigurerAdapter { + + private final SpringBootSecurity springBootSecurity; + + public ActuatorSecurity(SpringBootSecurity springBootSecurity) { + this.springBootSecurity = springBootSecurity; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .requestMatcher(this.springBootSecurity.endpointIds(SpringBootSecurity.ALL_ENDPOINTS)) + .authorizeRequests().anyRequest().authenticated() + .and() + .httpBasic(); + } + + } + } diff --git a/spring-boot-samples/spring-boot-sample-web-method-security/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-web-method-security/src/main/resources/application.properties index c19741f86f..21bb689e99 100644 --- a/spring-boot-samples/spring-boot-sample-web-method-security/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-web-method-security/src/main/resources/application.properties @@ -1,2 +1,3 @@ spring.thymeleaf.cache: false -logging.level.org.springframework.security: INFO \ No newline at end of file +logging.level.org.springframework.security: INFO +endpoints.all.web.enabled=true \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-web-method-security/src/test/java/sample/security/method/SampleMethodSecurityApplicationTests.java b/spring-boot-samples/spring-boot-sample-web-method-security/src/test/java/sample/security/method/SampleMethodSecurityApplicationTests.java index 27087525f2..fee2546c87 100644 --- a/spring-boot-samples/spring-boot-sample-web-method-security/src/test/java/sample/security/method/SampleMethodSecurityApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-web-method-security/src/test/java/sample/security/method/SampleMethodSecurityApplicationTests.java @@ -105,8 +105,10 @@ public class SampleMethodSecurityApplicationTests { @Test public void testManagementProtected() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); ResponseEntity<String> entity = this.restTemplate - .getForEntity("/application/beans", String.class); + .exchange("/application/beans", HttpMethod.GET, new HttpEntity<Void>(headers), String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } diff --git a/spring-boot-samples/spring-boot-sample-web-secure-custom/src/main/java/sample/web/secure/custom/SampleWebSecureCustomApplication.java b/spring-boot-samples/spring-boot-sample-web-secure-custom/src/main/java/sample/web/secure/custom/SampleWebSecureCustomApplication.java index 8c20144931..7d6f9f3fe0 100644 --- a/spring-boot-samples/spring-boot-sample-web-secure-custom/src/main/java/sample/web/secure/custom/SampleWebSecureCustomApplication.java +++ b/spring-boot-samples/spring-boot-sample-web-secure-custom/src/main/java/sample/web/secure/custom/SampleWebSecureCustomApplication.java @@ -20,10 +20,8 @@ import java.util.Date; import java.util.Map; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -60,7 +58,6 @@ public class SampleWebSecureCustomApplication implements WebMvcConfigurer { } @Configuration - @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) protected static class ApplicationSecurity extends WebSecurityConfigurerAdapter { @Override diff --git a/spring-boot-samples/spring-boot-sample-web-secure-jdbc/src/main/java/sample/web/secure/jdbc/SampleWebSecureJdbcApplication.java b/spring-boot-samples/spring-boot-sample-web-secure-jdbc/src/main/java/sample/web/secure/jdbc/SampleWebSecureJdbcApplication.java index ffa4575063..17d3557f1b 100644 --- a/spring-boot-samples/spring-boot-sample-web-secure-jdbc/src/main/java/sample/web/secure/jdbc/SampleWebSecureJdbcApplication.java +++ b/spring-boot-samples/spring-boot-sample-web-secure-jdbc/src/main/java/sample/web/secure/jdbc/SampleWebSecureJdbcApplication.java @@ -23,10 +23,8 @@ import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -63,7 +61,6 @@ public class SampleWebSecureJdbcApplication implements WebMvcConfigurer { } @Configuration - @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) protected static class ApplicationSecurity extends WebSecurityConfigurerAdapter { @Autowired diff --git a/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/web/secure/SampleWebSecureApplication.java b/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/web/secure/SampleWebSecureApplication.java index 89dafe3c63..46547a0431 100644 --- a/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/web/secure/SampleWebSecureApplication.java +++ b/spring-boot-samples/spring-boot-sample-web-secure/src/main/java/sample/web/secure/SampleWebSecureApplication.java @@ -20,10 +20,9 @@ import java.util.Date; import java.util.Map; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.SpringBootSecurity; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -60,12 +59,19 @@ public class SampleWebSecureApplication implements WebMvcConfigurer { } @Configuration - @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) protected static class ApplicationSecurity extends WebSecurityConfigurerAdapter { + private final SpringBootSecurity springBootSecurity; + + public ApplicationSecurity(SpringBootSecurity springBootSecurity) { + this.springBootSecurity = springBootSecurity; + } + @Override protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests().anyRequest().fullyAuthenticated().and().formLogin() + http.authorizeRequests() + .requestMatchers(this.springBootSecurity.staticResources()).permitAll() + .anyRequest().fullyAuthenticated().and().formLogin() .loginPage("/login").failureUrl("/login?error").permitAll().and() .logout().permitAll(); } diff --git a/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/application.properties index e1935fa698..0eef6747e4 100644 --- a/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-web-secure/src/main/resources/application.properties @@ -1,6 +1,5 @@ spring.thymeleaf.cache: false security.basic.enabled: false # demo only: -security.user.password: password logging.level.org.springframework.security: INFO logging.level.org.springframework.boot.actuate.audit.listener.AuditListener: DEBUG \ No newline at end of file diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/security/MockMvcSecurityAutoConfigurationIntegrationTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/security/MockMvcSecurityAutoConfigurationIntegrationTests.java index 29901612b6..8d596764ce 100644 --- a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/security/MockMvcSecurityAutoConfigurationIntegrationTests.java +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/security/MockMvcSecurityAutoConfigurationIntegrationTests.java @@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; @@ -39,7 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @WebMvcTest @RunWith(SpringRunner.class) -@TestPropertySource(properties = { "security.user.password=secret", "debug=true" }) +@TestPropertySource(properties = { "debug=true" }) public class MockMvcSecurityAutoConfigurationIntegrationTests { @Autowired @@ -53,7 +54,8 @@ public class MockMvcSecurityAutoConfigurationIntegrationTests { @Test public void unauthorizedResponseWithNoUser() throws Exception { - this.mockMvc.perform(get("/")).andExpect(status().isUnauthorized()); + this.mockMvc.perform(get("/") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized()); } @Test diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/security/SecurityTestApplication.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/security/SecurityTestApplication.java index 5672a782d2..26150503b6 100644 --- a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/security/SecurityTestApplication.java +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/security/SecurityTestApplication.java @@ -18,7 +18,11 @@ package org.springframework.boot.test.autoconfigure.security; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -30,6 +34,13 @@ import org.springframework.web.bind.annotation.RestController; @SpringBootApplication public class SecurityTestApplication { + @Bean + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user").password("secret").roles("USER").build()); + return manager; + } + @RestController static class MyController { diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/DefaultEndpointPathResolver.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/DefaultEndpointPathResolver.java new file mode 100644 index 0000000000..eb69142354 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/DefaultEndpointPathResolver.java @@ -0,0 +1,33 @@ +/* + * 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.endpoint; + +/** + * {@link EndpointPathResolver} implementation that does not support + * resolving endpoint paths. + * + * @author Madhura Bhave + */ +public class DefaultEndpointPathResolver implements EndpointPathResolver { + + @Override + public String resolvePath(String endpointId) { + throw new UnsupportedOperationException("Not supported: Endpoints not available"); + } + +} + diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/IgnoredRequestCustomizer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointPathResolver.java similarity index 54% rename from spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/IgnoredRequestCustomizer.java rename to spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointPathResolver.java index a5d750dd09..d468f8f218 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/IgnoredRequestCustomizer.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointPathResolver.java @@ -14,24 +14,24 @@ * limitations under the License. */ -package org.springframework.boot.autoconfigure.security; - -import org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer; +package org.springframework.boot.endpoint; /** - * Customizer that can be implemented by beans to configure paths that need to be ignored - * by Spring Boot's default Spring Security configuration. + * Strategy interface that can be used to resolve endpoint paths + * based on endpoint id. * * @author Madhura Bhave - * @since 1.5.0 + * @since 2.0.0 */ -@FunctionalInterface -public interface IgnoredRequestCustomizer { +public interface EndpointPathResolver { /** - * Customize the provided {@link IgnoredRequestConfigurer}. - * @param configurer the configurer to customize + * Resolve endpoint path based on id. + * + * @param endpointId the endpoint id + * @return the resolved path */ - void customize(IgnoredRequestConfigurer configurer); + String resolvePath(String endpointId); } + diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/DefaultEndpointPathResolverTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/DefaultEndpointPathResolverTests.java new file mode 100644 index 0000000000..1ead8ffb1f --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/DefaultEndpointPathResolverTests.java @@ -0,0 +1,40 @@ +/* + * 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.endpoint; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * Tests for {@link DefaultEndpointPathResolver}. + * + * @author Madhura Bhave + */ +public class DefaultEndpointPathResolverTests { + + private DefaultEndpointPathResolver resolver = new DefaultEndpointPathResolver(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void resolveShouldThrowException() throws Exception { + this.thrown.expect(UnsupportedOperationException.class); + this.resolver.resolvePath("my-id"); + } +}