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