AbstractSecureRegistryTest.java

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

import org.apache.commons.io.FileUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.minikdc.MiniKdc;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.util.KerberosName;
import org.apache.hadoop.service.Service;
import org.apache.hadoop.service.ServiceOperations;
import org.apache.hadoop.registry.RegistryTestHelper;
import org.apache.hadoop.registry.client.impl.zk.RegistrySecurity;
import org.apache.hadoop.registry.client.impl.zk.ZookeeperConfigOptions;
import org.apache.hadoop.registry.server.services.AddingCompositeService;
import org.apache.hadoop.registry.server.services.MicroZookeeperService;
import org.apache.hadoop.registry.server.services.MicroZookeeperServiceKeys;
import org.apache.hadoop.test.TestName;
import org.apache.hadoop.util.Shell;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

/**
 * Add kerberos tests. This is based on the (JUnit3) KerberosSecurityTestcase
 * and its test case, <code>TestMiniKdc</code>
 */
@Timeout(900)
public class AbstractSecureRegistryTest extends RegistryTestHelper {
  public static final String REALM = "EXAMPLE.COM";
  public static final String ZOOKEEPER = "zookeeper";
  public static final String ZOOKEEPER_LOCALHOST = "zookeeper/localhost";
  public static final String ZOOKEEPER_1270001 = "zookeeper/127.0.0.1";
  public static final String ZOOKEEPER_REALM = "zookeeper@" + REALM;
  public static final String ZOOKEEPER_CLIENT_CONTEXT = ZOOKEEPER;
  public static final String ZOOKEEPER_SERVER_CONTEXT = "ZOOKEEPER_SERVER";

  public static final String ZOOKEEPER_LOCALHOST_REALM =
      ZOOKEEPER_LOCALHOST + "@" + REALM;
  public static final String ALICE = "alice";
  public static final String ALICE_CLIENT_CONTEXT = "alice";
  public static final String ALICE_LOCALHOST = "alice/localhost";
  public static final String BOB = "bob";
  public static final String BOB_CLIENT_CONTEXT = "bob";
  public static final String BOB_LOCALHOST = "bob/localhost";


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

  public static final Configuration CONF;

  static {
    CONF = new Configuration();
    CONF.set("hadoop.security.authentication", "kerberos");
    CONF.setBoolean("hadoop.security.authorization", true);
  }

  private static final AddingCompositeService classTeardown =
      new AddingCompositeService("classTeardown");

  // static initializer guarantees it is always started
  // ahead of any @BeforeClass methods
  static {
    classTeardown.init(CONF);
    classTeardown.start();
  }

  public static final String SUN_SECURITY_KRB5_DEBUG =
      "sun.security.krb5.debug";

  private final AddingCompositeService teardown =
      new AddingCompositeService("teardown");

  protected static MiniKdc kdc;
  protected static File keytab_zk;
  protected static File keytab_bob;
  protected static File keytab_alice;
  protected static File kdcWorkDir;
  protected static Properties kdcConf;
  protected static RegistrySecurity registrySecurity;

  @RegisterExtension
  private TestName methodName = new TestName();
  protected MicroZookeeperService secureZK;
  protected static File jaasFile;
  private LoginContext zookeeperLogin;
  private static String zkServerPrincipal;

  /**
   * All class initialization for this test class
   * @throws Exception
   */
  @BeforeAll
  public static void beforeSecureRegistryTestClass() throws Exception {
    registrySecurity = new RegistrySecurity("registrySecurity");
    registrySecurity.init(CONF);
    setupKDCAndPrincipals();
    RegistrySecurity.clearJaasSystemProperties();
    RegistrySecurity.bindJVMtoJAASFile(jaasFile);
    initHadoopSecurity();
  }

  @AfterAll
  public static void afterSecureRegistryTestClass() throws
      Exception {
    describe(LOG, "teardown of class");
    classTeardown.close();
    teardownKDC();
  }

  /**
   * give our thread a name
   */
  @BeforeEach
  public void nameThread() {
    Thread.currentThread().setName("JUnit");
  }

  /**
   * For unknown reasons, the before-class setting of the JVM properties were
   * not being picked up. This method addresses that by setting them
   * before every test case
   */
  @BeforeEach
  public void beforeSecureRegistryTest() {

  }

  @AfterEach
  public void afterSecureRegistryTest() throws IOException {
    describe(LOG, "teardown of instance");
    teardown.close();
    stopSecureZK();
  }

  protected static void addToClassTeardown(Service svc) {
    classTeardown.addService(svc);
  }

  protected void addToTeardown(Service svc) {
    teardown.addService(svc);
  }


  public static void teardownKDC() throws Exception {
    if (kdc != null) {
      kdc.stop();
      kdc = null;
    }
  }

  /**
   * Sets up the KDC and a set of principals in the JAAS file
   *
   * @throws Exception
   */
  public static void setupKDCAndPrincipals() throws Exception {
    // set up the KDC
    File target = new File(System.getProperty("test.dir", "target"));
    kdcWorkDir = new File(target, "kdc");
    kdcWorkDir.mkdirs();
    if (!kdcWorkDir.mkdirs()) {
      assertTrue(kdcWorkDir.isDirectory());
    }
    kdcConf = MiniKdc.createConf();
    kdcConf.setProperty(MiniKdc.DEBUG, "true");
    kdc = new MiniKdc(kdcConf, kdcWorkDir);
    kdc.start();

    keytab_zk = createKeytab(ZOOKEEPER, "zookeeper.keytab");
    keytab_alice = createKeytab(ALICE, "alice.keytab");
    keytab_bob = createKeytab(BOB, "bob.keytab");
    zkServerPrincipal = Shell.WINDOWS ? ZOOKEEPER_1270001 : ZOOKEEPER_LOCALHOST;

    StringBuilder jaas = new StringBuilder(1024);
    jaas.append(registrySecurity.createJAASEntry(ZOOKEEPER_CLIENT_CONTEXT,
        ZOOKEEPER, keytab_zk));
    jaas.append(registrySecurity.createJAASEntry(ZOOKEEPER_SERVER_CONTEXT,
        zkServerPrincipal, keytab_zk));
    jaas.append(registrySecurity.createJAASEntry(ALICE_CLIENT_CONTEXT,
        ALICE_LOCALHOST , keytab_alice));
    jaas.append(registrySecurity.createJAASEntry(BOB_CLIENT_CONTEXT,
        BOB_LOCALHOST, keytab_bob));

    jaasFile = new File(kdcWorkDir, "jaas.txt");
    FileUtils.write(jaasFile, jaas.toString(), StandardCharsets.UTF_8);
    LOG.info("\n"+ jaas);
    RegistrySecurity.bindJVMtoJAASFile(jaasFile);
  }


  //
  protected static final String kerberosRule =
      "RULE:[1:$1@$0](.*@EXAMPLE.COM)s/@.*//\nDEFAULT";

  /**
   * Init hadoop security by setting up the UGI config
   */
  public static void initHadoopSecurity() {

    UserGroupInformation.setConfiguration(CONF);

    KerberosName.setRules(kerberosRule);
  }

  /**
   * Stop the secure ZK and log out the ZK account
   */
  public synchronized void stopSecureZK() {
    ServiceOperations.stop(secureZK);
    secureZK = null;
    logout(zookeeperLogin);
    zookeeperLogin = null;
  }


  public static MiniKdc getKdc() {
    return kdc;
  }

  public static File getKdcWorkDir() {
    return kdcWorkDir;
  }

  public static Properties getKdcConf() {
    return kdcConf;
  }

  /**
   * Create a secure instance
   * @param name instance name
   * @return the instance
   * @throws Exception
   */
  protected static MicroZookeeperService createSecureZKInstance(String name)
      throws Exception {
    String context = ZOOKEEPER_SERVER_CONTEXT;
    Configuration conf = new Configuration();

    File testdir = new File(System.getProperty("test.dir", "target"));
    File workDir = new File(testdir, name);
    if (!workDir.mkdirs()) {
      assertTrue(workDir.isDirectory());
    }
    System.setProperty(
        ZookeeperConfigOptions.PROP_ZK_SERVER_MAINTAIN_CONNECTION_DESPITE_SASL_FAILURE,
        "false");
    RegistrySecurity.validateContext(context);
    conf.set(MicroZookeeperServiceKeys.KEY_REGISTRY_ZKSERVICE_JAAS_CONTEXT,
        context);
    MicroZookeeperService secureZK = new MicroZookeeperService(name);
    secureZK.init(conf);
    LOG.info(secureZK.getDiagnostics());
    return secureZK;
  }

  /**
   * Create the keytabl for the given principal, includes
   * raw principal and $principal/localhost
   * @param principal principal short name
   * @param filename filename of keytab
   * @return file of keytab
   * @throws Exception
   */
  public static File createKeytab(String principal,
      String filename) throws Exception {
    assertNotEmpty("empty principal", principal);
    assertNotEmpty("empty host", filename);
    assertNotNull(kdc, "Null KDC");
    File keytab = new File(kdcWorkDir, filename);
    kdc.createPrincipal(keytab,
        principal,
        principal + "/localhost",
        principal + "/127.0.0.1");
    return keytab;
  }

  public static String getPrincipalAndRealm(String principal) {
    return principal + "@" + getRealm();
  }

  protected static String getRealm() {
    return kdc.getRealm();
  }


  /**
   * Log in, defaulting to the client context
   * @param principal principal
   * @param context context
   * @param keytab keytab
   * @return the logged in context
   * @throws LoginException failure to log in
   * @throws FileNotFoundException no keytab
   */
  protected LoginContext login(String principal,
      String context, File keytab) throws LoginException,
      FileNotFoundException {
    LOG.info("Logging in as {} in context {} with keytab {}",
        principal, context, keytab);
    if (!keytab.exists()) {
      throw new FileNotFoundException(keytab.getAbsolutePath());
    }
    Set<Principal> principals = new HashSet<Principal>();
    principals.add(new KerberosPrincipal(principal));
    Subject subject = new Subject(false, principals, new HashSet<Object>(),
        new HashSet<Object>());
    LoginContext login;
    login = new LoginContext(context, subject, null,
        KerberosConfiguration.createClientConfig(principal, keytab));
    login.login();
    return login;
  }


  /**
   * Start the secure ZK instance using the test method name as the path.
   * As the entry is saved to the {@link #secureZK} field, it
   * is automatically stopped after the test case.
   * @throws Exception on any failure
   */
  protected synchronized void startSecureZK() throws Exception {
    assertNull(secureZK, "Zookeeper is already running");

    zookeeperLogin = login(zkServerPrincipal,
        ZOOKEEPER_SERVER_CONTEXT,
        keytab_zk);
    secureZK = createSecureZKInstance("test-" + methodName.getMethodName());
    secureZK.start();
  }

}