Expose Health details if user has authority
If the Princial is a Spring Security Authentication object and the request doesn't have the right roles, check the authorities. Fixes gh-8471
This commit is contained in:
parent
caefb23401
commit
d4b52a3538
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.springframework.boot.actuate.cloudfoundry;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.HealthEndpoint;
|
||||
|
|
@ -36,7 +38,7 @@ class CloudFoundryHealthMvcEndpoint extends HealthMvcEndpoint {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected boolean exposeHealthDetails(HttpServletRequest request) {
|
||||
protected boolean exposeHealthDetails(HttpServletRequest request, Principal principal) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.boot.actuate.endpoint.mvc;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -33,7 +34,10 @@ import org.springframework.context.EnvironmentAware;
|
|||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
|
|
@ -133,12 +137,12 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
|
|||
|
||||
@ActuatorGetMapping
|
||||
@ResponseBody
|
||||
public Object invoke(HttpServletRequest request) {
|
||||
public Object invoke(HttpServletRequest request, Principal principal) {
|
||||
if (!getDelegate().isEnabled()) {
|
||||
// Shouldn't happen because the request mapping should not be registered
|
||||
return getDisabledResponse();
|
||||
}
|
||||
Health health = getHealth(request);
|
||||
Health health = getHealth(request, principal);
|
||||
HttpStatus status = getStatus(health);
|
||||
if (status != null) {
|
||||
return new ResponseEntity<Health>(health, status);
|
||||
|
|
@ -160,13 +164,13 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
|
|||
return null;
|
||||
}
|
||||
|
||||
private Health getHealth(HttpServletRequest request) {
|
||||
private Health getHealth(HttpServletRequest request, Principal principal) {
|
||||
long accessTime = System.currentTimeMillis();
|
||||
if (isCacheStale(accessTime)) {
|
||||
this.lastAccess = accessTime;
|
||||
this.cached = getDelegate().invoke();
|
||||
}
|
||||
if (exposeHealthDetails(request)) {
|
||||
if (exposeHealthDetails(request, principal)) {
|
||||
return this.cached;
|
||||
}
|
||||
return Health.status(this.cached.getStatus()).build();
|
||||
|
|
@ -179,14 +183,24 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
|
|||
return (accessTime - this.lastAccess) >= getDelegate().getTimeToLive();
|
||||
}
|
||||
|
||||
protected boolean exposeHealthDetails(HttpServletRequest request) {
|
||||
protected boolean exposeHealthDetails(HttpServletRequest request, Principal principal) {
|
||||
if (!this.secure) {
|
||||
return true;
|
||||
}
|
||||
for (String role : getRoles()) {
|
||||
if (request.isUserInRole(role) || request.isUserInRole("ROLE_" + role)) {
|
||||
List<String> roles = getRoles();
|
||||
for (String role : roles) {
|
||||
if (request.isUserInRole(role)) {
|
||||
return true;
|
||||
}
|
||||
if (isSpringSecurityAuthentication(principal)) {
|
||||
Authentication authentication = (Authentication) principal;
|
||||
for (GrantedAuthority authority : authentication.getAuthorities()) {
|
||||
String name = authority.getAuthority();
|
||||
if (role.equals(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -201,4 +215,9 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint
|
|||
return Arrays.asList(roles);
|
||||
}
|
||||
|
||||
private boolean isSpringSecurityAuthentication(Principal principal) {
|
||||
return ClassUtils.isPresent("org.springframework.security.core.Authentication",
|
||||
null) && (principal instanceof Authentication);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public class HealthMvcEndpointAutoConfigurationTests {
|
|||
this.context.refresh();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
Health health = (Health) this.context.getBean(HealthMvcEndpoint.class)
|
||||
.invoke(request);
|
||||
.invoke(request, null);
|
||||
assertThat(health.getStatus()).isEqualTo(Status.UP);
|
||||
assertThat(health.getDetails().get("foo")).isNull();
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ public class HealthMvcEndpointAutoConfigurationTests {
|
|||
"management.security.enabled=false");
|
||||
this.context.refresh();
|
||||
Health health = (Health) this.context.getBean(HealthMvcEndpoint.class)
|
||||
.invoke(null);
|
||||
.invoke(null, null);
|
||||
assertThat(health.getStatus()).isEqualTo(Status.UP);
|
||||
Health map = (Health) health.getDetails().get("test");
|
||||
assertThat(map.getDetails().get("foo")).isEqualTo("bar");
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class CloudFoundryHealthMvcEndpointTests {
|
|||
given(endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
given(endpoint.isSensitive()).willReturn(false);
|
||||
Object result = mvc.invoke(null);
|
||||
Object result = mvc.invoke(null, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.mvc;
|
|||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
|
|
@ -34,9 +35,12 @@ import org.springframework.http.ResponseEntity;
|
|||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
|
|
@ -52,7 +56,7 @@ public class HealthMvcEndpointTests {
|
|||
|
||||
private static final PropertySource<?> SECURITY_ROLES = new MapPropertySource("test",
|
||||
Collections.<String, Object>singletonMap("management.security.roles",
|
||||
"HERO, USER"));
|
||||
"HERO"));
|
||||
|
||||
private HttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
|
|
@ -62,13 +66,11 @@ public class HealthMvcEndpointTests {
|
|||
|
||||
private MockEnvironment environment;
|
||||
|
||||
private HttpServletRequest user = createAuthenticationToken("ROLE_USER");
|
||||
private HttpServletRequest defaultUser = createAuthenticationRequest("ROLE_ACTUATOR");
|
||||
|
||||
private HttpServletRequest actuator = createAuthenticationToken("ROLE_ACTUATOR");
|
||||
private HttpServletRequest hero = createAuthenticationRequest("HERO");
|
||||
|
||||
private HttpServletRequest hero = createAuthenticationToken("ROLE_HERO");
|
||||
|
||||
private HttpServletRequest createAuthenticationToken(String role) {
|
||||
private HttpServletRequest createAuthenticationRequest(String role) {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
servletContext.declareRoles(role);
|
||||
return new MockHttpServletRequest(servletContext);
|
||||
|
|
@ -86,7 +88,7 @@ public class HealthMvcEndpointTests {
|
|||
@Test
|
||||
public void up() {
|
||||
given(this.endpoint.invoke()).willReturn(new Health.Builder().up().build());
|
||||
Object result = this.mvc.invoke(this.request);
|
||||
Object result = this.mvc.invoke(this.request, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
}
|
||||
|
|
@ -95,7 +97,7 @@ public class HealthMvcEndpointTests {
|
|||
@Test
|
||||
public void down() {
|
||||
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
|
||||
Object result = this.mvc.invoke(this.request);
|
||||
Object result = this.mvc.invoke(this.request, null);
|
||||
assertThat(result instanceof ResponseEntity).isTrue();
|
||||
ResponseEntity<Health> response = (ResponseEntity<Health>) result;
|
||||
assertThat(response.getBody().getStatus() == Status.DOWN).isTrue();
|
||||
|
|
@ -109,7 +111,7 @@ public class HealthMvcEndpointTests {
|
|||
.willReturn(new Health.Builder().status("OK").build());
|
||||
this.mvc.setStatusMapping(
|
||||
Collections.singletonMap("OK", HttpStatus.INTERNAL_SERVER_ERROR));
|
||||
Object result = this.mvc.invoke(this.request);
|
||||
Object result = this.mvc.invoke(this.request, null);
|
||||
assertThat(result instanceof ResponseEntity).isTrue();
|
||||
ResponseEntity<Health> response = (ResponseEntity<Health>) result;
|
||||
assertThat(response.getBody().getStatus().equals(new Status("OK"))).isTrue();
|
||||
|
|
@ -123,7 +125,7 @@ public class HealthMvcEndpointTests {
|
|||
.willReturn(new Health.Builder().outOfService().build());
|
||||
this.mvc.setStatusMapping(Collections.singletonMap("out-of-service",
|
||||
HttpStatus.INTERNAL_SERVER_ERROR));
|
||||
Object result = this.mvc.invoke(this.request);
|
||||
Object result = this.mvc.invoke(this.request, null);
|
||||
assertThat(result instanceof ResponseEntity).isTrue();
|
||||
ResponseEntity<Health> response = (ResponseEntity<Health>) result;
|
||||
assertThat(response.getBody().getStatus().equals(Status.OUT_OF_SERVICE)).isTrue();
|
||||
|
|
@ -134,7 +136,7 @@ public class HealthMvcEndpointTests {
|
|||
public void presenceOfRightRoleShouldExposeDetails() {
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.actuator);
|
||||
Object result = this.mvc.invoke(this.defaultUser, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
|
||||
|
|
@ -145,7 +147,7 @@ public class HealthMvcEndpointTests {
|
|||
this.mvc = new HealthMvcEndpoint(this.endpoint, false);
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.user);
|
||||
Object result = this.mvc.invoke(this.defaultUser, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
|
||||
|
|
@ -155,18 +157,32 @@ public class HealthMvcEndpointTests {
|
|||
public void rightRoleNotPresentShouldNotExposeDetails() {
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.user);
|
||||
Object result = this.mvc.invoke(this.hero, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rightAuthorityPresentShouldExposeDetails() throws Exception {
|
||||
this.environment.getPropertySources().addLast(SECURITY_ROLES);
|
||||
Authentication principal = mock(Authentication.class);
|
||||
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("HERO"));
|
||||
doReturn(authorities).when(principal).getAuthorities();
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.defaultUser, principal);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customRolePresentShouldExposeDetails() {
|
||||
this.environment.getPropertySources().addLast(SECURITY_ROLES);
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.hero);
|
||||
Object result = this.mvc.invoke(this.hero, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
|
||||
|
|
@ -177,12 +193,25 @@ public class HealthMvcEndpointTests {
|
|||
this.environment.getPropertySources().addLast(SECURITY_ROLES);
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.actuator);
|
||||
Object result = this.mvc.invoke(this.defaultUser, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customRoleFromListShouldExposeDetails() {
|
||||
// gh-8314
|
||||
this.mvc = new HealthMvcEndpoint(this.endpoint, true,
|
||||
Arrays.asList("HERO", "USER"));
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.hero, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customRoleFromListShouldNotExposeDetailsForDefaultRole() {
|
||||
// gh-8314
|
||||
|
|
@ -190,10 +219,10 @@ public class HealthMvcEndpointTests {
|
|||
Arrays.asList("HERO", "USER"));
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.hero);
|
||||
Object result = this.mvc.invoke(this.defaultUser, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
assertThat(((Health) result).getDetails().get("foo")).isEqualTo("bar");
|
||||
assertThat(((Health) result).getDetails().get("foo")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -201,14 +230,14 @@ public class HealthMvcEndpointTests {
|
|||
given(this.endpoint.getTimeToLive()).willReturn(10000L);
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.actuator);
|
||||
Object result = this.mvc.invoke(this.defaultUser, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
Health health = (Health) result;
|
||||
assertThat(health.getStatus() == Status.UP).isTrue();
|
||||
assertThat(health.getDetails()).hasSize(1);
|
||||
assertThat(health.getDetails().get("foo")).isEqualTo("bar");
|
||||
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
|
||||
result = this.mvc.invoke(this.request); // insecure now
|
||||
result = this.mvc.invoke(this.request, null); // insecure now
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
health = (Health) result;
|
||||
// so the result is cached
|
||||
|
|
@ -222,11 +251,11 @@ public class HealthMvcEndpointTests {
|
|||
given(this.endpoint.getTimeToLive()).willReturn(0L);
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.request);
|
||||
Object result = this.mvc.invoke(this.request, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
|
||||
result = this.mvc.invoke(this.request);
|
||||
result = this.mvc.invoke(this.request, null);
|
||||
@SuppressWarnings("unchecked")
|
||||
Health health = ((ResponseEntity<Health>) result).getBody();
|
||||
assertThat(health.getStatus() == Status.DOWN).isTrue();
|
||||
|
|
@ -237,12 +266,12 @@ public class HealthMvcEndpointTests {
|
|||
given(this.endpoint.getTimeToLive()).willReturn(50L);
|
||||
given(this.endpoint.invoke())
|
||||
.willReturn(new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.request);
|
||||
Object result = this.mvc.invoke(this.request, null);
|
||||
assertThat(result instanceof Health).isTrue();
|
||||
assertThat(((Health) result).getStatus() == Status.UP).isTrue();
|
||||
Thread.sleep(100);
|
||||
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
|
||||
result = this.mvc.invoke(this.request);
|
||||
result = this.mvc.invoke(this.request, null);
|
||||
@SuppressWarnings("unchecked")
|
||||
Health health = ((ResponseEntity<Health>) result).getBody();
|
||||
assertThat(health.getStatus() == Status.DOWN).isTrue();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.springframework.boot.actuate.endpoint.mvc;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
|
@ -35,12 +37,15 @@ import org.springframework.boot.junit.runner.classpath.ModifiedClassPathRunner;
|
|||
import org.springframework.boot.test.util.EnvironmentTestUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
|
@ -63,13 +68,13 @@ public class NoSpringSecurityHealthMvcEndpointIntegrationTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void healthDetailNotPresent() throws Exception {
|
||||
public void healthWhenRightRoleNotPresentShouldExposeHealthDetails() throws Exception {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.register(TestConfiguration.class);
|
||||
this.context.refresh();
|
||||
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
|
||||
mockMvc.perform(get("/health")).andExpect(status().isOk())
|
||||
mockMvc.perform(get("/health").with(getRequestPostProcessor())).andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("\"status\":\"UP\"")));
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +92,17 @@ public class NoSpringSecurityHealthMvcEndpointIntegrationTests {
|
|||
"\"status\":\"UP\",\"test\":{\"status\":\"UP\",\"hello\":\"world\"}")));
|
||||
}
|
||||
|
||||
private RequestPostProcessor getRequestPostProcessor() {
|
||||
return new RequestPostProcessor() {
|
||||
@Override
|
||||
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
|
||||
Principal principal = mock(Principal.class);
|
||||
request.setUserPrincipal(principal);
|
||||
return request;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ImportAutoConfiguration({ JacksonAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class,
|
||||
EndpointWebMvcAutoConfiguration.class,
|
||||
|
|
|
|||
Loading…
Reference in New Issue