Browse Source

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

Closes gh-11869
pull/12129/merge
Andy Wilkinson 8 years ago
parent
commit
3e4baf744e
  1. 21
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java
  2. 20
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java
  3. 5
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java
  4. 83
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionTests.java
  5. 89
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointWebExtensionTests.java
  6. 15
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java
  7. 43
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SecurityContext.java
  8. 11
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java
  9. 2
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java
  10. 19
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java
  11. 24
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java
  12. 87
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java
  13. 24
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java
  14. 28
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java
  15. 90
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java
  16. 31
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java
  17. 4
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ShowDetails.java
  18. 8
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java
  19. 18
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java
  20. 10
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java
  21. 90
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java
  22. 43
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java
  23. 36
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java
  24. 42
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java
  25. 6
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java
  26. 3
      spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc
  27. 10
      spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc

21
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java

@ -16,6 +16,9 @@
package org.springframework.boot.actuate.autoconfigure.health; 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.HealthEndpoint;
import org.springframework.boot.actuate.health.ShowDetails; import org.springframework.boot.actuate.health.ShowDetails;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@ -29,9 +32,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
public class HealthEndpointProperties { public class HealthEndpointProperties {
/** /**
* Whether to show full health details. * When to show full health details.
*/
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 ShowDetails showDetails = ShowDetails.WHEN_AUTHENTICATED; private Set<String> roles = new HashSet<>();
public ShowDetails getShowDetails() { public ShowDetails getShowDetails() {
return this.showDetails; return this.showDetails;
@ -41,4 +50,12 @@ public class HealthEndpointProperties {
this.showDetails = showDetails; this.showDetails = showDetails;
} }
public Set<String> getRoles() {
return this.roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
} }

20
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java

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

5
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java

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

83
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionTests.java

@ -17,17 +17,19 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import java.security.Principal; import java.security.Principal;
import java.util.Map;
import org.junit.Test; 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.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.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
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.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
@ -63,12 +65,17 @@ public class HealthEndpointWebExtensionTests {
.withPropertyValues("management.health.status.http-mapping.CUSTOM=500") .withPropertyValues("management.health.status.http-mapping.CUSTOM=500")
.run((context) -> { .run((context) -> {
Object extension = context.getBean(HealthEndpointWebExtension.class); Object extension = context.getBean(HealthEndpointWebExtension.class);
HealthStatusHttpMapper mapper = (HealthStatusHttpMapper) ReflectionTestUtils HealthWebEndpointResponseMapper responseMapper = (HealthWebEndpointResponseMapper) ReflectionTestUtils
.getField(extension, "statusHttpMapper"); .getField(extension, "responseMapper");
Map<String, Integer> statusMappings = mapper.getStatusMapping(); Class<SecurityContext> securityContext = SecurityContext.class;
assertThat(statusMappings).containsEntry("DOWN", 503); assertThat(responseMapper
assertThat(statusMappings).containsEntry("OUT_OF_SERVICE", 503); .map(Health.down().build(), mock(securityContext))
assertThat(statusMappings).containsEntry("CUSTOM", 500); .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) -> { this.contextRunner.run((context) -> {
HealthEndpointWebExtension extension = context HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class); .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) -> { this.contextRunner.run((context) -> {
HealthEndpointWebExtension extension = context HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class); .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(); .isNotEmpty();
}); });
} }
@ -110,9 +120,60 @@ public class HealthEndpointWebExtensionTests {
.run((context) -> { .run((context) -> {
HealthEndpointWebExtension extension = context HealthEndpointWebExtension extension = context
.getBean(HealthEndpointWebExtension.class); .getBean(HealthEndpointWebExtension.class);
assertThat(extension.getHealth(mock(Principal.class)).getBody() assertThat(extension.getHealth(mock(SecurityContext.class)).getBody()
.getDetails()).isEmpty(); .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();
});
}
} }

89
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointWebExtensionTests.java

@ -17,15 +17,15 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import java.security.Principal; import java.security.Principal;
import java.util.Map;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Mono; 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.Health;
import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthIndicator; 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.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator; import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; 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 org.springframework.test.util.ReflectionTestUtils;
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.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
@ -69,12 +70,17 @@ public class ReactiveHealthEndpointWebExtensionTests {
.run((context) -> { .run((context) -> {
Object extension = context Object extension = context
.getBean(ReactiveHealthEndpointWebExtension.class); .getBean(ReactiveHealthEndpointWebExtension.class);
HealthStatusHttpMapper mapper = (HealthStatusHttpMapper) ReflectionTestUtils HealthWebEndpointResponseMapper responseMapper = (HealthWebEndpointResponseMapper) ReflectionTestUtils
.getField(extension, "statusHttpMapper"); .getField(extension, "responseMapper");
Map<String, Integer> statusMappings = mapper.getStatusMapping(); Class<SecurityContext> securityContext = SecurityContext.class;
assertThat(statusMappings).containsEntry("DOWN", 503); assertThat(responseMapper
assertThat(statusMappings).containsEntry("OUT_OF_SERVICE", 503); .map(Health.down().build(), mock(securityContext))
assertThat(statusMappings).containsEntry("CUSTOM", 500); .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 ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class); .getBean(ReactiveHealthEndpointWebExtension.class);
Health endpointHealth = endpoint.health(); Health endpointHealth = endpoint.health();
Health extensionHealth = extension.health(mock(Principal.class)) SecurityContext securityContext = mock(SecurityContext.class);
.block().getBody(); given(securityContext.getPrincipal())
.willReturn(mock(Principal.class));
Health extensionHealth = extension.health(securityContext).block()
.getBody();
assertThat(endpointHealth.getDetails()) assertThat(endpointHealth.getDetails())
.containsOnlyKeys("application", "first", "second"); .containsOnlyKeys("application", "first", "second");
assertThat(extensionHealth.getDetails()) assertThat(extensionHealth.getDetails())
@ -100,7 +109,8 @@ public class ReactiveHealthEndpointWebExtensionTests {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
ReactiveHealthEndpointWebExtension extension = context ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class); .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) -> { this.contextRunner.run((context) -> {
ReactiveHealthEndpointWebExtension extension = context ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class); .getBean(ReactiveHealthEndpointWebExtension.class);
assertThat(extension.health(mock(Principal.class)).block().getBody() SecurityContext securityContext = mock(SecurityContext.class);
.getDetails()).isNotEmpty(); given(securityContext.getPrincipal()).willReturn(mock(Principal.class));
assertThat(extension.health(securityContext).block().getBody().getDetails())
.isNotEmpty();
}); });
} }
@ -133,11 +145,60 @@ public class ReactiveHealthEndpointWebExtensionTests {
.run((context) -> { .run((context) -> {
ReactiveHealthEndpointWebExtension extension = context ReactiveHealthEndpointWebExtension extension = context
.getBean(ReactiveHealthEndpointWebExtension.class); .getBean(ReactiveHealthEndpointWebExtension.class);
assertThat(extension.health(mock(Principal.class)).block().getBody() SecurityContext securityContext = mock(SecurityContext.class);
assertThat(extension.health(securityContext).block().getBody()
.getDetails()).isEmpty(); .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 @Configuration
static class HealthIndicatorsConfiguration { static class HealthIndicatorsConfiguration {

15
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java

@ -16,7 +16,6 @@
package org.springframework.boot.actuate.endpoint; package org.springframework.boot.actuate.endpoint;
import java.security.Principal;
import java.util.Map; import java.util.Map;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
@ -30,24 +29,26 @@ import org.springframework.util.Assert;
*/ */
public class InvocationContext { public class InvocationContext {
private final Principal principal; private final SecurityContext securityContext;
private final Map<String, Object> arguments; private final Map<String, Object> arguments;
/** /**
* Creates a new context for an operation being invoked by the given {@code principal} * Creates a new context for an operation being invoked by the given {@code principal}
* with the given available {@code arguments}. * with the given available {@code arguments}.
* @param principal the principal invoking the operation. May be {@code null} * @param securityContext the current security context. Never {@code null}
* @param arguments the arguments available to the operation. Never {@code null} * @param arguments the arguments available to the operation. Never {@code null}
*/ */
public InvocationContext(Principal principal, Map<String, Object> arguments) { public InvocationContext(SecurityContext securityContext,
Map<String, Object> arguments) {
Assert.notNull(securityContext, "SecurityContext must not be null");
Assert.notNull(arguments, "Arguments must not be null"); Assert.notNull(arguments, "Arguments must not be null");
this.principal = principal; this.securityContext = securityContext;
this.arguments = arguments; this.arguments = arguments;
} }
public Principal getPrincipal() { public SecurityContext getSecurityContext() {
return this.principal; return this.securityContext;
} }
public Map<String, Object> getArguments() { public Map<String, Object> getArguments() {

43
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SecurityContext.java

@ -0,0 +1,43 @@
/*
* Copyright 2012-2018 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.actuate.endpoint;
import java.security.Principal;
/**
* Security context in which an endpoint is being invoked.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public interface SecurityContext {
/**
* Return the currently authenticated {@link Principal} or {@code null}.
* @return the principal or {@code null}
*/
Principal getPrincipal();
/**
* Returns {@code true} if the currently authenticated user is in the given
* {@code role}, or false otherwise.
* @param role name of the role
* @return {@code true} if the user is in the given role
*/
boolean isUserInRole(String role);
}

11
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java

@ -22,6 +22,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter; import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
@ -89,7 +90,10 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
return false; return false;
} }
if (Principal.class.equals(parameter.getType())) { if (Principal.class.equals(parameter.getType())) {
return context.getPrincipal() == null; return context.getSecurityContext().getPrincipal() == null;
}
if (SecurityContext.class.equals(parameter.getType())) {
return false;
} }
return context.getArguments().get(parameter.getName()) == null; return context.getArguments().get(parameter.getName()) == null;
} }
@ -102,7 +106,10 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
private Object resolveArgument(OperationParameter parameter, private Object resolveArgument(OperationParameter parameter,
InvocationContext context) { InvocationContext context) {
if (Principal.class.equals(parameter.getType())) { if (Principal.class.equals(parameter.getType())) {
return context.getPrincipal(); return context.getSecurityContext().getPrincipal();
}
if (SecurityContext.class.equals(parameter.getType())) {
return context.getSecurityContext();
} }
Object value = context.getArguments().get(parameter.getName()); Object value = context.getArguments().get(parameter.getName());
return this.parameterValueMapper.mapParameterValue(parameter, value); return this.parameterValueMapper.mapParameterValue(parameter, value);

2
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java vendored

@ -75,7 +75,7 @@ public class CachingOperationInvoker implements OperationInvoker {
} }
private boolean hasInput(InvocationContext context) { private boolean hasInput(InvocationContext context) {
if (context.getPrincipal() != null) { if (context.getSecurityContext().getPrincipal() != null) {
return true; return true;
} }
Map<String, Object> arguments = context.getArguments(); Map<String, Object> arguments = context.getArguments();

19
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint.jmx; package org.springframework.boot.actuate.endpoint.jmx;
import java.security.Principal;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -33,6 +34,7 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -97,7 +99,8 @@ public class EndpointMBean implements DynamicMBean {
String[] parameterNames = operation.getParameters().stream() String[] parameterNames = operation.getParameters().stream()
.map(JmxOperationParameter::getName).toArray(String[]::new); .map(JmxOperationParameter::getName).toArray(String[]::new);
Map<String, Object> arguments = getArguments(parameterNames, params); Map<String, Object> arguments = getArguments(parameterNames, params);
Object result = operation.invoke(new InvocationContext(null, arguments)); Object result = operation
.invoke(new InvocationContext(new JmxSecurityContext(), arguments));
if (REACTOR_PRESENT) { if (REACTOR_PRESENT) {
result = ReactiveHandler.handle(result); result = ReactiveHandler.handle(result);
} }
@ -149,4 +152,18 @@ public class EndpointMBean implements DynamicMBean {
} }
private static final class JmxSecurityContext implements SecurityContext {
@Override
public Principal getPrincipal() {
return null;
}
@Override
public boolean isUserInRole(String role) {
return false;
}
}
} }

24
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java

@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.web.jersey;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.Principal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -40,6 +41,7 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -150,7 +152,7 @@ public class JerseyEndpointResourceFactory {
arguments.putAll(extractQueryParameters(data)); arguments.putAll(extractQueryParameters(data));
try { try {
Object response = this.operation.invoke(new InvocationContext( Object response = this.operation.invoke(new InvocationContext(
data.getSecurityContext().getUserPrincipal(), arguments)); new JerseySecurityContext(data.getSecurityContext()), arguments));
return convertToJaxRsResponse(response, data.getRequest().getMethod()); return convertToJaxRsResponse(response, data.getRequest().getMethod());
} }
catch (InvalidEndpointRequestException ex) { catch (InvalidEndpointRequestException ex) {
@ -275,4 +277,24 @@ public class JerseyEndpointResourceFactory {
} }
private static final class JerseySecurityContext implements SecurityContext {
private final javax.ws.rs.core.SecurityContext securityContext;
private JerseySecurityContext(javax.ws.rs.core.SecurityContext securityContext) {
this.securityContext = securityContext;
}
@Override
public Principal getPrincipal() {
return this.securityContext.getUserPrincipal();
}
@Override
public boolean isUserInRole(String role) {
return this.securityContext.isUserInRole(role);
}
}
} }

87
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java

@ -21,6 +21,7 @@ import java.security.Principal;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -30,6 +31,7 @@ import reactor.core.scheduler.Schedulers;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -40,6 +42,10 @@ import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicat
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -256,33 +262,55 @@ public abstract class AbstractWebFluxEndpointHandlerMapping
private static final class ReactiveWebOperationAdapter private static final class ReactiveWebOperationAdapter
implements ReactiveWebOperation { implements ReactiveWebOperation {
private static final Principal NO_PRINCIPAL = new Principal() { private final OperationInvoker invoker;
private final Supplier<Mono<? extends SecurityContext>> securityContextSupplier;
@Override private ReactiveWebOperationAdapter(OperationInvoker invoker) {
public String getName() { this.invoker = invoker;
throw new UnsupportedOperationException(); if (ClassUtils.isPresent(
"org.springframework.security.core.context.ReactiveSecurityContextHolder",
getClass().getClassLoader())) {
this.securityContextSupplier = this::springSecurityContext;
} }
else {
this.securityContextSupplier = this::emptySecurityContext;
}
}
}; public Mono<? extends SecurityContext> springSecurityContext() {
return ReactiveSecurityContextHolder.getContext()
.map((securityContext) -> new ReactiveSecurityContext(
securityContext.getAuthentication()))
.switchIfEmpty(Mono.just(new ReactiveSecurityContext(null)));
}
private final OperationInvoker invoker; public Mono<SecurityContext> emptySecurityContext() {
return Mono.just(new SecurityContext() {
private ReactiveWebOperationAdapter(OperationInvoker invoker) { @Override
this.invoker = invoker; public Principal getPrincipal() {
return null;
}
@Override
public boolean isUserInRole(String role) {
return false;
}
});
} }
@Override @Override
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange,
Map<String, String> body) { Map<String, String> body) {
return exchange.getPrincipal().defaultIfEmpty(NO_PRINCIPAL) Map<String, Object> arguments = getArguments(exchange, body);
.flatMap((principal) -> { return this.securityContextSupplier.get()
Map<String, Object> arguments = getArguments(exchange, body); .map((securityContext) -> new InvocationContext(securityContext,
return handleResult( arguments))
(Publisher<?>) this.invoker.invoke(new InvocationContext( .flatMap((invocationContext) -> handleResult(
principal == NO_PRINCIPAL ? null : principal, (Publisher<?>) this.invoker.invoke(invocationContext),
arguments)), exchange.getRequest().getMethod()));
exchange.getRequest().getMethod());
});
} }
private Map<String, Object> getArguments(ServerWebExchange exchange, private Map<String, Object> getArguments(ServerWebExchange exchange,
@ -358,4 +386,29 @@ public abstract class AbstractWebFluxEndpointHandlerMapping
} }
} }
private static final class ReactiveSecurityContext implements SecurityContext {
private final Authentication authentication;
ReactiveSecurityContext(Authentication authentication) {
this.authentication = authentication;
}
@Override
public Principal getPrincipal() {
return this.authentication;
}
@Override
public boolean isUserInRole(String role) {
if (this.authentication == null) {
return false;
}
return AuthorityReactiveAuthorizationManager.hasRole(role)
.check(Mono.just(this.authentication), null).block().isGranted();
}
}
} }

24
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java

@ -17,6 +17,7 @@
package org.springframework.boot.actuate.endpoint.web.servlet; package org.springframework.boot.actuate.endpoint.web.servlet;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.security.Principal;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -29,6 +30,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -243,7 +245,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping
try { try {
return handleResult( return handleResult(
this.invoker.invoke(new InvocationContext( this.invoker.invoke(new InvocationContext(
request.getUserPrincipal(), arguments)), new ServletSecurityContext(request), arguments)),
HttpMethod.valueOf(request.getMethod())); HttpMethod.valueOf(request.getMethod()));
} }
catch (InvalidEndpointRequestException ex) { catch (InvalidEndpointRequestException ex) {
@ -312,4 +314,24 @@ public abstract class AbstractWebMvcEndpointHandlerMapping
} }
private static final class ServletSecurityContext implements SecurityContext {
private final HttpServletRequest request;
private ServletSecurityContext(HttpServletRequest request) {
this.request = request;
}
@Override
public Principal getPrincipal() {
return this.request.getUserPrincipal();
}
@Override
public boolean isUserInRole(String role) {
return this.request.isUserInRole(role);
}
}
} }

28
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java

@ -16,12 +16,10 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.security.Principal; import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.lang.Nullable;
/** /**
* {@link EndpointWebExtension} for the {@link HealthEndpoint}. * {@link EndpointWebExtension} for the {@link HealthEndpoint}.
@ -39,31 +37,23 @@ public class HealthEndpointWebExtension {
private final HealthIndicator delegate; private final HealthIndicator delegate;
private final HealthStatusHttpMapper statusHttpMapper; private final HealthWebEndpointResponseMapper responseMapper;
private final ShowDetails showDetails;
public HealthEndpointWebExtension(HealthIndicator delegate, public HealthEndpointWebExtension(HealthIndicator delegate,
HealthStatusHttpMapper statusHttpMapper, ShowDetails showDetails) { HealthWebEndpointResponseMapper responseMapper) {
this.delegate = delegate; this.delegate = delegate;
this.statusHttpMapper = statusHttpMapper; this.responseMapper = responseMapper;
this.showDetails = showDetails;
} }
@ReadOperation @ReadOperation
public WebEndpointResponse<Health> getHealth(@Nullable Principal principal) { public WebEndpointResponse<Health> getHealth(SecurityContext securityContext) {
return getHealth(principal, this.showDetails); return this.responseMapper.map(this.delegate.health(), securityContext);
} }
public WebEndpointResponse<Health> getHealth(Principal principal, public WebEndpointResponse<Health> getHealth(SecurityContext securityContext,
ShowDetails showDetails) { ShowDetails showDetails) {
Health health = this.delegate.health(); return this.responseMapper.map(this.delegate.health(), securityContext,
Integer status = this.statusHttpMapper.mapStatus(health.getStatus()); showDetails);
if (showDetails == ShowDetails.NEVER
|| (showDetails == ShowDetails.WHEN_AUTHENTICATED && principal == null)) {
health = Health.status(health.getStatus()).build();
}
return new WebEndpointResponse<>(health, status);
} }
} }

90
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java

@ -0,0 +1,90 @@
/*
* Copyright 2012-2018 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.actuate.health;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.util.CollectionUtils;
/**
* Maps a {@link Health} to a {@WebEndpointResponse}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class HealthWebEndpointResponseMapper {
private final HealthStatusHttpMapper statusHttpMapper;
private final ShowDetails showDetails;
private final Set<String> authorizedRoles;
public HealthWebEndpointResponseMapper(HealthStatusHttpMapper statusHttpMapper,
ShowDetails showDetails, Set<String> authorizedRoles) {
this.statusHttpMapper = statusHttpMapper;
this.showDetails = showDetails;
this.authorizedRoles = authorizedRoles;
}
/**
* Maps the given {@code health} to a {@link WebEndpointResponse}, honouring the
* mapper's default {@link ShowDetails} using the given {@code securityContext}.
* @param health the health to map
* @param securityContext the security context
* @return the mapped response
*/
public WebEndpointResponse<Health> map(Health health,
SecurityContext securityContext) {
return map(health, securityContext, this.showDetails);
}
/**
* Maps the given {@code health} to a {@link WebEndpointResponse}, honouring the given
* {@showDetails} using the given {@code securityContext}.
* @param health the health to map
* @param securityContext the security context
* @param showDetails when to show details in the response
* @return the mapped response
*/
public WebEndpointResponse<Health> map(Health health, SecurityContext securityContext,
ShowDetails showDetails) {
Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
if (showDetails == ShowDetails.NEVER
|| (showDetails == ShowDetails.WHEN_AUTHORIZED
&& (securityContext.getPrincipal() == null
|| !isUserInRole(securityContext)))) {
health = Health.status(health.getStatus()).build();
}
return new WebEndpointResponse<>(health, status);
}
private boolean isUserInRole(SecurityContext securityContext) {
if (CollectionUtils.isEmpty(this.authorizedRoles)) {
return true;
}
for (String role : this.authorizedRoles) {
if (securityContext.isUserInRole(role)) {
return true;
}
}
return false;
}
}

31
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java

@ -16,14 +16,12 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.security.Principal;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.lang.Nullable;
/** /**
* Reactive {@link EndpointWebExtension} for the {@link HealthEndpoint}. * Reactive {@link EndpointWebExtension} for the {@link HealthEndpoint}.
@ -36,33 +34,24 @@ public class ReactiveHealthEndpointWebExtension {
private final ReactiveHealthIndicator delegate; private final ReactiveHealthIndicator delegate;
private final HealthStatusHttpMapper statusHttpMapper; private final HealthWebEndpointResponseMapper responseMapper;
private final ShowDetails showDetails;
public ReactiveHealthEndpointWebExtension(ReactiveHealthIndicator delegate, public ReactiveHealthEndpointWebExtension(ReactiveHealthIndicator delegate,
HealthStatusHttpMapper statusHttpMapper, ShowDetails showDetails) { HealthWebEndpointResponseMapper responseMapper) {
this.delegate = delegate; this.delegate = delegate;
this.statusHttpMapper = statusHttpMapper; this.responseMapper = responseMapper;
this.showDetails = showDetails;
} }
@ReadOperation @ReadOperation
public Mono<WebEndpointResponse<Health>> health(@Nullable Principal principal) { public Mono<WebEndpointResponse<Health>> health(SecurityContext securityContext) {
return health(principal, this.showDetails); return this.delegate.health()
.map((health) -> this.responseMapper.map(health, securityContext));
} }
public Mono<WebEndpointResponse<Health>> health(Principal principal, public Mono<WebEndpointResponse<Health>> health(SecurityContext securityContext,
ShowDetails showDetails) { ShowDetails showDetails) {
return this.delegate.health().map((health) -> { return this.delegate.health().map((health) -> this.responseMapper.map(health,
Integer status = this.statusHttpMapper.mapStatus(health.getStatus()); securityContext, showDetails));
if (showDetails == ShowDetails.NEVER
|| (showDetails == ShowDetails.WHEN_AUTHENTICATED
&& principal == null)) {
health = Health.status(health.getStatus()).build();
}
return new WebEndpointResponse<>(health, status);
});
} }
} }

4
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ShowDetails.java

@ -31,9 +31,9 @@ public enum ShowDetails {
NEVER, NEVER,
/** /**
* Show details in the response when accessed by an authenticated user. * Show details in the response when accessed by an authorized user.
*/ */
WHEN_AUTHENTICATED, WHEN_AUTHORIZED,
/** /**
* Always show details in the response. * Always show details in the response.

8
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java

@ -27,6 +27,7 @@ import org.junit.Test;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameters; import org.springframework.boot.actuate.endpoint.invoke.OperationParameters;
@ -34,6 +35,7 @@ import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link DiscoveredOperationsFactory}. * Tests for {@link DiscoveredOperationsFactory}.
@ -106,7 +108,8 @@ public class DiscoveredOperationsFactoryTests {
TestOperation operation = getFirst( TestOperation operation = getFirst(
this.factory.createOperations("test", new ExampleWithParams())); this.factory.createOperations("test", new ExampleWithParams()));
Map<String, Object> params = Collections.singletonMap("name", 123); Map<String, Object> params = Collections.singletonMap("name", 123);
Object result = operation.invoke(new InvocationContext(null, params)); Object result = operation
.invoke(new InvocationContext(mock(SecurityContext.class), params));
assertThat(result).isEqualTo("123"); assertThat(result).isEqualTo("123");
} }
@ -116,7 +119,8 @@ public class DiscoveredOperationsFactoryTests {
this.invokerAdvisors.add(advisor); this.invokerAdvisors.add(advisor);
TestOperation operation = getFirst( TestOperation operation = getFirst(
this.factory.createOperations("test", new ExampleRead())); this.factory.createOperations("test", new ExampleRead()));
operation.invoke(new InvocationContext(null, Collections.emptyMap())); operation.invoke(new InvocationContext(mock(SecurityContext.class),
Collections.emptyMap()));
assertThat(advisor.getEndpointId()).isEqualTo("test"); assertThat(advisor.getEndpointId()).isEqualTo("test");
assertThat(advisor.getOperationType()).isEqualTo(OperationType.READ); assertThat(advisor.getOperationType()).isEqualTo(OperationType.READ);
assertThat(advisor.getParameters()).isEmpty(); assertThat(advisor.getParameters()).isEmpty();

18
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java

@ -25,12 +25,14 @@ import org.junit.rules.ExpectedException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link ReflectiveOperationInvoker}. * Tests for {@link ReflectiveOperationInvoker}.
@ -84,8 +86,8 @@ public class ReflectiveOperationInvokerTests {
public void invokeShouldInvokeMethod() { public void invokeShouldInvokeMethod() {
ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target,
this.operationMethod, this.parameterValueMapper); this.operationMethod, this.parameterValueMapper);
Object result = invoker.invoke( Object result = invoker.invoke(new InvocationContext(mock(SecurityContext.class),
new InvocationContext(null, Collections.singletonMap("name", "boot"))); Collections.singletonMap("name", "boot")));
assertThat(result).isEqualTo("toob"); assertThat(result).isEqualTo("toob");
} }
@ -94,8 +96,8 @@ public class ReflectiveOperationInvokerTests {
ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target,
this.operationMethod, this.parameterValueMapper); this.operationMethod, this.parameterValueMapper);
this.thrown.expect(MissingParametersException.class); this.thrown.expect(MissingParametersException.class);
invoker.invoke( invoker.invoke(new InvocationContext(mock(SecurityContext.class),
new InvocationContext(null, Collections.singletonMap("name", null))); Collections.singletonMap("name", null)));
} }
@Test @Test
@ -104,8 +106,8 @@ public class ReflectiveOperationInvokerTests {
Example.class, "reverseNullable", String.class), OperationType.READ); Example.class, "reverseNullable", String.class), OperationType.READ);
ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target,
operationMethod, this.parameterValueMapper); operationMethod, this.parameterValueMapper);
Object result = invoker.invoke( Object result = invoker.invoke(new InvocationContext(mock(SecurityContext.class),
new InvocationContext(null, Collections.singletonMap("name", null))); Collections.singletonMap("name", null)));
assertThat(result).isEqualTo("llun"); assertThat(result).isEqualTo("llun");
} }
@ -113,8 +115,8 @@ public class ReflectiveOperationInvokerTests {
public void invokeShouldResolveParameters() { public void invokeShouldResolveParameters() {
ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target,
this.operationMethod, this.parameterValueMapper); this.operationMethod, this.parameterValueMapper);
Object result = invoker.invoke( Object result = invoker.invoke(new InvocationContext(mock(SecurityContext.class),
new InvocationContext(null, Collections.singletonMap("name", 1234))); Collections.singletonMap("name", 1234)));
assertThat(result).isEqualTo("4321"); assertThat(result).isEqualTo("4321");
} }

10
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java vendored

@ -25,6 +25,7 @@ import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -67,7 +68,8 @@ public class CachingOperationInvokerTests {
private void assertCacheIsUsed(Map<String, Object> parameters) { private void assertCacheIsUsed(Map<String, Object> parameters) {
OperationInvoker target = mock(OperationInvoker.class); OperationInvoker target = mock(OperationInvoker.class);
Object expected = new Object(); Object expected = new Object();
InvocationContext context = new InvocationContext(null, parameters); InvocationContext context = new InvocationContext(mock(SecurityContext.class),
parameters);
given(target.invoke(context)).willReturn(expected); given(target.invoke(context)).willReturn(expected);
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L); CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
Object response = invoker.invoke(context); Object response = invoker.invoke(context);
@ -84,7 +86,8 @@ public class CachingOperationInvokerTests {
Map<String, Object> parameters = new HashMap<>(); Map<String, Object> parameters = new HashMap<>();
parameters.put("test", "value"); parameters.put("test", "value");
parameters.put("something", null); parameters.put("something", null);
InvocationContext context = new InvocationContext(null, parameters); InvocationContext context = new InvocationContext(mock(SecurityContext.class),
parameters);
given(target.invoke(context)).willReturn(new Object()); given(target.invoke(context)).willReturn(new Object());
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L); CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
invoker.invoke(context); invoker.invoke(context);
@ -97,7 +100,8 @@ public class CachingOperationInvokerTests {
public void targetInvokedWhenCacheExpires() throws InterruptedException { public void targetInvokedWhenCacheExpires() throws InterruptedException {
OperationInvoker target = mock(OperationInvoker.class); OperationInvoker target = mock(OperationInvoker.class);
Map<String, Object> parameters = new HashMap<>(); Map<String, Object> parameters = new HashMap<>();
InvocationContext context = new InvocationContext(null, parameters); InvocationContext context = new InvocationContext(mock(SecurityContext.class),
parameters);
given(target.invoke(context)).willReturn(new Object()); given(target.invoke(context)).willReturn(new Object());
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 50L); CachingOperationInvoker invoker = new CachingOperationInvoker(target, 50L);
invoker.invoke(context); invoker.invoke(context);

90
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java

@ -30,6 +30,7 @@ import java.util.function.Supplier;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@ -361,6 +362,52 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
.expectBody(String.class).isEqualTo("Zoe")); .expectBody(String.class).isEqualTo("Zoe"));
} }
@Test
public void securityContextIsAvailableAndHasNullPrincipalWhenRequestHasNoPrincipal() {
load(SecurityContextEndpointConfiguration.class,
(client) -> client.get().uri("/securitycontext")
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
.isOk().expectBody(String.class).isEqualTo("None"));
}
@Test
public void securityContextIsAvailableAndHasPrincipalWhenRequestHasPrincipal() {
load((context) -> {
this.authenticatedContextCustomizer.accept(context);
context.register(SecurityContextEndpointConfiguration.class);
}, (client) -> client.get().uri("/securitycontext")
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk()
.expectBody(String.class).isEqualTo("Alice"));
}
@Test
public void userInRoleReturnsFalseWhenRequestHasNoPrincipal() {
load(UserInRoleEndpointConfiguration.class,
(client) -> client.get().uri("/userinrole?role=ADMIN")
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
.isOk().expectBody(String.class).isEqualTo("ADMIN: false"));
}
@Test
public void userInRoleReturnsFalseWhenUserIsNotInRole() {
load((context) -> {
this.authenticatedContextCustomizer.accept(context);
context.register(UserInRoleEndpointConfiguration.class);
}, (client) -> client.get().uri("/userinrole?role=ADMIN")
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk()
.expectBody(String.class).isEqualTo("ADMIN: false"));
}
@Test
public void userInRoleReturnsTrueWhenUserIsInRole() {
load((context) -> {
this.authenticatedContextCustomizer.accept(context);
context.register(UserInRoleEndpointConfiguration.class);
}, (client) -> client.get().uri("/userinrole?role=ACTUATOR")
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk()
.expectBody(String.class).isEqualTo("ACTUATOR: true"));
}
protected abstract int getPort(T context); protected abstract int getPort(T context);
protected void validateErrorBody(WebTestClient.BodyContentSpec body, protected void validateErrorBody(WebTestClient.BodyContentSpec body,
@ -581,6 +628,28 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
} }
@Configuration
@Import(BaseConfiguration.class)
protected static class SecurityContextEndpointConfiguration {
@Bean
public SecurityContextEndpoint securityContextEndpoint() {
return new SecurityContextEndpoint();
}
}
@Configuration
@Import(BaseConfiguration.class)
protected static class UserInRoleEndpointConfiguration {
@Bean
public UserInRoleEndpoint userInRoleEndpoint() {
return new UserInRoleEndpoint();
}
}
@Endpoint(id = "test") @Endpoint(id = "test")
static class TestEndpoint { static class TestEndpoint {
@ -779,6 +848,27 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
} }
@Endpoint(id = "securitycontext")
static class SecurityContextEndpoint {
@ReadOperation
public String read(SecurityContext securityContext) {
Principal principal = securityContext.getPrincipal();
return principal == null ? "None" : principal.getName();
}
}
@Endpoint(id = "userinrole")
static class UserInRoleEndpoint {
@ReadOperation
public String read(SecurityContext securityContext, String role) {
return role + ": " + securityContext.isUserInRole(role);
}
}
public interface EndpointDelegate { public interface EndpointDelegate {
void write(); void write();

43
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java

@ -17,7 +17,7 @@
package org.springframework.boot.actuate.endpoint.web.jersey; package org.springframework.boot.actuate.endpoint.web.jersey;
import java.io.IOException; import java.io.IOException;
import java.security.Principal; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
@ -25,7 +25,6 @@ import javax.servlet.Filter;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.ContextResolver;
@ -47,6 +46,11 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@ -131,7 +135,18 @@ public class JerseyWebEndpointIntegrationTests extends
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
filterChain.doFilter(new MockPrincipalWrapper(request), response); SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(new UsernamePasswordAuthenticationToken(
"Alice", "secret",
Arrays.asList(new SimpleGrantedAuthority("ROLE_ACTUATOR"))));
SecurityContextHolder.setContext(context);
try {
filterChain.doFilter(new SecurityContextHolderAwareRequestWrapper(
request, "ROLE_"), response);
}
finally {
SecurityContextHolder.clearContext();
}
} }
}; };
@ -139,28 +154,6 @@ public class JerseyWebEndpointIntegrationTests extends
} }
private static class MockPrincipalWrapper extends HttpServletRequestWrapper {
MockPrincipalWrapper(HttpServletRequest request) {
super(request);
}
@Override
public Principal getUserPrincipal() {
return new MockPrincipal();
}
}
private static class MockPrincipal implements Principal {
@Override
public String getName() {
return "Alice";
}
}
private static final class ObjectMapperContextResolver private static final class ObjectMapperContextResolver
implements ContextResolver<ObjectMapper> { implements ContextResolver<ObjectMapper> {

36
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java

@ -16,7 +16,6 @@
package org.springframework.boot.actuate.endpoint.web.reactive; package org.springframework.boot.actuate.endpoint.web.reactive;
import java.security.Principal;
import java.util.Arrays; import java.util.Arrays;
import org.junit.Test; import org.junit.Test;
@ -40,10 +39,12 @@ 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.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
@ -152,8 +153,12 @@ public class WebFluxEndpointIntegrationTests extends
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, public Mono<Void> filter(ServerWebExchange exchange,
WebFilterChain chain) { WebFilterChain chain) {
return chain.filter( return chain.filter(exchange).subscriberContext(
new MockPrincipalServerWebExchangeDecorator(exchange)); ReactiveSecurityContextHolder.withAuthentication(
new UsernamePasswordAuthenticationToken("Alice",
"secret",
Arrays.asList(new SimpleGrantedAuthority(
"ROLE_ACTUATOR")))));
} }
}; };
@ -161,27 +166,4 @@ public class WebFluxEndpointIntegrationTests extends
} }
private static class MockPrincipalServerWebExchangeDecorator
extends ServerWebExchangeDecorator {
MockPrincipalServerWebExchangeDecorator(ServerWebExchange delegate) {
super(delegate);
}
@Override
public Mono<Principal> getPrincipal() {
return Mono.just(new MockPrincipal());
}
}
private static class MockPrincipal implements Principal {
@Override
public String getName() {
return "Alice";
}
}
} }

42
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java

@ -17,14 +17,12 @@
package org.springframework.boot.actuate.endpoint.web.servlet; package org.springframework.boot.actuate.endpoint.web.servlet;
import java.io.IOException; import java.io.IOException;
import java.security.Principal;
import java.util.Arrays; import java.util.Arrays;
import javax.servlet.Filter; import javax.servlet.Filter;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.junit.Test; import org.junit.Test;
@ -48,6 +46,11 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment; 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.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@ -145,7 +148,18 @@ public class MvcWebEndpointIntegrationTests extends
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
filterChain.doFilter(new MockPrincipalWrapper(request), response); SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(new UsernamePasswordAuthenticationToken(
"Alice", "secret",
Arrays.asList(new SimpleGrantedAuthority("ROLE_ACTUATOR"))));
SecurityContextHolder.setContext(context);
try {
filterChain.doFilter(new SecurityContextHolderAwareRequestWrapper(
request, "ROLE_"), response);
}
finally {
SecurityContextHolder.clearContext();
}
} }
}; };
@ -153,26 +167,4 @@ public class MvcWebEndpointIntegrationTests extends
} }
private static class MockPrincipalWrapper extends HttpServletRequestWrapper {
MockPrincipalWrapper(HttpServletRequest request) {
super(request);
}
@Override
public Principal getUserPrincipal() {
return new MockPrincipal();
}
}
private static class MockPrincipal implements Principal {
@Override
public String getName() {
return "Alice";
}
}
} }

6
spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java

@ -16,6 +16,8 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import org.junit.Test; import org.junit.Test;
@ -75,7 +77,9 @@ public class HealthEndpointWebIntegrationTests {
return new HealthEndpointWebExtension( return new HealthEndpointWebExtension(
new CompositeHealthIndicatorFactory().createHealthIndicator( new CompositeHealthIndicatorFactory().createHealthIndicator(
new OrderedHealthAggregator(), healthIndicators), new OrderedHealthAggregator(), healthIndicators),
new HealthStatusHttpMapper(), ShowDetails.ALWAYS); new HealthWebEndpointResponseMapper(new HealthStatusHttpMapper(),
ShowDetails.ALWAYS,
new HashSet<>(Arrays.asList("ACTUATOR"))));
} }
@Bean @Bean

3
spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc

@ -1200,7 +1200,8 @@ content into your application. Rather, pick only the properties that you need.
# HEALTH ENDPOINT ({sc-spring-boot-actuator}/health/HealthEndpoint.{sc-ext}[HealthEndpoint], {sc-spring-boot-actuator-autoconfigure}/health/HealthEndpointProperties.{sc-ext}[HealthEndpointProperties]) # HEALTH ENDPOINT ({sc-spring-boot-actuator}/health/HealthEndpoint.{sc-ext}[HealthEndpoint], {sc-spring-boot-actuator-autoconfigure}/health/HealthEndpointProperties.{sc-ext}[HealthEndpointProperties])
management.endpoint.health.cache.time-to-live=0ms # Maximum time that a response can be cached. management.endpoint.health.cache.time-to-live=0ms # Maximum time that a response can be cached.
management.endpoint.health.enabled= # Whether to enable the health endpoint. management.endpoint.health.enabled= # Whether to enable the health endpoint.
management.endpoint.health.show-details=false # Whether to show full health details instead of just the status when exposed over a potentially insecure connection. management.endpoint.health.roles= # Roles used to determine whether or not a user is authorized to be shown details. When empty, all authenticated users are authorized.
management.endpoint.health.show-details=when-authorized # When to show full health details.
# HEAP DUMP ENDPOINT ({sc-spring-boot-actuator}/management/HeapDumpWebEndpoint.{sc-ext}[HeapDumpWebEndpoint]) # HEAP DUMP ENDPOINT ({sc-spring-boot-actuator}/management/HeapDumpWebEndpoint.{sc-ext}[HeapDumpWebEndpoint])
management.endpoint.heapdump.cache.time-to-live=0ms # Maximum time that a response can be cached. management.endpoint.heapdump.cache.time-to-live=0ms # Maximum time that a response can be cached.

10
spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc

@ -523,14 +523,18 @@ following values:
|`never` |`never`
|Details are never shown. |Details are never shown.
|`when-authenticated` |`when-authorized`
|Details are only shown to authenticated users. |Details are only shown to authorized users. Authorized roles can be configured using
`management.endpoint.health.roles`.
|`always` |`always`
|Details are shown to all users. |Details are shown to all users.
|=== |===
The default value is `when-authenticated`. The default value is `when-authorized`. A user is considered to be authorized when they
are in one or more of the endpoint's roles. If the endpoint has no configured roles
(the default) all authenticated users are considered to be authorized. The roles can
be configured using the `management.endpoint.health.roles` property.
NOTE: If you have secured your application and wish to use `always`, your security NOTE: If you have secured your application and wish to use `always`, your security
configuration must permit access to the health endpoint for both authenticated and configuration must permit access to the health endpoint for both authenticated and

Loading…
Cancel
Save