/*
* 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.accumulo.harness;
import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static org.junit.Assert.assertTrue;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.accumulo.cluster.ClusterUser;
import org.apache.accumulo.core.client.security.tokens.AuthenticationToken;
import org.apache.accumulo.core.client.security.tokens.KerberosToken;
import org.apache.accumulo.core.client.security.tokens.PasswordToken;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.minicluster.impl.MiniAccumuloClusterImpl;
import org.apache.accumulo.minicluster.impl.MiniAccumuloConfigImpl;
import org.apache.accumulo.server.security.handler.KerberosAuthenticator;
import org.apache.accumulo.server.security.handler.KerberosAuthorizor;
import org.apache.accumulo.server.security.handler.KerberosPermissionHandler;
import org.apache.accumulo.test.functional.NativeMapIT;
import org.apache.accumulo.test.util.CertUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Harness that sets up a MiniAccumuloCluster in a manner expected for Accumulo integration tests.
*/
public class MiniClusterHarness {
private static final Logger log = LoggerFactory.getLogger(MiniClusterHarness.class);
private static final AtomicLong COUNTER = new AtomicLong(0);
public static final String USE_SSL_FOR_IT_OPTION = "org.apache.accumulo.test.functional.useSslForIT",
USE_CRED_PROVIDER_FOR_IT_OPTION = "org.apache.accumulo.test.functional.useCredProviderForIT",
USE_KERBEROS_FOR_IT_OPTION = "org.apache.accumulo.test.functional.useKrbForIT", TRUE = Boolean.toString(true);
// TODO These are defined in MiniKdc >= 2.6.0. Can be removed when minimum Hadoop dependency is increased to that.
public static final String JAVA_SECURITY_KRB5_CONF = "java.security.krb5.conf", SUN_SECURITY_KRB5_DEBUG = "sun.security.krb5.debug";
/**
* Create a MiniAccumuloCluster using the given Token as the credentials for the root user.
*/
public MiniAccumuloClusterImpl create(AuthenticationToken token) throws Exception {
return create(MiniClusterHarness.class.getName(), Long.toString(COUNTER.incrementAndGet()), token);
}
public MiniAccumuloClusterImpl create(AuthenticationToken token, TestingKdc kdc) throws Exception {
return create(MiniClusterHarness.class.getName(), Long.toString(COUNTER.incrementAndGet()), token, kdc);
}
public MiniAccumuloClusterImpl create(AccumuloITBase testBase, AuthenticationToken token) throws Exception {
return create(testBase.getClass().getName(), testBase.testName.getMethodName(), token);
}
public MiniAccumuloClusterImpl create(AccumuloITBase testBase, AuthenticationToken token, TestingKdc kdc) throws Exception {
return create(testBase, token, kdc, MiniClusterConfigurationCallback.NO_CALLBACK);
}
public MiniAccumuloClusterImpl create(AccumuloITBase testBase, AuthenticationToken token, TestingKdc kdc, MiniClusterConfigurationCallback configCallback)
throws Exception {
return create(testBase.getClass().getName(), testBase.testName.getMethodName(), token, configCallback, kdc);
}
public MiniAccumuloClusterImpl create(AccumuloClusterHarness testBase, AuthenticationToken token, TestingKdc kdc) throws Exception {
return create(testBase.getClass().getName(), testBase.testName.getMethodName(), token, testBase, kdc);
}
public MiniAccumuloClusterImpl create(AccumuloClusterHarness testBase, AuthenticationToken token, MiniClusterConfigurationCallback callback) throws Exception {
return create(testBase.getClass().getName(), testBase.testName.getMethodName(), token, callback);
}
public MiniAccumuloClusterImpl create(String testClassName, String testMethodName, AuthenticationToken token) throws Exception {
return create(testClassName, testMethodName, token, MiniClusterConfigurationCallback.NO_CALLBACK);
}
public MiniAccumuloClusterImpl create(String testClassName, String testMethodName, AuthenticationToken token, TestingKdc kdc) throws Exception {
return create(testClassName, testMethodName, token, MiniClusterConfigurationCallback.NO_CALLBACK, kdc);
}
public MiniAccumuloClusterImpl create(String testClassName, String testMethodName, AuthenticationToken token, MiniClusterConfigurationCallback configCallback)
throws Exception {
return create(testClassName, testMethodName, token, configCallback, null);
}
public MiniAccumuloClusterImpl create(String testClassName, String testMethodName, AuthenticationToken token,
MiniClusterConfigurationCallback configCallback, TestingKdc kdc) throws Exception {
requireNonNull(token);
checkArgument(token instanceof PasswordToken || token instanceof KerberosToken, "A PasswordToken or KerberosToken is required");
String rootPasswd;
if (token instanceof PasswordToken) {
rootPasswd = new String(((PasswordToken) token).getPassword(), UTF_8);
} else {
rootPasswd = UUID.randomUUID().toString();
}
File baseDir = AccumuloClusterHarness.createTestDir(testClassName + "_" + testMethodName);
MiniAccumuloConfigImpl cfg = new MiniAccumuloConfigImpl(baseDir, rootPasswd);
// Enable native maps by default
cfg.setNativeLibPaths(NativeMapIT.nativeMapLocation().getAbsolutePath());
cfg.setProperty(Property.TSERV_NATIVEMAP_ENABLED, Boolean.TRUE.toString());
Configuration coreSite = new Configuration(false);
// Setup SSL and credential providers if the properties request such
configureForEnvironment(cfg, getClass(), AccumuloClusterHarness.getSslDir(baseDir), coreSite, kdc);
// Invoke the callback for tests to configure MAC before it starts
configCallback.configureMiniCluster(cfg, coreSite);
MiniAccumuloClusterImpl miniCluster = new MiniAccumuloClusterImpl(cfg);
// Write out any configuration items to a file so HDFS will pick them up automatically (from the classpath)
if (coreSite.size() > 0) {
File csFile = new File(miniCluster.getConfig().getConfDir(), "core-site.xml");
if (csFile.exists())
throw new RuntimeException(csFile + " already exist");
OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(miniCluster.getConfig().getConfDir(), "core-site.xml")));
coreSite.writeXml(out);
out.close();
}
return miniCluster;
}
protected void configureForEnvironment(MiniAccumuloConfigImpl cfg, Class<?> testClass, File folder, Configuration coreSite, TestingKdc kdc) {
if (TRUE.equals(System.getProperty(USE_SSL_FOR_IT_OPTION))) {
configureForSsl(cfg, folder);
}
if (TRUE.equals(System.getProperty(USE_CRED_PROVIDER_FOR_IT_OPTION))) {
cfg.setUseCredentialProvider(true);
}
if (TRUE.equals(System.getProperty(USE_KERBEROS_FOR_IT_OPTION))) {
if (TRUE.equals(System.getProperty(USE_SSL_FOR_IT_OPTION))) {
throw new RuntimeException("Cannot use both SSL and Kerberos");
}
try {
configureForKerberos(cfg, folder, coreSite, kdc);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize KDC", e);
}
}
}
protected void configureForSsl(MiniAccumuloConfigImpl cfg, File folder) {
Map<String,String> siteConfig = cfg.getSiteConfig();
if (TRUE.equals(siteConfig.get(Property.INSTANCE_RPC_SSL_ENABLED.getKey()))) {
// already enabled; don't mess with it
return;
}
File sslDir = new File(folder, "ssl");
assertTrue(sslDir.mkdirs() || sslDir.isDirectory());
File rootKeystoreFile = new File(sslDir, "root-" + cfg.getInstanceName() + ".jks");
File localKeystoreFile = new File(sslDir, "local-" + cfg.getInstanceName() + ".jks");
File publicTruststoreFile = new File(sslDir, "public-" + cfg.getInstanceName() + ".jks");
final String rootKeystorePassword = "root_keystore_password", truststorePassword = "truststore_password";
try {
new CertUtils(Property.RPC_SSL_KEYSTORE_TYPE.getDefaultValue(), "o=Apache Accumulo,cn=MiniAccumuloCluster", "RSA", 2048, "sha1WithRSAEncryption")
.createAll(rootKeystoreFile, localKeystoreFile, publicTruststoreFile, cfg.getInstanceName(), rootKeystorePassword, cfg.getRootPassword(),
truststorePassword);
} catch (Exception e) {
throw new RuntimeException("error creating MAC keystore", e);
}
siteConfig.put(Property.INSTANCE_RPC_SSL_ENABLED.getKey(), "true");
siteConfig.put(Property.RPC_SSL_KEYSTORE_PATH.getKey(), localKeystoreFile.getAbsolutePath());
siteConfig.put(Property.RPC_SSL_KEYSTORE_PASSWORD.getKey(), cfg.getRootPassword());
siteConfig.put(Property.RPC_SSL_TRUSTSTORE_PATH.getKey(), publicTruststoreFile.getAbsolutePath());
siteConfig.put(Property.RPC_SSL_TRUSTSTORE_PASSWORD.getKey(), truststorePassword);
cfg.setSiteConfig(siteConfig);
}
protected void configureForKerberos(MiniAccumuloConfigImpl cfg, File folder, Configuration coreSite, TestingKdc kdc) throws Exception {
Map<String,String> siteConfig = cfg.getSiteConfig();
if (TRUE.equals(siteConfig.get(Property.INSTANCE_RPC_SSL_ENABLED.getKey()))) {
throw new RuntimeException("Cannot use both SSL and SASL/Kerberos");
}
if (TRUE.equals(siteConfig.get(Property.INSTANCE_RPC_SASL_ENABLED.getKey()))) {
// already enabled
return;
}
if (null == kdc) {
throw new IllegalStateException("MiniClusterKdc was null");
}
log.info("Enabling Kerberos/SASL for minicluster");
// Turn on SASL and set the keytab/principal information
cfg.setProperty(Property.INSTANCE_RPC_SASL_ENABLED, "true");
ClusterUser serverUser = kdc.getAccumuloServerUser();
cfg.setProperty(Property.GENERAL_KERBEROS_KEYTAB, serverUser.getKeytab().getAbsolutePath());
cfg.setProperty(Property.GENERAL_KERBEROS_PRINCIPAL, serverUser.getPrincipal());
cfg.setProperty(Property.INSTANCE_SECURITY_AUTHENTICATOR, KerberosAuthenticator.class.getName());
cfg.setProperty(Property.INSTANCE_SECURITY_AUTHORIZOR, KerberosAuthorizor.class.getName());
cfg.setProperty(Property.INSTANCE_SECURITY_PERMISSION_HANDLER, KerberosPermissionHandler.class.getName());
// Piggy-back on the "system user" credential, but use it as a normal KerberosToken, not the SystemToken.
cfg.setProperty(Property.TRACE_USER, serverUser.getPrincipal());
cfg.setProperty(Property.TRACE_TOKEN_TYPE, KerberosToken.CLASS_NAME);
// Pass down some KRB5 debug properties
Map<String,String> systemProperties = cfg.getSystemProperties();
systemProperties.put(JAVA_SECURITY_KRB5_CONF, System.getProperty(JAVA_SECURITY_KRB5_CONF, ""));
systemProperties.put(SUN_SECURITY_KRB5_DEBUG, System.getProperty(SUN_SECURITY_KRB5_DEBUG, "false"));
cfg.setSystemProperties(systemProperties);
// Make sure UserGroupInformation will do the correct login
coreSite.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos");
cfg.setRootUserName(kdc.getRootUser().getPrincipal());
}
}