Allow the user to opt-out of anonymous access restrictions for /health
By default, when /health is accessed anonymously, the details are stripped, i.e. the response will only indicate UP or DOWN. Furthermore the response is cached for a configurable period to prevent a denial of service attack. This commit adds a configuration property, endpoints.health.restrict-anonymous-access, that can be set to false to allow full anonymous access to /health. When full access is allowed, the details will be included in the response and the response will not be cached. Closes gh-1977
This commit is contained in:
parent
5854ea189e
commit
26a511495e
|
|
@ -30,6 +30,7 @@ import org.springframework.util.Assert;
|
|||
*
|
||||
* @author Dave Syer
|
||||
* @author Christian Dupuis
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = true)
|
||||
public class HealthEndpoint extends AbstractEndpoint<Health> {
|
||||
|
|
@ -38,18 +39,7 @@ public class HealthEndpoint extends AbstractEndpoint<Health> {
|
|||
|
||||
private long timeToLive = 1000;
|
||||
|
||||
/**
|
||||
* Time to live for cached result. If accessed anonymously, we might need to cache the
|
||||
* result of this endpoint to prevent a DOS attack.
|
||||
* @return time to live in milliseconds (default 1000)
|
||||
*/
|
||||
public long getTimeToLive() {
|
||||
return this.timeToLive;
|
||||
}
|
||||
|
||||
public void setTimeToLive(long ttl) {
|
||||
this.timeToLive = ttl;
|
||||
}
|
||||
private boolean restrictAnonymousAccess = true;
|
||||
|
||||
/**
|
||||
* Create a new {@link HealthIndicator} instance.
|
||||
|
|
@ -69,6 +59,27 @@ public class HealthEndpoint extends AbstractEndpoint<Health> {
|
|||
this.healthIndicator = healthIndicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time to live for cached result. If accessed anonymously, we might need to cache the
|
||||
* result of this endpoint to prevent a DOS attack.
|
||||
* @return time to live in milliseconds (default 1000)
|
||||
*/
|
||||
public long getTimeToLive() {
|
||||
return this.timeToLive;
|
||||
}
|
||||
|
||||
public void setTimeToLive(long ttl) {
|
||||
this.timeToLive = ttl;
|
||||
}
|
||||
|
||||
public boolean isRestrictAnonymousAccess() {
|
||||
return this.restrictAnonymousAccess;
|
||||
}
|
||||
|
||||
public void setRestrictAnonymousAccess(boolean restrictAnonymousAccess) {
|
||||
this.restrictAnonymousAccess = restrictAnonymousAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke all {@link HealthIndicator} delegates and collect their health information.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
|||
*
|
||||
* @author Christian Dupuis
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public class HealthMvcEndpoint implements MvcEndpoint {
|
||||
|
|
@ -121,7 +122,7 @@ public class HealthMvcEndpoint implements MvcEndpoint {
|
|||
// Not too worried about concurrent access here, the worst that can happen is the
|
||||
// odd extra call to delegate.invoke()
|
||||
this.cached = health;
|
||||
if (!secure(principal)) {
|
||||
if (this.delegate.isRestrictAnonymousAccess() && !secure(principal)) {
|
||||
// If not secure we only expose the status
|
||||
health = Health.status(health.getStatus()).build();
|
||||
}
|
||||
|
|
@ -133,15 +134,20 @@ public class HealthMvcEndpoint implements MvcEndpoint {
|
|||
}
|
||||
|
||||
private boolean useCachedValue(Principal principal) {
|
||||
long currentAccess = System.currentTimeMillis();
|
||||
if (this.cached == null || secure(principal)
|
||||
|| (currentAccess - this.lastAccess) > this.delegate.getTimeToLive()) {
|
||||
this.lastAccess = currentAccess;
|
||||
long accessTime = System.currentTimeMillis();
|
||||
if (cacheIsStale(accessTime) || secure(principal)
|
||||
|| !this.delegate.isRestrictAnonymousAccess()) {
|
||||
this.lastAccess = accessTime;
|
||||
return false;
|
||||
}
|
||||
return this.cached != null;
|
||||
}
|
||||
|
||||
private boolean cacheIsStale(long accessTime) {
|
||||
return this.cached == null
|
||||
|| (accessTime - this.lastAccess) > this.delegate.getTimeToLive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return "/" + this.delegate.getId();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ import org.springframework.http.ResponseEntity;
|
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
|
@ -93,6 +96,7 @@ public class HealthMvcEndpointTests {
|
|||
public void secure() {
|
||||
given(this.endpoint.invoke()).willReturn(
|
||||
new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
given(this.endpoint.isRestrictAnonymousAccess()).willReturn(true);
|
||||
Object result = this.mvc.invoke(this.user);
|
||||
assertTrue(result instanceof Health);
|
||||
assertTrue(((Health) result).getStatus() == Status.UP);
|
||||
|
|
@ -102,6 +106,7 @@ public class HealthMvcEndpointTests {
|
|||
@Test
|
||||
public void secureNotCached() {
|
||||
given(this.endpoint.getTimeToLive()).willReturn(10000L);
|
||||
given(this.endpoint.isRestrictAnonymousAccess()).willReturn(true);
|
||||
given(this.endpoint.invoke()).willReturn(
|
||||
new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.user);
|
||||
|
|
@ -117,16 +122,66 @@ public class HealthMvcEndpointTests {
|
|||
@Test
|
||||
public void unsecureCached() {
|
||||
given(this.endpoint.getTimeToLive()).willReturn(10000L);
|
||||
given(this.endpoint.isRestrictAnonymousAccess()).willReturn(true);
|
||||
given(this.endpoint.invoke()).willReturn(
|
||||
new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(this.user);
|
||||
assertTrue(result instanceof Health);
|
||||
assertTrue(((Health) result).getStatus() == Status.UP);
|
||||
Health health = (Health) result;
|
||||
assertTrue(health.getStatus() == Status.UP);
|
||||
assertThat(health.getDetails().size(), is(equalTo(1)));
|
||||
assertThat(health.getDetails().get("foo"), is(equalTo((Object) "bar")));
|
||||
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
|
||||
result = this.mvc.invoke(null); // insecure now
|
||||
Health health = (Health) result;
|
||||
assertTrue(result instanceof Health);
|
||||
health = (Health) result;
|
||||
// so the result is cached
|
||||
assertTrue(health.getStatus() == Status.UP);
|
||||
// but the details are hidden
|
||||
assertThat(health.getDetails().size(), is(equalTo(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsecureAnonymousAccessUnrestricted() {
|
||||
given(this.endpoint.invoke()).willReturn(
|
||||
new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
given(this.endpoint.isRestrictAnonymousAccess()).willReturn(false);
|
||||
Object result = this.mvc.invoke(null);
|
||||
assertTrue(result instanceof Health);
|
||||
assertTrue(((Health) result).getStatus() == Status.UP);
|
||||
assertEquals("bar", ((Health) result).getDetails().get("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsecureIsNotCachedWhenAnonymousAccessIsUnrestricted() {
|
||||
given(this.endpoint.getTimeToLive()).willReturn(10000L);
|
||||
given(this.endpoint.isRestrictAnonymousAccess()).willReturn(false);
|
||||
given(this.endpoint.invoke()).willReturn(
|
||||
new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(null);
|
||||
assertTrue(result instanceof Health);
|
||||
assertTrue(((Health) result).getStatus() == Status.UP);
|
||||
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
|
||||
result = this.mvc.invoke(null);
|
||||
@SuppressWarnings("unchecked")
|
||||
Health health = ((ResponseEntity<Health>) result).getBody();
|
||||
assertTrue(health.getStatus() == Status.DOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newValueIsReturnedOnceTtlExpires() throws InterruptedException {
|
||||
given(this.endpoint.getTimeToLive()).willReturn(50L);
|
||||
given(this.endpoint.isRestrictAnonymousAccess()).willReturn(true);
|
||||
given(this.endpoint.invoke()).willReturn(
|
||||
new Health.Builder().up().withDetail("foo", "bar").build());
|
||||
Object result = this.mvc.invoke(null);
|
||||
assertTrue(result instanceof Health);
|
||||
assertTrue(((Health) result).getStatus() == Status.UP);
|
||||
Thread.sleep(100);
|
||||
given(this.endpoint.invoke()).willReturn(new Health.Builder().down().build());
|
||||
result = this.mvc.invoke(null);
|
||||
@SuppressWarnings("unchecked")
|
||||
Health health = ((ResponseEntity<Health>) result).getBody();
|
||||
assertTrue(health.getStatus() == Status.DOWN);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -404,6 +404,7 @@ content into your application; rather pick only the properties that you need.
|
|||
endpoints.health.id=health
|
||||
endpoints.health.sensitive=false
|
||||
endpoints.health.enabled=true
|
||||
endpoints.health.restrict-anonymous-access=true
|
||||
endpoints.health.time-to-live=1000
|
||||
endpoints.info.id=info
|
||||
endpoints.info.sensitive=false
|
||||
|
|
|
|||
Loading…
Reference in New Issue