/* * Copyright (c) 2015 EMC Corporation * All Rights Reserved */ package controllers; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; import models.security.UserInfo; import org.apache.commons.collections.map.MultiKeyMap; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpStatus; import play.Logger; import play.Play; import play.cache.Cache; import play.data.validation.Validation; import play.exceptions.UnexpectedException; import play.mvc.Before; import play.mvc.Catch; import play.mvc.Controller; import play.mvc.Http; import play.mvc.Router; import play.mvc.Util; import play.mvc.With; import util.BourneUtil; import util.DisasterRecoveryUtils; import util.LicenseUtils; import util.MessagesUtils; import util.VirtualDataCenterUtils; import com.emc.storageos.model.vdc.VirtualDataCenterRestRep; import com.emc.storageos.services.util.SecurityUtils; import com.emc.vipr.client.exceptions.ViPRHttpException; import com.emc.vipr.model.sys.ClusterInfo; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import controllers.deadbolt.Deadbolt; import controllers.deadbolt.Unrestricted; import controllers.security.Security; import controllers.util.FlashException; /** * Common controller interceptor to add some renderArgs used * in the main layout. This should be used @With(Common.class) * in all controllers that render using the main layout. * * This also contains some utilities to provide access to user and other * data from controllers. * * @author Chris Dail */ @With(Deadbolt.class) public class Common extends Controller { // Common Arg constants public static final String USER = "currentUser"; public static final String VDCS = "vdcs"; public static final String TOKEN = "token"; public static final String AUTHENTICITY_TOKEN = "authenticityToken"; public static final String NOTIFICATIONS = "notifications"; public static final String REFERRER = "referrer"; public static final String ANGULAR_RENDER_ARGS = "angularRenderArgs"; public static final String CACHE_EXPR = "2min"; public static final String PATH_SANITIZER = "pathSanitizer"; private static final MultiKeyMap XSS_SANITIZERS = new MultiKeyMap() { { put("/orders/submitOrder","mountPoint", PATH_SANITIZER); put("/orders/submitOrder","mountPath", PATH_SANITIZER); put("/ldap/save","ldapSources.managerDn", PATH_SANITIZER); put("/usergroup/save","userGroup.name", PATH_SANITIZER); put("/config/passwords","user", PATH_SANITIZER); put("/customConfigs/preview", "value", PATH_SANITIZER); } }; @Before(priority = 0) @Unrestricted public static void checkSetup() { if (StringUtils.isNotBlank(Security.getAuthToken())) { if (!Setup.isInitialSetupComplete()) { if (Security.isApiRequest()) { error(MessagesUtils.get("setup.notLicensed.message")); } Logger.info("Running Setup ..."); Setup.index(); } else if (!LicenseUtils.isLicensed(true)) { if (Security.isApiRequest()) { error(MessagesUtils.get("setup.notLicensed.message")); } Logger.info("Not licensed"); Setup.license(); } } } @Before(priority = 0) public static void csrfCheck() { boolean isPost = StringUtils.equals(request.method, "POST"); boolean isFormEncoded = StringUtils.equals(request.contentType, "application/x-www-form-urlencoded"); boolean isApiRequest = StringUtils.startsWith(request.path, "/api/"); if (isPost && isFormEncoded && !isApiRequest) { String authenticityToken = SecurityUtils.stripXSS(params.get("authenticityToken")); if (authenticityToken == null) { Logger.warn("No authenticity token from %s for request: %s", request.remoteAddress, request.url); } checkAuthenticity(); } } @Before(priority = 0) public static void xssCheck() { for (String param : params.all().keySet()) { // skip xss sanitation for fields which name contains password if (param.toLowerCase().contains("password") || param.toLowerCase().contains("username")) { Logger.debug("skip sanitation for " + param); return; } String[] data = params.getAll(param); if ((data != null) && (data.length > 0)) { String sanitizer = (String)XSS_SANITIZERS.get(request.path, param); Logger.debug("Cleaning data for [ %s ] [ %s ]", request.path, param); String[] cleanValues = new String[data.length]; for (int i = 0; i < data.length; ++i) { if (PATH_SANITIZER.equals(sanitizer)){ cleanValues[i] = SecurityUtils.stripPathXSS(data[i]); } else { cleanValues[i] = SecurityUtils.stripXSS(data[i]); } params.put(param, cleanValues); } } } } @Before(priority = 5) public static void addCommonRenderArgs() { // Set cache control. We don't want caching of our dynamic pages response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.setHeader("Pragma", "no-cache"); UserInfo userInfo = Security.getUserInfo(); angularRenderArgs().put(USER, userInfo); angularRenderArgs().put(AUTHENTICITY_TOKEN, SecurityUtils.stripXSS(session.getAuthenticityToken())); renderArgs.put(USER, userInfo); renderArgs.put(TOKEN, Security.getAuthToken()); renderArgs.put(VDCS, getVDCs()); // Notifications are only shown for tenant approvers if (Security.isTenantApprover() && DisasterRecoveryUtils.isActiveSite()) { renderArgs.put(NOTIFICATIONS, Notifications.getNotifications()); } addReferrer(); } @Util public static Map<String, Object> angularRenderArgs() { @SuppressWarnings("unchecked") Map<String, Object> scope = (Map<String, Object>) renderArgs.get(ANGULAR_RENDER_ARGS); if (scope == null) { scope = new HashMap<String, Object>(); renderArgs.put(ANGULAR_RENDER_ARGS, scope); } return scope; } @Catch(ViPRHttpException.class) public static void jerseyException(Throwable e) { handleExpiredToken(e); } /** * Extremely low priority exception handler that will automatically * flashException and redirect for action methods decorated with a * FlashException annotation. Rethrow if the annotation is * absent. */ @Catch(value = Exception.class, priority = Integer.MAX_VALUE) public static void flashExceptionHandler(Exception e) throws Exception { FlashException handler = getActionAnnotation(FlashException.class); if (handler != null) { flashException(e); if (handler.keep()) { params.flash(); Validation.keep(); } String action = handler.value(); String[] referrer = handler.referrer(); if (!action.isEmpty()) { if (!action.contains(".")) { action = getControllerClass().getName() + "." + action; } redirect(action); } else if (referrer != null && referrer.length > 0) { Http.Header headerReferrer = request.headers.get("referer"); if (headerReferrer != null && StringUtils.isNotBlank(headerReferrer.value())) { Pattern p = Pattern.compile(StringUtils.join(referrer, "|"), Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(headerReferrer.value()); if (m.find()) { redirectToReferrer(); } else { Logger.error(String.format("The redirect page is not valid base on the FlashException referrer restriction: %s", referrer.toString())); } } else { Logger.error("Unable to redirect. No referrer available in request header"); } } else { redirectToReferrer(); } } } /** * Redirects back to the request header referrer * with params.flash() and Validation.keep(). */ @Util public static void handleError() { params.flash(); Validation.keep(); redirectToReferrer(); } private static void redirectToReferrer() { Http.Header referrer = request.headers.get("referer"); if (referrer != null && StringUtils.isNotBlank(referrer.value())) { redirect(referrer.value()); } else { String msg = "Unable to redirect. No referrer available in request header"; Logger.error(msg); throw new RuntimeException(msg); } } @Util public static void handleExpiredToken(Throwable throwable) { if (throwable instanceof ViPRHttpException) { ViPRHttpException ve = (ViPRHttpException) throwable; if (ve.getHttpCode() == HttpStatus.SC_UNAUTHORIZED) { Logger.info("Clearing auth token"); // Auth token may have expired Security.clearAuthToken(); Security.redirectToAuthPage(); } } } @Util public static boolean hasRequestMethod(String... methods) { return Sets.newHashSet(methods).contains(request.actionMethod); } /** * Gets a user printable message for an exception. * * @param throwable Exception to get the message for */ @Util public static String getUserMessage(Throwable throwable) { String message = throwable.getMessage(); if (message == null) { message = throwable.getClass().getName(); } return message; } /** * This method captures the user printable message for an exception. It logs it to the log file and * adds the message to the flash error for display. For AJAX requests, the message is rendered as an error. * * @param throwable Exception to get the message for */ @Util public static void flashException(Throwable throwable) { // Check for logout handleExpiredToken(throwable); String message = getUserMessage(throwable); Logger.error(throwable, message); if (request.isAjax()) { error(503, message); } flash.error(MessagesUtils.escape(message)); } /** * Gets the referrer URL. If there is a flash scope value, it takes precedence over a query parameter. * * @return the referrer URL. */ @Util public static String getReferrer() { String referrerFlash = flash.get(REFERRER); String referrerParam = params.get(REFERRER); String referrer = null; if (StringUtils.isNotBlank(referrerFlash)) { referrer = referrerFlash; } else if (StringUtils.isNotBlank(referrerParam)) { referrer = referrerParam; } if (referrer != null) { return toSafeRedirectURL(referrer); } else { return null; } } @Util public static String toSafeRedirectURL(String url) { String cleanUrl = ""; try { // Remove Host and port from referrer URI uriObject = new URI(url); cleanUrl += uriObject.getPath(); String query = uriObject.getQuery(); if (!StringUtils.isBlank(query)) { cleanUrl += "?" + query; } } catch (URISyntaxException ignore) { Logger.error(ignore.getMessage()); } return cleanUrl; } /** * Sets the referrer URL into the flash scope before a redirect. * * @param url the referrer URL. */ @Util public static void setReferrer(String url) { if (StringUtils.isNotBlank(url)) { flash.put(REFERRER, url); } } /** * Adds the current referrer to the render args. */ @Util public static void addReferrer() { String referrer = getReferrer(); if (StringUtils.isNotBlank(referrer)) { renderArgs.put(REFERRER, referrer); } } /** * Redirects back to the referrer, if set. */ @Util public static void backToReferrer() { String referrer = getReferrer(); if (StringUtils.isNotBlank(referrer)) { redirect(referrer); } } @Util public static String reverseRoute(Class<? extends Controller> controllerClass, String action) { return reverseRoute(controllerClass.getName() + "." + action); } @Util public static String reverseRoute(Class<? extends Controller> controllerClass, String action, String key, Object value) { return reverseRoute(controllerClass.getName() + "." + action, key, value); } @Util public static String reverseRoute(Class<? extends Controller> controllerClass, String action, Map<String, Object> args) { return reverseRoute(controllerClass.getName() + "." + action, args); } private static String reverseRoute(String action, String key, Object value) { Map<String, Object> args = Maps.newHashMap(); args.put(key, value); return reverseRoute(action, args); } private static String reverseRoute(String action, Map<String, Object> args) { return Router.reverse(action, args).url; } private static String reverseRoute(String action) { return Router.reverse(action).url; } @Util public static ClusterInfo getClusterInfo() { ClusterInfo clusterInfo = null; try { clusterInfo = BourneUtil.getSysClient().upgrade().getClusterInfo(); } catch (ViPRHttpException e) { Logger.error(e, "Failed to get cluster state"); error(e.getHttpCode(), e.getMessage()); } return clusterInfo; } /** * Gets the clusterInfo if the user has access * * @return the cluster info, or null */ @Util public static ClusterInfo getClusterInfoWithRoleCheck() { if (Security.isTenantAdmin() || Security.isSystemMonitor() || Security.isSystemAdminOrRestrictedSystemAdmin() || Security.isSecurityAdminOrRestrictedSecurityAdmin()) { return getClusterInfo(); } else { return null; } } public static void clusterInfoWithRoleCheckJson() { renderJSON(getClusterInfoWithRoleCheck()); } /** * Determines if the cluster is in a stable state. * * @return */ @Util public static boolean isClusterStable() { if (Play.mode.isDev()) { return true; } return StringUtils.equalsIgnoreCase("STABLE", getClusterInfo().getCurrentState()); } @Util public static List<VirtualDataCenterRestRep> getVDCs() { List<VirtualDataCenterRestRep> vdcs = Cache.get(VDCS, List.class); if (vdcs == null) { try { vdcs = VirtualDataCenterUtils.getAllVDCs(); } // If there is an exception, return an empty list. We don't want an exception getting VDCS to prevent all // pages from loading catch (Exception e) { Logger.error(e, "Unable to retrieve VDCs"); return Lists.newArrayList(); } Cache.set(VDCS, vdcs, CACHE_EXPR); } return vdcs; } @Util public static Throwable unwrap(Throwable t) { Throwable cause = t; while ((cause instanceof UnexpectedException) || (cause instanceof ExecutionException)) { cause = cause.getCause(); } return cause != null ? cause : t; } @Util public static void copyRenderArgsToAngular() { copyRenderArgsToAngular(USER, NOTIFICATIONS, TOKEN, VDCS); } @Util public static void copyRenderArgsToAngular(String... exclude) { angularRenderArgs().putAll(renderArgs.data); angularRenderArgs().remove(ANGULAR_RENDER_ARGS); angularRenderArgs().keySet().removeAll(Arrays.asList(exclude)); } @Util public static void flashParamsExcept(String... paramNames) { Set<String> names = Sets.newHashSet(params.all().keySet()); names.removeAll(Arrays.asList(paramNames)); String[] array = new String[names.size()]; params.flash(names.toArray(array)); } public static void redirectTo(String action) { redirect(action); } }