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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,6 @@
package org.springframework.boot.actuate.endpoint;
import java.security.Principal;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
@ -30,24 +29,26 @@ import org.springframework.util.Assert;
*/
public class InvocationContext {
private final Principal principal;
private final SecurityContext securityContext;
private final Map<String, Object> arguments;
/**
* Creates a new context for an operation being invoked by the given {@code principal}
* 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}
*/
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");
this.principal = principal;
this.securityContext = securityContext;
this.arguments = arguments;
}
public Principal getPrincipal() {
return this.principal;
public SecurityContext getSecurityContext() {
return this.securityContext;
}
public Map<String, Object> getArguments() {

View File

@ -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);
}

View File

@ -22,6 +22,7 @@ import java.util.Set;
import java.util.stream.Collectors;
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.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
@ -89,7 +90,10 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
return false;
}
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;
}
@ -102,7 +106,10 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
private Object resolveArgument(OperationParameter parameter,
InvocationContext context) {
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());
return this.parameterValueMapper.mapParameterValue(parameter, value);

View File

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

View File

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint.jmx;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
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.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -97,7 +99,8 @@ public class EndpointMBean implements DynamicMBean {
String[] parameterNames = operation.getParameters().stream()
.map(JmxOperationParameter::getName).toArray(String[]::new);
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) {
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;
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.web.jersey;
import java.io.IOException;
import java.io.InputStream;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
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.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -150,7 +152,7 @@ public class JerseyEndpointResourceFactory {
arguments.putAll(extractQueryParameters(data));
try {
Object response = this.operation.invoke(new InvocationContext(
data.getSecurityContext().getUserPrincipal(), arguments));
new JerseySecurityContext(data.getSecurityContext()), arguments));
return convertToJaxRsResponse(response, data.getRequest().getMethod());
}
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);
}
}
}

View File

@ -21,6 +21,7 @@ import java.security.Principal;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
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.InvocationContext;
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.web.EndpointMapping;
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.HttpStatus;
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.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
@ -256,33 +262,55 @@ public abstract class AbstractWebFluxEndpointHandlerMapping
private static final class ReactiveWebOperationAdapter
implements ReactiveWebOperation {
private static final Principal NO_PRINCIPAL = new Principal() {
@Override
public String getName() {
throw new UnsupportedOperationException();
}
};
private final OperationInvoker invoker;
private final Supplier<Mono<? extends SecurityContext>> securityContextSupplier;
private ReactiveWebOperationAdapter(OperationInvoker invoker) {
this.invoker = invoker;
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)));
}
public Mono<SecurityContext> emptySecurityContext() {
return Mono.just(new SecurityContext() {
@Override
public Principal getPrincipal() {
return null;
}
@Override
public boolean isUserInRole(String role) {
return false;
}
});
}
@Override
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange,
Map<String, String> body) {
return exchange.getPrincipal().defaultIfEmpty(NO_PRINCIPAL)
.flatMap((principal) -> {
Map<String, Object> arguments = getArguments(exchange, body);
return handleResult(
(Publisher<?>) this.invoker.invoke(new InvocationContext(
principal == NO_PRINCIPAL ? null : principal,
arguments)),
exchange.getRequest().getMethod());
});
Map<String, Object> arguments = getArguments(exchange, body);
return this.securityContextSupplier.get()
.map((securityContext) -> new InvocationContext(securityContext,
arguments))
.flatMap((invocationContext) -> handleResult(
(Publisher<?>) this.invoker.invoke(invocationContext),
exchange.getRequest().getMethod()));
}
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();
}
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.boot.actuate.endpoint.web.servlet;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
@ -29,6 +30,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
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.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -243,7 +245,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping
try {
return handleResult(
this.invoker.invoke(new InvocationContext(
request.getUserPrincipal(), arguments)),
new ServletSecurityContext(request), arguments)),
HttpMethod.valueOf(request.getMethod()));
}
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);
}
}
}

View File

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

View File

@ -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;
}
}

View File

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

View File

@ -31,9 +31,9 @@ public enum ShowDetails {
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.

View File

@ -27,6 +27,7 @@ import org.junit.Test;
import org.springframework.boot.actuate.endpoint.InvocationContext;
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.OperationInvokerAdvisor;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DiscoveredOperationsFactory}.
@ -106,7 +108,8 @@ public class DiscoveredOperationsFactoryTests {
TestOperation operation = getFirst(
this.factory.createOperations("test", new ExampleWithParams()));
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");
}
@ -116,7 +119,8 @@ public class DiscoveredOperationsFactoryTests {
this.invokerAdvisors.add(advisor);
TestOperation operation = getFirst(
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.getOperationType()).isEqualTo(OperationType.READ);
assertThat(advisor.getParameters()).isEmpty();

View File

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

View File

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

View File

@ -30,6 +30,7 @@ import java.util.function.Supplier;
import org.junit.Test;
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.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@ -361,6 +362,52 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
.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 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")
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 {
void write();

View File

@ -17,7 +17,7 @@
package org.springframework.boot.actuate.endpoint.web.jersey;
import java.io.IOException;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
@ -25,7 +25,6 @@ import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
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.core.env.Environment;
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.web.filter.OncePerRequestFilter;
@ -131,7 +135,18 @@ public class JerseyWebEndpointIntegrationTests extends
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
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
implements ContextResolver<ObjectMapper> {

View File

@ -16,7 +16,6 @@
package org.springframework.boot.actuate.endpoint.web.reactive;
import java.security.Principal;
import java.util.Arrays;
import org.junit.Test;
@ -40,10 +39,12 @@ import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.reactive.config.EnableWebFlux;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
@ -152,8 +153,12 @@ public class WebFluxEndpointIntegrationTests extends
@Override
public Mono<Void> filter(ServerWebExchange exchange,
WebFilterChain chain) {
return chain.filter(
new MockPrincipalServerWebExchangeDecorator(exchange));
return chain.filter(exchange).subscriberContext(
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";
}
}
}

View File

@ -17,14 +17,12 @@
package org.springframework.boot.actuate.endpoint.web.servlet;
import java.io.IOException;
import java.security.Principal;
import java.util.Arrays;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
@ -48,6 +46,11 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
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.filter.OncePerRequestFilter;
@ -145,7 +148,18 @@ public class MvcWebEndpointIntegrationTests extends
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
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";
}
}
}

View File

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

View File

@ -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])
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.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])
management.endpoint.heapdump.cache.time-to-live=0ms # Maximum time that a response can be cached.

View File

@ -523,14 +523,18 @@ following values:
|`never`
|Details are never shown.
|`when-authenticated`
|Details are only shown to authenticated users.
|`when-authorized`
|Details are only shown to authorized users. Authorized roles can be configured using
`management.endpoint.health.roles`.
|`always`
|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
configuration must permit access to the health endpoint for both authenticated and