blob: 62cbb659869ba6cb0407188d0fdf74ada9b017d5 [file] [log] [blame]
/**
* 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();
}
}