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

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