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(
this.roleResolver.getProperty("roles", "ROLE_ACTUATOR"))));
for (GrantedAuthority authority : authentication.getAuthorities()) {
String name = authority.getAuthority();
for (String role : roles) { for (String role : roles) {
if (role.equals(name) || ("ROLE_" + role).equals(name)) { if (request.isUserInRole(role) || request.isUserInRole("ROLE_" + role)) {
return true; 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();
} }
......
...@@ -18,20 +18,21 @@ package org.springframework.boot.actuate.endpoint.mvc; ...@@ -18,20 +18,21 @@ package org.springframework.boot.actuate.endpoint.mvc;
import java.util.Collections; import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
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;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.env.MockEnvironment;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.mock.web.MockServletContext;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
...@@ -44,36 +45,36 @@ import static org.mockito.Mockito.mock; ...@@ -44,36 +45,36 @@ import static org.mockito.Mockito.mock;
* @author Dave Syer * @author Dave Syer
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Madhura Bhave
*/ */
public class HealthMvcEndpointTests { public class HealthMvcEndpointTests {
private static final PropertySource<?> NON_SENSITIVE = new MapPropertySource("test",
Collections.<String, Object>singletonMap("endpoints.health.sensitive",
"false"));
private static final PropertySource<?> SECURITY_ROLES = new MapPropertySource("test", private static final PropertySource<?> SECURITY_ROLES = new MapPropertySource("test",
Collections.<String, Object>singletonMap("management.security.roles", Collections.<String, Object>singletonMap("management.security.roles",
"HERO, USER")); "HERO, USER"));
private HttpServletRequest request = new MockHttpServletRequest();
private HealthEndpoint endpoint = null; private HealthEndpoint endpoint = null;
private HealthMvcEndpoint mvc = null; private HealthMvcEndpoint mvc = null;
private MockEnvironment environment; private MockEnvironment environment;
private UsernamePasswordAuthenticationToken user = createAuthenticationToken( private HttpServletRequest user = createAuthenticationToken(
"ROLE_USER"); "ROLE_USER");
private UsernamePasswordAuthenticationToken actuator = createAuthenticationToken( private HttpServletRequest actuator = createAuthenticationToken(
"ROLE_ACTUATOR"); "ROLE_ACTUATOR");
private UsernamePasswordAuthenticationToken hero = createAuthenticationToken( private HttpServletRequest hero = createAuthenticationToken(
"ROLE_HERO"); "ROLE_HERO");
private UsernamePasswordAuthenticationToken createAuthenticationToken( private HttpServletRequest createAuthenticationToken(
String authority) { String role) {
return new UsernamePasswordAuthenticationToken("user", "password", MockServletContext servletContext = new MockServletContext();
AuthorityUtils.commaSeparatedStringToAuthorityList(authority)); servletContext.declareRoles(role);
return new MockHttpServletRequest(servletContext);
} }
@Before @Before
...@@ -88,7 +89,7 @@ public class HealthMvcEndpointTests { ...@@ -88,7 +89,7 @@ public class HealthMvcEndpointTests {
@Test @Test
public void up() { public void up() {
given(this.endpoint.invoke()).willReturn(new Health.Builder().up().build()); given(this.endpoint.invoke()).willReturn(new Health.Builder().up().build());
Object result = this.mvc.invoke(null); Object result = this.mvc.invoke(this.request);
assertThat(result instanceof Health).isTrue(); assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue(); assertThat(((Health) result).getStatus() == Status.UP).isTrue();
} }
...@@ -97,7 +98,7 @@ public class HealthMvcEndpointTests { ...@@ -97,7 +98,7 @@ public class HealthMvcEndpointTests {
@Test @Test
public void down() { public void down() {
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build()); given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
Object result = this.mvc.invoke(null); Object result = this.mvc.invoke(this.request);
assertThat(result instanceof ResponseEntity).isTrue(); assertThat(result instanceof ResponseEntity).isTrue();
ResponseEntity<Health> response = (ResponseEntity<Health>) result; ResponseEntity<Health> response = (ResponseEntity<Health>) result;
assertThat(response.getBody().getStatus() == Status.DOWN).isTrue(); assertThat(response.getBody().getStatus() == Status.DOWN).isTrue();
...@@ -111,7 +112,7 @@ public class HealthMvcEndpointTests { ...@@ -111,7 +112,7 @@ public class HealthMvcEndpointTests {
.willReturn(new Health.Builder().status("OK").build()); .willReturn(new Health.Builder().status("OK").build());
this.mvc.setStatusMapping( this.mvc.setStatusMapping(
Collections.singletonMap("OK", HttpStatus.INTERNAL_SERVER_ERROR)); Collections.singletonMap("OK", HttpStatus.INTERNAL_SERVER_ERROR));
Object result = this.mvc.invoke(null); Object result = this.mvc.invoke(this.request);
assertThat(result instanceof ResponseEntity).isTrue(); assertThat(result instanceof ResponseEntity).isTrue();
ResponseEntity<Health> response = (ResponseEntity<Health>) result; ResponseEntity<Health> response = (ResponseEntity<Health>) result;
assertThat(response.getBody().getStatus().equals(new Status("OK"))).isTrue(); assertThat(response.getBody().getStatus().equals(new Status("OK"))).isTrue();
...@@ -125,7 +126,7 @@ public class HealthMvcEndpointTests { ...@@ -125,7 +126,7 @@ public class HealthMvcEndpointTests {
.willReturn(new Health.Builder().outOfService().build()); .willReturn(new Health.Builder().outOfService().build());
this.mvc.setStatusMapping(Collections.singletonMap("out-of-service", this.mvc.setStatusMapping(Collections.singletonMap("out-of-service",
HttpStatus.INTERNAL_SERVER_ERROR)); HttpStatus.INTERNAL_SERVER_ERROR));
Object result = this.mvc.invoke(null); Object result = this.mvc.invoke(this.request);
assertThat(result instanceof ResponseEntity).isTrue(); assertThat(result instanceof ResponseEntity).isTrue();
ResponseEntity<Health> response = (ResponseEntity<Health>) result; ResponseEntity<Health> response = (ResponseEntity<Health>) result;
assertThat(response.getBody().getStatus().equals(Status.OUT_OF_SERVICE)).isTrue(); assertThat(response.getBody().getStatus().equals(Status.OUT_OF_SERVICE)).isTrue();
...@@ -133,10 +134,9 @@ public class HealthMvcEndpointTests { ...@@ -133,10 +134,9 @@ public class HealthMvcEndpointTests {
} }
@Test @Test
public void secureEvenWhenNotSensitive() { public void presenceOfRightRoleShouldExposeDetails() {
given(this.endpoint.invoke()) given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); .willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
given(this.endpoint.isSensitive()).willReturn(false);
Object result = this.mvc.invoke(this.actuator); Object result = this.mvc.invoke(this.actuator);
assertThat(result instanceof Health).isTrue(); assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue(); assertThat(((Health) result).getStatus() == Status.UP).isTrue();
...@@ -144,7 +144,18 @@ public class HealthMvcEndpointTests { ...@@ -144,7 +144,18 @@ public class HealthMvcEndpointTests {
} }
@Test @Test
public void secureNonAdmin() { public void managementSecurityDisabledShouldExposeDetails() throws Exception {
this.mvc = new HealthMvcEndpoint(this.endpoint, false);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(this.user);
assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
}
@Test
public void rightRoleNotPresentShouldNotExposeDetails() {
given(this.endpoint.invoke()) given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); .willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(this.user); Object result = this.mvc.invoke(this.user);
...@@ -154,7 +165,7 @@ public class HealthMvcEndpointTests { ...@@ -154,7 +165,7 @@ public class HealthMvcEndpointTests {
} }
@Test @Test
public void secureCustomRole() { public void customRolePresentShouldExposeDetails() {
this.environment.getPropertySources().addLast(SECURITY_ROLES); this.environment.getPropertySources().addLast(SECURITY_ROLES);
given(this.endpoint.invoke()) given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); .willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
...@@ -165,7 +176,7 @@ public class HealthMvcEndpointTests { ...@@ -165,7 +176,7 @@ public class HealthMvcEndpointTests {
} }
@Test @Test
public void secureCustomRoleNoAccess() { public void customRoleShouldNotExposeDetailsForDefaultRole() {
this.environment.getPropertySources().addLast(SECURITY_ROLES); this.environment.getPropertySources().addLast(SECURITY_ROLES);
given(this.endpoint.invoke()) given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); .willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
...@@ -178,7 +189,6 @@ public class HealthMvcEndpointTests { ...@@ -178,7 +189,6 @@ public class HealthMvcEndpointTests {
@Test @Test
public void healthIsCached() { public void healthIsCached() {
given(this.endpoint.getTimeToLive()).willReturn(10000L); given(this.endpoint.getTimeToLive()).willReturn(10000L);
given(this.endpoint.isSensitive()).willReturn(true);
given(this.endpoint.invoke()) given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); .willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(this.actuator); Object result = this.mvc.invoke(this.actuator);
...@@ -188,7 +198,7 @@ public class HealthMvcEndpointTests { ...@@ -188,7 +198,7 @@ public class HealthMvcEndpointTests {
assertThat(health.getDetails()).hasSize(1); assertThat(health.getDetails()).hasSize(1);
assertThat(health.getDetails().get("foo")).isEqualTo("bar"); assertThat(health.getDetails().get("foo")).isEqualTo("bar");
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build()); given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
result = this.mvc.invoke(null); // insecure now result = this.mvc.invoke(this.request); // insecure now
assertThat(result instanceof Health).isTrue(); assertThat(result instanceof Health).isTrue();
health = (Health) result; health = (Health) result;
// so the result is cached // so the result is cached
...@@ -197,52 +207,16 @@ public class HealthMvcEndpointTests { ...@@ -197,52 +207,16 @@ public class HealthMvcEndpointTests {
assertThat(health.getDetails()).isEmpty(); assertThat(health.getDetails()).isEmpty();
} }
@Test
public void insecureAnonymousAccessUnrestricted() {
this.mvc = new HealthMvcEndpoint(this.endpoint, false);
this.mvc.setEnvironment(this.environment);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(null);
assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
}
@Test
public void insensitiveAnonymousAccessRestricted() {
this.environment.getPropertySources().addLast(NON_SENSITIVE);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(null);
assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
assertThat(((Health) result).getDetails().get("foo")).isNull();
}
@Test
public void insecureInsensitiveAnonymousAccessUnrestricted() {
this.mvc = new HealthMvcEndpoint(this.endpoint, false);
this.mvc.setEnvironment(this.environment);
this.environment.getPropertySources().addLast(NON_SENSITIVE);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(null);
assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
}
@Test @Test
public void noCachingWhenTimeToLiveIsZero() { public void noCachingWhenTimeToLiveIsZero() {
given(this.endpoint.getTimeToLive()).willReturn(0L); given(this.endpoint.getTimeToLive()).willReturn(0L);
given(this.endpoint.invoke()) given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); .willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(null); Object result = this.mvc.invoke(this.request);
assertThat(result instanceof Health).isTrue(); assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue(); assertThat(((Health) result).getStatus() == Status.UP).isTrue();
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build()); given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
result = this.mvc.invoke(null); result = this.mvc.invoke(this.request);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Health health = ((ResponseEntity<Health>) result).getBody(); Health health = ((ResponseEntity<Health>) result).getBody();
assertThat(health.getStatus() == Status.DOWN).isTrue(); assertThat(health.getStatus() == Status.DOWN).isTrue();
...@@ -251,59 +225,16 @@ public class HealthMvcEndpointTests { ...@@ -251,59 +225,16 @@ public class HealthMvcEndpointTests {
@Test @Test
public void newValueIsReturnedOnceTtlExpires() throws InterruptedException { public void newValueIsReturnedOnceTtlExpires() throws InterruptedException {
given(this.endpoint.getTimeToLive()).willReturn(50L); given(this.endpoint.getTimeToLive()).willReturn(50L);
given(this.endpoint.isSensitive()).willReturn(false);
given(this.endpoint.invoke()) given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build()); .willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(null); Object result = this.mvc.invoke(this.request);
assertThat(result instanceof Health).isTrue(); assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue(); assertThat(((Health) result).getStatus() == Status.UP).isTrue();
Thread.sleep(100); Thread.sleep(100);
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build()); given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
result = this.mvc.invoke(null); result = this.mvc.invoke(this.request);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Health health = ((ResponseEntity<Health>) result).getBody(); Health health = ((ResponseEntity<Health>) result).getBody();
assertThat(health.getStatus() == Status.DOWN).isTrue(); assertThat(health.getStatus() == Status.DOWN).isTrue();
} }
@Test
public void detailIsHiddenWhenAllEndpointsAreSensitive() {
EnvironmentTestUtils.addEnvironment(this.environment, "endpoints.sensitive:true");
this.mvc = new HealthMvcEndpoint(this.endpoint, false);
this.mvc.setEnvironment(this.environment);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(null);
assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
assertThat(((Health) result).getDetails().get("foo")).isNull();
}
@Test
public void detailIsHiddenWhenHealthEndpointIsSensitive() {
EnvironmentTestUtils.addEnvironment(this.environment,
"endpoints.health.sensitive:true");
this.mvc = new HealthMvcEndpoint(this.endpoint, false);
this.mvc.setEnvironment(this.environment);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(null);
assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
assertThat(((Health) result).getDetails().get("foo")).isNull();
}
@Test
public void detailIsHiddenWhenOnlyHealthEndpointIsSensitive() {
EnvironmentTestUtils.addEnvironment(this.environment,
"endpoints.health.sensitive:true", "endpoints.sensitive:false");
this.mvc = new HealthMvcEndpoint(this.endpoint, false);
this.mvc.setEnvironment(this.environment);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(null);
assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
assertThat(((Health) result).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.
......
...@@ -536,11 +536,32 @@ all enabled endpoints to be exposed over HTTP. The default convention is to use ...@@ -536,11 +536,32 @@ all enabled endpoints to be exposed over HTTP. The default convention is to use
[[production-ready-sensitive-endpoints]] [[production-ready-sensitive-endpoints]]
=== Securing sensitive endpoints === Accessing sensitive endpoints
If you add '`Spring Security`' to your project, all sensitive endpoints exposed over HTTP By default all sensitive HTTP endpoints are secured such that only users that have an
will be protected. By default '`basic`' authentication will be used with the username `ACTUATOR` role may access them. Security is enforced using the standard
`user` and a generated password (which is printed on the console when the application `HttpServletRequest.isUserInRole` method.
starts).
TIP: Use the `management.security.roles` property if you want something different to
`ACTUATOR`.
If you are deploying applications behind a firewall, you may prefer that all your actuator
endpoints can be accessed without requiring authentication. You can do this by changing
the `management.security.enabled` property:
.application.properties
[source,properties,indent=0]
----
management.security.enabled=false
----
NOTE: By default, actuator endpoints are exposed on the same port that serves regular
HTTP traffic. Take care not to accidentally expose sensitive information if you change
the `management.security.enabled` property.
If you're deploying applications publicly, you may want to add '`Spring Security`' to
handle user authentication. When '`Spring Security`' is added, by default '`basic`'
authentication will be used with the username `user` and a generated password (which is
printed on the console when the application starts).
TIP: Generated passwords are logged as the application starts. Search for '`Using default TIP: Generated passwords are logged as the application starts. Search for '`Using default
security password`'. security password`'.
...@@ -556,10 +577,6 @@ in your `application.properties`: ...@@ -556,10 +577,6 @@ in your `application.properties`:
management.security.roles=SUPERUSER management.security.roles=SUPERUSER
---- ----
TIP: If you don't use Spring Security and your HTTP endpoints are exposed publicly,
you should carefully consider which endpoints you enable. See
<<production-ready-customizing-endpoints>> for details of how you can set
`endpoints.enabled` to `false` then "`opt-in`" only specific endpoints.
[[production-ready-customizing-management-server-context-path]] [[production-ready-customizing-management-server-context-path]]
...@@ -1093,19 +1110,19 @@ Example: ...@@ -1093,19 +1110,19 @@ Example:
[source,java,indent=0] [source,java,indent=0]
---- ----
@Bean @Bean
@ExportMetricWriter @ExportMetricWriter
MetricWriter metricWriter(MetricExportProperties export) { MetricWriter metricWriter(MetricExportProperties export) {
return new RedisMetricRepository(connectionFactory, return new RedisMetricRepository(connectionFactory,
export.getRedis().getPrefix(), export.getRedis().getKey()); export.getRedis().getPrefix(), export.getRedis().getKey());
} }
---- ----
.application.properties .application.properties
[source,properties] [source,properties,indent=0]
---- ----
spring.metrics.export.redis.prefix: metrics.mysystem.${spring.application.name:application}.${random.value:0000} spring.metrics.export.redis.prefix: metrics.mysystem.${spring.application.name:application}.${random.value:0000}
spring.metrics.export.redis.key: keys.metrics.mysystem spring.metrics.export.redis.key: keys.metrics.mysystem
---- ----
The prefix is constructed with the application name and id at the end, so it can easily be used The prefix is constructed with the application name and id at the end, so it can easily be used
...@@ -1144,8 +1161,8 @@ Example: ...@@ -1144,8 +1161,8 @@ Example:
[source,indent=0] [source,indent=0]
---- ----
curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root
[ [
{ {
"metric": "counter.status.200.root", "metric": "counter.status.200.root",
"tags": { "tags": {
...@@ -1158,7 +1175,7 @@ curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root ...@@ -1158,7 +1175,7 @@ curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root
"1430492875": 6 "1430492875": 6
} }
} }
] ]
---- ----
...@@ -1177,14 +1194,14 @@ Alternatively, you can provide a `@Bean` of type `StatsdMetricWriter` and mark i ...@@ -1177,14 +1194,14 @@ Alternatively, you can provide a `@Bean` of type `StatsdMetricWriter` and mark i
[source,java,indent=0] [source,java,indent=0]
---- ----
@Value("${spring.application.name:application}.${random.value:0000}") @Value("${spring.application.name:application}.${random.value:0000}")
private String prefix = "metrics"; private String prefix = "metrics";
@Bean @Bean
@ExportMetricWriter @ExportMetricWriter
MetricWriter metricWriter() { MetricWriter metricWriter() {
return new StatsdMetricWriter(prefix, "localhost", 8125); return new StatsdMetricWriter(prefix, "localhost", 8125);
} }
---- ----
...@@ -1200,11 +1217,11 @@ Example: ...@@ -1200,11 +1217,11 @@ Example:
[source,java,indent=0] [source,java,indent=0]
---- ----
@Bean @Bean
@ExportMetricWriter @ExportMetricWriter
MetricWriter metricWriter(MBeanExporter exporter) { MetricWriter metricWriter(MBeanExporter exporter) {
return new JmxMetricWriter(exporter); return new JmxMetricWriter(exporter);
} }
---- ----
Each metric is exported as an individual MBean. The format for the `ObjectNames` is given Each metric is exported as an individual MBean. The format for the `ObjectNames` is given
...@@ -1312,7 +1329,7 @@ and obtain basic information about the last 100 requests: ...@@ -1312,7 +1329,7 @@ and obtain basic information about the last 100 requests:
[source,json,indent=0] [source,json,indent=0]
---- ----
[{ [{
"timestamp": 1394343677415, "timestamp": 1394343677415,
"info": { "info": {
"method": "GET", "method": "GET",
...@@ -1396,6 +1413,67 @@ customize the file name and path via the `Writer` constructor. ...@@ -1396,6 +1413,67 @@ customize the file name and path via the `Writer` constructor.
[[production-ready-cloudfoundry]]
== Cloud Foundry support
Spring Boot's actuator module includes additional support that is activated when you
deploy to a compatible Cloud Foundry instance. The `/cloudfoundryapplication` path
provides an alternative secured route to all `NamedMvcEndpoint` beans.
The extended support allows Cloud Foundry management UIs (such as the web
application that you can use to view deployed applications) to be augmented with Spring
Boot actuator information. For example, an application status page may include full health
information instead of the typical "`running`" or "`stopped`" status.
NOTE: The `/cloudfoundryapplication` path is not directly accessible to regular users.
In order to use the endpoint a valid UAA token must be passed with the request.
[[production-ready-cloudfoundry-disable]]
=== Disabling extended Cloud Foundry actuator support
If you want to fully disable the `/cloudfoundryapplication` endpoints you can add the
following to your `application.properties` file:
.application.properties
[source,properties,indent=0]
----
management.cloudfoundry.enabled=false
----
[[production-ready-cloudfoundry-ssl]]
=== Cloud Foundry self signed certificates
By default, the security verification for `/cloudfoundryapplication` endpoints makes SSL
calls to various Cloud Foundry services. If your Cloud Foundry UAA or Cloud Controller
services use self-signed certificates you will need to set the following property:
.application.properties
[source,properties,indent=0]
----
management.cloudfoundry.skip-ssl-validation=true
----
[[production-ready-cloudfoundry-custom-security]]
=== Custom security configuration
If you define custom security configuration, and you want extended Cloud Foundry actuator
support, you'll should ensure that `/cloudfoundryapplication/**` paths are open. Without
a direct open route, your Cloud Foundry application manager will not be able to obtain
endpoint data.
For Spring Security, you'll typically include something like
`mvcMatchers("/cloudfoundryapplication/**").permitAll()` in your configuration:
[source,java,indent=0]
----
include::{code-examples}/cloudfoundry/CloudFoundryIgnorePathsExample.java[tag=security]
----
[[production-ready-whats-next]] [[production-ready-whats-next]]
== What to read next == What to read next
If you want to explore some of the concepts discussed in this chapter, you can take a If you want to explore some of the concepts discussed in this chapter, you can take a
......
/*
* 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