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

import java.io.File;
import javax.security.auth.login.Configuration;

import org.apache.curator.test.TestingServer;
import org.apache.hadoop.minikdc.MiniKdc;
import org.apache.oozie.service.HadoopAccessorService;
import org.apache.oozie.service.Services;
import org.apache.oozie.util.JaasConfiguration;
import org.apache.zookeeper.server.ZooKeeperSaslServer;

/**
 * Provides a version of {@link ZKXTestCase} with security.  A MiniKdc will be started (so no special outside setup is needed) and
 * the embedded ZooKeeper provided by this class will support connecting to it with SASL/Kerberos authentication.  However,
 * currently, the client returned by {@link #getClient()) and the client used by DummyZKOozie do not authenticate, so they won't
 * have full access to any znodes with "sasl" ACLs (this is not always true, see {@link #setupZKServer()).
 * <p>
 * Anything using {@link ZKUtils} can connect using authentication by simply setting "oozie.zookeeper.secure" to "true" before
 * creating the first thing that uses ZKUtils.  Make sure to set it back to false when done.
 */
public abstract class ZKXTestCaseWithSecurity extends ZKXTestCase {
    private MiniKdc kdc = null;
    private File keytabFile;
    private String originalKeytabLoc;
    private String originalPrincipal;

    /**
     * The primary part of the principal name for the Kerberos user
     */
    protected static final String PRIMARY_PRINCIPAL = "oozie";

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        // Set the keytab location and principal to the miniKDC values
        originalKeytabLoc = Services.get().getConf().get(HadoopAccessorService.KERBEROS_KEYTAB);
        originalPrincipal = Services.get().getConf().get(HadoopAccessorService.KERBEROS_PRINCIPAL);
        Services.get().getConf().set(HadoopAccessorService.KERBEROS_KEYTAB, keytabFile.getAbsolutePath());
        Services.get().getConf().set(HadoopAccessorService.KERBEROS_PRINCIPAL, getPrincipal());
    }

    @Override
    protected void tearDown() throws Exception {
        // Restore these values
        Services.get().getConf().set(HadoopAccessorService.KERBEROS_KEYTAB, originalKeytabLoc);
        Services.get().getConf().set(HadoopAccessorService.KERBEROS_PRINCIPAL, originalPrincipal);
        // Just in case the test forgets to set this back
        Services.get().getConf().set("oozie.zookeeper.secure", "false");
        super.tearDown();
        if (kdc != null) {
            kdc.stop();
        }
    }

    /**
     * Creates and sets up the embedded ZooKeeper server.  Test subclasses should have no reason to override this method.
     * <p>
     * Here we override it to start the MiniKdc, set the jaas configuration, configure ZooKeeper for SASL/Kerberos authentication
     * and ACLs, and to start the ZooKeeper server.
     * <p>
     * Unfortunately, ZooKeeper security requires setting the security for the entire JVM.  And for the tests, we're running the
     * ZK server and one or more clients from the same JVM, so things get messy.  There are two ways to tell ZooKeeper to
     * authenticate: (1) set the system property, "java.security.auth.login.config", to a jaas.conf file and (2) create a
     * javax.security.auth.login.Configuration object with the same info as the jaas.conf and set it.  In either case, once set and
     * something has authenticated, it seems that it can't be unset or changed, and there's no way to log out.  By setting the
     * system property, "javax.security.auth.useSubjectCredsOnly", to "false" we can sort-of change the jaas Configuration, but its
     * kind of funny about it.  Another effect of this is that we have to add jaas entries for the "Server" and "Client" here
     * instead of just the "Server" here and the "Client" in the normal place ({@link ZKUtils}) or it will be unable to find the
     * "Client" info.  Also, because there is no way to logout, once any client has authenticated once, all subsequent clients will
     * automatically connect using the same authentication; trying to stop this is futile and either results in an error or has no
     * effect.  This means that there's no way to do any tests with an unauthenticated client.  Also, if any tests using secure
     * ZooKeeper get run before tests not using secure ZooKeeper, they will likely fail because it will try to use authentication:
     * so they should be run separately.
     *
     * @return the embedded ZooKeeper server
     * @throws Exception
     */
    @Override
    protected TestingServer setupZKServer() throws Exception {
        // Not entirely sure exactly what "javax.security.auth.useSubjectCredsOnly=false" does, but it has something to do with
        // re-authenticating in cases where it otherwise wouldn't.  One of the sections on this page briefly mentions it:
        // http://docs.oracle.com/javase/7/docs/technotes/guides/security/jgss/tutorials/Troubleshooting.html
        setSystemProperty("javax.security.auth.useSubjectCredsOnly", "false");

        // Setup KDC and principal
        kdc = new MiniKdc(MiniKdc.createConf(), new File(getTestCaseDir()));
        kdc.start();
        keytabFile = new File(getTestCaseDir(), "test.keytab");
        String serverPrincipal = "zookeeper/" + kdc.getHost();
        kdc.createPrincipal(keytabFile, getPrincipal(), serverPrincipal);

        setSystemProperty("zookeeper.authProvider.1", "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
        setSystemProperty("zookeeper.kerberos.removeHostFromPrincipal", "true");
        setSystemProperty("zookeeper.kerberos.removeRealmFromPrincipal", "true");

        JaasConfiguration.addEntry("Server", serverPrincipal, keytabFile.getAbsolutePath());
        // Here's where we add the "Client" to the jaas configuration, even though we'd like not to
        JaasConfiguration.addEntry("Client", getPrincipal(), keytabFile.getAbsolutePath());
        Configuration.setConfiguration(JaasConfiguration.getInstance());

        setSystemProperty(ZooKeeperSaslServer.LOGIN_CONTEXT_NAME_KEY, "Server");

        return new TestingServer();
    }

    /**
     * Returns the principal of the Kerberos user.  This would be {@link #PRIMARY_PRINCIPAL}/_host_
     *
     * @return the principal of the Kerberos user
     */
    protected String getPrincipal() {
        return PRIMARY_PRINCIPAL + "/" + kdc.getHost();
    }
}

