Merge branch '1.5.x'

pull/7869/head
Phillip Webb 8 years ago
commit 24f5125a8b

@ -54,7 +54,6 @@ import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
@ -162,7 +161,7 @@ public class EndpointWebMvcManagementContextConfiguration {
@ConditionalOnEnabledEndpoint("health")
public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) {
HealthMvcEndpoint healthMvcEndpoint = new HealthMvcEndpoint(delegate,
isHealthSecure());
this.managementServerProperties.getSecurity().isEnabled());
if (this.healthMvcEndpointProperties.getMapping() != null) {
healthMvcEndpoint
.addStatusMapping(this.healthMvcEndpointProperties.getMapping());
@ -206,17 +205,6 @@ public class EndpointWebMvcManagementContextConfiguration {
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 {
@Override

@ -16,7 +16,7 @@
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.mvc.HealthMvcEndpoint;
@ -36,7 +36,7 @@ class CloudFoundryHealthMvcEndpoint extends HealthMvcEndpoint {
}
@Override
protected boolean exposeHealthDetails(Principal principal) {
protected boolean exposeHealthDetails(HttpServletRequest request) {
return true;
}

@ -16,12 +16,11 @@
package org.springframework.boot.actuate.endpoint.mvc;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
@ -33,10 +32,7 @@ import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@ -49,6 +45,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
* @author Andy Wilkinson
* @author Phillip Webb
* @author Eddú Meléndez
* @author Madhura Bhave
* @since 1.1.0
*/
@ConfigurationProperties(prefix = "endpoints.health")
@ -59,11 +56,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
private Map<String, HttpStatus> statusMapping = new HashMap<String, HttpStatus>();
private RelaxedPropertyResolver healthPropertyResolver;
private RelaxedPropertyResolver endpointPropertyResolver;
private RelaxedPropertyResolver roleResolver;
private RelaxedPropertyResolver securityPropertyResolver;
private long lastAccess = 0;
@ -86,11 +79,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
@Override
public void setEnvironment(Environment environment) {
this.healthPropertyResolver = new RelaxedPropertyResolver(environment,
"endpoints.health.");
this.endpointPropertyResolver = new RelaxedPropertyResolver(environment,
"endpoints.");
this.roleResolver = new RelaxedPropertyResolver(environment,
this.securityPropertyResolver = new RelaxedPropertyResolver(environment,
"management.security.");
}
@ -136,12 +125,12 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Object invoke(Principal principal) {
public Object invoke(HttpServletRequest request) {
if (!getDelegate().isEnabled()) {
// Shouldn't happen because the request mapping should not be registered
return getDisabledResponse();
}
Health health = getHealth(principal);
Health health = getHealth(request);
HttpStatus status = getStatus(health);
if (status != null) {
return new ResponseEntity<Health>(health, status);
@ -163,13 +152,13 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
return null;
}
private Health getHealth(Principal principal) {
private Health getHealth(HttpServletRequest request) {
long accessTime = System.currentTimeMillis();
if (isCacheStale(accessTime)) {
this.lastAccess = accessTime;
this.cached = getDelegate().invoke();
}
if (exposeHealthDetails(principal)) {
if (exposeHealthDetails(request)) {
return this.cached;
}
return Health.status(this.cached.getStatus()).build();
@ -182,44 +171,19 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
return (accessTime - this.lastAccess) >= getDelegate().getTimeToLive();
}
protected boolean exposeHealthDetails(Principal principal) {
return isSecure(principal) || isUnrestricted();
}
private boolean isSecure(Principal principal) {
if (principal == null || principal.getClass().getName().contains("Anonymous")) {
return false;
protected boolean exposeHealthDetails(HttpServletRequest request) {
if (!this.secure) {
return true;
}
if (isSpringSecurityAuthentication(principal)) {
Authentication authentication = (Authentication) principal;
List<String> roles = Arrays.asList(StringUtils
.trimArrayElements(StringUtils.commaDelimitedListToStringArray(
this.roleResolver.getProperty("roles", "ROLE_ACTUATOR"))));
for (GrantedAuthority authority : authentication.getAuthorities()) {
String name = authority.getAuthority();
for (String role : roles) {
if (role.equals(name) || ("ROLE_" + role).equals(name)) {
return true;
}
}
String[] roles = StringUtils.commaDelimitedListToStringArray(
this.securityPropertyResolver.getProperty("roles", "ROLE_ACTUATOR"));
roles = StringUtils.trimArrayElements(roles);
for (String role : roles) {
if (request.isUserInRole(role) || request.isUserInRole("ROLE_" + role)) {
return true;
}
}
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 @@
"type": "java.util.Map<java.lang.String,java.lang.Object>",
"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",
"type": "java.lang.Boolean",

@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
@ -60,8 +61,9 @@ public class HealthMvcEndpointAutoConfigurationTests {
this.context.setServletContext(new MockServletContext());
this.context.register(TestConfiguration.class);
this.context.refresh();
MockHttpServletRequest request = new MockHttpServletRequest();
Health health = (Health) this.context.getBean(HealthMvcEndpoint.class)
.invoke(null);
.invoke(request);
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails().get("foo")).isNull();
}

@ -18,20 +18,21 @@ package org.springframework.boot.actuate.endpoint.mvc;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.actuate.health.Health;
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.PropertySource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ -44,36 +45,36 @@ import static org.mockito.Mockito.mock;
* @author Dave Syer
* @author Andy Wilkinson
* @author Eddú Meléndez
* @author Madhura Bhave
*/
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",
Collections.<String, Object>singletonMap("management.security.roles",
"HERO, USER"));
private HttpServletRequest request = new MockHttpServletRequest();
private HealthEndpoint endpoint = null;
private HealthMvcEndpoint mvc = null;
private MockEnvironment environment;
private UsernamePasswordAuthenticationToken user = createAuthenticationToken(
private HttpServletRequest user = createAuthenticationToken(
"ROLE_USER");
private UsernamePasswordAuthenticationToken actuator = createAuthenticationToken(
private HttpServletRequest actuator = createAuthenticationToken(
"ROLE_ACTUATOR");
private UsernamePasswordAuthenticationToken hero = createAuthenticationToken(
private HttpServletRequest hero = createAuthenticationToken(
"ROLE_HERO");
private UsernamePasswordAuthenticationToken createAuthenticationToken(
String authority) {
return new UsernamePasswordAuthenticationToken("user", "password",
AuthorityUtils.commaSeparatedStringToAuthorityList(authority));
private HttpServletRequest createAuthenticationToken(
String role) {
MockServletContext servletContext = new MockServletContext();
servletContext.declareRoles(role);
return new MockHttpServletRequest(servletContext);
}
@Before
@ -88,7 +89,7 @@ public class HealthMvcEndpointTests {
@Test
public void up() {
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(((Health) result).getStatus() == Status.UP).isTrue();
}
@ -97,7 +98,7 @@ public class HealthMvcEndpointTests {
@Test
public void down() {
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();
ResponseEntity<Health> response = (ResponseEntity<Health>) result;
assertThat(response.getBody().getStatus() == Status.DOWN).isTrue();
@ -111,7 +112,7 @@ public class HealthMvcEndpointTests {
.willReturn(new Health.Builder().status("OK").build());
this.mvc.setStatusMapping(
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();
ResponseEntity<Health> response = (ResponseEntity<Health>) result;
assertThat(response.getBody().getStatus().equals(new Status("OK"))).isTrue();
@ -125,7 +126,7 @@ public class HealthMvcEndpointTests {
.willReturn(new Health.Builder().outOfService().build());
this.mvc.setStatusMapping(Collections.singletonMap("out-of-service",
HttpStatus.INTERNAL_SERVER_ERROR));
Object result = this.mvc.invoke(null);
Object result = this.mvc.invoke(this.request);
assertThat(result instanceof ResponseEntity).isTrue();
ResponseEntity<Health> response = (ResponseEntity<Health>) result;
assertThat(response.getBody().getStatus().equals(Status.OUT_OF_SERVICE)).isTrue();
@ -133,10 +134,9 @@ public class HealthMvcEndpointTests {
}
@Test
public void secureEvenWhenNotSensitive() {
public void presenceOfRightRoleShouldExposeDetails() {
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
given(this.endpoint.isSensitive()).willReturn(false);
Object result = this.mvc.invoke(this.actuator);
assertThat(result instanceof Health).isTrue();
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
@ -144,7 +144,18 @@ public class HealthMvcEndpointTests {
}
@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())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(this.user);
@ -154,7 +165,7 @@ public class HealthMvcEndpointTests {
}
@Test
public void secureCustomRole() {
public void customRolePresentShouldExposeDetails() {
this.environment.getPropertySources().addLast(SECURITY_ROLES);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
@ -165,7 +176,7 @@ public class HealthMvcEndpointTests {
}
@Test
public void secureCustomRoleNoAccess() {
public void customRoleShouldNotExposeDetailsForDefaultRole() {
this.environment.getPropertySources().addLast(SECURITY_ROLES);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
@ -178,7 +189,6 @@ public class HealthMvcEndpointTests {
@Test
public void healthIsCached() {
given(this.endpoint.getTimeToLive()).willReturn(10000L);
given(this.endpoint.isSensitive()).willReturn(true);
given(this.endpoint.invoke())
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
Object result = this.mvc.invoke(this.actuator);
@ -188,7 +198,7 @@ public class HealthMvcEndpointTests {
assertThat(health.getDetails()).hasSize(1);
assertThat(health.getDetails().get("foo")).isEqualTo("bar");
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();
health = (Health) result;
// so the result is cached
@ -197,52 +207,16 @@ public class HealthMvcEndpointTests {
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
public void noCachingWhenTimeToLiveIsZero() {
given(this.endpoint.getTimeToLive()).willReturn(0L);
given(this.endpoint.invoke())
.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(((Health) result).getStatus() == Status.UP).isTrue();
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
result = this.mvc.invoke(null);
result = this.mvc.invoke(this.request);
@SuppressWarnings("unchecked")
Health health = ((ResponseEntity<Health>) result).getBody();
assertThat(health.getStatus() == Status.DOWN).isTrue();
@ -251,59 +225,16 @@ public class HealthMvcEndpointTests {
@Test
public void newValueIsReturnedOnceTtlExpires() throws InterruptedException {
given(this.endpoint.getTimeToLive()).willReturn(50L);
given(this.endpoint.isSensitive()).willReturn(false);
given(this.endpoint.invoke())
.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(((Health) result).getStatus() == Status.UP).isTrue();
Thread.sleep(100);
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
result = this.mvc.invoke(null);
result = this.mvc.invoke(this.request);
@SuppressWarnings("unchecked")
Health health = ((ResponseEntity<Health>) result).getBody();
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
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.junit.runner.classpath.ClassPathExclusions;
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.Configuration;
import org.springframework.mock.web.MockServletContext;
@ -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.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("spring-security-*.jar")
@ -61,14 +63,28 @@ public class NoSpringSecurityHealthMvcEndpointIntegrationTests {
}
@Test
public void healthDetailIsPresent() throws Exception {
public void healthDetailNotPresent() throws Exception {
this.context = new AnnotationConfigWebApplicationContext();
this.context.setServletContext(new MockServletContext());
this.context.register(TestConfiguration.class);
this.context.refresh();
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
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,

@ -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.address= # Network address that the management endpoints should bind to.
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.security.enabled=true # Enable security.
management.security.roles=ACTUATOR # Comma-separated list of roles that can access the management endpoint.

@ -545,7 +545,7 @@ buildscript {
}
springBoot {
layoutFactory = new com.example.CustomLayoutFactory()
layoutFactory = new com.example.CustomLayoutFactory()
}
----

@ -177,12 +177,12 @@ element):
[source,xml,indent=0]
----
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
----
and (inside `<plugins/>`):

@ -536,11 +536,32 @@ all enabled endpoints to be exposed over HTTP. The default convention is to use
[[production-ready-sensitive-endpoints]]
=== Securing sensitive endpoints
If you add '`Spring Security`' to your project, all sensitive endpoints exposed over HTTP
will be protected. 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).
=== Accessing sensitive endpoints
By default all sensitive HTTP endpoints are secured such that only users that have an
`ACTUATOR` role may access them. Security is enforced using the standard
`HttpServletRequest.isUserInRole` method.
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
security password`'.
@ -556,10 +577,6 @@ in your `application.properties`:
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]]
@ -1093,19 +1110,19 @@ Example:
[source,java,indent=0]
----
@Bean
@ExportMetricWriter
MetricWriter metricWriter(MetricExportProperties export) {
return new RedisMetricRepository(connectionFactory,
export.getRedis().getPrefix(), export.getRedis().getKey());
}
@Bean
@ExportMetricWriter
MetricWriter metricWriter(MetricExportProperties export) {
return new RedisMetricRepository(connectionFactory,
export.getRedis().getPrefix(), export.getRedis().getKey());
}
----
.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.key: keys.metrics.mysystem
spring.metrics.export.redis.prefix: metrics.mysystem.${spring.application.name:application}.${random.value:0000}
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
@ -1144,21 +1161,21 @@ Example:
[source,indent=0]
----
curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root
[
{
"metric": "counter.status.200.root",
"tags": {
"domain": "org.springframework.metrics",
"process": "b968a76"
},
"aggregateTags": [],
"dps": {
"1430492872": 2,
"1430492875": 6
curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root
[
{
"metric": "counter.status.200.root",
"tags": {
"domain": "org.springframework.metrics",
"process": "b968a76"
},
"aggregateTags": [],
"dps": {
"1430492872": 2,
"1430492875": 6
}
}
}
]
]
----
@ -1177,14 +1194,14 @@ Alternatively, you can provide a `@Bean` of type `StatsdMetricWriter` and mark i
[source,java,indent=0]
----
@Value("${spring.application.name:application}.${random.value:0000}")
private String prefix = "metrics";
@Value("${spring.application.name:application}.${random.value:0000}")
private String prefix = "metrics";
@Bean
@ExportMetricWriter
MetricWriter metricWriter() {
return new StatsdMetricWriter(prefix, "localhost", 8125);
}
@Bean
@ExportMetricWriter
MetricWriter metricWriter() {
return new StatsdMetricWriter(prefix, "localhost", 8125);
}
----
@ -1200,11 +1217,11 @@ Example:
[source,java,indent=0]
----
@Bean
@ExportMetricWriter
MetricWriter metricWriter(MBeanExporter exporter) {
return new JmxMetricWriter(exporter);
}
@Bean
@ExportMetricWriter
MetricWriter metricWriter(MBeanExporter exporter) {
return new JmxMetricWriter(exporter);
}
----
Each metric is exported as an individual MBean. The format for the `ObjectNames` is given
@ -1231,24 +1248,24 @@ Example:
[source,java,indent=0]
----
@Autowired
private MetricExportProperties export;
@Autowired
private MetricExportProperties export;
@Bean
public PublicMetrics metricsAggregate() {
return new MetricReaderPublicMetrics(aggregatesMetricReader());
}
@Bean
public PublicMetrics metricsAggregate() {
return new MetricReaderPublicMetrics(aggregatesMetricReader());
}
private MetricReader globalMetricsForAggregation() {
return new RedisMetricRepository(this.connectionFactory,
this.export.getRedis().getAggregatePrefix(), this.export.getRedis().getKey());
}
private MetricReader globalMetricsForAggregation() {
return new RedisMetricRepository(this.connectionFactory,
this.export.getRedis().getAggregatePrefix(), this.export.getRedis().getKey());
}
private MetricReader aggregatesMetricReader() {
AggregateMetricReader repository = new AggregateMetricReader(
globalMetricsForAggregation());
return repository;
}
private MetricReader aggregatesMetricReader() {
AggregateMetricReader repository = new AggregateMetricReader(
globalMetricsForAggregation());
return repository;
}
----
NOTE: The example above uses `MetricExportProperties` to inject and extract the key and
@ -1312,34 +1329,34 @@ and obtain basic information about the last 100 requests:
[source,json,indent=0]
----
[{
"timestamp": 1394343677415,
"info": {
"method": "GET",
"path": "/trace",
"headers": {
"request": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Connection": "keep-alive",
"Accept-Encoding": "gzip, deflate",
"User-Agent": "Mozilla/5.0 Gecko/Firefox",
"Accept-Language": "en-US,en;q=0.5",
"Cookie": "_ga=GA1.1.827067509.1390890128; ..."
"Authorization": "Basic ...",
"Host": "localhost:8080"
},
"response": {
"Strict-Transport-Security": "max-age=31536000 ; includeSubDomains",
"X-Application-Context": "application:8080",
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
}
}
},{
"timestamp": 1394343684465,
...
}]
[{
"timestamp": 1394343677415,
"info": {
"method": "GET",
"path": "/trace",
"headers": {
"request": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Connection": "keep-alive",
"Accept-Encoding": "gzip, deflate",
"User-Agent": "Mozilla/5.0 Gecko/Firefox",
"Accept-Language": "en-US,en;q=0.5",
"Cookie": "_ga=GA1.1.827067509.1390890128; ..."
"Authorization": "Basic ...",
"Host": "localhost:8080"
},
"response": {
"Strict-Transport-Security": "max-age=31536000 ; includeSubDomains",
"X-Application-Context": "application:8080",
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
}
}
},{
"timestamp": 1394343684465,
...
}]
----
@ -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]]
== What to read next
If you want to explore some of the concepts discussed in this chapter, you can take a

@ -1123,8 +1123,8 @@ Cloud Foundry you can add the following to your `manifest.yml`:
[source,yaml,indent=0]
----
---
env:
JAVA_OPTS: "-Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n"
env:
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

@ -0,0 +1,52 @@
/*
* 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
}
}
Loading…
Cancel
Save