/**
 * 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.security.ShellBasedIdMapping.PassThroughMap;
import org.apache.hadoop.security.ShellBasedIdMapping.StaticMapping;
import org.junit.Test;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

public class TestShellBasedIdMapping {
  
  private static final Map<Integer, Integer> EMPTY_PASS_THROUGH_MAP =
      new PassThroughMap<Integer>();
  
  private void createStaticMapFile(final File smapFile, final String smapStr)
      throws IOException {
    OutputStream out = new FileOutputStream(smapFile);
    out.write(smapStr.getBytes());
    out.close();
  }

  @Test
  public void testStaticMapParsing() throws IOException {
    File tempStaticMapFile = File.createTempFile("nfs-", ".map");
    final String staticMapFileContents =
        "uid 10 100\n" +
        "gid 10 200\n" +
        "uid 11 201 # comment at the end of a line\n" +
        "uid 12 301\n" +
        "# Comment at the beginning of a line\n" +
        "    # Comment that starts late in the line\n" +
        "uid 10000 10001# line without whitespace before comment\n" +
        "uid 13 302\n" +
        "gid\t11\t201\n" + // Tabs instead of spaces.
        "\n" + // Entirely empty line.
        "gid 12 202\n" +
        "uid 4294967294 123\n" +
        "gid 4294967295 321";
    createStaticMapFile(tempStaticMapFile, staticMapFileContents);

    StaticMapping parsedMap =
        ShellBasedIdMapping.parseStaticMap(tempStaticMapFile);    
    assertEquals(10, (int)parsedMap.uidMapping.get(100));
    assertEquals(11, (int)parsedMap.uidMapping.get(201));
    assertEquals(12, (int)parsedMap.uidMapping.get(301));
    assertEquals(13, (int)parsedMap.uidMapping.get(302));
    assertEquals(10, (int)parsedMap.gidMapping.get(200));
    assertEquals(11, (int)parsedMap.gidMapping.get(201));
    assertEquals(12, (int)parsedMap.gidMapping.get(202));
    assertEquals(10000, (int)parsedMap.uidMapping.get(10001));
    // Ensure pass-through of unmapped IDs works.
    assertEquals(1000, (int)parsedMap.uidMapping.get(1000));
    
    assertEquals(-2, (int)parsedMap.uidMapping.get(123));
    assertEquals(-1, (int)parsedMap.gidMapping.get(321));
    
  }
  
  @Test
  public void testStaticMapping() throws IOException {
    assumeTrue(!Shell.WINDOWS);
    Map<Integer, Integer> uidStaticMap = new PassThroughMap<Integer>();
    Map<Integer, Integer> gidStaticMap = new PassThroughMap<Integer>();
    
    uidStaticMap.put(11501, 10);
    gidStaticMap.put(497, 200);
    
    // Maps for id to name map
    BiMap<Integer, String> uMap = HashBiMap.create();
    BiMap<Integer, String> gMap = HashBiMap.create();
    
    String GET_ALL_USERS_CMD =
        "echo \"atm:x:1000:1000:Aaron T. Myers,,,:/home/atm:/bin/bash\n"
        + "hdfs:x:11501:10787:Grid Distributed File System:/home/hdfs:/bin/bash\""
        + " | cut -d: -f1,3";
    
    String GET_ALL_GROUPS_CMD = "echo \"hdfs:*:11501:hrt_hdfs\n"
        + "mapred:x:497\n"
        + "mapred2:x:498\""
        + " | cut -d: -f1,3";

    ShellBasedIdMapping.updateMapInternal(uMap, "user", GET_ALL_USERS_CMD, ":",
        uidStaticMap);
    ShellBasedIdMapping.updateMapInternal(gMap, "group", GET_ALL_GROUPS_CMD, ":",
        gidStaticMap);
    
    assertEquals("hdfs", uMap.get(10));
    assertEquals(10, (int)uMap.inverse().get("hdfs"));
    assertEquals("atm", uMap.get(1000));
    assertEquals(1000, (int)uMap.inverse().get("atm"));
    
    assertEquals("hdfs", gMap.get(11501));
    assertEquals(11501, (int)gMap.inverse().get("hdfs"));
    assertEquals("mapred", gMap.get(200));
    assertEquals(200, (int)gMap.inverse().get("mapred"));
    assertEquals("mapred2", gMap.get(498));
    assertEquals(498, (int)gMap.inverse().get("mapred2"));
  }

  // Test staticMap refreshing
  @Test
  public void testStaticMapUpdate() throws IOException {
    assumeTrue(!Shell.WINDOWS);
    File tempStaticMapFile = File.createTempFile("nfs-", ".map");
    tempStaticMapFile.delete();
    Configuration conf = new Configuration();
    conf.setLong(IdMappingConstant.USERGROUPID_UPDATE_MILLIS_KEY, 1000);    
    conf.set(IdMappingConstant.STATIC_ID_MAPPING_FILE_KEY,
        tempStaticMapFile.getPath());

    ShellBasedIdMapping refIdMapping =
        new ShellBasedIdMapping(conf, true);
    ShellBasedIdMapping incrIdMapping = new ShellBasedIdMapping(conf);

    BiMap<Integer, String> uidNameMap = refIdMapping.getUidNameMap();
    BiMap<Integer, String> gidNameMap = refIdMapping.getGidNameMap();

    // Force empty map, to see effect of incremental map update of calling
    // getUid()
    incrIdMapping.clearNameMaps();
    uidNameMap = refIdMapping.getUidNameMap();
    {
      BiMap.Entry<Integer, String> me = uidNameMap.entrySet().iterator().next();
      Integer id = me.getKey();
      String name = me.getValue();

      // The static map is empty, so the id found for "name" would be
      // the same as "id"
      Integer nid = incrIdMapping.getUid(name);
      assertEquals(id, nid);
      
      // Clear map and update staticMap file
      incrIdMapping.clearNameMaps();
      Integer rid = id + 10000;
      String smapStr = "uid " + rid + " " + id;
      createStaticMapFile(tempStaticMapFile, smapStr);

      // Now the id found for "name" should be the id specified by
      // the staticMap
      nid = incrIdMapping.getUid(name);
      assertEquals(rid, nid);
    }

    // Force empty map, to see effect of incremental map update of calling
    // getGid()
    incrIdMapping.clearNameMaps();
    gidNameMap = refIdMapping.getGidNameMap();
    {
      BiMap.Entry<Integer, String> me = gidNameMap.entrySet().iterator().next();
      Integer id = me.getKey();
      String name = me.getValue();

      // The static map is empty, so the id found for "name" would be
      // the same as "id"
      Integer nid = incrIdMapping.getGid(name);
      assertEquals(id, nid);

      // Clear map and update staticMap file
      incrIdMapping.clearNameMaps();
      Integer rid = id + 10000;
      String smapStr = "gid " + rid + " " + id;
      // Sleep a bit to avoid that two changes have the same modification time
      try {Thread.sleep(1000);} catch (InterruptedException e) {}
      createStaticMapFile(tempStaticMapFile, smapStr);

      // Now the id found for "name" should be the id specified by
      // the staticMap
      nid = incrIdMapping.getGid(name);
      assertEquals(rid, nid);
    }
  }

  @Test
  public void testDuplicates() throws IOException {
    assumeTrue(!Shell.WINDOWS);
    String GET_ALL_USERS_CMD = "echo \"root:x:0:0:root:/root:/bin/bash\n"
        + "hdfs:x:11501:10787:Grid Distributed File System:/home/hdfs:/bin/bash\n"
        + "hdfs:x:11502:10788:Grid Distributed File System:/home/hdfs:/bin/bash\n"
        + "hdfs1:x:11501:10787:Grid Distributed File System:/home/hdfs:/bin/bash\n"
        + "hdfs2:x:11502:10787:Grid Distributed File System:/home/hdfs:/bin/bash\n"
        + "bin:x:2:2:bin:/bin:/bin/sh\n"
        + "bin:x:1:1:bin:/bin:/sbin/nologin\n"
        + "daemon:x:1:1:daemon:/usr/sbin:/bin/sh\n"
        + "daemon:x:2:2:daemon:/sbin:/sbin/nologin\""
        + " | cut -d: -f1,3";
    String GET_ALL_GROUPS_CMD = "echo \"hdfs:*:11501:hrt_hdfs\n"
        + "mapred:x:497\n"
        + "mapred2:x:497\n"
        + "mapred:x:498\n" 
        + "mapred3:x:498\"" 
        + " | cut -d: -f1,3";
    // Maps for id to name map
    BiMap<Integer, String> uMap = HashBiMap.create();
    BiMap<Integer, String> gMap = HashBiMap.create();

    ShellBasedIdMapping.updateMapInternal(uMap, "user", GET_ALL_USERS_CMD, ":",
        EMPTY_PASS_THROUGH_MAP);
    assertEquals(5, uMap.size());
    assertEquals("root", uMap.get(0));
    assertEquals("hdfs", uMap.get(11501));
    assertEquals("hdfs2",uMap.get(11502));
    assertEquals("bin", uMap.get(2));
    assertEquals("daemon", uMap.get(1));

    ShellBasedIdMapping.updateMapInternal(gMap, "group", GET_ALL_GROUPS_CMD, ":",
        EMPTY_PASS_THROUGH_MAP);
    assertTrue(gMap.size() == 3);
    assertEquals("hdfs",gMap.get(11501));
    assertEquals("mapred", gMap.get(497));
    assertEquals("mapred3", gMap.get(498));
  }

  @Test
  public void testIdOutOfIntegerRange() throws IOException {
    assumeTrue(!Shell.WINDOWS);
    String GET_ALL_USERS_CMD = "echo \""
        + "nfsnobody:x:4294967294:4294967294:Anonymous NFS User:/var/lib/nfs:/sbin/nologin\n"
        + "nfsnobody1:x:4294967295:4294967295:Anonymous NFS User:/var/lib/nfs1:/sbin/nologin\n"
        + "maxint:x:2147483647:2147483647:Grid Distributed File System:/home/maxint:/bin/bash\n"
        + "minint:x:2147483648:2147483648:Grid Distributed File System:/home/minint:/bin/bash\n"
        + "archivebackup:*:1031:4294967294:Archive Backup:/home/users/archivebackup:/bin/sh\n"
        + "hdfs:x:11501:10787:Grid Distributed File System:/home/hdfs:/bin/bash\n"
        + "daemon:x:2:2:daemon:/sbin:/sbin/nologin\""
        + " | cut -d: -f1,3";
    String GET_ALL_GROUPS_CMD = "echo \""
        + "hdfs:*:11501:hrt_hdfs\n"
        + "rpcuser:*:29:\n"
        + "nfsnobody:*:4294967294:\n"
        + "nfsnobody1:*:4294967295:\n"
        + "maxint:*:2147483647:\n"
        + "minint:*:2147483648:\n"
        + "mapred3:x:498\"" 
        + " | cut -d: -f1,3";
    // Maps for id to name map
    BiMap<Integer, String> uMap = HashBiMap.create();
    BiMap<Integer, String> gMap = HashBiMap.create();

    ShellBasedIdMapping.updateMapInternal(uMap, "user", GET_ALL_USERS_CMD, ":",
        EMPTY_PASS_THROUGH_MAP);
    assertTrue(uMap.size() == 7);
    assertEquals("nfsnobody", uMap.get(-2));
    assertEquals("nfsnobody1", uMap.get(-1));
    assertEquals("maxint", uMap.get(2147483647));
    assertEquals("minint", uMap.get(-2147483648));
    assertEquals("archivebackup", uMap.get(1031));
    assertEquals("hdfs",uMap.get(11501));
    assertEquals("daemon", uMap.get(2));

    ShellBasedIdMapping.updateMapInternal(gMap, "group", GET_ALL_GROUPS_CMD, ":",
        EMPTY_PASS_THROUGH_MAP);
    assertTrue(gMap.size() == 7);
    assertEquals("hdfs",gMap.get(11501));
    assertEquals("rpcuser", gMap.get(29));
    assertEquals("nfsnobody", gMap.get(-2));
    assertEquals("nfsnobody1", gMap.get(-1));
    assertEquals("maxint", gMap.get(2147483647));
    assertEquals("minint", gMap.get(-2147483648));
    assertEquals("mapred3", gMap.get(498));
  }

  @Test
  public void testUserUpdateSetting() throws IOException {
    ShellBasedIdMapping iug = new ShellBasedIdMapping(new Configuration());
    assertEquals(iug.getTimeout(),
        IdMappingConstant.USERGROUPID_UPDATE_MILLIS_DEFAULT);

    Configuration conf = new Configuration();
    conf.setLong(IdMappingConstant.USERGROUPID_UPDATE_MILLIS_KEY, 0);
    iug = new ShellBasedIdMapping(conf);
    assertEquals(iug.getTimeout(), IdMappingConstant.USERGROUPID_UPDATE_MILLIS_MIN);

    conf.setLong(IdMappingConstant.USERGROUPID_UPDATE_MILLIS_KEY,
        IdMappingConstant.USERGROUPID_UPDATE_MILLIS_DEFAULT * 2);
    iug = new ShellBasedIdMapping(conf);
    assertEquals(iug.getTimeout(),
        IdMappingConstant.USERGROUPID_UPDATE_MILLIS_DEFAULT * 2);
  }
  
  @Test
  public void testUpdateMapIncr() throws IOException {
    Configuration conf = new Configuration();
    conf.setLong(IdMappingConstant.USERGROUPID_UPDATE_MILLIS_KEY, 600000);
    ShellBasedIdMapping refIdMapping =
        new ShellBasedIdMapping(conf, true);
    ShellBasedIdMapping incrIdMapping = new ShellBasedIdMapping(conf);

    // Command such as "getent passwd <userName>" will return empty string if
    // <username> is numerical, remove them from the map for testing purpose.
    BiMap<Integer, String> uidNameMap = refIdMapping.getUidNameMap();
    BiMap<Integer, String> gidNameMap = refIdMapping.getGidNameMap();

    // Force empty map, to see effect of incremental map update of calling
    // getUserName()
    incrIdMapping.clearNameMaps();
    uidNameMap = refIdMapping.getUidNameMap();
    for (BiMap.Entry<Integer, String> me : uidNameMap.entrySet()) {
      Integer id = me.getKey();
      String name = me.getValue();
      String tname = incrIdMapping.getUserName(id, null);
      assertEquals(name, tname);
    }
    assertEquals(uidNameMap.size(), incrIdMapping.getUidNameMap().size());

    // Force empty map, to see effect of incremental map update of calling
    // getUid()
    incrIdMapping.clearNameMaps();
    for (BiMap.Entry<Integer, String> me : uidNameMap.entrySet()) {
      Integer id = me.getKey();
      String name = me.getValue();
      Integer tid = incrIdMapping.getUid(name);
      assertEquals(id, tid);
    }
    assertEquals(uidNameMap.size(), incrIdMapping.getUidNameMap().size());

    // Force empty map, to see effect of incremental map update of calling
    // getGroupName()
    incrIdMapping.clearNameMaps();
    gidNameMap = refIdMapping.getGidNameMap();
    for (BiMap.Entry<Integer, String> me : gidNameMap.entrySet()) {
      Integer id = me.getKey();
      String name = me.getValue();
      String tname = incrIdMapping.getGroupName(id, null);
      assertEquals(name, tname);
    }
    assertEquals(gidNameMap.size(), incrIdMapping.getGidNameMap().size());

    // Force empty map, to see effect of incremental map update of calling
    // getGid()
    incrIdMapping.clearNameMaps();
    gidNameMap = refIdMapping.getGidNameMap();
    for (BiMap.Entry<Integer, String> me : gidNameMap.entrySet()) {
      Integer id = me.getKey();
      String name = me.getValue();
      Integer tid = incrIdMapping.getGid(name);
      assertEquals(id, tid);
    }
    assertEquals(gidNameMap.size(), incrIdMapping.getGidNameMap().size());
  }
}
