Commit 24f5125a authored by Phillip Webb's avatar Phillip Webb

Merge branch '1.5.x'

parents 634dd41b 530c3cd3
...@@ -54,7 +54,6 @@ import org.springframework.context.annotation.ConditionContext; ...@@ -54,7 +54,6 @@ import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
...@@ -162,7 +161,7 @@ public class EndpointWebMvcManagementContextConfiguration { ...@@ -162,7 +161,7 @@ public class EndpointWebMvcManagementContextConfiguration {
@ConditionalOnEnabledEndpoint("health") @ConditionalOnEnabledEndpoint("health")
public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) { public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) {
HealthMvcEndpoint healthMvcEndpoint = new HealthMvcEndpoint(delegate, HealthMvcEndpoint healthMvcEndpoint = new HealthMvcEndpoint(delegate,
isHealthSecure()); this.managementServerProperties.getSecurity().isEnabled());
if (this.healthMvcEndpointProperties.getMapping() != null) { if (this.healthMvcEndpointProperties.getMapping() != null) {
healthMvcEndpoint healthMvcEndpoint
.addStatusMapping(this.healthMvcEndpointProperties.getMapping()); .addStatusMapping(this.healthMvcEndpointProperties.getMapping());
...@@ -206,17 +205,6 @@ public class EndpointWebMvcManagementContextConfiguration { ...@@ -206,17 +205,6 @@ public class EndpointWebMvcManagementContextConfiguration {
return new AuditEventsMvcEndpoint(auditEventRepository); return new AuditEventsMvcEndpoint(auditEventRepository);
} }
private boolean isHealthSecure() {
return isSpringSecurityAvailable()
&& this.managementServerProperties.getSecurity().isEnabled();
}
private boolean isSpringSecurityAvailable() {
return ClassUtils.isPresent(
"org.springframework.security.config.annotation.web.WebSecurityConfigurer",
getClass().getClassLoader());
}
private static class LogFileCondition extends SpringBootCondition { private static class LogFileCondition extends SpringBootCondition {
@Override @Override
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
package org.springframework.boot.actuate.cloudfoundry; package org.springframework.boot.actuate.cloudfoundry;
import java.security.Principal; import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint;
...@@ -36,7 +36,7 @@ class CloudFoundryHealthMvcEndpoint extends HealthMvcEndpoint { ...@@ -36,7 +36,7 @@ class CloudFoundryHealthMvcEndpoint extends HealthMvcEndpoint {
} }
@Override @Override
protected boolean exposeHealthDetails(Principal principal) { protected boolean exposeHealthDetails(HttpServletRequest request) {
return true; return true;
} }
......
...@@ -16,12 +16,11 @@ ...@@ -16,12 +16,11 @@
package org.springframework.boot.actuate.endpoint.mvc; package org.springframework.boot.actuate.endpoint.mvc;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status; import org.springframework.boot.actuate.health.Status;
...@@ -33,10 +32,7 @@ import org.springframework.core.env.Environment; ...@@ -33,10 +32,7 @@ import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
...@@ -49,6 +45,7 @@ import org.springframework.web.bind.annotation.ResponseBody; ...@@ -49,6 +45,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Madhura Bhave
* @since 1.1.0 * @since 1.1.0
*/ */
@ConfigurationProperties(prefix = "endpoints.health") @ConfigurationProperties(prefix = "endpoints.health")
...@@ -59,11 +56,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint ...@@ -59,11 +56,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
private Map<String, HttpStatus> statusMapping = new HashMap<String, HttpStatus>(); private Map<String, HttpStatus> statusMapping = new HashMap<String, HttpStatus>();
private RelaxedPropertyResolver healthPropertyResolver; private RelaxedPropertyResolver securityPropertyResolver;
private RelaxedPropertyResolver endpointPropertyResolver;
private RelaxedPropertyResolver roleResolver;
private long lastAccess = 0; private long lastAccess = 0;
...@@ -86,11 +79,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint ...@@ -86,11 +79,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
@Override @Override
public void setEnvironment(Environment environment) { public void setEnvironment(Environment environment) {
this.healthPropertyResolver = new RelaxedPropertyResolver(environment, this.securityPropertyResolver = new RelaxedPropertyResolver(environment,
"endpoints.health.");
this.endpointPropertyResolver = new RelaxedPropertyResolver(environment,
"endpoints.");
this.roleResolver = new RelaxedPropertyResolver(environment,
"management.security."); "management.security.");
} }
...@@ -136,12 +125,12 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint ...@@ -136,12 +125,12 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody @ResponseBody
public Object invoke(Principal principal) { public Object invoke(HttpServletRequest request) {
if (!getDelegate().isEnabled()) { if (!getDelegate().isEnabled()) {
// Shouldn't happen because the request mapping should not be registered // Shouldn't happen because the request mapping should not be registered
return getDisabledResponse(); return getDisabledResponse();
} }
Health health = getHealth(principal); Health health = getHealth(request);
HttpStatus status = getStatus(health); HttpStatus status = getStatus(health);
if (status != null) { if (status != null) {
return new ResponseEntity<Health>(health, status); return new ResponseEntity<Health>(health, status);
...@@ -163,13 +152,13 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint ...@@ -163,13 +152,13 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
return null; return null;
} }
private Health getHealth(Principal principal) { private Health getHealth(HttpServletRequest request) {
long accessTime = System.currentTimeMillis(); long accessTime = System.currentTimeMillis();
if (isCacheStale(accessTime)) { if (isCacheStale(accessTime)) {
this.lastAccess = accessTime; this.lastAccess = accessTime;
this.cached = getDelegate().invoke(); this.cached = getDelegate().invoke();
} }
if (exposeHealthDetails(principal)) { if (exposeHealthDetails(request)) {
return this.cached; return this.cached;
} }
return Health.status(this.cached.getStatus()).build(); return Health.status(this.cached.getStatus()).build();
...@@ -182,44 +171,19 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint ...@@ -182,44 +171,19 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
return (accessTime - this.lastAccess) >= getDelegate().getTimeToLive(); return (accessTime - this.lastAccess) >= getDelegate().getTimeToLive();
} }
protected boolean exposeHealthDetails(Principal principal) { protected boolean exposeHealthDetails(HttpServletRequest request) {
return isSecure(principal) || isUnrestricted(); if (!this.secure) {
} return true;
private boolean isSecure(Principal principal) {
if (principal == null || principal.getClass().getName().contains("Anonymous")) {
return false;
} }
if (isSpringSecurityAuthentication(principal)) { String[] roles = StringUtils.commaDelimitedListToStringArray(
Authentication authentication = (Authentication) principal; this.securityPropertyResolver.getProperty("roles", "ROLE_ACTUATOR"));
List<String> roles = Arrays.asList(StringUtils roles = StringUtils.trimArrayElements(roles);
.trimArrayElements(StringUtils.commaDelimitedListToStringArray( for (String role : roles) {
this.roleResolver.getProperty("roles", "ROLE_ACTUATOR")))); if (request.isUserInRole(role) || request.isUserInRole("ROLE_" + role)) {
for (GrantedAuthority authority : authentication.getAuthorities()) { return true;
String name = authority.getAuthority();
for (String role : roles) {
if (role.equals(name) || ("ROLE_" + role).equals(name)) {
return true;
}
}
} }
} }
return false; return false;
} }
private boolean isSpringSecurityAuthentication(Principal principal) {
return ClassUtils.isPresent("org.springframework.security.core.Authentication",
null) && (principal instanceof Authentication);
}
private boolean isUnrestricted() {
Boolean sensitive = this.healthPropertyResolver.getProperty("sensitive",
Boolean.class);
if (sensitive == null) {
sensitive = this.endpointPropertyResolver.getProperty("sensitive",
Boolean.class);
}
return !this.secure && !Boolean.TRUE.equals(sensitive);
}
} }
...@@ -85,6 +85,18 @@ ...@@ -85,6 +85,18 @@
"type": "java.util.Map<java.lang.String,java.lang.Object>", "type": "java.util.Map<java.lang.String,java.lang.Object>",
"description": "Arbitrary properties to add to the info endpoint." "description": "Arbitrary properties to add to the info endpoint."
}, },
{
"name": "management.cloudfoundry.enabled",
"type": "java.lang.Boolean",
"description": "Enable extended Cloud Foundry actuator endpoints.",
"defaultValue": true
},
{
"name": "management.cloudfoundry.skip-ssl-validation",
"type": "java.lang.Boolean",
"description": "Skip SSL verification for Cloud Foundry actuator endpoint security calls.",
"defaultValue": false
},
{ {
"name": "management.health.cassandra.enabled", "name": "management.health.cassandra.enabled",
"type": "java.lang.Boolean", "type": "java.lang.Boolean",
......
...@@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; ...@@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext; import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
...@@ -60,8 +61,9 @@ public class HealthMvcEndpointAutoConfigurationTests { ...@@ -60,8 +61,9 @@ public class HealthMvcEndpointAutoConfigurationTests {
this.context.setServletContext(new MockServletContext()); this.context.setServletContext(new MockServletContext());
this.context.register(TestConfiguration.class); this.context.register(TestConfiguration.class);
this.context.refresh(); this.context.refresh();
MockHttpServletRequest request = new MockHttpServletRequest();
Health health = (Health) this.context.getBean(HealthMvcEndpoint.class) Health health = (Health) this.context.getBean(HealthMvcEndpoint.class)
.invoke(null); .invoke(request);
assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails().get("foo")).isNull(); assertThat(health.getDetails().get("foo")).isNull();
} }
......
...@@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfi ...@@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfi
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.junit.runner.classpath.ClassPathExclusions; import org.springframework.boot.junit.runner.classpath.ClassPathExclusions;
import org.springframework.boot.junit.runner.classpath.ModifiedClassPathRunner; import org.springframework.boot.junit.runner.classpath.ModifiedClassPathRunner;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockServletContext; import org.springframework.mock.web.MockServletContext;
...@@ -48,6 +49,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. ...@@ -48,6 +49,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* Integration tests for the health endpoint when Spring Security is not available. * Integration tests for the health endpoint when Spring Security is not available.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Madhura Bhave
*/ */
@RunWith(ModifiedClassPathRunner.class) @RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("spring-security-*.jar") @ClassPathExclusions("spring-security-*.jar")
...@@ -61,14 +63,28 @@ public class NoSpringSecurityHealthMvcEndpointIntegrationTests { ...@@ -61,14 +63,28 @@ public class NoSpringSecurityHealthMvcEndpointIntegrationTests {
} }
@Test @Test
public void healthDetailIsPresent() throws Exception { public void healthDetailNotPresent() throws Exception {
this.context = new AnnotationConfigWebApplicationContext(); this.context = new AnnotationConfigWebApplicationContext();
this.context.setServletContext(new MockServletContext()); this.context.setServletContext(new MockServletContext());
this.context.register(TestConfiguration.class); this.context.register(TestConfiguration.class);
this.context.refresh(); this.context.refresh();
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
mockMvc.perform(get("/health")).andExpect(status().isOk()) mockMvc.perform(get("/health")).andExpect(status().isOk())
.andExpect(content().string(containsString("\"hello\":\"world\""))); .andExpect(content().string(containsString("\"status\":\"UP\"")));
}
@Test
public void healthDetailPresent() throws Exception {
this.context = new AnnotationConfigWebApplicationContext();
this.context.setServletContext(new MockServletContext());
this.context.register(TestConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"management.security.enabled:false");
this.context.refresh();
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
mockMvc.perform(get("/health")).andExpect(status().isOk())
.andExpect(content().string(containsString(
"\"status\":\"UP\",\"test\":{\"status\":\"UP\",\"hello\":\"world\"}")));
} }
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, @ImportAutoConfiguration({ JacksonAutoConfiguration.class,
......
...@@ -1056,6 +1056,8 @@ content into your application; rather pick only the properties that you need. ...@@ -1056,6 +1056,8 @@ content into your application; rather pick only the properties that you need.
management.add-application-context-header=true # Add the "X-Application-Context" HTTP header in each response. management.add-application-context-header=true # Add the "X-Application-Context" HTTP header in each response.
management.address= # Network address that the management endpoints should bind to. management.address= # Network address that the management endpoints should bind to.
management.context-path= # Management endpoint context-path. For instance `/actuator` management.context-path= # Management endpoint context-path. For instance `/actuator`
management.cloudfoundry.enabled= # Enable extended Cloud Foundry actuator endpoints
management.cloudfoundry.skip-ssl-validation= # Skip SSL verification for Cloud Foundry actuator endpoint security calls
management.port= # Management endpoint HTTP port. Uses the same port as the application by default. Configure a different port to use management-specific SSL. management.port= # Management endpoint HTTP port. Uses the same port as the application by default. Configure a different port to use management-specific SSL.
management.security.enabled=true # Enable security. management.security.enabled=true # Enable security.
management.security.roles=ACTUATOR # Comma-separated list of roles that can access the management endpoint. management.security.roles=ACTUATOR # Comma-separated list of roles that can access the management endpoint.
......
...@@ -545,7 +545,7 @@ buildscript { ...@@ -545,7 +545,7 @@ buildscript {
} }
springBoot { springBoot {
layoutFactory = new com.example.CustomLayoutFactory() layoutFactory = new com.example.CustomLayoutFactory()
} }
---- ----
......
...@@ -177,12 +177,12 @@ element): ...@@ -177,12 +177,12 @@ element):
[source,xml,indent=0] [source,xml,indent=0]
---- ----
<resources> <resources>
<resource> <resource>
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
<filtering>true</filtering> <filtering>true</filtering>
</resource> </resource>
</resources> </resources>
---- ----
and (inside `<plugins/>`): and (inside `<plugins/>`):
......
...@@ -1123,8 +1123,8 @@ Cloud Foundry you can add the following to your `manifest.yml`: ...@@ -1123,8 +1123,8 @@ Cloud Foundry you can add the following to your `manifest.yml`:
[source,yaml,indent=0] [source,yaml,indent=0]
---- ----
--- ---
env: env:
JAVA_OPTS: "-Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n" JAVA_OPTS: "-Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n"
---- ----
TIP: Notice that you don't need to pass an `address=NNNN` option to `-Xrunjdwp`. If TIP: Notice that you don't need to pass an `address=NNNN` option to `-Xrunjdwp`. If
......
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cloudfoundry;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Example for custom Cloud Foundry actuator ignored paths.
*
* @author Phillip Webb
*/
public class CloudFoundryIgnorePathsExample {
@Configuration
static class CustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
// @formatter:off
// tag::security[]
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers("/cloudfoundryapplication/**")
.permitAll()
.mvcMatchers("/mypath")
.hasAnyRole("SUPERUSER")
.anyRequest()
.authenticated().and()
.httpBasic();
}
// end::security[]
// @formatter:on
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment