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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.PrivilegedExceptionAction;
import javax.management.ObjectName;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.LocalHBaseCluster;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.Waiter;
import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
import org.apache.hadoop.hbase.security.token.TokenProvider;
import org.apache.hadoop.hbase.testclassification.MediumTests;
import org.apache.hadoop.hbase.testclassification.MiscTests;
import org.apache.hadoop.hbase.util.CommonFSUtils;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.hadoop.minikdc.MiniKdc;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.http.auth.AuthSchemeProvider;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.KerberosCredentials;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.config.Lookup;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.impl.auth.SPNegoSchemeFactory;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Testing info servers for admin acl.
 */
@Tag(MiscTests.TAG)
@Tag(MediumTests.TAG)
public class TestInfoServersACL {

  private static final Logger LOG = LoggerFactory.getLogger(TestInfoServersACL.class);
  private final static HBaseTestingUtility UTIL = new HBaseTestingUtility();
  private static Configuration conf;

  protected static String USERNAME;
  private static LocalHBaseCluster CLUSTER;
  private static final File KEYTAB_FILE = new File(UTIL.getDataTestDir("keytab").toUri().getPath());
  private static MiniKdc KDC;
  private static String HOST = "localhost";
  private static String PRINCIPAL;
  private static String HTTP_PRINCIPAL;

  // user/group present in hbase.admin.acl
  private static final String USER_ADMIN_STR = "admin";

  // user with no permissions
  private static final String USER_NONE_STR = "none";

  @BeforeAll
  public static void beforeClass() throws Exception {
    conf = UTIL.getConfiguration();
    KDC = UTIL.setupMiniKdc(KEYTAB_FILE);
    USERNAME = UserGroupInformation.getLoginUser().getShortUserName();
    PRINCIPAL = USERNAME + "/" + HOST;
    HTTP_PRINCIPAL = "HTTP/" + HOST;
    // Create principals for services and the test users
    KDC.createPrincipal(KEYTAB_FILE, PRINCIPAL, HTTP_PRINCIPAL, USER_ADMIN_STR, USER_NONE_STR);
    UTIL.startMiniZKCluster();

    HBaseKerberosUtils.setSecuredConfiguration(conf, PRINCIPAL + "@" + KDC.getRealm(),
      HTTP_PRINCIPAL + "@" + KDC.getRealm());
    HBaseKerberosUtils.setSSLConfiguration(UTIL, TestInfoServersACL.class);

    conf.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, TokenProvider.class.getName());
    UTIL.startMiniDFSCluster(1);
    Path rootdir = UTIL.getDataTestDirOnTestFS("TestInfoServersACL");
    CommonFSUtils.setRootDir(conf, rootdir);

    // The info servers do not run in tests by default.
    // Set them to ephemeral ports so they will start
    // setup configuration
    conf.setInt(HConstants.MASTER_INFO_PORT, 0);
    conf.setInt(HConstants.REGIONSERVER_INFO_PORT, 0);

    conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos");
    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, HTTP_PRINCIPAL);
    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, KEYTAB_FILE.getAbsolutePath());

    // ACL lists work only when "hadoop.security.authorization" is set to true
    conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true);
    // only user admin will have acl access
    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, USER_ADMIN_STR);
    // conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, "");

    CLUSTER = new LocalHBaseCluster(conf, 1);
    CLUSTER.startup();
    CLUSTER.getActiveMaster().waitForMetaOnline();
  }

  /**
   * Helper method to shut down the cluster (if running)
   */
  @AfterAll
  public static void shutDownMiniCluster() throws Exception {
    if (CLUSTER != null) {
      CLUSTER.shutdown();
      CLUSTER.join();
    }
    if (KDC != null) {
      KDC.stop();
    }
    UTIL.shutdownMiniCluster();
  }

  @Test
  public void testAuthorizedUser() throws Exception {
    UserGroupInformation admin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
    admin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        // Check the expected content is present in the http response
        String expectedContent = "Get Log Level";
        Pair<Integer, String> pair = getLogLevelPage();
        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
        assertTrue(pair.getSecond().contains(expectedContent),
          "expected=" + expectedContent + ", content=" + pair.getSecond());
        return null;
      }
    });
  }

  @Test
  public void testUnauthorizedUser() throws Exception {
    UserGroupInformation nonAdmin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        Pair<Integer, String> pair = getLogLevelPage();
        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
        return null;
      }
    });
  }

  @Test
  public void testTableActionsAvailableForAdmins() throws Exception {
    final String expectedAuthorizedContent = "Actions";
    UserGroupInformation admin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
    admin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        // Check the expected content is present in the http response
        Pair<Integer, String> pair = getTablePage(TableName.META_TABLE_NAME);
        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
        assertTrue(pair.getSecond().contains(expectedAuthorizedContent),
          "expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond());
        return null;
      }
    });

    UserGroupInformation nonAdmin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        Pair<Integer, String> pair = getTablePage(TableName.META_TABLE_NAME);
        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
        assertFalse(pair.getSecond().contains(expectedAuthorizedContent),
          "should not find=" + expectedAuthorizedContent + ", content=" + pair.getSecond());
        return null;
      }
    });
  }

  @Test
  public void testLogsAvailableForAdmins() throws Exception {
    final String expectedAuthorizedContent = "Directory: /logs/";
    UserGroupInformation admin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
    admin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        // Check the expected content is present in the http response
        Pair<Integer, String> pair = getLogsPage();
        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
        assertTrue(pair.getSecond().contains(expectedAuthorizedContent),
          "expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond());
        return null;
      }
    });

    UserGroupInformation nonAdmin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        Pair<Integer, String> pair = getLogsPage();
        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
        return null;
      }
    });
  }

  @Test
  public void testDumpActionsAvailableForAdmins() throws Exception {
    final String expectedAuthorizedContent = "Master status for";
    UserGroupInformation admin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
    admin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        // Check the expected content is present in the http response
        Pair<Integer, String> pair = getMasterDumpPage();
        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
        assertTrue(pair.getSecond().contains(expectedAuthorizedContent),
          "expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond());
        return null;
      }
    });

    UserGroupInformation nonAdmin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        Pair<Integer, String> pair = getMasterDumpPage();
        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
        return null;
      }
    });
  }

  @Test
  public void testStackActionsAvailableForAdmins() throws Exception {
    final String expectedAuthorizedContent = "Process Thread Dump";
    UserGroupInformation admin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
    admin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        // Check the expected content is present in the http response
        Pair<Integer, String> pair = getStacksPage();
        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
        assertTrue(pair.getSecond().contains(expectedAuthorizedContent),
          "expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond());
        return null;
      }
    });

    UserGroupInformation nonAdmin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        Pair<Integer, String> pair = getStacksPage();
        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
        return null;
      }
    });
  }

  @Test
  public void testJmxAvailableForAdmins() throws Exception {
    final String expectedAuthorizedContent = "Hadoop:service=HBase";
    UTIL.waitFor(30000, new Waiter.Predicate<Exception>() {
      @Override
      public boolean evaluate() throws Exception {
        for (ObjectName name : ManagementFactory.getPlatformMBeanServer()
          .queryNames(new ObjectName("*:*"), null)) {
          if (name.toString().contains(expectedAuthorizedContent)) {
            LOG.info("{}", name);
            return true;
          }
        }
        return false;
      }
    });
    UserGroupInformation admin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
    admin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        // Check the expected content is present in the http response
        Pair<Integer, String> pair = getJmxPage();
        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
        assertTrue(pair.getSecond().contains(expectedAuthorizedContent),
          "expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond());
        return null;
      }
    });

    UserGroupInformation nonAdmin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        Pair<Integer, String> pair = getJmxPage();
        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
        return null;
      }
    });
  }

  @Test
  public void testMetricsAvailableForAdmins() throws Exception {
    // Looks like there's nothing exported to this, but leave it since
    // it's Hadoop2 only and will eventually be removed due to that.
    final String expectedAuthorizedContent = "";
    UserGroupInformation admin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
    admin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        // Check the expected content is present in the http response
        Pair<Integer, String> pair = getMetricsPage();
        if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) {
          // Not on hadoop 2
          return null;
        }
        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
        assertTrue(pair.getSecond().contains(expectedAuthorizedContent),
          "expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond());
        return null;
      }
    });

    UserGroupInformation nonAdmin = UserGroupInformation
      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
      @Override
      public Void run() throws Exception {
        Pair<Integer, String> pair = getMetricsPage();
        if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) {
          // Not on hadoop 2
          return null;
        }
        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
        return null;
      }
    });
  }

  private String getInfoServerHostAndPort() {
    return "http://localhost:" + CLUSTER.getActiveMaster().getInfoServer().getPort();
  }

  private Pair<Integer, String> getLogLevelPage() throws Exception {
    // Build the url which we want to connect to
    URL url = new URL(getInfoServerHostAndPort() + "/logLevel");
    return getUrlContent(url);
  }

  private Pair<Integer, String> getTablePage(TableName tn) throws Exception {
    URL url = new URL(getInfoServerHostAndPort() + "/table.jsp?name=" + tn.getNameAsString());
    return getUrlContent(url);
  }

  private Pair<Integer, String> getLogsPage() throws Exception {
    URL url = new URL(getInfoServerHostAndPort() + "/logs/");
    return getUrlContent(url);
  }

  private Pair<Integer, String> getMasterDumpPage() throws Exception {
    URL url = new URL(getInfoServerHostAndPort() + "/dump");
    return getUrlContent(url);
  }

  private Pair<Integer, String> getStacksPage() throws Exception {
    URL url = new URL(getInfoServerHostAndPort() + "/stacks");
    return getUrlContent(url);
  }

  private Pair<Integer, String> getJmxPage() throws Exception {
    URL url = new URL(getInfoServerHostAndPort() + "/jmx");
    return getUrlContent(url);
  }

  private Pair<Integer, String> getMetricsPage() throws Exception {
    URL url = new URL(getInfoServerHostAndPort() + "/metrics");
    return getUrlContent(url);
  }

  /**
   * Retrieves the content of the specified URL. The content will only be returned if the status
   * code for the operation was HTTP 200/OK.
   */
  private Pair<Integer, String> getUrlContent(URL url) throws Exception {
    try (CloseableHttpClient client =
      createHttpClient(UserGroupInformation.getCurrentUser().getUserName())) {
      CloseableHttpResponse resp = client.execute(new HttpGet(url.toURI()));
      int code = resp.getStatusLine().getStatusCode();
      if (code == HttpURLConnection.HTTP_OK) {
        return new Pair<>(code, EntityUtils.toString(resp.getEntity()));
      }
      return new Pair<>(code, null);
    }
  }

  private CloseableHttpClient createHttpClient(String clientPrincipal) throws Exception {
    // Logs in with Kerberos via GSS
    GSSManager gssManager = GSSManager.getInstance();
    // jGSS Kerberos login constant
    Oid oid = new Oid("1.2.840.113554.1.2.2");
    GSSName gssClient = gssManager.createName(clientPrincipal, GSSName.NT_USER_NAME);
    GSSCredential credential = gssManager.createCredential(gssClient,
      GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);

    Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider> create()
      .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true)).build();

    BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));

    return HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry)
      .setDefaultCredentialsProvider(credentialsProvider).build();
  }
}
