/**
 * 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.hbase.zookeeper;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.zookeeper.ZKUtil;
import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(MediumTests.class)
public class TestZooKeeperACL {
  private final static Log LOG = LogFactory.getLog(TestZooKeeperACL.class);
  private final static HBaseTestingUtility TEST_UTIL =
      new HBaseTestingUtility();

  private static ZooKeeperWatcher zkw;
  private static boolean secureZKAvailable;
  
  @BeforeClass
  public static void setUpBeforeClass() throws Exception {
    File saslConfFile = File.createTempFile("tmp", "jaas.conf");
    FileWriter fwriter = new FileWriter(saslConfFile);

    fwriter.write("" +
      "Server {\n" +
        "org.apache.zookeeper.server.auth.DigestLoginModule required\n" +
        "user_hbase=\"secret\";\n" +
      "};\n" +
      "Client {\n" +
        "org.apache.zookeeper.server.auth.DigestLoginModule required\n" +
        "username=\"hbase\"\n" +
        "password=\"secret\";\n" +
      "};" + "\n");
    fwriter.close();
    System.setProperty("java.security.auth.login.config",
        saslConfFile.getAbsolutePath());
    System.setProperty("zookeeper.authProvider.1",
        "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");

    TEST_UTIL.getConfiguration().setBoolean("dfs.support.append", true);
    TEST_UTIL.getConfiguration().setInt("hbase.zookeeper.property.maxClientCnxns", 1000);

    // If Hadoop is missing HADOOP-7070 the cluster will fail to start due to
    // the JAAS configuration required by ZK being clobbered by Hadoop 
    try {
      TEST_UTIL.startMiniCluster();
    } catch (IOException e) {
      LOG.warn("Hadoop is missing HADOOP-7070", e);
      secureZKAvailable = false;
      return;
    }
    zkw = new ZooKeeperWatcher(
      new Configuration(TEST_UTIL.getConfiguration()),
        TestZooKeeper.class.getName(), null);
  }

  /**
   * @throws java.lang.Exception
   */
  @AfterClass
  public static void tearDownAfterClass() throws Exception {
    if (!secureZKAvailable) {
      return;
    }
    TEST_UTIL.shutdownMiniCluster();
  }

  /**
   * @throws java.lang.Exception
   */
  @Before
  public void setUp() throws Exception {
    if (!secureZKAvailable) {
      return;
    }
    TEST_UTIL.ensureSomeRegionServersAvailable(2);
  }

  /**
   * Create a node and check its ACL. When authentication is enabled on 
   * Zookeeper, all nodes (except /hbase/root-region-server, /hbase/master
   * and /hbase/hbaseid) should be created so that only the hbase server user
   * (master or region server user) that created them can access them, and
   * this user should have all permissions on this node. For
   * /hbase/root-region-server, /hbase/master, and /hbase/hbaseid the
   * permissions should be as above, but should also be world-readable. First
   * we check the general case of /hbase nodes in the following test, and
   * then check the subset of world-readable nodes in the three tests after
   * that.
   */
  @Test (timeout=30000)
  public void testHBaseRootZNodeACL() throws Exception {
    if (!secureZKAvailable) {
      return;
    }

    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
        .getACL("/hbase", new Stat());
    assertEquals(acls.size(),1);
    assertEquals(acls.get(0).getId().getScheme(),"sasl");
    assertEquals(acls.get(0).getId().getId(),"hbase");
    assertEquals(acls.get(0).getPerms(), ZooDefs.Perms.ALL);
  }

  /**
   * When authentication is enabled on Zookeeper, /hbase/root-region-server
   * should be created with 2 ACLs: one specifies that the hbase user has
   * full access to the node; the other, that it is world-readable.
   */
  @Test (timeout=30000)
  public void testHBaseRootRegionServerZNodeACL() throws Exception {
    if (!secureZKAvailable) {
      return;
    }

    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
        .getACL("/hbase/root-region-server", new Stat());
    assertEquals(acls.size(),2);

    boolean foundWorldReadableAcl = false;
    boolean foundHBaseOwnerAcl = false;
    for(int i = 0; i < 2; i++) {
      if (acls.get(i).getId().getScheme().equals("world") == true) {
        assertEquals(acls.get(0).getId().getId(),"anyone");
        assertEquals(acls.get(0).getPerms(), ZooDefs.Perms.READ);
        foundWorldReadableAcl = true;
      }
      else {
        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
          assertEquals(acls.get(1).getId().getId(),"hbase");
          assertEquals(acls.get(1).getId().getScheme(),"sasl");
          foundHBaseOwnerAcl = true;
        } else { // error: should not get here: test fails.
          assertTrue(false);
        }
      }
    }
    assertTrue(foundWorldReadableAcl);
    assertTrue(foundHBaseOwnerAcl);
  }

  /**
   * When authentication is enabled on Zookeeper, /hbase/master should be
   * created with 2 ACLs: one specifies that the hbase user has full access
   * to the node; the other, that it is world-readable.
   */
  @Test (timeout=30000)
  public void testHBaseMasterServerZNodeACL() throws Exception {
    if (!secureZKAvailable) {
      return;
    }

    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
        .getACL("/hbase/master", new Stat());
    assertEquals(acls.size(),2);

    boolean foundWorldReadableAcl = false;
    boolean foundHBaseOwnerAcl = false;
    for(int i = 0; i < 2; i++) {
      if (acls.get(i).getId().getScheme().equals("world") == true) {
        assertEquals(acls.get(0).getId().getId(),"anyone");
        assertEquals(acls.get(0).getPerms(), ZooDefs.Perms.READ);
        foundWorldReadableAcl = true;
      } else {
        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
          assertEquals(acls.get(1).getId().getId(),"hbase");
          assertEquals(acls.get(1).getId().getScheme(),"sasl");
          foundHBaseOwnerAcl = true;
        } else { // error: should not get here: test fails.
          assertTrue(false);
        }
      }
    }
    assertTrue(foundWorldReadableAcl);
    assertTrue(foundHBaseOwnerAcl);
  }

  /**
   * When authentication is enabled on Zookeeper, /hbase/hbaseid should be
   * created with 2 ACLs: one specifies that the hbase user has full access
   * to the node; the other, that it is world-readable.
   */
  @Test (timeout=30000)
  public void testHBaseIDZNodeACL() throws Exception {
    if (!secureZKAvailable) {
      return;
    }

    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
        .getACL("/hbase/hbaseid", new Stat());
    assertEquals(acls.size(),2);

    boolean foundWorldReadableAcl = false;
    boolean foundHBaseOwnerAcl = false;
    for(int i = 0; i < 2; i++) {
      if (acls.get(i).getId().getScheme().equals("world") == true) {
        assertEquals(acls.get(0).getId().getId(),"anyone");
        assertEquals(acls.get(0).getPerms(), ZooDefs.Perms.READ);
        foundWorldReadableAcl = true;
      } else {
        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
          assertEquals(acls.get(1).getId().getId(),"hbase");
          assertEquals(acls.get(1).getId().getScheme(),"sasl");
          foundHBaseOwnerAcl = true;
        } else { // error: should not get here: test fails.
          assertTrue(false);
        }
      }
    }
    assertTrue(foundWorldReadableAcl);
    assertTrue(foundHBaseOwnerAcl);
  }

  /**
   * Finally, we check the ACLs of a node outside of the /hbase hierarchy and
   * verify that its ACL is simply 'hbase:Perms.ALL'.
   */
  @Test
  public void testOutsideHBaseNodeACL() throws Exception {
    if (!secureZKAvailable) {
      return;
    }

    ZKUtil.createWithParents(zkw, "/testACLNode");
    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
        .getACL("/testACLNode", new Stat());
    assertEquals(acls.size(),1);
    assertEquals(acls.get(0).getId().getScheme(),"sasl");
    assertEquals(acls.get(0).getId().getId(),"hbase");
    assertEquals(acls.get(0).getPerms(), ZooDefs.Perms.ALL);
  }

  /**
   * Check if ZooKeeper JaasConfiguration is valid.
   */
  @Test
  public void testIsZooKeeperSecure() throws Exception {
    boolean testJaasConfig = ZKUtil.isSecureZooKeeper(new Configuration(TEST_UTIL.getConfiguration()));
    assertEquals(testJaasConfig, secureZKAvailable);
    // Define Jaas configuration without ZooKeeper Jaas config
    File saslConfFile = File.createTempFile("tmp", "fakeJaas.conf");
    FileWriter fwriter = new FileWriter(saslConfFile);

    fwriter.write("");
    fwriter.close();
    System.setProperty("java.security.auth.login.config",
        saslConfFile.getAbsolutePath());

    testJaasConfig = ZKUtil.isSecureZooKeeper(new Configuration(TEST_UTIL.getConfiguration()));
    assertEquals(testJaasConfig, false);
    saslConfFile.delete();
  }
}

