/*
* Copyright 2012-2017 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.mvc;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.context.properties.ConfigurationProperties;
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.web.bind.annotation.ResponseBody;
/**
* Adapter to expose {@link HealthEndpoint} as an {@link MvcEndpoint}.
*
* @author Christian Dupuis
* @author Dave Syer
* @author Andy Wilkinson
* @author Phillip Webb
* @author EddĂș MelĂ©ndez
* @author Madhura Bhave
* @since 1.1.0
*/
@ConfigurationProperties(prefix = "endpoints.health")
public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter<HealthEndpoint> {
private static final List<String> DEFAULT_ROLES = Arrays.asList("ROLE_ACTUATOR");
private final boolean secure;
private final List<String> roles;
private Map<String, HttpStatus> statusMapping = new HashMap<>();
private long lastAccess = 0;
private Health cached;
public HealthMvcEndpoint(HealthEndpoint delegate) {
this(delegate, true);
}
public HealthMvcEndpoint(HealthEndpoint delegate, boolean secure) {
this(delegate, secure, new ArrayList<>(DEFAULT_ROLES));
}
public HealthMvcEndpoint(HealthEndpoint delegate, boolean secure,
List<String> roles) {
super(delegate);
Assert.notNull(roles, "Roles must not be null");
this.secure = secure;
this.roles = roles;
setupDefaultStatusMapping();
}
private void setupDefaultStatusMapping() {
addStatusMapping(Status.DOWN, HttpStatus.SERVICE_UNAVAILABLE);
addStatusMapping(Status.OUT_OF_SERVICE, HttpStatus.SERVICE_UNAVAILABLE);
}
/**
* Set specific status mappings.
* @param statusMapping a map of status code to {@link HttpStatus}
*/
public void setStatusMapping(Map<String, HttpStatus> statusMapping) {
Assert.notNull(statusMapping, "StatusMapping must not be null");
this.statusMapping = new HashMap<>(statusMapping);
}
/**
* Add specific status mappings to the existing set.
* @param statusMapping a map of status code to {@link HttpStatus}
*/
public void addStatusMapping(Map<String, HttpStatus> statusMapping) {
Assert.notNull(statusMapping, "StatusMapping must not be null");
this.statusMapping.putAll(statusMapping);
}
/**
* Add a status mapping to the existing set.
* @param status the status to map
* @param httpStatus the http status
*/
public void addStatusMapping(Status status, HttpStatus httpStatus) {
Assert.notNull(status, "Status must not be null");
Assert.notNull(httpStatus, "HttpStatus must not be null");
addStatusMapping(status.getCode(), httpStatus);
}
/**
* Add a status mapping to the existing set.
* @param statusCode the status code to map
* @param httpStatus the http status
*/
public void addStatusMapping(String statusCode, HttpStatus httpStatus) {
Assert.notNull(statusCode, "StatusCode must not be null");
Assert.notNull(httpStatus, "HttpStatus must not be null");
this.statusMapping.put(statusCode, httpStatus);
}
@ActuatorGetMapping
@ResponseBody
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, principal);
HttpStatus status = getStatus(health);
if (status != null) {
return new ResponseEntity<>(health, status);
}
return health;
}
private HttpStatus getStatus(Health health) {
String code = getUniformValue(health.getStatus().getCode());
if (code != null) {
return this.statusMapping.keySet().stream()
.filter((key) -> code.equals(getUniformValue(key)))
.map(this.statusMapping::get).findFirst().orElse(null);
}
return null;
}
private String getUniformValue(String code) {
if (code == null) {
return null;
}
StringBuilder builder = new StringBuilder();
for (char ch : code.toCharArray()) {
if (Character.isAlphabetic(ch) || Character.isDigit(ch)) {
builder.append(Character.toLowerCase(ch));
}
}
return builder.toString();
}
private Health getHealth(HttpServletRequest request, Principal principal) {
long accessTime = System.currentTimeMillis();
if (isCacheStale(accessTime)) {
this.lastAccess = accessTime;
this.cached = getDelegate().invoke();
}
if (exposeHealthDetails(request, principal)) {
return this.cached;
}
return Health.status(this.cached.getStatus()).build();
}
private boolean isCacheStale(long accessTime) {
if (this.cached == null) {
return true;
}
return (accessTime - this.lastAccess) >= getDelegate().getTimeToLive();
}
protected boolean exposeHealthDetails(HttpServletRequest request,
Principal principal) {
if (!this.secure) {
return true;
}
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;
}
private List<String> getRoles() {
return this.roles;
}
private boolean isSpringSecurityAuthentication(Principal principal) {
return ClassUtils.isPresent("org.springframework.security.core.Authentication",
null) && principal instanceof Authentication;
}
}