/*
 * 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;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.testclassification.MiscTests;
import org.apache.hadoop.hbase.testclassification.SmallTests;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap;

@Category({ MiscTests.class, SmallTests.class })
public class TestHBaseConfiguration {
  @ClassRule
  public static final HBaseClassTestRule CLASS_RULE =
    HBaseClassTestRule.forClass(TestHBaseConfiguration.class);

  private static final Logger LOG = LoggerFactory.getLogger(TestHBaseConfiguration.class);

  private static HBaseCommonTestingUtility UTIL = new HBaseCommonTestingUtility();

  @AfterClass
  public static void tearDown() throws IOException {
    UTIL.cleanupTestDir();
  }

  @Test
  public void testSubset() {
    Configuration conf = HBaseConfiguration.create();
    // subset is used in TableMapReduceUtil#initCredentials to support different security
    // configurations between source and destination clusters, so we'll use that as an example
    String prefix = "hbase.mapred.output.";
    conf.set("hbase.security.authentication", "kerberos");
    conf.set("hbase.regionserver.kerberos.principal", "hbasesource");
    HBaseConfiguration.setWithPrefix(conf, prefix, ImmutableMap
      .of("hbase.regionserver.kerberos.principal", "hbasedest", "", "shouldbemissing").entrySet());

    Configuration subsetConf = HBaseConfiguration.subset(conf, prefix);
    assertNull(subsetConf.get(prefix + "hbase.regionserver.kerberos.principal"));
    assertEquals("hbasedest", subsetConf.get("hbase.regionserver.kerberos.principal"));
    assertNull(subsetConf.get("hbase.security.authentication"));
    assertNull(subsetConf.get(""));

    Configuration mergedConf = HBaseConfiguration.create(conf);
    HBaseConfiguration.merge(mergedConf, subsetConf);

    assertEquals("hbasedest", mergedConf.get("hbase.regionserver.kerberos.principal"));
    assertEquals("kerberos", mergedConf.get("hbase.security.authentication"));
    assertEquals("shouldbemissing", mergedConf.get(prefix));
  }

  @Test
  public void testGetPassword() throws Exception {
    Configuration conf = HBaseConfiguration.create();
    conf.set(ReflectiveCredentialProviderClient.CREDENTIAL_PROVIDER_PATH, "jceks://file"
      + new File(UTIL.getDataTestDir().toUri().getPath(), "foo.jks").getCanonicalPath());
    ReflectiveCredentialProviderClient client = new ReflectiveCredentialProviderClient();
    if (client.isHadoopCredentialProviderAvailable()) {
      char[] keyPass = { 'k', 'e', 'y', 'p', 'a', 's', 's' };
      char[] storePass = { 's', 't', 'o', 'r', 'e', 'p', 'a', 's', 's' };
      client.createEntry(conf, "ssl.keypass.alias", keyPass);
      client.createEntry(conf, "ssl.storepass.alias", storePass);

      String keypass = HBaseConfiguration.getPassword(conf, "ssl.keypass.alias", null);
      assertEquals(keypass, new String(keyPass));

      String storepass = HBaseConfiguration.getPassword(conf, "ssl.storepass.alias", null);
      assertEquals(storepass, new String(storePass));
    }
  }

  @Test
  public void testSecurityConfCaseInsensitive() {
    Configuration conf = HBaseConfiguration.create();
    conf.set("hbase.security.authentication", "kerberos");
    Assert.assertTrue(User.isHBaseSecurityEnabled(conf));

    conf.set("hbase.security.authentication", "KERBEROS");
    Assert.assertTrue(User.isHBaseSecurityEnabled(conf));

    conf.set("hbase.security.authentication", "KERBeros");
    Assert.assertTrue(User.isHBaseSecurityEnabled(conf));
  }

  @Test
  public void testGetConfigOfShortcircuitRead() throws Exception {
    Configuration conf = HBaseConfiguration.create();
    Configuration.addDefaultResource("hdfs-scr-disabled.xml");
    assertEquals("hdfs-scr-disabled.xml",
      conf.getPropertySources("dfs.client.read.shortcircuit")[0]);
    assertEquals("false", conf.get("dfs.client.read.shortcircuit"));
    assertNull(conf.get("dfs.domain.socket.path"));
    Configuration.addDefaultResource("hdfs-scr-enabled.xml");
    assertEquals("hdfs-scr-enabled.xml",
      conf.getPropertySources("dfs.client.read.shortcircuit")[0]);
    assertEquals("hdfs-scr-enabled.xml", conf.getPropertySources("dfs.domain.socket.path")[0]);
    assertEquals("true", conf.get("dfs.client.read.shortcircuit"));
    assertEquals("/var/lib/hadoop-hdfs/dn_socket", conf.get("dfs.domain.socket.path"));
  }

  @Test
  public void testDeprecatedConfigurations() {
    // Configuration.addDeprecations before create Configuration object
    Configuration.addDeprecations(new Configuration.DeprecationDelta[] {
      new Configuration.DeprecationDelta("hbase.deprecated.conf", "hbase.new.conf"),
      new Configuration.DeprecationDelta("hbase.deprecated.conf2", "hbase.new.conf2") });
    Configuration conf = HBaseConfiguration.create();
    conf.addResource("hbase-deprecated-conf.xml");
    assertEquals("1000", conf.get("hbase.new.conf"));
    assertEquals("1000", conf.get("hbase.new.conf2"));
  }

  private static class ReflectiveCredentialProviderClient {
    public static final String HADOOP_CRED_PROVIDER_FACTORY_CLASS_NAME =
      "org.apache.hadoop.security.alias.JavaKeyStoreProvider$Factory";
    public static final String HADOOP_CRED_PROVIDER_FACTORY_GET_PROVIDERS_METHOD_NAME =
      "getProviders";

    public static final String HADOOP_CRED_PROVIDER_CLASS_NAME =
      "org.apache.hadoop.security.alias.CredentialProvider";
    public static final String HADOOP_CRED_PROVIDER_GET_CREDENTIAL_ENTRY_METHOD_NAME =
      "getCredentialEntry";
    public static final String HADOOP_CRED_PROVIDER_GET_ALIASES_METHOD_NAME = "getAliases";
    public static final String HADOOP_CRED_PROVIDER_CREATE_CREDENTIAL_ENTRY_METHOD_NAME =
      "createCredentialEntry";
    public static final String HADOOP_CRED_PROVIDER_FLUSH_METHOD_NAME = "flush";

    public static final String HADOOP_CRED_ENTRY_CLASS_NAME =
      "org.apache.hadoop.security.alias.CredentialProvider$CredentialEntry";
    public static final String HADOOP_CRED_ENTRY_GET_CREDENTIAL_METHOD_NAME = "getCredential";

    public static final String CREDENTIAL_PROVIDER_PATH =
      "hadoop.security.credential.provider.path";

    private static Object hadoopCredProviderFactory = null;
    private static Method getProvidersMethod = null;
    private static Method getCredentialEntryMethod = null;
    private static Method getCredentialMethod = null;
    private static Method createCredentialEntryMethod = null;
    private static Method flushMethod = null;
    private static Boolean hadoopClassesAvailable = null;

    /**
     * Determine if we can load the necessary CredentialProvider classes. Only loaded the first
     * time, so subsequent invocations of this method should return fast.
     * @return True if the CredentialProvider classes/methods are available, false otherwise.
     */
    private boolean isHadoopCredentialProviderAvailable() {
      if (null != hadoopClassesAvailable) {
        // Make sure everything is initialized as expected
        if (
          hadoopClassesAvailable && null != getProvidersMethod && null != hadoopCredProviderFactory
            && null != getCredentialEntryMethod && null != getCredentialMethod
        ) {
          return true;
        } else {
          // Otherwise we failed to load it
          return false;
        }
      }

      hadoopClassesAvailable = false;

      // Load Hadoop CredentialProviderFactory
      Class<?> hadoopCredProviderFactoryClz;
      try {
        hadoopCredProviderFactoryClz = Class.forName(HADOOP_CRED_PROVIDER_FACTORY_CLASS_NAME);
      } catch (ClassNotFoundException e) {
        return false;
      }
      // Instantiate Hadoop CredentialProviderFactory
      try {
        hadoopCredProviderFactory =
          hadoopCredProviderFactoryClz.getDeclaredConstructor().newInstance();
      } catch (Exception e) {
        return false;
      }

      try {
        getProvidersMethod = loadMethod(hadoopCredProviderFactoryClz,
          HADOOP_CRED_PROVIDER_FACTORY_GET_PROVIDERS_METHOD_NAME, Configuration.class);
        // Load Hadoop CredentialProvider
        Class<?> hadoopCredProviderClz;
        hadoopCredProviderClz = Class.forName(HADOOP_CRED_PROVIDER_CLASS_NAME);
        getCredentialEntryMethod = loadMethod(hadoopCredProviderClz,
          HADOOP_CRED_PROVIDER_GET_CREDENTIAL_ENTRY_METHOD_NAME, String.class);

        createCredentialEntryMethod = loadMethod(hadoopCredProviderClz,
          HADOOP_CRED_PROVIDER_CREATE_CREDENTIAL_ENTRY_METHOD_NAME, String.class, char[].class);

        flushMethod = loadMethod(hadoopCredProviderClz, HADOOP_CRED_PROVIDER_FLUSH_METHOD_NAME);

        // Load Hadoop CredentialEntry
        Class<?> hadoopCredentialEntryClz;
        try {
          hadoopCredentialEntryClz = Class.forName(HADOOP_CRED_ENTRY_CLASS_NAME);
        } catch (ClassNotFoundException e) {
          LOG.error("Failed to load class:" + e);
          return false;
        }

        getCredentialMethod =
          loadMethod(hadoopCredentialEntryClz, HADOOP_CRED_ENTRY_GET_CREDENTIAL_METHOD_NAME);
      } catch (Exception e1) {
        return false;
      }

      hadoopClassesAvailable = true;
      LOG.info("Credential provider classes have been"
        + " loaded and initialized successfully through reflection.");
      return true;
    }

    private Method loadMethod(Class<?> clz, String name, Class<?>... classes) throws Exception {
      Method method;
      try {
        method = clz.getMethod(name, classes);
      } catch (SecurityException e) {
        fail("security exception caught for: " + name + " in " + clz.getCanonicalName());
        throw e;
      } catch (NoSuchMethodException e) {
        LOG.error("Failed to load the " + name + ": " + e);
        fail("no such method: " + name + " in " + clz.getCanonicalName());
        throw e;
      }
      return method;
    }

    /**
     * Wrapper to fetch the configured {@code List<CredentialProvider>}s. Configuration with
     * GENERAL_SECURITY_CREDENTIAL_PROVIDER_PATHS defined
     * @return List of CredentialProviders, or null if they could not be loaded
     */
    @SuppressWarnings("unchecked")
    protected List<Object> getCredentialProviders(Configuration conf) {
      // Call CredentialProviderFactory.getProviders(Configuration)
      Object providersObj;
      try {
        providersObj = getProvidersMethod.invoke(hadoopCredProviderFactory, conf);
      } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
        LOG.error("Failed to invoke: " + getProvidersMethod.getName() + ": " + e);
        return null;
      }

      // Cast the Object to List<Object> (actually List<CredentialProvider>)
      try {
        return (List<Object>) providersObj;
      } catch (ClassCastException e) {
        return null;
      }
    }

    /**
     * Create a CredentialEntry using the configured Providers. If multiple CredentialProviders are
     * configured, the first will be used. Configuration for the CredentialProvider CredentialEntry
     * name (alias) The credential
     */
    public void createEntry(Configuration conf, String name, char[] credential) throws Exception {
      if (!isHadoopCredentialProviderAvailable()) {
        return;
      }

      List<Object> providers = getCredentialProviders(conf);
      if (null == providers) {
        throw new IOException(
          "Could not fetch any CredentialProviders, " + "is the implementation available?");
      }

      Object provider = providers.get(0);
      createEntryInProvider(provider, name, credential);
    }

    /**
     * Create a CredentialEntry with the give name and credential in the credentialProvider. The
     * credentialProvider argument must be an instance of Hadoop CredentialProvider. Instance of
     * CredentialProvider CredentialEntry name (alias) The credential to store
     */
    private void createEntryInProvider(Object credentialProvider, String name, char[] credential)
      throws Exception {
      if (!isHadoopCredentialProviderAvailable()) {
        return;
      }

      try {
        createCredentialEntryMethod.invoke(credentialProvider, name, credential);
      } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
        return;
      }

      flushMethod.invoke(credentialProvider);
    }
  }
}
