Use role-based security to show details in the health endpoint
Closes gh-11869
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user