001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.security;
019
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.io.InputStreamReader;
025import java.nio.charset.Charset;
026import java.util.HashMap;
027import java.util.Map;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import org.apache.commons.io.Charsets;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.apache.hadoop.conf.Configuration;
035import org.apache.hadoop.util.Time;
036
037import com.google.common.annotations.VisibleForTesting;
038import com.google.common.collect.BiMap;
039import com.google.common.collect.HashBiMap;
040
041/**
042 * A simple shell-based implementation of {@link IdMappingServiceProvider} 
043 * Map id to user name or group name. It does update every 15 minutes. Only a
044 * single instance of this class is expected to be on the server.
045 * 
046 * The maps are incrementally updated as described below:
047 *   1. Initialize the maps as empty. 
048 *   2. Incrementally update the maps
049 *      - When ShellBasedIdMapping is requested for user or group name given 
050 *        an ID, or for ID given a user or group name, do look up in the map
051 *        first, if it doesn't exist, find the corresponding entry with shell
052 *        command, and insert the entry to the maps.
053 *      - When group ID is requested for a given group name, and if the
054 *        group name is numerical, the full group map is loaded. Because we
055 *        don't have a good way to find the entry for a numerical group name,
056 *        loading the full map helps to get in all entries.
057 *   3. Periodically refresh the maps for both user and group, e.g,
058 *      do step 1.
059 *   Note: for testing purpose, step 1 may initial the maps with full mapping
060 *   when using constructor
061 *   {@link ShellBasedIdMapping#ShellBasedIdMapping(Configuration, boolean)}.
062 */
063public class ShellBasedIdMapping implements IdMappingServiceProvider {
064
065  private static final Log LOG =
066      LogFactory.getLog(ShellBasedIdMapping.class);
067
068  private final static String OS = System.getProperty("os.name");
069
070  /** Shell commands to get users and groups */
071  static final String GET_ALL_USERS_CMD = "getent passwd | cut -d: -f1,3";
072  static final String GET_ALL_GROUPS_CMD = "getent group | cut -d: -f1,3";
073  static final String MAC_GET_ALL_USERS_CMD = "dscl . -list /Users UniqueID";
074  static final String MAC_GET_ALL_GROUPS_CMD = "dscl . -list /Groups PrimaryGroupID";
075
076  private final File staticMappingFile;
077  private StaticMapping staticMapping = null;
078  // Last time the static map was modified, measured time difference in
079  // milliseconds since midnight, January 1, 1970 UTC
080  private long lastModificationTimeStaticMap = 0;
081  
082  private boolean constructFullMapAtInit = false;
083
084  // Used for parsing the static mapping file.
085  private static final Pattern EMPTY_LINE = Pattern.compile("^\\s*$");
086  private static final Pattern COMMENT_LINE = Pattern.compile("^\\s*#.*$");
087  private static final Pattern MAPPING_LINE =
088      Pattern.compile("^(uid|gid)\\s+(\\d+)\\s+(\\d+)\\s*(#.*)?$");
089
090  final private long timeout;
091  
092  // Maps for id to name map. Guarded by this object monitor lock
093  private BiMap<Integer, String> uidNameMap = HashBiMap.create();
094  private BiMap<Integer, String> gidNameMap = HashBiMap.create();
095  private long lastUpdateTime = 0; // Last time maps were updated
096
097  /*
098   * Constructor
099   * @param conf the configuration
100   * @param constructFullMapAtInit initialize the maps with full mapping when
101   *        true, otherwise initialize the maps to empty. This parameter is
102   *        intended for testing only, its default is false.
103   */
104  @VisibleForTesting
105  public ShellBasedIdMapping(Configuration conf,
106      boolean constructFullMapAtInit) throws IOException {
107    this.constructFullMapAtInit = constructFullMapAtInit;
108    long updateTime = conf.getLong(
109        IdMappingConstant.USERGROUPID_UPDATE_MILLIS_KEY,
110        IdMappingConstant.USERGROUPID_UPDATE_MILLIS_DEFAULT);
111    // Minimal interval is 1 minute
112    if (updateTime < IdMappingConstant.USERGROUPID_UPDATE_MILLIS_MIN) {
113      LOG.info("User configured user account update time is less"
114          + " than 1 minute. Use 1 minute instead.");
115      timeout = IdMappingConstant.USERGROUPID_UPDATE_MILLIS_MIN;
116    } else {
117      timeout = updateTime;
118    }
119    
120    String staticFilePath = 
121        conf.get(IdMappingConstant.STATIC_ID_MAPPING_FILE_KEY,
122            IdMappingConstant.STATIC_ID_MAPPING_FILE_DEFAULT);
123    staticMappingFile = new File(staticFilePath);
124    updateStaticMapping();
125    updateMaps();
126  }
127
128  /*
129   * Constructor
130   * initialize user and group maps to empty
131   * @param conf the configuration
132   */
133  public ShellBasedIdMapping(Configuration conf) throws IOException {
134    this(conf, false);
135  }
136
137  @VisibleForTesting
138  public long getTimeout() {
139    return timeout;
140  }
141
142  @VisibleForTesting
143  public BiMap<Integer, String> getUidNameMap() {
144    return uidNameMap;
145  }
146
147  @VisibleForTesting
148  public BiMap<Integer, String> getGidNameMap() {
149    return gidNameMap;
150  }
151
152  @VisibleForTesting  
153  synchronized public void clearNameMaps() {
154    uidNameMap.clear();
155    gidNameMap.clear();
156    lastUpdateTime = Time.monotonicNow();
157  }  
158
159  synchronized private boolean isExpired() {
160    return Time.monotonicNow() - lastUpdateTime > timeout;
161  }
162
163  // If can't update the maps, will keep using the old ones
164  private void checkAndUpdateMaps() {
165    if (isExpired()) {
166      LOG.info("Update cache now");
167      try {
168        updateMaps();
169      } catch (IOException e) {
170        LOG.error("Can't update the maps. Will use the old ones,"
171            + " which can potentially cause problem.", e);
172      }
173    }
174  }
175
176  private static final String DUPLICATE_NAME_ID_DEBUG_INFO =
177      "NFS gateway could have problem starting with duplicate name or id on the host system.\n"
178      + "This is because HDFS (non-kerberos cluster) uses name as the only way to identify a user or group.\n"
179      + "The host system with duplicated user/group name or id might work fine most of the time by itself.\n"
180      + "However when NFS gateway talks to HDFS, HDFS accepts only user and group name.\n"
181      + "Therefore, same name means the same user or same group. To find the duplicated names/ids, one can do:\n"
182      + "<getent passwd | cut -d: -f1,3> and <getent group | cut -d: -f1,3> on Linux systems,\n"
183      + "<dscl . -list /Users UniqueID> and <dscl . -list /Groups PrimaryGroupID> on MacOS.";
184  
185  private static void reportDuplicateEntry(final String header,
186      final Integer key, final String value,
187      final Integer ekey, final String evalue) {    
188      LOG.warn("\n" + header + String.format(
189          "new entry (%d, %s), existing entry: (%d, %s).%n%s%n%s",
190          key, value, ekey, evalue,
191          "The new entry is to be ignored for the following reason.",
192          DUPLICATE_NAME_ID_DEBUG_INFO));
193  }
194
195  /**
196   * uid and gid are defined as uint32 in linux. Some systems create
197   * (intended or unintended) <nfsnobody, 4294967294> kind of <name,Id>
198   * mapping, where 4294967294 is 2**32-2 as unsigned int32. As an example,
199   *   https://bugzilla.redhat.com/show_bug.cgi?id=511876.
200   * Because user or group id are treated as Integer (signed integer or int32)
201   * here, the number 4294967294 is out of range. The solution is to convert
202   * uint32 to int32, so to map the out-of-range ID to the negative side of
203   * Integer, e.g. 4294967294 maps to -2 and 4294967295 maps to -1.
204   */
205  private static Integer parseId(final String idStr) {
206    Long longVal = Long.parseLong(idStr);
207    int intVal = longVal.intValue();
208    return Integer.valueOf(intVal);
209  }
210  
211  /**
212   * Get the list of users or groups returned by the specified command,
213   * and save them in the corresponding map.
214   * @throws IOException 
215   */
216  @VisibleForTesting
217  public static boolean updateMapInternal(BiMap<Integer, String> map,
218      String mapName, String command, String regex,
219      Map<Integer, Integer> staticMapping) throws IOException  {
220    boolean updated = false;
221    BufferedReader br = null;
222    try {
223      Process process = Runtime.getRuntime().exec(
224          new String[] { "bash", "-c", command });
225      br = new BufferedReader(
226          new InputStreamReader(process.getInputStream(),
227                                Charset.defaultCharset()));
228      String line = null;
229      while ((line = br.readLine()) != null) {
230        String[] nameId = line.split(regex);
231        if ((nameId == null) || (nameId.length != 2)) {
232          throw new IOException("Can't parse " + mapName + " list entry:" + line);
233        }
234        LOG.debug("add to " + mapName + "map:" + nameId[0] + " id:" + nameId[1]);
235        // HDFS can't differentiate duplicate names with simple authentication
236        final Integer key = staticMapping.get(parseId(nameId[1]));
237        final String value = nameId[0];
238        if (map.containsKey(key)) {
239          final String prevValue = map.get(key);
240          if (value.equals(prevValue)) {
241            // silently ignore equivalent entries
242            continue;
243          }
244          reportDuplicateEntry(
245              "Got multiple names associated with the same id: ",
246              key, value, key, prevValue);           
247          continue;
248        }
249        if (map.containsValue(value)) {
250          final Integer prevKey = map.inverse().get(value);
251          reportDuplicateEntry(
252              "Got multiple ids associated with the same name: ",
253              key, value, prevKey, value);
254          continue;
255        }
256        map.put(key, value);
257        updated = true;
258      }
259      LOG.debug("Updated " + mapName + " map size: " + map.size());
260      
261    } catch (IOException e) {
262      LOG.error("Can't update " + mapName + " map");
263      throw e;
264    } finally {
265      if (br != null) {
266        try {
267          br.close();
268        } catch (IOException e1) {
269          LOG.error("Can't close BufferedReader of command result", e1);
270        }
271      }
272    }
273    return updated;
274  }
275
276  private boolean checkSupportedPlatform() {
277    if (!OS.startsWith("Linux") && !OS.startsWith("Mac")) {
278      LOG.error("Platform is not supported:" + OS
279          + ". Can't update user map and group map and"
280          + " 'nobody' will be used for any user and group.");
281      return false;
282    }
283    return true;
284  }
285
286  private static boolean isInteger(final String s) {
287    try { 
288      Integer.parseInt(s); 
289    } catch(NumberFormatException e) { 
290      return false; 
291    }
292    // only got here if we didn't return false
293    return true;
294  }
295
296  private synchronized void updateStaticMapping() throws IOException {
297    final boolean init = (staticMapping == null);
298    //
299    // if the static mapping file
300    //   - was modified after last update, load the map again;
301    //   - did not exist but was added since last update, load the map;
302    //   - existed before but deleted since last update, clear the map
303    //
304    if (staticMappingFile.exists()) {
305      // check modification time, reload the file if the last modification
306      // time changed since prior load.
307      long lmTime = staticMappingFile.lastModified();
308      if (lmTime != lastModificationTimeStaticMap) {
309        LOG.info(init? "Using " : "Reloading " + "'" + staticMappingFile
310            + "' for static UID/GID mapping...");
311        lastModificationTimeStaticMap = lmTime;
312        staticMapping = parseStaticMap(staticMappingFile);        
313      }
314    } else {
315      if (init) {
316        staticMapping = new StaticMapping(new HashMap<Integer, Integer>(),
317            new HashMap<Integer, Integer>());
318      }
319      if (lastModificationTimeStaticMap != 0 || init) {
320        // print the following log at initialization or when the static
321        // mapping file was deleted after prior load
322        LOG.info("Not doing static UID/GID mapping because '"
323            + staticMappingFile + "' does not exist.");
324      }
325      lastModificationTimeStaticMap = 0;
326      staticMapping.clear();
327    }
328  }
329
330  /*
331   * Refresh static map, and reset the other maps to empty.
332   * For testing code, a full map may be re-constructed here when the object
333   * was created with constructFullMapAtInit being set to true.
334   */
335  synchronized public void updateMaps() throws IOException {
336    if (!checkSupportedPlatform()) {
337      return;
338    }
339
340    if (constructFullMapAtInit) {
341      loadFullMaps();
342      // set constructFullMapAtInit to false to allow testing code to
343      // do incremental update to maps after initial construction
344      constructFullMapAtInit = false;
345    } else {
346      updateStaticMapping();
347      clearNameMaps();
348    }
349  }
350  
351  synchronized private void loadFullUserMap() throws IOException {
352    BiMap<Integer, String> uMap = HashBiMap.create();
353    if (OS.startsWith("Mac")) {
354      updateMapInternal(uMap, "user", MAC_GET_ALL_USERS_CMD, "\\s+",
355          staticMapping.uidMapping);
356    } else {
357      updateMapInternal(uMap, "user", GET_ALL_USERS_CMD, ":",
358          staticMapping.uidMapping);
359    }
360    uidNameMap = uMap;
361    lastUpdateTime = Time.monotonicNow();
362  }
363
364  synchronized private void loadFullGroupMap() throws IOException {
365    BiMap<Integer, String> gMap = HashBiMap.create();
366
367    if (OS.startsWith("Mac")) {
368      updateMapInternal(gMap, "group", MAC_GET_ALL_GROUPS_CMD, "\\s+",
369          staticMapping.gidMapping);
370    } else {
371      updateMapInternal(gMap, "group", GET_ALL_GROUPS_CMD, ":",
372          staticMapping.gidMapping);
373    }
374    gidNameMap = gMap;
375    lastUpdateTime = Time.monotonicNow();
376  }
377
378  synchronized private void loadFullMaps() throws IOException {
379    loadFullUserMap();
380    loadFullGroupMap();
381  }
382
383  // search for id with given name, return "<name>:<id>"
384  // return
385  //     getent group <name> | cut -d: -f1,3
386  // OR
387  //     id -u <name> | awk '{print "<name>:"$1 }'
388  //
389  private String getName2IdCmdLinux(final String name, final boolean isGrp) {
390    String cmd;
391    if (isGrp) {
392      cmd = "getent group " + name + " | cut -d: -f1,3";   
393    } else {
394      cmd = "id -u " + name + " | awk '{print \"" + name + ":\"$1 }'";
395    }
396    return cmd;
397  }
398  
399  // search for name with given id, return "<name>:<id>"
400  private String getId2NameCmdLinux(final int id, final boolean isGrp) {
401    String cmd = "getent ";
402    cmd += isGrp? "group " : "passwd ";
403    cmd += String.valueOf(id) + " | cut -d: -f1,3";
404    return cmd;
405  }
406
407  // "dscl . -read /Users/<name> | grep UniqueID" returns "UniqueId: <id>",
408  // "dscl . -read /Groups/<name> | grep PrimaryGroupID" returns "PrimaryGoupID: <id>"
409  // The following method returns a command that uses awk to process the result,
410  // of these commands, and returns "<name> <id>", to simulate one entry returned by 
411  // MAC_GET_ALL_USERS_CMD or MAC_GET_ALL_GROUPS_CMD.
412  // Specificially, this method returns:
413  // id -u <name> | awk '{print "<name>:"$1 }'
414  // OR
415  // dscl . -read /Groups/<name> | grep PrimaryGroupID | awk '($1 == "PrimaryGroupID:") { print "<name> " $2 }'
416  //
417  private String getName2IdCmdMac(final String name, final boolean isGrp) {
418    String cmd;
419    if (isGrp) {
420      cmd = "dscl . -read /Groups/" + name;
421      cmd += " | grep PrimaryGroupID | awk '($1 == \"PrimaryGroupID:\") ";
422      cmd += "{ print \"" + name + "  \" $2 }'";
423    } else {
424      cmd = "id -u " + name + " | awk '{print \"" + name + "  \"$1 }'";
425    }
426    return cmd;
427  }
428
429  // "dscl . -search /Users UniqueID <id>" returns 
430  //    <name> UniqueID = (
431  //      <id>
432  //    )
433  // "dscl . -search /Groups PrimaryGroupID <id>" returns
434  //    <name> PrimaryGroupID = (
435  //      <id>
436  //    )
437  // The following method returns a command that uses sed to process the
438  // the result and returns "<name> <id>" to simulate one entry returned
439  // by MAC_GET_ALL_USERS_CMD or MAC_GET_ALL_GROUPS_CMD.
440  // For certain negative id case like nfsnobody, the <id> is quoted as
441  // "<id>", added one sed section to remove the quote.
442  // Specifically, the method returns:
443  // dscl . -search /Users UniqueID <id> | sed 'N;s/\\n//g;N;s/\\n//g' | sed 's/UniqueID =//g' | sed 's/)//g' | sed 's/\"//g'
444  // OR
445  // dscl . -search /Groups PrimaryGroupID <id> | sed 'N;s/\\n//g;N;s/\\n//g' | sed 's/PrimaryGroupID =//g' | sed 's/)//g' | sed 's/\"//g'
446  //
447  private String getId2NameCmdMac(final int id, final boolean isGrp) {
448    String cmd = "dscl . -search /";
449    cmd += isGrp? "Groups PrimaryGroupID " : "Users UniqueID ";
450    cmd += String.valueOf(id);
451    cmd += " | sed 'N;s/\\n//g;N;s/\\n//g' | sed 's/";
452    cmd += isGrp? "PrimaryGroupID" : "UniqueID";
453    cmd += " = (//g' | sed 's/)//g' | sed 's/\\\"//g'";
454    return cmd;
455  }
456
457  synchronized private void updateMapIncr(final String name,
458      final boolean isGrp) throws IOException {
459    if (!checkSupportedPlatform()) {
460      return;
461    }
462    if (isInteger(name) && isGrp) {
463      loadFullGroupMap();
464      return;
465    }
466
467    boolean updated = false;
468    updateStaticMapping();
469
470    if (OS.startsWith("Linux")) {
471      if (isGrp) {
472        updated = updateMapInternal(gidNameMap, "group",
473            getName2IdCmdLinux(name, true), ":",
474            staticMapping.gidMapping);
475      } else {
476        updated = updateMapInternal(uidNameMap, "user",
477            getName2IdCmdLinux(name, false), ":",
478            staticMapping.uidMapping);
479      }
480    } else {
481      // Mac
482      if (isGrp) {        
483        updated = updateMapInternal(gidNameMap, "group",
484            getName2IdCmdMac(name, true), "\\s+",
485            staticMapping.gidMapping);
486      } else {
487        updated = updateMapInternal(uidNameMap, "user",
488            getName2IdCmdMac(name, false), "\\s+",
489            staticMapping.uidMapping);
490      }
491    }
492    if (updated) {
493      lastUpdateTime = Time.monotonicNow();
494    }
495  }
496
497  synchronized private void updateMapIncr(final int id,
498      final boolean isGrp) throws IOException {
499    if (!checkSupportedPlatform()) {
500      return;
501    }
502    
503    boolean updated = false;
504    updateStaticMapping();
505
506    if (OS.startsWith("Linux")) {
507      if (isGrp) {
508        updated = updateMapInternal(gidNameMap, "group",
509            getId2NameCmdLinux(id, true), ":",
510            staticMapping.gidMapping);
511      } else {
512        updated = updateMapInternal(uidNameMap, "user",
513            getId2NameCmdLinux(id, false), ":",
514            staticMapping.uidMapping);
515      }
516    } else {
517      // Mac
518      if (isGrp) {
519        updated = updateMapInternal(gidNameMap, "group",
520            getId2NameCmdMac(id, true), "\\s+",
521            staticMapping.gidMapping);
522      } else {
523        updated = updateMapInternal(uidNameMap, "user",
524            getId2NameCmdMac(id, false), "\\s+",
525            staticMapping.uidMapping);
526      }
527    }
528    if (updated) {
529      lastUpdateTime = Time.monotonicNow();
530    }
531  }
532
533  @SuppressWarnings("serial")
534  static final class PassThroughMap<K> extends HashMap<K, K> {
535    
536    public PassThroughMap() {
537      this(new HashMap<K, K>());
538    }
539    
540    public PassThroughMap(Map<K, K> mapping) {
541      super();
542      for (Map.Entry<K, K> entry : mapping.entrySet()) {
543        super.put(entry.getKey(), entry.getValue());
544      }
545    }
546
547    @SuppressWarnings("unchecked")
548    @Override
549    public K get(Object key) {
550      if (super.containsKey(key)) {
551        return super.get(key);
552      } else {
553        return (K) key;
554      }
555    }
556  }
557  
558  @VisibleForTesting
559  static final class StaticMapping {
560    final Map<Integer, Integer> uidMapping;
561    final Map<Integer, Integer> gidMapping;
562    
563    public StaticMapping(Map<Integer, Integer> uidMapping,
564        Map<Integer, Integer> gidMapping) {
565      this.uidMapping = new PassThroughMap<Integer>(uidMapping);
566      this.gidMapping = new PassThroughMap<Integer>(gidMapping);
567    }
568
569    public void clear() {
570      uidMapping.clear();
571      gidMapping.clear();
572    }
573
574    public boolean isNonEmpty() {
575      return uidMapping.size() > 0 || gidMapping.size() > 0;
576    }
577  }
578  
579  static StaticMapping parseStaticMap(File staticMapFile)
580      throws IOException {
581    
582    Map<Integer, Integer> uidMapping = new HashMap<Integer, Integer>();
583    Map<Integer, Integer> gidMapping = new HashMap<Integer, Integer>();
584    
585    BufferedReader in = new BufferedReader(new InputStreamReader(
586        new FileInputStream(staticMapFile), Charsets.UTF_8));
587    
588    try {
589      String line = null;
590      while ((line = in.readLine()) != null) {
591        // Skip entirely empty and comment lines.
592        if (EMPTY_LINE.matcher(line).matches() ||
593            COMMENT_LINE.matcher(line).matches()) {
594          continue;
595        }
596        
597        Matcher lineMatcher = MAPPING_LINE.matcher(line);
598        if (!lineMatcher.matches()) {
599          LOG.warn("Could not parse line '" + line + "'. Lines should be of " +
600              "the form '[uid|gid] [remote id] [local id]'. Blank lines and " +
601              "everything following a '#' on a line will be ignored.");
602          continue;
603        }
604        
605        // We know the line is fine to parse without error checking like this
606        // since it matched the regex above.
607        String firstComponent = lineMatcher.group(1);
608        int remoteId = parseId(lineMatcher.group(2));
609        int localId = parseId(lineMatcher.group(3));
610        if (firstComponent.equals("uid")) {
611          uidMapping.put(localId, remoteId);
612        } else {
613          gidMapping.put(localId, remoteId);
614        }
615      }
616    } finally {
617      in.close();
618    }
619    
620    return new StaticMapping(uidMapping, gidMapping);
621  }
622
623  synchronized public int getUid(String user) throws IOException {
624    checkAndUpdateMaps();
625
626    Integer id = uidNameMap.inverse().get(user);
627    if (id == null) {
628      updateMapIncr(user, false);
629      id = uidNameMap.inverse().get(user);
630      if (id == null) {
631        throw new IOException("User just deleted?:" + user);
632      }
633    }
634    return id.intValue();
635  }
636
637  synchronized public int getGid(String group) throws IOException {
638    checkAndUpdateMaps();
639
640    Integer id = gidNameMap.inverse().get(group);
641    if (id == null) {
642      updateMapIncr(group, true);
643      id = gidNameMap.inverse().get(group);
644      if (id == null) {
645        throw new IOException("No such group:" + group);
646      }
647    }
648    return id.intValue();
649  }
650
651  synchronized public String getUserName(int uid, String unknown) {
652    checkAndUpdateMaps();
653    String uname = uidNameMap.get(uid);
654    if (uname == null) {
655      try {
656        updateMapIncr(uid, false);
657      } catch (Exception e) {        
658      }
659      uname = uidNameMap.get(uid);
660      if (uname == null) {     
661        LOG.warn("Can't find user name for uid " + uid
662            + ". Use default user name " + unknown);
663        uname = unknown;
664      }
665    }
666    return uname;
667  }
668
669  synchronized public String getGroupName(int gid, String unknown) {
670    checkAndUpdateMaps();
671    String gname = gidNameMap.get(gid);
672    if (gname == null) {
673      try {
674        updateMapIncr(gid, true);
675      } catch (Exception e) {        
676      }
677      gname = gidNameMap.get(gid);
678      if (gname == null) {
679        LOG.warn("Can't find group name for gid " + gid
680            + ". Use default group name " + unknown);
681        gname = unknown;
682      }
683    }
684    return gname;
685  }
686
687  // When can't map user, return user name's string hashcode
688  public int getUidAllowingUnknown(String user) {
689    checkAndUpdateMaps();
690    int uid;
691    try {
692      uid = getUid(user);
693    } catch (IOException e) {
694      uid = user.hashCode();
695      LOG.info("Can't map user " + user + ". Use its string hashcode:" + uid);
696    }
697    return uid;
698  }
699
700  // When can't map group, return group name's string hashcode
701  public int getGidAllowingUnknown(String group) {
702    checkAndUpdateMaps();
703    int gid;
704    try {
705      gid = getGid(group);
706    } catch (IOException e) {
707      gid = group.hashCode();
708      LOG.info("Can't map group " + group + ". Use its string hashcode:" + gid);
709    }
710    return gid;
711  }
712}