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,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