/** * 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.hadoop.security; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.StringTokenizer; import java.util.TreeSet; import javax.security.auth.login.LoginException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.util.Shell; import org.apache.hadoop.io.Text; import org.apache.hadoop.io.WritableUtils; /** An implementation of UserGroupInformation in the Unix system */ public class UnixUserGroupInformation extends UserGroupInformation { public static final String DEFAULT_USERNAME = "DrWho"; public static final String DEFAULT_GROUP = "Tardis"; final static public String UGI_PROPERTY_NAME = "hadoop.job.ugi"; final static private HashMap<String, UnixUserGroupInformation> user2UGIMap = new HashMap<String, UnixUserGroupInformation>(); /** Create an immutable {@link UnixUserGroupInformation} object. */ public static UnixUserGroupInformation createImmutable(String[] ugi) { return new UnixUserGroupInformation(ugi) { public void readFields(DataInput in) throws IOException { throw new UnsupportedOperationException(); } }; } private String userName; private String[] groupNames; /** Default constructor */ public UnixUserGroupInformation() { } /** Constructor with parameters user name and its group names. * The first entry in the groups list is the default group. * * @param userName a user's name * @param groupNames groups list, first of which is the default group * @exception IllegalArgumentException if any argument is null */ public UnixUserGroupInformation(String userName, String[] groupNames) { setUserGroupNames(userName, groupNames); } /** Constructor with parameter user/group names * * @param ugi an array containing user/group names, the first * element of which is the user name, the second of * which is the default group name. * @exception IllegalArgumentException if the array size is less than 2 * or any element is null. */ public UnixUserGroupInformation(String[] ugi) { if (ugi==null || ugi.length < 2) { throw new IllegalArgumentException( "Parameter does contain at least "+ "one user name and one group name"); } String[] groupNames = new String[ugi.length-1]; System.arraycopy(ugi, 1, groupNames, 0, groupNames.length); setUserGroupNames(ugi[0], groupNames); } /* Set this object's user name and group names * * @param userName a user's name * @param groupNames groups list, the first of which is the default group * @exception IllegalArgumentException if any argument is null */ private void setUserGroupNames(String userName, String[] groupNames) { if (userName==null || userName.length()==0 || groupNames== null || groupNames.length==0) { throw new IllegalArgumentException( "Parameters should not be null or an empty string/array"); } for (int i=0; i<groupNames.length; i++) { if(groupNames[i] == null || groupNames[i].length() == 0) { throw new IllegalArgumentException("A null group name at index " + i); } } this.userName = userName; this.groupNames = groupNames; } /** Return an array of group names */ public String[] getGroupNames() { return groupNames; } /** Return the user's name */ public String getUserName() { return userName; } /* The following two methods implements Writable interface */ final private static String UGI_TECHNOLOGY = "STRING_UGI"; /** Deserialize this object * First check if this is a UGI in the string format. * If no, throw an IOException; otherwise * set this object's fields by reading them from the given data input * * @param in input stream * @exception IOException is thrown if encounter any error when reading */ public void readFields(DataInput in) throws IOException { // read UGI type first String ugiType = Text.readString(in); if (!UGI_TECHNOLOGY.equals(ugiType)) { throw new IOException("Expect UGI prefix: " + UGI_TECHNOLOGY + ", but receive a prefix: " + ugiType); } // read this object userName = Text.readString(in); int numOfGroups = WritableUtils.readVInt(in); groupNames = new String[numOfGroups]; for (int i = 0; i < numOfGroups; i++) { groupNames[i] = Text.readString(in); } } /** Serialize this object * First write a string marking that this is a UGI in the string format, * then write this object's serialized form to the given data output * * @param out output stream * @exception IOException if encounter any error during writing */ public void write(DataOutput out) throws IOException { // write a prefix indicating the type of UGI being written Text.writeString(out, UGI_TECHNOLOGY); // write this object Text.writeString(out, userName); WritableUtils.writeVInt(out, groupNames.length); for (String groupName : groupNames) { Text.writeString(out, groupName); } } /* The following two methods deal with transferring UGI through conf. * In this pass of implementation we store UGI as a string in conf. * Later we may change it to be a more general approach that stores * it as a byte array */ /** Store the given <code>ugi</code> as a comma separated string in * <code>conf</code> as a property <code>attr</code> * * The String starts with the user name followed by the default group names, * and other group names. * * @param conf configuration * @param attr property name * @param ugi a UnixUserGroupInformation */ public static void saveToConf(Configuration conf, String attr, UnixUserGroupInformation ugi ) { conf.set(attr, ugi.toString()); } /** Read a UGI from the given <code>conf</code> * * The object is expected to store with the property name <code>attr</code> * as a comma separated string that starts * with the user name followed by group names. * If the property name is not defined, return null. * It's assumed that there is only one UGI per user. If this user already * has a UGI in the ugi map, return the ugi in the map. * Otherwise, construct a UGI from the configuration, store it in the * ugi map and return it. * * @param conf configuration * @param attr property name * @return a UnixUGI * @throws LoginException if the stored string is ill-formatted. */ public static UnixUserGroupInformation readFromConf( Configuration conf, String attr) throws LoginException { String[] ugi = conf.getStrings(attr); if(ugi == null) { return null; } UnixUserGroupInformation currentUGI = null; if (ugi.length>0 ){ currentUGI = user2UGIMap.get(ugi[0]); } if (currentUGI == null) { try { currentUGI = new UnixUserGroupInformation(ugi); user2UGIMap.put(currentUGI.getUserName(), currentUGI); } catch (IllegalArgumentException e) { throw new LoginException("Login failed: "+e.getMessage()); } } return currentUGI; } /** * Get current user's name and the names of all its groups from Unix. * It's assumed that there is only one UGI per user. If this user already * has a UGI in the ugi map, return the ugi in the map. * Otherwise get the current user's information from Unix, store it * in the map, and return it. * * If the current user's UNIX username or groups are configured in such a way * to throw an Exception, for example if the user uses LDAP, then this method * will use a the {@link #DEFAULT_USERNAME} and {@link #DEFAULT_GROUP} * constants. */ public static UnixUserGroupInformation login() throws LoginException { try { String userName; // if an exception occurs, then uses the // default user try { userName = getUnixUserName(); } catch (Exception e) { userName = DEFAULT_USERNAME; } // check if this user already has a UGI object in the ugi map UnixUserGroupInformation ugi = user2UGIMap.get(userName); if (ugi != null) { return ugi; } /* get groups list from UNIX. * It's assumed that the first group is the default group. */ String[] groupNames; // if an exception occurs, then uses the // default group try { groupNames = getUnixGroups(); } catch (Exception e) { groupNames = new String[1]; groupNames[0] = DEFAULT_GROUP; } // construct a Unix UGI ugi = new UnixUserGroupInformation(userName, groupNames); user2UGIMap.put(ugi.getUserName(), ugi); return ugi; } catch (Exception e) { throw new LoginException("Login failed: "+e.getMessage()); } } /** Equivalent to login(conf, false). */ public static UnixUserGroupInformation login(Configuration conf) throws LoginException { return login(conf, false); } /** Get a user's name & its group names from the given configuration; * If it is not defined in the configuration, get the current user's * information from Unix. * If the user has a UGI in the ugi map, return the one in * the UGI map. * * @param conf either a job configuration or client's configuration * @param save saving it to conf? * @return UnixUserGroupInformation a user/group information * @exception LoginException if not able to get the user/group information */ public static UnixUserGroupInformation login(Configuration conf, boolean save ) throws LoginException { UnixUserGroupInformation ugi = readFromConf(conf, UGI_PROPERTY_NAME); if (ugi == null) { ugi = login(); LOG.debug("Unix Login: " + ugi); if (save) { saveToConf(conf, UGI_PROPERTY_NAME, ugi); } } return ugi; } /* Return a string representation of a string array. * Two strings are separated by a blank. */ private static String toString(String[] strArray) { if (strArray==null || strArray.length==0) { return ""; } StringBuilder buf = new StringBuilder(strArray[0]); for (int i=1; i<strArray.length; i++) { buf.append(' '); buf.append(strArray[i]); } return buf.toString(); } /** Get current user's name from Unix by running the command whoami. * * @return current user's name * @throws IOException if encounter any error while running the command */ static String getUnixUserName() throws IOException { String[] result = executeShellCommand( new String[]{Shell.USER_NAME_COMMAND}); if (result.length!=1) { throw new IOException("Expect one token as the result of " + Shell.USER_NAME_COMMAND + ": " + toString(result)); } return result[0]; } /** Get the current user's group list from Unix by running the command groups * * @return the groups list that the current user belongs to * @throws IOException if encounter any error when running the command */ private static String[] getUnixGroups() throws IOException { return executeShellCommand(Shell.getGROUPS_COMMAND()); } /* Execute a command and return the result as an array of Strings */ private static String[] executeShellCommand(String[] command) throws IOException { String groups = Shell.execCommand(command); StringTokenizer tokenizer = new StringTokenizer(groups); int numOfTokens = tokenizer.countTokens(); String[] tokens = new String[numOfTokens]; for (int i=0; tokenizer.hasMoreTokens(); i++) { tokens[i] = tokenizer.nextToken(); } return tokens; } /** Decide if two UGIs are the same * * @param other other object * @return true if they are the same; false otherwise. */ public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof UnixUserGroupInformation)) { return false; } UnixUserGroupInformation otherUGI = (UnixUserGroupInformation)other; // check userName if (userName == null) { if (otherUGI.getUserName() != null) { return false; } } else { if (!userName.equals(otherUGI.getUserName())) { return false; } } // checkGroupNames if (groupNames == otherUGI.groupNames) { return true; } if (groupNames.length != otherUGI.groupNames.length) { return false; } // check default group name if (groupNames.length>0 && !groupNames[0].equals(otherUGI.groupNames[0])) { return false; } // check all group names, ignoring the order return new TreeSet<String>(Arrays.asList(groupNames)).equals( new TreeSet<String>(Arrays.asList(otherUGI.groupNames))); } /** Returns a hash code for this UGI. * The hash code for a UGI is the hash code of its user name string. * * @return a hash code value for this UGI. */ public int hashCode() { return getUserName().hashCode(); } /** Convert this object to a string * * @return a comma separated string containing the user name and group names */ public String toString() { StringBuilder buf = new StringBuilder(); buf.append(userName); for (String groupName : groupNames) { buf.append(','); buf.append(groupName); } return buf.toString(); } @Override public String getName() { return toString(); } }