/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.ranger.audit.provider; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.rmi.dgc.VMID; import java.security.Principal; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.TimeZone; import java.util.UUID; import java.util.regex.Pattern; import javax.security.auth.Subject; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authentication.util.KerberosName; import org.apache.hadoop.security.authentication.util.KerberosUtil; import org.apache.log4j.helpers.LogLog; import org.apache.ranger.authorization.hadoop.utils.RangerCredentialProvider; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import static org.apache.hadoop.util.PlatformName.IBM_JAVA; public class MiscUtil { private static final Log logger = LogFactory.getLog(MiscUtil.class); public static final String TOKEN_START = "%"; public static final String TOKEN_END = "%"; public static final String TOKEN_HOSTNAME = "hostname"; public static final String TOKEN_APP_TYPE = "app-type"; public static final String TOKEN_JVM_INSTANCE = "jvm-instance"; public static final String TOKEN_TIME = "time:"; public static final String TOKEN_PROPERTY = "property:"; public static final String TOKEN_ENV = "env:"; public static final String ESCAPE_STR = "\\"; static VMID sJvmID = new VMID(); public static String LINE_SEPARATOR = System.getProperty("line.separator"); private static Gson sGsonBuilder = null; private static String sApplicationType = null; private static UserGroupInformation ugiLoginUser = null; private static Subject subjectLoginUser = null; private static String local_hostname = null; private static Map<String, LogHistory> logHistoryList = new Hashtable<String, LogHistory>(); private static int logInterval = 30000; // 30 seconds static { try { sGsonBuilder = new GsonBuilder().setDateFormat( "yyyy-MM-dd HH:mm:ss.SSS").create(); } catch (Throwable excp) { LogLog.warn( "failed to create GsonBuilder object. stringify() will return obj.toString(), instead of Json", excp); } initLocalHost(); } public static String replaceTokens(String str, long time) { if (str == null) { return str; } if (time <= 0) { time = System.currentTimeMillis(); } for (int startPos = 0; startPos < str.length();) { int tagStartPos = str.indexOf(TOKEN_START, startPos); if (tagStartPos == -1) { break; } int tagEndPos = str.indexOf(TOKEN_END, tagStartPos + TOKEN_START.length()); if (tagEndPos == -1) { break; } String tag = str.substring(tagStartPos, tagEndPos + TOKEN_END.length()); String token = tag.substring(TOKEN_START.length(), tag.lastIndexOf(TOKEN_END)); String val = ""; if (token != null) { if (token.equals(TOKEN_HOSTNAME)) { val = getHostname(); } else if (token.equals(TOKEN_APP_TYPE)) { val = getApplicationType(); } else if (token.equals(TOKEN_JVM_INSTANCE)) { val = getJvmInstanceId(); } else if (token.startsWith(TOKEN_PROPERTY)) { String propertyName = token.substring(TOKEN_PROPERTY .length()); val = getSystemProperty(propertyName); } else if (token.startsWith(TOKEN_ENV)) { String envName = token.substring(TOKEN_ENV.length()); val = getEnv(envName); } else if (token.startsWith(TOKEN_TIME)) { String dtFormat = token.substring(TOKEN_TIME.length()); val = getFormattedTime(time, dtFormat); } } if (val == null) { val = ""; } str = str.substring(0, tagStartPos) + val + str.substring(tagEndPos + TOKEN_END.length()); startPos = tagStartPos + val.length(); } return str; } public static String getHostname() { String ret = local_hostname; if (ret == null) { initLocalHost(); ret = local_hostname; if (ret == null) { ret = "unknown"; } } return ret; } public static void setApplicationType(String applicationType) { sApplicationType = applicationType; } public static String getApplicationType() { return sApplicationType; } public static String getJvmInstanceId() { Integer val = Integer.valueOf(sJvmID.toString().hashCode()); long longVal = val.longValue(); String ret = Long.toString(Math.abs(longVal)); return ret; } public static String getSystemProperty(String propertyName) { String ret = null; try { ret = propertyName != null ? System.getProperty(propertyName) : null; } catch (Exception excp) { LogLog.warn("getSystemProperty(" + propertyName + ") failed", excp); } return ret; } public static String getEnv(String envName) { String ret = null; try { ret = envName != null ? System.getenv(envName) : null; } catch (Exception excp) { LogLog.warn("getenv(" + envName + ") failed", excp); } return ret; } public static String getFormattedTime(long time, String format) { String ret = null; try { SimpleDateFormat sdf = new SimpleDateFormat(format); ret = sdf.format(time); } catch (Exception excp) { LogLog.warn("SimpleDateFormat.format() failed: " + format, excp); } return ret; } public static void createParents(File file) { if (file != null) { String parentName = file.getParent(); if (parentName != null) { File parentDir = new File(parentName); if (!parentDir.exists()) { if (!parentDir.mkdirs()) { LogLog.warn("createParents(): failed to create " + parentDir.getAbsolutePath()); } } } } } public static long getNextRolloverTime(long lastRolloverTime, long interval) { long now = System.currentTimeMillis() / 1000 * 1000; // round to second if (lastRolloverTime <= 0) { // should this be set to the next multiple-of-the-interval from // start of the day? return now + interval; } else if (lastRolloverTime <= now) { long nextRolloverTime = now + interval; // keep it at 'interval' boundary long trimInterval = (nextRolloverTime - lastRolloverTime) % interval; return nextRolloverTime - trimInterval; } else { return lastRolloverTime; } } public static long getRolloverStartTime(long nextRolloverTime, long interval) { return (nextRolloverTime <= interval) ? System.currentTimeMillis() : nextRolloverTime - interval; } public static int parseInteger(String str, int defValue) { int ret = defValue; if (str != null) { try { ret = Integer.parseInt(str); } catch (Exception excp) { // ignore } } return ret; } public static String generateUniqueId() { return UUID.randomUUID().toString(); } public static <T> String stringify(T log) { String ret = null; if (log != null) { if (log instanceof String) { ret = (String) log; } else if (MiscUtil.sGsonBuilder != null) { ret = MiscUtil.sGsonBuilder.toJson(log); } else { ret = log.toString(); } } return ret; } static public <T> T fromJson(String jsonStr, Class<T> clazz) { return sGsonBuilder.fromJson(jsonStr, clazz); } public static String getStringProperty(Properties props, String propName) { String ret = null; if (props != null && propName != null) { String val = props.getProperty(propName); if (val != null) { ret = val; } } return ret; } public static boolean getBooleanProperty(Properties props, String propName, boolean defValue) { boolean ret = defValue; if (props != null && propName != null) { String val = props.getProperty(propName); if (val != null) { ret = Boolean.valueOf(val); } } return ret; } public static int getIntProperty(Properties props, String propName, int defValue) { int ret = defValue; if (props != null && propName != null) { String val = props.getProperty(propName); if (val != null) { try { ret = Integer.parseInt(val); } catch (NumberFormatException excp) { ret = defValue; } } } return ret; } public static long getLongProperty(Properties props, String propName, long defValue) { long ret = defValue; if (props != null && propName != null) { String val = props.getProperty(propName); if (val != null) { try { ret = Long.parseLong(val); } catch (NumberFormatException excp) { ret = defValue; } } } return ret; } public static Map<String, String> getPropertiesWithPrefix(Properties props, String prefix) { Map<String, String> prefixedProperties = new HashMap<String, String>(); if (props != null && prefix != null) { for (String key : props.stringPropertyNames()) { if (key == null) { continue; } String val = props.getProperty(key); if (key.startsWith(prefix)) { key = key.substring(prefix.length()); if (key == null) { continue; } prefixedProperties.put(key, val); } } } return prefixedProperties; } /** * @param destListStr * @param delim * @return */ public static List<String> toArray(String destListStr, String delim) { List<String> list = new ArrayList<String>(); if (destListStr != null && !destListStr.isEmpty()) { StringTokenizer tokenizer = new StringTokenizer(destListStr, delim.trim()); while (tokenizer.hasMoreTokens()) { list.add(tokenizer.nextToken()); } } return list; } public static String getCredentialString(String url, String alias) { String ret = null; if (url != null && alias != null) { char[] cred = RangerCredentialProvider.getInstance() .getCredentialString(url, alias); if (cred != null) { ret = new String(cred); } } return ret; } public static UserGroupInformation createUGIFromSubject(Subject subject) throws IOException { logger.info("SUBJECT " + (subject == null ? "not found" : "found")); UserGroupInformation ugi = null; if (subject != null) { logger.info("SUBJECT.PRINCIPALS.size()=" + subject.getPrincipals().size()); Set<Principal> principals = subject.getPrincipals(); for (Principal principal : principals) { logger.info("SUBJECT.PRINCIPAL.NAME=" + principal.getName()); } try { // Do not remove the below statement. The default // getLoginUser does some initialization which is needed // for getUGIFromSubject() to work. UserGroupInformation.getLoginUser(); logger.info("Default UGI before using new Subject:" + UserGroupInformation.getLoginUser()); } catch (Throwable t) { logger.error(t); } ugi = UserGroupInformation.getUGIFromSubject(subject); logger.info("SUBJECT.UGI.NAME=" + ugi.getUserName() + ", ugi=" + ugi); } else { logger.info("Server username is not available"); } return ugi; } /** * @param ugiLoginUser */ public static void setUGILoginUser(UserGroupInformation newUGI, Subject newSubject) { if (newUGI != null) { UserGroupInformation.setLoginUser(newUGI); ugiLoginUser = newUGI; logger.info("Setting UGI=" + newUGI); } else { logger.error("UGI is null. Not setting it."); } if (newSubject != null) { logger.info("Setting SUBJECT"); subjectLoginUser = newSubject; } } public static UserGroupInformation getUGILoginUser() { UserGroupInformation ret = ugiLoginUser; if (ret == null) { try { // Do not cache ugiLoginUser if it is not explicitly set with // setUGILoginUser. // It appears that the user represented by // the returned object is periodically logged out and logged back // in when the token is scheduled to expire. So it is better // to get the user object every time from UserGroupInformation class and // not cache it ret = getLoginUser(); } catch (IOException e) { logger.error("Error getting UGI.", e); } } if(ret != null) { try { ret.checkTGTAndReloginFromKeytab(); } catch(IOException ioe) { logger.error("Error renewing TGT and relogin. Ignoring Exception, and continuing with the old TGT", ioe); } } return ret; } /** * Execute the {@link PrivilegedExceptionAction} on the {@link UserGroupInformation} if it's set, otherwise call it directly */ public static <X> X executePrivilegedAction(final PrivilegedExceptionAction<X> action) throws Exception { final UserGroupInformation ugi = getUGILoginUser(); if (ugi != null) { return ugi.doAs(action); } else { return action.run(); } } /** * Execute the {@link PrivilegedAction} on the {@link UserGroupInformation} if it's set, otherwise call it directly. */ public static <X> X executePrivilegedAction(final PrivilegedAction<X> action) { final UserGroupInformation ugi = getUGILoginUser(); if (ugi != null) { return ugi.doAs(action); } else { return action.run(); } } public static Subject getSubjectLoginUser() { return subjectLoginUser; } public static String getKerberosNamesRules() { return KerberosName.getRules(); } /** * * @param principal * This could be in the format abc/host@domain.com * @return */ static public String getShortNameFromPrincipalName(String principal) { if (principal == null) { return null; } try { // Assuming it is kerberos name for now KerberosName kerbrosName = new KerberosName(principal); String userName = kerbrosName.getShortName(); userName = StringUtils.substringBefore(userName, "/"); userName = StringUtils.substringBefore(userName, "@"); return userName; } catch (Throwable t) { logger.error("Error converting kerberos name. principal=" + principal + ", KerberosName.rules=" + KerberosName.getRules()); } return principal; } /** * @param userName * @return */ static public Set<String> getGroupsForRequestUser(String userName) { if (userName != null) { try { UserGroupInformation ugi = UserGroupInformation .createRemoteUser(userName); String[] groups = ugi.getGroupNames(); if (groups != null && groups.length > 0) { Set<String> groupsSet = new java.util.HashSet<String>(); for (String group : groups) { groupsSet.add(group); } return groupsSet; } } catch (Throwable e) { logErrorMessageByInterval(logger, "Error getting groups for users. userName=" + userName, e); } } return Collections.emptySet(); } static public boolean logErrorMessageByInterval(Log useLogger, String message) { return logErrorMessageByInterval(useLogger, message, null); } /** * @param string * @param e */ static public boolean logErrorMessageByInterval(Log useLogger, String message, Throwable e) { if (message == null) { return false; } LogHistory log = logHistoryList.get(message); if (log == null) { log = new LogHistory(); logHistoryList.put(message, log); } if ((System.currentTimeMillis() - log.lastLogTime) > logInterval) { log.lastLogTime = System.currentTimeMillis(); int counter = log.counter; log.counter = 0; if (counter > 0) { message += ". Messages suppressed before: " + counter; } if (e == null) { useLogger.error(message); } else { useLogger.error(message, e); } return true; } else { log.counter++; } return false; } public static void setUGIFromJAASConfig(String jaasConfigAppName) throws Exception { String keytabFile = null; String principal = null; UserGroupInformation ugi = null; if (logger.isDebugEnabled()){ logger.debug("===> MiscUtil.setUGIFromJAASConfig() jaasConfigAppName: " + jaasConfigAppName); } try { AppConfigurationEntry entries[] = Configuration.getConfiguration().getAppConfigurationEntry(jaasConfigAppName); if(!ArrayUtils.isEmpty(entries)) { for (AppConfigurationEntry entry : entries) { if (entry.getOptions().get("keyTab") != null) { keytabFile = (String) entry.getOptions().get("keyTab"); } if (entry.getOptions().get("principal") != null) { principal = (String) entry.getOptions().get("principal"); } if (!StringUtils.isEmpty(principal) && !StringUtils.isEmpty(keytabFile)) { break; } } if (!StringUtils.isEmpty(principal) && !StringUtils.isEmpty(keytabFile)) { // This will login and set the UGI UserGroupInformation.loginUserFromKeytab(principal, keytabFile); ugi = UserGroupInformation.getLoginUser(); } else { String error_mesage = "Unable to get the principal/keytab from jaasConfigAppName: " + jaasConfigAppName; logger.error(error_mesage); throw new Exception(error_mesage); } logger.info("MiscUtil.setUGIFromJAASConfig() UGI: " + ugi + " principal: " + principal + " keytab: " + keytabFile); } else { logger.warn("JAASConfig file not found! Ranger Plugin will not working in a Secure Cluster..."); } } catch ( Exception e) { logger.error("Unable to set UGI for Principal: " + principal + " keytab: " + keytabFile ); throw e; } if (logger.isDebugEnabled()) { logger.debug("<=== MiscUtil.setUGIFromJAASConfig() jaasConfigAppName: " + jaasConfigAppName + " UGI: " + ugi + " principal: " + principal + " keytab: " + keytabFile); } } public static void authWithConfig(String appName, Configuration config) throws LoginException { LoginContext loginContext = null; try { if (config != null) { logger.info("Getting AppConfigrationEntry[] for appName=" + appName + ", config=" + config.toString()); AppConfigurationEntry[] entries = config .getAppConfigurationEntry(appName); if (entries != null) { logger.info("Got " + entries.length + " AppConfigrationEntry elements for appName=" + appName); for (AppConfigurationEntry appEntry : entries) { logger.info("APP_ENTRY:getLoginModuleName()=" + appEntry.getLoginModuleName()); logger.info("APP_ENTRY:getControlFlag()=" + appEntry.getControlFlag()); logger.info("APP_ENTRY.getOptions()=" + appEntry.getOptions()); } } loginContext = new LoginContext(appName, new Subject(), null, config); logger.info("Login in for appName=" + appName); loginContext.login(); logger.info("Principals after login=" + loginContext.getSubject().getPrincipals()); logger.info("UserGroupInformation.loginUserFromSubject(): appName=" + appName + ", principals=" + loginContext.getSubject().getPrincipals()); UserGroupInformation ugi = MiscUtil .createUGIFromSubject(loginContext.getSubject()); if (ugi != null) { MiscUtil.setUGILoginUser(ugi, loginContext.getSubject()); } // UserGroupInformation.loginUserFromSubject(loginContext // .getSubject()); logger.info("POST UserGroupInformation.loginUserFromSubject UGI=" + UserGroupInformation.getLoginUser()); } } catch (Throwable t) { logger.fatal("Error logging as appName=" + appName + ", config=" + config.toString() + ", error=" + t.getMessage()); } finally { if (loginContext != null) { loginContext.logout(); } } } public static void authWithKerberos(String keytab, String principal, String nameRules) { if (keytab == null || principal == null) { return; } Subject serverSubject = new Subject(); int successLoginCount = 0; String[] spnegoPrincipals = null; try { if (principal.equals("*")) { spnegoPrincipals = KerberosUtil.getPrincipalNames(keytab, Pattern.compile("HTTP/.*")); if (spnegoPrincipals.length == 0) { logger.error("No principals found in keytab=" + keytab); } } else { spnegoPrincipals = new String[] { principal }; } if (nameRules != null) { KerberosName.setRules(nameRules); } boolean useKeytab = true; if (!useKeytab) { logger.info("Creating UGI with subject"); LoginContext loginContext = null; List<LoginContext> loginContexts = new ArrayList<LoginContext>(); for (String spnegoPrincipal : spnegoPrincipals) { try { logger.info("Login using keytab " + keytab + ", for principal " + spnegoPrincipal); final KerberosConfiguration kerberosConfiguration = new KerberosConfiguration( keytab, spnegoPrincipal); loginContext = new LoginContext("", serverSubject, null, kerberosConfiguration); loginContext.login(); successLoginCount++; logger.info("Login success keytab " + keytab + ", for principal " + spnegoPrincipal); loginContexts.add(loginContext); } catch (Throwable t) { logger.error("Login failed keytab " + keytab + ", for principal " + spnegoPrincipal, t); } if (successLoginCount > 0) { logger.info("Total login success count=" + successLoginCount); try { UserGroupInformation .loginUserFromSubject(serverSubject); // UserGroupInformation ugi = // createUGIFromSubject(serverSubject); // if (ugi != null) { // setUGILoginUser(ugi, serverSubject); // } } catch (Throwable e) { logger.error("Error creating UGI from subject. subject=" + serverSubject); } finally { if (loginContext != null) { loginContext.logout(); } } } else { logger.error("Total logins were successfull from keytab=" + keytab + ", principal=" + principal); } } } else { logger.info("Creating UGI from keytab directly. keytab=" + keytab + ", principal=" + spnegoPrincipals[0]); UserGroupInformation ugi = UserGroupInformation .loginUserFromKeytabAndReturnUGI(spnegoPrincipals[0], keytab); MiscUtil.setUGILoginUser(ugi, null); } } catch (Throwable t) { logger.error("Failed to login with given keytab and principal", t); } } static class LogHistory { long lastLogTime = 0; int counter = 0; } /** * Kerberos context configuration for the JDK GSS library. */ private static class KerberosConfiguration extends Configuration { private String keytab; private String principal; public KerberosConfiguration(String keytab, String principal) { this.keytab = keytab; this.principal = principal; } @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { Map<String, String> options = new HashMap<String, String>(); if (IBM_JAVA) { options.put("useKeytab", keytab.startsWith("file://") ? keytab : "file://" + keytab); options.put("principal", principal); options.put("credsType", "acceptor"); } else { options.put("keyTab", keytab); options.put("principal", principal); options.put("useKeyTab", "true"); options.put("storeKey", "true"); options.put("doNotPrompt", "true"); options.put("useTicketCache", "true"); options.put("renewTGT", "true"); options.put("isInitiator", "false"); } options.put("refreshKrb5Config", "true"); String ticketCache = System.getenv("KRB5CCNAME"); if (ticketCache != null) { if (IBM_JAVA) { options.put("useDefaultCcache", "true"); // The first value searched when "useDefaultCcache" is used. System.setProperty("KRB5CCNAME", ticketCache); options.put("renewTGT", "true"); options.put("credsType", "both"); } else { options.put("ticketCache", ticketCache); } } if (logger.isDebugEnabled()) { options.put("debug", "true"); } return new AppConfigurationEntry[] { new AppConfigurationEntry( KerberosUtil.getKrb5LoginModuleName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), }; } } public static UserGroupInformation getLoginUser() throws IOException { return UserGroupInformation.getLoginUser(); } private static void initLocalHost() { if ( logger.isDebugEnabled() ) { logger.debug("==> MiscUtil.initLocalHost()"); } try { local_hostname = InetAddress.getLocalHost().getHostName(); } catch (Throwable excp) { LogLog.warn("getHostname()", excp); } if ( logger.isDebugEnabled() ) { logger.debug("<== MiscUtil.initLocalHost()"); } } public static Date getUTCDateForLocalDate(Date date) { TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT+0"); Calendar local = Calendar.getInstance(); int offset = local.getTimeZone().getOffset(local.getTimeInMillis()); GregorianCalendar utc = new GregorianCalendar(gmtTimeZone); utc.setTimeInMillis(date.getTime()); utc.add(Calendar.MILLISECOND, -offset); return utc.getTime(); } public static Date getUTCDate() { TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT+0"); Calendar local = Calendar.getInstance(); int offset = local.getTimeZone().getOffset(local.getTimeInMillis()); GregorianCalendar utc = new GregorianCalendar(gmtTimeZone); utc.setTimeInMillis(local.getTimeInMillis()); utc.add(Calendar.MILLISECOND, -offset); return utc.getTime(); } }