diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java index 8e072f7..fce4e97 100644 --- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java +++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java @@ -1584,6 +1584,18 @@ public void setSparkConfigUpdated(boolean isSparkConfigUpdated) { "Top level directory where operation logs are stored if logging functionality is enabled"), HIVE_SERVER2_LOGGING_OPERATION_VERBOSE("hive.server2.logging.operation.verbose", false, "When true, HS2 operation logs available for clients will be verbose"), + + // Cookie based authentication + HIVE_SERVER2_COOKIE_AUTH_ENABLED("hive.server2.cookie.auth.enabled", false, + "When true, HS2 will use cookie based authentication mechanism"), + HIVE_SERVER2_COOKIE_MAX_AGE("hive.server2.cookie.max.age", "86400s", + new TimeValidator(TimeUnit.SECONDS), + "Maximum age in seconds for server side cookie used by HS2 in HTTP mode."), + HIVE_SERVER2_COOKIE_DOMAIN("hive.server2.cookie.domain", null, + "Domain for the HS2 generated cookies"), + HIVE_SERVER2_COOKIE_PATH("hive.server2.cookie.path", null, + "Path for the HS2 generated cookies"), + // logging configuration HIVE_LOG4J_FILE("hive.log4j.file", "", "Hive log4j configuration file.\n" + diff --git a/service/src/java/org/apache/hive/service/CookieSigner.java b/service/src/java/org/apache/hive/service/CookieSigner.java new file mode 100644 index 0000000..dfa67ce --- /dev/null +++ b/service/src/java/org/apache/hive/service/CookieSigner.java @@ -0,0 +1,83 @@ +package org.apache.hive.service; + +import org.apache.commons.codec.binary.Base64; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * The cookie signer generates a signature based on SHA digest + * and appends it to the cookie value generated at the + * server side. It uses SHA digest algorithm to sign and verify signatures. + */ +public class CookieSigner { + private static final String SIGNATURE = "&s="; + private static final String SHA_STRING = "SHA"; + private byte[] secretBytes; + + + /** + * Constructor + * @param secret Secret Bytes + */ + public CookieSigner(byte[] secret) { + if (secret == null) { + throw new IllegalArgumentException(" NULL Secret Bytes"); + } + this.secretBytes = secret.clone(); + } + + + /** + * Sign the cookie given the string token as input. + * @param str Input token + * @return Signed token that can be used to create a cookie + */ + public String signCookie(String str) { + if (str == null || str.isEmpty()) { + throw new IllegalArgumentException("NULL or empty string to sign"); + } + String signature = getSignature(str); + return str + SIGNATURE + signature; + } + + + /** + * Verify a signed string and extracts the original string. + * @param signedStr The already signed string + * @return Raw Value of the string without the signature + */ + public String verifyAndExtract(String signedStr) { + int index = signedStr.lastIndexOf(SIGNATURE); + if (index == -1) { + throw new IllegalArgumentException("Invalid input sign: " + signedStr); + } + String originalSignature = signedStr.substring(index + SIGNATURE.length()); + String rawValue = signedStr.substring(0, index); + String currentSignature = getSignature(rawValue); + if (!originalSignature.equals(currentSignature)) { + throw new IllegalArgumentException("Invalid sign, original = " + originalSignature + + " current = " + currentSignature); + } + return rawValue; + } + + + /** + * Get the signature of the input string based on SHA digest algorithm. + * @param str Input token + * @return Signed String + */ + private String getSignature(String str) { + try { + MessageDigest md = MessageDigest.getInstance(SHA_STRING); + md.update(str.getBytes()); + md.update(secretBytes); + byte[] digest = md.digest(); + return new Base64(0).encodeToString(digest); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException("Invalid SHA digest String: " + SHA_STRING + + " " + ex.getMessage(), ex); + } + } +} diff --git a/service/src/java/org/apache/hive/service/ServiceUtils.java b/service/src/java/org/apache/hive/service/ServiceUtils.java index e712aaf..03cbb09 100644 --- a/service/src/java/org/apache/hive/service/ServiceUtils.java +++ b/service/src/java/org/apache/hive/service/ServiceUtils.java @@ -17,8 +17,26 @@ */ package org.apache.hive.service; -public class ServiceUtils { +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class ServiceUtils { + private static final Log LOG = LogFactory.getLog(ServiceUtils.class); + private static final String COOKIE_ATTR_SEPARATOR = "&"; + private static final String COOKIE_CLIENT_USER_NAME = "cu"; + private static final String COOKIE_CLIENT_IP_ADDRESS = "ip"; + private final static Set COOKIE_ATTRIBUTES = + new HashSet(Arrays.asList(COOKIE_CLIENT_USER_NAME, COOKIE_CLIENT_IP_ADDRESS)); + + /* * Get the index separating the user name from domain name (the user's name up * to the first '/' or '@'). @@ -41,4 +59,65 @@ public static int indexOfDomainMatch(String userName) { } return endIdx; } -} \ No newline at end of file + + + /** + * Creates and returns a HS2 cookie token. + * @param clientUserName Client User name. + * @param clientIpAddress Client IP Address. + * @return An unsigned cookie token generated from input parameters. + */ + public static String createCookieToken(String clientUserName, String clientIpAddress) { + StringBuffer sb = new StringBuffer(); + sb.append(COOKIE_CLIENT_USER_NAME).append("=").append(clientUserName). + append(COOKIE_ATTR_SEPARATOR); + sb.append(COOKIE_CLIENT_IP_ADDRESS).append("=").append(clientIpAddress); + return sb.toString(); + } + + + /** + * Parses a cookie token to client user name and client IP Address. + * @param tokenStr Token String. + * @param clientUserName contains Client User name after this function call. + * @param clientIpAddress contains Client IP Address after this function call. + * @return true if tokenStr is of valid format, else returns false. + */ + public static boolean parseCookieToken(String tokenStr, + String clientUserName, String clientIpAddress) { + Map map = splitCookieToken(tokenStr); + + if (!map.keySet().equals(COOKIE_ATTRIBUTES)) { + LOG.error("Invalid token with missing attributes " + tokenStr); + return false; + } + clientUserName = map.get(COOKIE_CLIENT_USER_NAME); + clientIpAddress = map.get(COOKIE_CLIENT_IP_ADDRESS); + return true; + } + + + /** + * Splits the cookie token into attributes pairs. + * @param str input token. + * @return a map with the attribute pairs of the token if the input is valid. + * Else, returns null. + */ + private static Map splitCookieToken(String tokenStr) { + Map map = new HashMap(); + StringTokenizer st = new StringTokenizer(tokenStr, COOKIE_ATTR_SEPARATOR); + + while (st.hasMoreTokens()) { + String part = st.nextToken(); + int separator = part.indexOf('='); + if (separator == -1) { + LOG.error("Invalid token string " + tokenStr); + return null; + } + String key = part.substring(0, separator); + String value = part.substring(separator + 1); + map.put(key, value); + } + return map; + } +} diff --git a/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java b/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java index fde39d2..77a9feb 100644 --- a/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java +++ b/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java @@ -20,10 +20,14 @@ import java.io.IOException; import java.security.PrivilegedExceptionAction; +import java.util.Arrays; import java.util.Map; +import java.util.Random; import java.util.Set; +import java.util.StringTokenizer; import javax.servlet.ServletException; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -31,6 +35,8 @@ import org.apache.commons.codec.binary.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.conf.HiveConf.ConfVars; import org.apache.hadoop.hive.shims.HadoopShims.KerberosNameShim; import org.apache.hadoop.hive.shims.ShimLoader; import org.apache.hadoop.security.UserGroupInformation; @@ -41,6 +47,8 @@ import org.apache.hive.service.auth.HttpAuthenticationException; import org.apache.hive.service.auth.PasswdAuthenticationProvider; import org.apache.hive.service.cli.session.SessionManager; +import org.apache.hive.service.CookieSigner; +import org.apache.hive.service.ServiceUtils; import org.apache.thrift.TProcessor; import org.apache.thrift.protocol.TProtocolFactory; import org.apache.thrift.server.TServlet; @@ -63,41 +71,81 @@ private final String authType; private final UserGroupInformation serviceUGI; private final UserGroupInformation httpUGI; - + private HiveConf hiveConf = new HiveConf(); + + // Class members for cookie based authentication. + private CookieSigner signer; + public static final String AUTH_COOKIE = "hive.server2.auth"; + private static final Random RAN = new Random(); + private String cookieDomain; + private String cookiePath; + private int cookieMaxAge; + + public ThriftHttpServlet(TProcessor processor, TProtocolFactory protocolFactory, String authType, UserGroupInformation serviceUGI, UserGroupInformation httpUGI) { super(processor, protocolFactory); this.authType = authType; this.serviceUGI = serviceUGI; this.httpUGI = httpUGI; + // Initialize the cookie based authentication related variables. + if (hiveConf.getBoolVar(ConfVars.HIVE_SERVER2_COOKIE_AUTH_ENABLED)) { + // Generate the signer with secret. + String secret = Long.toString(RAN.nextLong()); + LOG.debug("Using the random number as the secret for cookie generation " + secret); + this.signer = new CookieSigner(secret.getBytes()); + this.cookieMaxAge = hiveConf.getIntVar(ConfVars.HIVE_SERVER2_COOKIE_MAX_AGE); + this.cookieDomain = hiveConf.getVar(ConfVars.HIVE_SERVER2_COOKIE_DOMAIN); + this.cookiePath = hiveConf.getVar(ConfVars.HIVE_SERVER2_COOKIE_PATH); + } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String clientUserName; + String clientUserName = null; String clientIpAddress; + boolean requireNewCookie = false; + try { - // For a kerberos setup - if(isKerberosAuthMode(authType)) { - clientUserName = doKerberosAuth(request); - String doAsQueryParam = getDoAsQueryParam(request.getQueryString()); - if (doAsQueryParam != null) { - SessionManager.setProxyUserName(doAsQueryParam); + // If the cookie based authentication is already enabled, parse the + // request and validate the request cookies. + if (hiveConf.getBoolVar(ConfVars.HIVE_SERVER2_COOKIE_AUTH_ENABLED)) { + clientUserName = validateCookie(request); + requireNewCookie = clientUserName != null; + if (requireNewCookie) { + LOG.debug("Could not validate cookie sent, will try to generate a new cookie"); } } - else { - clientUserName = doPasswdAuth(request, authType); + // If the cookie based authentication is not enabled or the request does + // not have a valid cookie, use the kerberos or password based authentication + // depending on the server setup. + if (clientUserName == null) { + // For a kerberos setup + if (isKerberosAuthMode(authType)) { + clientUserName = doKerberosAuth(request); + String doAsQueryParam = getDoAsQueryParam(request.getQueryString()); + if (doAsQueryParam != null) { + SessionManager.setProxyUserName(doAsQueryParam); + } + } + // For password based authentication + else { + clientUserName = doPasswdAuth(request, authType); + } } - LOG.debug("Client username: " + clientUserName); // Set the thread local username to be used for doAs if true SessionManager.setUserName(clientUserName); - clientIpAddress = request.getRemoteAddr(); - LOG.debug("Client IP Address: " + clientIpAddress); // Set the thread local ip address - SessionManager.setIpAddress(clientIpAddress); - + SessionManager.setIpAddress(clientIpAddress); + // Generate new cookie and add it to the response + if (requireNewCookie && + !authType.equalsIgnoreCase(HiveAuthFactory.AuthTypes.NOSASL.toString())) { + String cookieToken = ServiceUtils.createCookieToken(clientUserName, clientIpAddress); + LOG.debug("Cookie added for clientUserName " + clientUserName); + response.addCookie(createCookie(signer.signCookie(cookieToken))); + } super.doPost(request, response); } catch (HttpAuthenticationException e) { @@ -107,7 +155,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) if(isKerberosAuthMode(authType)) { response.addHeader(HttpAuthUtils.WWW_AUTHENTICATE, HttpAuthUtils.NEGOTIATE); } - response.getWriter().println("Authentication Error: " + e.getMessage()); + response.getWriter().println("Authentication Error: " + e.getMessage()); } finally { // Clear the thread locals @@ -116,7 +164,106 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) SessionManager.clearProxyUserName(); } } + + + /** + * Retrieves the client name from cookieString. If the cookie does not + * correspond to a valid client, the function returns null. + * @param cookieString HTTP Request cookies separated by newline character. + * @param request HTTP Request + * @return Client Username if cookieString has a HS2 Generated cookie that is currently valid. + * Else, returns null. + */ + private String getClientNameFromCookie(String cookieString, HttpServletRequest request) { + StringTokenizer cookieParser = new StringTokenizer(cookieString, ";\n"); + String nvp, cName, cValue; + int l; + + while (cookieParser.hasMoreTokens()) { + nvp = cookieParser.nextToken(); + if (nvp.charAt(0) == ' ') { + nvp = new String(nvp.substring(1)); + } + l = nvp.indexOf('='); + if (l == -1) { + continue; + } + //Get the actual cookie value + cName = new String(nvp.substring(0, l)); + cValue = new String(nvp.substring(l+1, nvp.length())); + // If the key matches AUTH_COOKIE, validate the value. + if (cName.equals(AUTH_COOKIE)) { + cValue = signer.verifyAndExtract(cValue); + if (cValue != null) { + String userName = ""; + String ipAddress = ""; + boolean validToken = ServiceUtils.parseCookieToken(cValue, userName, ipAddress); + + if (!validToken) { + LOG.warn("Invalid cookie token " + cValue); + continue; + } + if (!ipAddress.equals(request.getRemoteAddr())) { + LOG.warn("Invalid cookie ip address " + ipAddress + + " , expected :" + request.getRemoteAddr()); + continue; + } + //We have found a valid cookie in the client request. + LOG.debug("Validated the cookie for user " + userName); + return userName; + } + } + } + return null; + } + + /** + * Validate the request cookie. This function iterates over the request cookie headers + * and finds a cookie that represents a valid client/server session. If it finds one, it + * returns the client name associated with the session. Else, it returns null. + * @param request The HTTP Servlet Request send by the client + * @return Client Username if the request has valid HS2 cookie, else returns null + */ + private String validateCookie(HttpServletRequest request) { + String cookieString; + + // Find all the valid cookies associated with the request. + cookieString = request.getHeader("COOKIE"); + if (cookieString == null) { + cookieString = request.getHeader("Cookie"); + } + if (cookieString == null) { + cookieString = request.getHeader("cookie"); + } + if (cookieString == null) { + LOG.debug("No valid cookies associated with the request " + request); + return null; + } + LOG.debug("Received cookie headers " + cookieString); + return getClientNameFromCookie(cookieString, request); + } + + + /** + * Generate a server side cookie given the cookie value as the input. + * @param str Input string token. + * @return The generated cookie. + */ + private Cookie createCookie(String str) { + Cookie cookie = new Cookie(AUTH_COOKIE, str); + + cookie.setMaxAge(cookieMaxAge); + if (cookieDomain != null) { + cookie.setDomain(cookieDomain); + } + if (cookiePath != null) { + cookie.setPath(cookiePath); + } + return cookie; + } + + /** * Do the LDAP/PAM authentication * @param request