Use role-based security to show details in the health endpoint

Closes gh-11869
This commit is contained in:
Andy Wilkinson
2018-02-20 07:34:26 +00:00
parent a5960bc0c3
commit 3e4baf744e
27 changed files with 661 additions and 203 deletions

View File

@@ -16,6 +16,9 @@
package org.springframework.boot.actuate.autoconfigure.health;
import java.util.HashSet;
import java.util.Set;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.ShowDetails;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -29,9 +32,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
public class HealthEndpointProperties {
/**
* Whether to show full health details.
* When to show full health details.
*/
private ShowDetails showDetails = ShowDetails.WHEN_AUTHENTICATED;
private ShowDetails showDetails = ShowDetails.WHEN_AUTHORIZED;
/**
* Roles used to determine whether or not a user is authorized to be shown details.
* When empty, all authenticated users are authorized.
*/
private Set<String> roles = new HashSet<>();
public ShowDetails getShowDetails() {
return this.showDetails;
@@ -41,4 +50,12 @@ public class HealthEndpointProperties {
this.showDetails = showDetails;
}
public Set<String> getRoles() {
return this.roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HealthStatusHttpMapper;
import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper;
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
@@ -59,6 +60,15 @@ class HealthEndpointWebExtensionConfiguration {
return statusHttpMapper;
}
@Bean
@ConditionalOnMissingBean
public HealthWebEndpointResponseMapper healthWebEndpointResponseMapper(
HealthStatusHttpMapper statusHttpMapper,
HealthEndpointProperties properties) {
return new HealthWebEndpointResponseMapper(statusHttpMapper,
properties.getShowDetails(), properties.getRoles());
}
@Configuration
@ConditionalOnWebApplication(type = Type.REACTIVE)
static class ReactiveWebHealthConfiguration {
@@ -81,10 +91,9 @@ class HealthEndpointWebExtensionConfiguration {
@ConditionalOnEnabledEndpoint
@ConditionalOnBean(HealthEndpoint.class)
public ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension(
HealthStatusHttpMapper healthStatusHttpMapper,
HealthEndpointProperties properties) {
HealthWebEndpointResponseMapper responseMapper) {
return new ReactiveHealthEndpointWebExtension(this.reactiveHealthIndicator,
healthStatusHttpMapper, properties.getShowDetails());
responseMapper);
}
}
@@ -99,11 +108,10 @@ class HealthEndpointWebExtensionConfiguration {
@ConditionalOnBean(HealthEndpoint.class)
public HealthEndpointWebExtension healthEndpointWebExtension(
ApplicationContext applicationContext,
HealthStatusHttpMapper healthStatusHttpMapper,
HealthEndpointProperties properties) {
HealthWebEndpointResponseMapper responseMapper) {
return new HealthEndpointWebExtension(
HealthIndicatorBeansComposite.get(applicationContext),
healthStatusHttpMapper, properties.getShowDetails());
responseMapper);
}
}

View File

@@ -24,6 +24,7 @@ import java.util.function.Function;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
@@ -58,8 +59,8 @@ public class CloudFoundryWebEndpointDiscovererTests {
for (ExposableWebEndpoint endpoint : endpoints) {
if (endpoint.getId().equals("health")) {
WebOperation operation = endpoint.getOperations().iterator().next();
assertThat(operation
.invoke(new InvocationContext(null, Collections.emptyMap())))
assertThat(operation.invoke(new InvocationContext(
mock(SecurityContext.class), Collections.emptyMap())))
.isEqualTo("cf");
}
}

View File

@@ -17,17 +17,19 @@
package org.springframework.boot.actuate.autoconfigure.health;
import java.security.Principal;
import java.util.Map;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
import org.springframework.boot.actuate.health.HealthStatusHttpMapper;
import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
@@ -63,12 +65,17 @@ public class HealthEndpointWebExtensionTests {
.withPropertyValues("management.health.status.http-mapping.CUSTOM=500")
.run((context) -> {
Object extension = context.getBean(HealthEndpointWebExtension.class);
HealthStatusHttpMapper mapper = (HealthStatusHttpMapper) ReflectionTestUtils
.getField(extension, "statusHttpMapper");
Map<String, Integer> statusMappings = mapper.getStatusMapping();
assertThat(statusMappings).containsEntry("DOWN", 503);
assertThat(statusMappings).containsEntry("OUT_OF_SERVICE", 503);
assertThat(statusMappings).containsEntry("CUSTOM", 500);
HealthWebEndpointResponseMapper responseMapper = (HealthWebEndpointResponseMapper) ReflectionTestUtils
.getField(extension, "responseMapper");
Class<SecurityContext> securityContext = SecurityContext.class;
assertThat(responseMapper
.map(Health.down().build(), mock(securityContext))
.getStatus()).isEqualTo(503);
assertThat(responseMapper.map(Health.status("OUT_OF_SERVICE").build(),
mock(securityContext)).getStatus()).isEqualTo(503);
assertThat(responseMapper
.map(Health.status("CUSTOM").build(), mock(securityContext))
.getStatus()).isEqualTo(500);
});
}
@@ -77,7 +84,8 @@ public class HealthEndpointWebExtensionTests {
this.contextRunner.run((context) -> {
HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class);
assertThat(extension.getHealth(null).getBody().getDetails()).isEmpty();
assertThat(extension.getHealth(mock(SecurityContext.class)).getBody()
.getDetails()).isEmpty();
});
}
@@ -86,7 +94,9 @@ public class HealthEndpointWebExtensionTests {
this.contextRunner.run((context) -> {
HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class);
assertThat(extension.getHealth(mock(Principal.class)).getBody().getDetails())
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal()).willReturn(mock(Principal.class));
assertThat(extension.getHealth(securityContext).getBody().getDetails())
.isNotEmpty();
});
}
@@ -110,9 +120,60 @@ public class HealthEndpointWebExtensionTests {
.run((context) -> {
HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class);
assertThat(extension.getHealth(mock(Principal.class)).getBody()
assertThat(extension.getHealth(mock(SecurityContext.class)).getBody()
.getDetails()).isEmpty();
});
}
@Test
public void detailsCanBeHiddenFromUnauthorizedUsers() {
this.contextRunner.withPropertyValues(
"management.endpoint.health.show-details=when-authorized",
"management.endpoint.health.roles=ACTUATOR").run((context) -> {
HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class);
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal())
.willReturn(mock(Principal.class));
given(securityContext.isUserInRole("ACTUATOR")).willReturn(false);
assertThat(
extension.getHealth(securityContext).getBody().getDetails())
.isEmpty();
});
}
@Test
public void detailsCanBeShownToAuthorizedUsers() {
this.contextRunner.withPropertyValues(
"management.endpoint.health.show-details=when-authorized",
"management.endpoint.health.roles=ACTUATOR").run((context) -> {
HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class);
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal())
.willReturn(mock(Principal.class));
given(securityContext.isUserInRole("ACTUATOR")).willReturn(true);
assertThat(
extension.getHealth(securityContext).getBody().getDetails())
.isNotEmpty();
});
}
@Test
public void roleCanBeCustomized() {
this.contextRunner.withPropertyValues(
"management.endpoint.health.show-details=when-authorized",
"management.endpoint.health.roles=ADMIN").run((context) -> {
HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class);
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal())
.willReturn(mock(Principal.class));
given(securityContext.isUserInRole("ADMIN")).willReturn(true);
assertThat(
extension.getHealth(securityContext).getBody().getDetails())
.isNotEmpty();
});
}
}

View File

@@ -17,15 +17,15 @@
package org.springframework.boot.actuate.autoconfigure.health;
import java.security.Principal;
import java.util.Map;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HealthStatusHttpMapper;
import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper;
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
@@ -34,6 +34,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
@@ -69,12 +70,17 @@ public class ReactiveHealthEndpointWebExtensionTests {
.run((context) -> {
Object extension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
HealthStatusHttpMapper mapper = (HealthStatusHttpMapper) ReflectionTestUtils
.getField(extension, "statusHttpMapper");
Map<String, Integer> statusMappings = mapper.getStatusMapping();
assertThat(statusMappings).containsEntry("DOWN", 503);
assertThat(statusMappings).containsEntry("OUT_OF_SERVICE", 503);
assertThat(statusMappings).containsEntry("CUSTOM", 500);
HealthWebEndpointResponseMapper responseMapper = (HealthWebEndpointResponseMapper) ReflectionTestUtils
.getField(extension, "responseMapper");
Class<SecurityContext> securityContext = SecurityContext.class;
assertThat(responseMapper
.map(Health.down().build(), mock(securityContext))
.getStatus()).isEqualTo(503);
assertThat(responseMapper.map(Health.status("OUT_OF_SERVICE").build(),
mock(securityContext)).getStatus()).isEqualTo(503);
assertThat(responseMapper
.map(Health.status("CUSTOM").build(), mock(securityContext))
.getStatus()).isEqualTo(500);
});
}
@@ -86,8 +92,11 @@ public class ReactiveHealthEndpointWebExtensionTests {
ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
Health endpointHealth = endpoint.health();
Health extensionHealth = extension.health(mock(Principal.class))
.block().getBody();
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal())
.willReturn(mock(Principal.class));
Health extensionHealth = extension.health(securityContext).block()
.getBody();
assertThat(endpointHealth.getDetails())
.containsOnlyKeys("application", "first", "second");
assertThat(extensionHealth.getDetails())
@@ -100,7 +109,8 @@ public class ReactiveHealthEndpointWebExtensionTests {
this.contextRunner.run((context) -> {
ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
assertThat(extension.health(null).block().getBody().getDetails()).isEmpty();
assertThat(extension.health(mock(SecurityContext.class)).block().getBody()
.getDetails()).isEmpty();
});
}
@@ -109,8 +119,10 @@ public class ReactiveHealthEndpointWebExtensionTests {
this.contextRunner.run((context) -> {
ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
assertThat(extension.health(mock(Principal.class)).block().getBody()
.getDetails()).isNotEmpty();
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal()).willReturn(mock(Principal.class));
assertThat(extension.health(securityContext).block().getBody().getDetails())
.isNotEmpty();
});
}
@@ -133,11 +145,60 @@ public class ReactiveHealthEndpointWebExtensionTests {
.run((context) -> {
ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
assertThat(extension.health(mock(Principal.class)).block().getBody()
SecurityContext securityContext = mock(SecurityContext.class);
assertThat(extension.health(securityContext).block().getBody()
.getDetails()).isEmpty();
});
}
@Test
public void detailsCanBeHiddenFromUnauthorizedUsers() {
this.contextRunner.withPropertyValues(
"management.endpoint.health.show-details=when-authorized",
"management.endpoint.health.roles=ACTUATOR").run((context) -> {
ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal())
.willReturn(mock(Principal.class));
given(securityContext.isUserInRole("ACTUATOR")).willReturn(false);
assertThat(extension.health(securityContext).block().getBody()
.getDetails()).isEmpty();
});
}
@Test
public void detailsCanBeShownToAuthorizedUsers() {
this.contextRunner.withPropertyValues(
"management.endpoint.health.show-details=when-authorized",
"management.endpoint.health.roles=ACTUATOR").run((context) -> {
ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal())
.willReturn(mock(Principal.class));
given(securityContext.isUserInRole("ACTUATOR")).willReturn(true);
assertThat(extension.health(securityContext).block().getBody()
.getDetails()).isNotEmpty();
});
}
@Test
public void roleCanBeCustomized() {
this.contextRunner.withPropertyValues(
"management.endpoint.health.show-details=when-authorized",
"management.endpoint.health.roles=ADMIN").run((context) -> {
ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
SecurityContext securityContext = mock(SecurityContext.class);
given(securityContext.getPrincipal())
.willReturn(mock(Principal.class));
given(securityContext.isUserInRole("ADMIN")).willReturn(true);
assertThat(extension.health(securityContext).block().getBody()
.getDetails()).isNotEmpty();
});
}
@Configuration
static class HealthIndicatorsConfiguration {