http://git-wip-us.apache.org/repos/asf/accumulo/blob/d28a3ee3/test/src/main/java/org/apache/accumulo/test/categories/AnyClusterTest.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/categories/AnyClusterTest.java index 0000000,0000000..765057e new file mode 100644 --- /dev/null +++ b/test/src/main/java/org/apache/accumulo/test/categories/AnyClusterTest.java @@@ -1,0 -1,0 +1,25 @@@ ++/* ++ * 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.test.categories; ++ ++/** ++ * Interface to be used with JUnit Category annotation to denote that the IntegrationTest can be used with any kind of cluster (a MiniAccumuloCluster or a ++ * StandaloneAccumuloCluster). ++ */ ++public interface AnyClusterTest { ++ ++}
http://git-wip-us.apache.org/repos/asf/accumulo/blob/d28a3ee3/test/src/main/java/org/apache/accumulo/test/categories/MiniClusterOnlyTest.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/categories/MiniClusterOnlyTest.java index 0000000,0000000..1a972ef new file mode 100644 --- /dev/null +++ b/test/src/main/java/org/apache/accumulo/test/categories/MiniClusterOnlyTest.java @@@ -1,0 -1,0 +1,24 @@@ ++/* ++ * 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.test.categories; ++ ++/** ++ * Interface to be used with JUnit Category annotation to denote that the IntegrationTest requires the use of a MiniAccumuloCluster. ++ */ ++public interface MiniClusterOnlyTest { ++ ++} http://git-wip-us.apache.org/repos/asf/accumulo/blob/d28a3ee3/test/src/main/java/org/apache/accumulo/test/categories/package-info.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/categories/package-info.java index 0000000,0000000..e7071fc new file mode 100644 --- /dev/null +++ b/test/src/main/java/org/apache/accumulo/test/categories/package-info.java @@@ -1,0 -1,0 +1,21 @@@ ++/* ++ * 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. ++ */ ++/** ++ * JUnit categories for the various types of Accumulo integration tests. ++ */ ++package org.apache.accumulo.test.categories; ++ http://git-wip-us.apache.org/repos/asf/accumulo/blob/d28a3ee3/test/src/main/java/org/apache/accumulo/test/functional/ClassLoaderIT.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/functional/ClassLoaderIT.java index 29f2780,0000000..8dbbc12 mode 100644,000000..100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/ClassLoaderIT.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/ClassLoaderIT.java @@@ -1,121 -1,0 +1,124 @@@ +/* + * 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.test.functional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +import org.apache.accumulo.core.client.BatchWriter; +import org.apache.accumulo.core.client.BatchWriterConfig; +import org.apache.accumulo.core.client.Connector; +import org.apache.accumulo.core.client.IteratorSetting; +import org.apache.accumulo.core.client.Scanner; +import org.apache.accumulo.core.data.Key; +import org.apache.accumulo.core.data.Mutation; +import org.apache.accumulo.core.data.Value; +import org.apache.accumulo.core.iterators.Combiner; +import org.apache.accumulo.core.iterators.IteratorUtil.IteratorScope; +import org.apache.accumulo.core.security.Authorizations; +import org.apache.accumulo.harness.AccumuloClusterHarness; +import org.apache.accumulo.minicluster.impl.MiniAccumuloClusterImpl; +import org.apache.hadoop.fs.FSDataOutputStream; ++import org.apache.accumulo.test.categories.MiniClusterOnlyTest; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.hamcrest.CoreMatchers; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; ++import org.junit.experimental.categories.Category; + +import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; + ++@Category(MiniClusterOnlyTest.class) +public class ClassLoaderIT extends AccumuloClusterHarness { + + private static final long ZOOKEEPER_PROPAGATION_TIME = 10 * 1000; + + @Override + protected int defaultTimeoutSeconds() { + return 2 * 60; + } + + private String rootPath; + + @Before + public void checkCluster() { + Assume.assumeThat(getClusterType(), CoreMatchers.is(ClusterType.MINI)); + MiniAccumuloClusterImpl mac = (MiniAccumuloClusterImpl) getCluster(); + rootPath = mac.getConfig().getDir().getAbsolutePath(); + } + + private static void copyStreamToFileSystem(FileSystem fs, String jarName, Path path) throws IOException { + byte[] buffer = new byte[10 * 1024]; + try (FSDataOutputStream dest = fs.create(path); InputStream stream = ClassLoaderIT.class.getResourceAsStream(jarName)) { + while (true) { + int n = stream.read(buffer, 0, buffer.length); + if (n <= 0) { + break; + } + dest.write(buffer, 0, n); + } + } + } + + @Test + public void test() throws Exception { + Connector c = getConnector(); + String tableName = getUniqueNames(1)[0]; + c.tableOperations().create(tableName); + BatchWriter bw = c.createBatchWriter(tableName, new BatchWriterConfig()); + Mutation m = new Mutation("row1"); + m.put("cf", "col1", "Test"); + bw.addMutation(m); + bw.close(); + scanCheck(c, tableName, "Test"); + FileSystem fs = getCluster().getFileSystem(); + Path jarPath = new Path(rootPath + "/lib/ext/Test.jar"); + copyStreamToFileSystem(fs, "/TestCombinerX.jar", jarPath); + sleepUninterruptibly(1, TimeUnit.SECONDS); + IteratorSetting is = new IteratorSetting(10, "TestCombiner", "org.apache.accumulo.test.functional.TestCombiner"); + Combiner.setColumns(is, Collections.singletonList(new IteratorSetting.Column("cf"))); + c.tableOperations().attachIterator(tableName, is, EnumSet.of(IteratorScope.scan)); + sleepUninterruptibly(ZOOKEEPER_PROPAGATION_TIME, TimeUnit.MILLISECONDS); + scanCheck(c, tableName, "TestX"); + fs.delete(jarPath, true); + copyStreamToFileSystem(fs, "/TestCombinerY.jar", jarPath); + sleepUninterruptibly(5, TimeUnit.SECONDS); + scanCheck(c, tableName, "TestY"); + fs.delete(jarPath, true); + } + + private void scanCheck(Connector c, String tableName, String expected) throws Exception { + Scanner bs = c.createScanner(tableName, Authorizations.EMPTY); + Iterator<Entry<Key,Value>> iterator = bs.iterator(); + assertTrue(iterator.hasNext()); + Entry<Key,Value> next = iterator.next(); + assertFalse(iterator.hasNext()); + assertEquals(expected, next.getValue().toString()); + } + +} http://git-wip-us.apache.org/repos/asf/accumulo/blob/d28a3ee3/test/src/main/java/org/apache/accumulo/test/functional/ConfigurableMacBase.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/functional/ConfigurableMacBase.java index 85246bf,0000000..71777bf mode 100644,000000..100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/ConfigurableMacBase.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/ConfigurableMacBase.java @@@ -1,185 -1,0 +1,188 @@@ +/* + * 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.test.functional; + +import static org.junit.Assert.assertTrue; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import org.apache.accumulo.core.client.AccumuloException; +import org.apache.accumulo.core.client.AccumuloSecurityException; +import org.apache.accumulo.core.client.ClientConfiguration; +import org.apache.accumulo.core.client.Connector; +import org.apache.accumulo.core.client.Instance; +import org.apache.accumulo.core.client.ZooKeeperInstance; +import org.apache.accumulo.core.client.security.tokens.PasswordToken; +import org.apache.accumulo.core.conf.Property; +import org.apache.accumulo.core.util.MonitorUtil; +import org.apache.accumulo.harness.AccumuloClusterHarness; +import org.apache.accumulo.harness.AccumuloITBase; +import org.apache.accumulo.minicluster.MiniAccumuloCluster; +import org.apache.accumulo.minicluster.impl.MiniAccumuloClusterImpl; +import org.apache.accumulo.minicluster.impl.MiniAccumuloConfigImpl; +import org.apache.accumulo.minicluster.impl.ZooKeeperBindException; ++import org.apache.accumulo.test.categories.MiniClusterOnlyTest; +import org.apache.accumulo.test.util.CertUtils; +import org.apache.commons.io.FileUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.zookeeper.KeeperException; +import org.junit.After; +import org.junit.Before; ++import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * General Integration-Test base class that provides access to a {@link MiniAccumuloCluster} for testing. Tests using these typically do very disruptive things + * to the instance, and require specific configuration. Most tests don't need this level of control and should extend {@link AccumuloClusterHarness} instead. + */ ++@Category(MiniClusterOnlyTest.class) +public class ConfigurableMacBase extends AccumuloITBase { + public static final Logger log = LoggerFactory.getLogger(ConfigurableMacBase.class); + + protected MiniAccumuloClusterImpl cluster; + + protected void configure(MiniAccumuloConfigImpl cfg, Configuration hadoopCoreSite) {} + + protected void beforeClusterStart(MiniAccumuloConfigImpl cfg) throws Exception {} + + protected static final String ROOT_PASSWORD = "testRootPassword1"; + + public static void configureForEnvironment(MiniAccumuloConfigImpl cfg, Class<?> testClass, File folder) { + if ("true".equals(System.getProperty("org.apache.accumulo.test.functional.useSslForIT"))) { + configureForSsl(cfg, folder); + } + if ("true".equals(System.getProperty("org.apache.accumulo.test.functional.useCredProviderForIT"))) { + cfg.setUseCredentialProvider(true); + } + } + + protected static void configureForSsl(MiniAccumuloConfigImpl cfg, File sslDir) { + 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; + } + + // create parent directories, and ensure sslDir is empty + assertTrue(sslDir.mkdirs() || sslDir.isDirectory()); + FileUtils.deleteQuietly(sslDir); + assertTrue(sslDir.mkdir()); + + 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); + } + + @Before + public void setUp() throws Exception { + createMiniAccumulo(); + Exception lastException = null; + for (int i = 0; i < 3; i++) { + try { + cluster.start(); + return; + } catch (ZooKeeperBindException e) { + lastException = e; + log.warn("Failed to start MiniAccumuloCluster, assumably due to ZooKeeper issues", lastException); + Thread.sleep(3000); + createMiniAccumulo(); + } + } + throw new RuntimeException("Failed to start MiniAccumuloCluster after three attempts", lastException); + } + + private void createMiniAccumulo() throws Exception { + // createTestDir will give us a empty directory, we don't need to clean it up ourselves + File baseDir = createTestDir(this.getClass().getName() + "_" + this.testName.getMethodName()); + MiniAccumuloConfigImpl cfg = new MiniAccumuloConfigImpl(baseDir, ROOT_PASSWORD); + String nativePathInDevTree = NativeMapIT.nativeMapLocation().getAbsolutePath(); + String nativePathInMapReduce = new File(System.getProperty("user.dir")).toString(); + cfg.setNativeLibPaths(nativePathInDevTree, nativePathInMapReduce); + cfg.setProperty(Property.GC_FILE_ARCHIVE, Boolean.TRUE.toString()); + Configuration coreSite = new Configuration(false); + configure(cfg, coreSite); + cfg.setProperty(Property.TSERV_NATIVEMAP_ENABLED, Boolean.TRUE.toString()); + configureForEnvironment(cfg, getClass(), getSslDir(baseDir)); + cluster = new MiniAccumuloClusterImpl(cfg); + if (coreSite.size() > 0) { + File csFile = new File(cluster.getConfig().getConfDir(), "core-site.xml"); + if (csFile.exists()) { + coreSite.addResource(new Path(csFile.getAbsolutePath())); + } + File tmp = new File(csFile.getAbsolutePath() + ".tmp"); + OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp)); + coreSite.writeXml(out); + out.close(); + assertTrue(tmp.renameTo(csFile)); + } + beforeClusterStart(cfg); + } + + @After + public void tearDown() throws Exception { + if (cluster != null) + try { + cluster.stop(); + } catch (Exception e) { + // ignored + } + } + + protected MiniAccumuloClusterImpl getCluster() { + return cluster; + } + + protected Connector getConnector() throws AccumuloException, AccumuloSecurityException { + return getCluster().getConnector("root", new PasswordToken(ROOT_PASSWORD)); + } + + protected Process exec(Class<?> clazz, String... args) throws IOException { + return getCluster().exec(clazz, args); + } + + protected String getMonitor() throws KeeperException, InterruptedException { + Instance instance = new ZooKeeperInstance(getCluster().getClientConfig()); + return MonitorUtil.getLocation(instance); + } + + protected ClientConfiguration getClientConfig() throws Exception { + return new ClientConfiguration(getCluster().getConfig().getClientConfFile()); + } + +} http://git-wip-us.apache.org/repos/asf/accumulo/blob/d28a3ee3/test/src/main/java/org/apache/accumulo/test/functional/KerberosIT.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/functional/KerberosIT.java index e636daa,0000000..1bdc71a mode 100644,000000..100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/KerberosIT.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/KerberosIT.java @@@ -1,656 -1,0 +1,659 @@@ +/* + * 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.test.functional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.PrivilegedExceptionAction; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +import org.apache.accumulo.cluster.ClusterUser; +import org.apache.accumulo.core.client.AccumuloException; +import org.apache.accumulo.core.client.AccumuloSecurityException; +import org.apache.accumulo.core.client.BatchScanner; +import org.apache.accumulo.core.client.BatchWriter; +import org.apache.accumulo.core.client.BatchWriterConfig; +import org.apache.accumulo.core.client.Connector; +import org.apache.accumulo.core.client.Scanner; +import org.apache.accumulo.core.client.TableExistsException; +import org.apache.accumulo.core.client.TableNotFoundException; +import org.apache.accumulo.core.client.admin.CompactionConfig; +import org.apache.accumulo.core.client.admin.DelegationTokenConfig; +import org.apache.accumulo.core.client.impl.AuthenticationTokenIdentifier; +import org.apache.accumulo.core.client.impl.DelegationTokenImpl; +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.core.data.Key; +import org.apache.accumulo.core.data.Mutation; +import org.apache.accumulo.core.data.Range; +import org.apache.accumulo.core.data.Value; +import org.apache.accumulo.core.metadata.MetadataTable; +import org.apache.accumulo.core.metadata.RootTable; +import org.apache.accumulo.core.security.Authorizations; +import org.apache.accumulo.core.security.ColumnVisibility; +import org.apache.accumulo.core.security.SystemPermission; +import org.apache.accumulo.core.security.TablePermission; +import org.apache.accumulo.harness.AccumuloITBase; +import org.apache.accumulo.harness.MiniClusterConfigurationCallback; +import org.apache.accumulo.harness.MiniClusterHarness; +import org.apache.accumulo.harness.TestingKdc; +import org.apache.accumulo.minicluster.ServerType; +import org.apache.accumulo.minicluster.impl.MiniAccumuloClusterImpl; +import org.apache.accumulo.minicluster.impl.MiniAccumuloConfigImpl; ++import org.apache.accumulo.test.categories.MiniClusterOnlyTest; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.minikdc.MiniKdc; +import org.apache.hadoop.security.UserGroupInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; ++import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + +/** + * MAC test which uses {@link MiniKdc} to simulate ta secure environment. Can be used as a sanity check for Kerberos/SASL testing. + */ ++@Category(MiniClusterOnlyTest.class) +public class KerberosIT extends AccumuloITBase { + private static final Logger log = LoggerFactory.getLogger(KerberosIT.class); + + private static TestingKdc kdc; + private static String krbEnabledForITs = null; + private static ClusterUser rootUser; + + @BeforeClass + public static void startKdc() throws Exception { + kdc = new TestingKdc(); + kdc.start(); + krbEnabledForITs = System.getProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION); + if (null == krbEnabledForITs || !Boolean.parseBoolean(krbEnabledForITs)) { + System.setProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION, "true"); + } + rootUser = kdc.getRootUser(); + } + + @AfterClass + public static void stopKdc() throws Exception { + if (null != kdc) { + kdc.stop(); + } + if (null != krbEnabledForITs) { + System.setProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION, krbEnabledForITs); + } + UserGroupInformation.setConfiguration(new Configuration(false)); + } + + @Override + public int defaultTimeoutSeconds() { + return 60 * 5; + } + + private MiniAccumuloClusterImpl mac; + + @Before + public void startMac() throws Exception { + MiniClusterHarness harness = new MiniClusterHarness(); + mac = harness.create(this, new PasswordToken("unused"), kdc, new MiniClusterConfigurationCallback() { + + @Override + public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) { + Map<String,String> site = cfg.getSiteConfig(); + site.put(Property.INSTANCE_ZK_TIMEOUT.getKey(), "15s"); + cfg.setSiteConfig(site); + } + + }); + + mac.getConfig().setNumTservers(1); + mac.start(); + // Enabled kerberos auth + Configuration conf = new Configuration(false); + conf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); + UserGroupInformation.setConfiguration(conf); + } + + @After + public void stopMac() throws Exception { + if (null != mac) { + mac.stop(); + } + } + + @Test + public void testAdminUser() throws Exception { + // Login as the client (provided to `accumulo init` as the "root" user) + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + final Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + + // The "root" user should have all system permissions + for (SystemPermission perm : SystemPermission.values()) { + assertTrue("Expected user to have permission: " + perm, conn.securityOperations().hasSystemPermission(conn.whoami(), perm)); + } + + // and the ability to modify the root and metadata tables + for (String table : Arrays.asList(RootTable.NAME, MetadataTable.NAME)) { + assertTrue(conn.securityOperations().hasTablePermission(conn.whoami(), table, TablePermission.ALTER_TABLE)); + } + return null; + } + }); + } + + @Test + public void testNewUser() throws Exception { + String newUser = testName.getMethodName(); + final File newUserKeytab = new File(kdc.getKeytabDir(), newUser + ".keytab"); + if (newUserKeytab.exists() && !newUserKeytab.delete()) { + log.warn("Unable to delete {}", newUserKeytab); + } + + // Create a new user + kdc.createPrincipal(newUserKeytab, newUser); + + final String newQualifiedUser = kdc.qualifyUser(newUser); + final HashSet<String> users = Sets.newHashSet(rootUser.getPrincipal()); + + // Login as the "root" user + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + log.info("Logged in as {}", rootUser.getPrincipal()); + + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + log.info("Created connector as {}", rootUser.getPrincipal()); + assertEquals(rootUser.getPrincipal(), conn.whoami()); + + // Make sure the system user doesn't exist -- this will force some RPC to happen server-side + createTableWithDataAndCompact(conn); + + assertEquals(users, conn.securityOperations().listLocalUsers()); + + return null; + } + }); + // Switch to a new user + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(newQualifiedUser, newUserKeytab.getAbsolutePath()); + log.info("Logged in as {}", newQualifiedUser); + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + Connector conn = mac.getConnector(newQualifiedUser, new KerberosToken()); + log.info("Created connector as {}", newQualifiedUser); + assertEquals(newQualifiedUser, conn.whoami()); + + // The new user should have no system permissions + for (SystemPermission perm : SystemPermission.values()) { + assertFalse(conn.securityOperations().hasSystemPermission(newQualifiedUser, perm)); + } + + users.add(newQualifiedUser); + + // Same users as before, plus the new user we just created + assertEquals(users, conn.securityOperations().listLocalUsers()); + return null; + } + + }); + } + + @Test + public void testUserPrivilegesThroughGrant() throws Exception { + String user1 = testName.getMethodName(); + final File user1Keytab = new File(kdc.getKeytabDir(), user1 + ".keytab"); + if (user1Keytab.exists() && !user1Keytab.delete()) { + log.warn("Unable to delete {}", user1Keytab); + } + + // Create some new users + kdc.createPrincipal(user1Keytab, user1); + + final String qualifiedUser1 = kdc.qualifyUser(user1); + + // Log in as user1 + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(user1, user1Keytab.getAbsolutePath()); + log.info("Logged in as {}", user1); + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + // Indirectly creates this user when we use it + Connector conn = mac.getConnector(qualifiedUser1, new KerberosToken()); + log.info("Created connector as {}", qualifiedUser1); + + // The new user should have no system permissions + for (SystemPermission perm : SystemPermission.values()) { + assertFalse(conn.securityOperations().hasSystemPermission(qualifiedUser1, perm)); + } + + return null; + } + }); + + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + conn.securityOperations().grantSystemPermission(qualifiedUser1, SystemPermission.CREATE_TABLE); + return null; + } + }); + + // Switch back to the original user + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(user1, user1Keytab.getAbsolutePath()); + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + Connector conn = mac.getConnector(qualifiedUser1, new KerberosToken()); + + // Shouldn't throw an exception since we granted the create table permission + final String table = testName.getMethodName() + "_user_table"; + conn.tableOperations().create(table); + + // Make sure we can actually use the table we made + BatchWriter bw = conn.createBatchWriter(table, new BatchWriterConfig()); + Mutation m = new Mutation("a"); + m.put("b", "c", "d"); + bw.addMutation(m); + bw.close(); + + conn.tableOperations().compact(table, new CompactionConfig().setWait(true).setFlush(true)); + return null; + } + }); + } + + @Test + public void testUserPrivilegesForTable() throws Exception { + String user1 = testName.getMethodName(); + final File user1Keytab = new File(kdc.getKeytabDir(), user1 + ".keytab"); + if (user1Keytab.exists() && !user1Keytab.delete()) { + log.warn("Unable to delete {}", user1Keytab); + } + + // Create some new users -- cannot contain realm + kdc.createPrincipal(user1Keytab, user1); + + final String qualifiedUser1 = kdc.qualifyUser(user1); + + // Log in as user1 + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(qualifiedUser1, user1Keytab.getAbsolutePath()); + log.info("Logged in as {}", user1); + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + // Indirectly creates this user when we use it + Connector conn = mac.getConnector(qualifiedUser1, new KerberosToken()); + log.info("Created connector as {}", qualifiedUser1); + + // The new user should have no system permissions + for (SystemPermission perm : SystemPermission.values()) { + assertFalse(conn.securityOperations().hasSystemPermission(qualifiedUser1, perm)); + } + return null; + } + + }); + + final String table = testName.getMethodName() + "_user_table"; + final String viz = "viz"; + + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + conn.tableOperations().create(table); + // Give our unprivileged user permission on the table we made for them + conn.securityOperations().grantTablePermission(qualifiedUser1, table, TablePermission.READ); + conn.securityOperations().grantTablePermission(qualifiedUser1, table, TablePermission.WRITE); + conn.securityOperations().grantTablePermission(qualifiedUser1, table, TablePermission.ALTER_TABLE); + conn.securityOperations().grantTablePermission(qualifiedUser1, table, TablePermission.DROP_TABLE); + conn.securityOperations().changeUserAuthorizations(qualifiedUser1, new Authorizations(viz)); + return null; + } + }); + + // Switch back to the original user + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(qualifiedUser1, user1Keytab.getAbsolutePath()); + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + Connector conn = mac.getConnector(qualifiedUser1, new KerberosToken()); + + // Make sure we can actually use the table we made + + // Write data + final long ts = 1000l; + BatchWriter bw = conn.createBatchWriter(table, new BatchWriterConfig()); + Mutation m = new Mutation("a"); + m.put("b", "c", new ColumnVisibility(viz.getBytes()), ts, "d"); + bw.addMutation(m); + bw.close(); + + // Compact + conn.tableOperations().compact(table, new CompactionConfig().setWait(true).setFlush(true)); + + // Alter + conn.tableOperations().setProperty(table, Property.TABLE_BLOOM_ENABLED.getKey(), "true"); + + // Read (and proper authorizations) + Scanner s = conn.createScanner(table, new Authorizations(viz)); + Iterator<Entry<Key,Value>> iter = s.iterator(); + assertTrue("No results from iterator", iter.hasNext()); + Entry<Key,Value> entry = iter.next(); + assertEquals(new Key("a", "b", "c", viz, ts), entry.getKey()); + assertEquals(new Value("d".getBytes()), entry.getValue()); + assertFalse("Had more results from iterator", iter.hasNext()); + return null; + } + }); + } + + @Test + public void testDelegationToken() throws Exception { + final String tableName = getUniqueNames(1)[0]; + + // Login as the "root" user + UserGroupInformation root = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + log.info("Logged in as {}", rootUser.getPrincipal()); + + final int numRows = 100, numColumns = 10; + + // As the "root" user, open up the connection and get a delegation token + final AuthenticationToken delegationToken = root.doAs(new PrivilegedExceptionAction<AuthenticationToken>() { + @Override + public AuthenticationToken run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + log.info("Created connector as {}", rootUser.getPrincipal()); + assertEquals(rootUser.getPrincipal(), conn.whoami()); + + conn.tableOperations().create(tableName); + BatchWriter bw = conn.createBatchWriter(tableName, new BatchWriterConfig()); + for (int r = 0; r < numRows; r++) { + Mutation m = new Mutation(Integer.toString(r)); + for (int c = 0; c < numColumns; c++) { + String col = Integer.toString(c); + m.put(col, col, col); + } + bw.addMutation(m); + } + bw.close(); + + return conn.securityOperations().getDelegationToken(new DelegationTokenConfig()); + } + }); + + // The above login with keytab doesn't have a way to logout, so make a fake user that won't have krb credentials + UserGroupInformation userWithoutPrivs = UserGroupInformation.createUserForTesting("fake_user", new String[0]); + int recordsSeen = userWithoutPrivs.doAs(new PrivilegedExceptionAction<Integer>() { + @Override + public Integer run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), delegationToken); + + BatchScanner bs = conn.createBatchScanner(tableName, Authorizations.EMPTY, 2); + bs.setRanges(Collections.singleton(new Range())); + int recordsSeen = Iterables.size(bs); + bs.close(); + return recordsSeen; + } + }); + + assertEquals(numRows * numColumns, recordsSeen); + } + + @Test + public void testDelegationTokenAsDifferentUser() throws Exception { + // Login as the "root" user + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + log.info("Logged in as {}", rootUser.getPrincipal()); + + final AuthenticationToken delegationToken; + try { + delegationToken = ugi.doAs(new PrivilegedExceptionAction<AuthenticationToken>() { + @Override + public AuthenticationToken run() throws Exception { + // As the "root" user, open up the connection and get a delegation token + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + log.info("Created connector as {}", rootUser.getPrincipal()); + assertEquals(rootUser.getPrincipal(), conn.whoami()); + return conn.securityOperations().getDelegationToken(new DelegationTokenConfig()); + } + }); + } catch (UndeclaredThrowableException ex) { + throw ex; + } + + // make a fake user that won't have krb credentials + UserGroupInformation userWithoutPrivs = UserGroupInformation.createUserForTesting("fake_user", new String[0]); + try { + // Use the delegation token to try to log in as a different user + userWithoutPrivs.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + mac.getConnector("some_other_user", delegationToken); + return null; + } + }); + fail("Using a delegation token as a different user should throw an exception"); + } catch (UndeclaredThrowableException e) { + Throwable cause = e.getCause(); + assertNotNull(cause); + // We should get an AccumuloSecurityException from trying to use a delegation token for the wrong user + assertTrue("Expected cause to be AccumuloSecurityException, but was " + cause.getClass(), cause instanceof AccumuloSecurityException); + } + } + + @Test + public void testGetDelegationTokenDenied() throws Exception { + String newUser = testName.getMethodName(); + final File newUserKeytab = new File(kdc.getKeytabDir(), newUser + ".keytab"); + if (newUserKeytab.exists() && !newUserKeytab.delete()) { + log.warn("Unable to delete {}", newUserKeytab); + } + + // Create a new user + kdc.createPrincipal(newUserKeytab, newUser); + + final String qualifiedNewUser = kdc.qualifyUser(newUser); + + // Login as a normal user + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(qualifiedNewUser, newUserKeytab.getAbsolutePath()); + try { + ugi.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + // As the "root" user, open up the connection and get a delegation token + Connector conn = mac.getConnector(qualifiedNewUser, new KerberosToken()); + log.info("Created connector as {}", qualifiedNewUser); + assertEquals(qualifiedNewUser, conn.whoami()); + + conn.securityOperations().getDelegationToken(new DelegationTokenConfig()); + return null; + } + }); + } catch (UndeclaredThrowableException ex) { + assertTrue(ex.getCause() instanceof AccumuloSecurityException); + } + } + + @Test + public void testRestartedMasterReusesSecretKey() throws Exception { + // Login as the "root" user + UserGroupInformation root = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + log.info("Logged in as {}", rootUser.getPrincipal()); + + // As the "root" user, open up the connection and get a delegation token + final AuthenticationToken delegationToken1 = root.doAs(new PrivilegedExceptionAction<AuthenticationToken>() { + @Override + public AuthenticationToken run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + log.info("Created connector as {}", rootUser.getPrincipal()); + assertEquals(rootUser.getPrincipal(), conn.whoami()); + + AuthenticationToken token = conn.securityOperations().getDelegationToken(new DelegationTokenConfig()); + + assertTrue("Could not get tables with delegation token", mac.getConnector(rootUser.getPrincipal(), token).tableOperations().list().size() > 0); + + return token; + } + }); + + log.info("Stopping master"); + mac.getClusterControl().stop(ServerType.MASTER); + Thread.sleep(5000); + log.info("Restarting master"); + mac.getClusterControl().start(ServerType.MASTER); + + // Make sure our original token is still good + root.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), delegationToken1); + + assertTrue("Could not get tables with delegation token", conn.tableOperations().list().size() > 0); + + return null; + } + }); + + // Get a new token, so we can compare the keyId on the second to the first + final AuthenticationToken delegationToken2 = root.doAs(new PrivilegedExceptionAction<AuthenticationToken>() { + @Override + public AuthenticationToken run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + log.info("Created connector as {}", rootUser.getPrincipal()); + assertEquals(rootUser.getPrincipal(), conn.whoami()); + + AuthenticationToken token = conn.securityOperations().getDelegationToken(new DelegationTokenConfig()); + + assertTrue("Could not get tables with delegation token", mac.getConnector(rootUser.getPrincipal(), token).tableOperations().list().size() > 0); + + return token; + } + }); + + // A restarted master should reuse the same secret key after a restart if the secret key hasn't expired (1day by default) + DelegationTokenImpl dt1 = (DelegationTokenImpl) delegationToken1; + DelegationTokenImpl dt2 = (DelegationTokenImpl) delegationToken2; + assertEquals(dt1.getIdentifier().getKeyId(), dt2.getIdentifier().getKeyId()); + } + + @Test(expected = AccumuloException.class) + public void testDelegationTokenWithInvalidLifetime() throws Throwable { + // Login as the "root" user + UserGroupInformation root = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + log.info("Logged in as {}", rootUser.getPrincipal()); + + // As the "root" user, open up the connection and get a delegation token + try { + root.doAs(new PrivilegedExceptionAction<AuthenticationToken>() { + @Override + public AuthenticationToken run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + log.info("Created connector as {}", rootUser.getPrincipal()); + assertEquals(rootUser.getPrincipal(), conn.whoami()); + + // Should fail + return conn.securityOperations().getDelegationToken(new DelegationTokenConfig().setTokenLifetime(Long.MAX_VALUE, TimeUnit.MILLISECONDS)); + } + }); + } catch (UndeclaredThrowableException e) { + Throwable cause = e.getCause(); + if (null != cause) { + throw cause; + } else { + throw e; + } + } + } + + @Test + public void testDelegationTokenWithReducedLifetime() throws Throwable { + // Login as the "root" user + UserGroupInformation root = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + log.info("Logged in as {}", rootUser.getPrincipal()); + + // As the "root" user, open up the connection and get a delegation token + final AuthenticationToken dt = root.doAs(new PrivilegedExceptionAction<AuthenticationToken>() { + @Override + public AuthenticationToken run() throws Exception { + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + log.info("Created connector as {}", rootUser.getPrincipal()); + assertEquals(rootUser.getPrincipal(), conn.whoami()); + + return conn.securityOperations().getDelegationToken(new DelegationTokenConfig().setTokenLifetime(5, TimeUnit.MINUTES)); + } + }); + + AuthenticationTokenIdentifier identifier = ((DelegationTokenImpl) dt).getIdentifier(); + assertTrue("Expected identifier to expire in no more than 5 minutes: " + identifier, + identifier.getExpirationDate() - identifier.getIssueDate() <= (5 * 60 * 1000)); + } + + @Test(expected = AccumuloSecurityException.class) + public void testRootUserHasIrrevocablePermissions() throws Exception { + // Login as the client (provided to `accumulo init` as the "root" user) + UserGroupInformation.loginUserFromKeytab(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + + final Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + + // The server-side implementation should prevent the revocation of the 'root' user's systems permissions + // because once they're gone, it's possible that they could never be restored. + conn.securityOperations().revokeSystemPermission(rootUser.getPrincipal(), SystemPermission.GRANT); + } + + /** + * Creates a table, adds a record to it, and then compacts the table. A simple way to make sure that the system user exists (since the master does an RPC to + * the tserver which will create the system user if it doesn't already exist). + */ + private void createTableWithDataAndCompact(Connector conn) throws TableNotFoundException, AccumuloSecurityException, AccumuloException, TableExistsException { + final String table = testName.getMethodName() + "_table"; + conn.tableOperations().create(table); + BatchWriter bw = conn.createBatchWriter(table, new BatchWriterConfig()); + Mutation m = new Mutation("a"); + m.put("b", "c", "d"); + bw.addMutation(m); + bw.close(); + conn.tableOperations().compact(table, new CompactionConfig().setFlush(true).setWait(true)); + } +} http://git-wip-us.apache.org/repos/asf/accumulo/blob/d28a3ee3/test/src/main/java/org/apache/accumulo/test/functional/KerberosProxyIT.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/functional/KerberosProxyIT.java index 2337f91,0000000..7264a42 mode 100644,000000..100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/KerberosProxyIT.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/KerberosProxyIT.java @@@ -1,482 -1,0 +1,485 @@@ +/* + * 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.test.functional; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.accumulo.cluster.ClusterUser; +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.core.rpc.UGIAssumingTransport; +import org.apache.accumulo.harness.AccumuloITBase; +import org.apache.accumulo.harness.MiniClusterConfigurationCallback; +import org.apache.accumulo.harness.MiniClusterHarness; +import org.apache.accumulo.harness.TestingKdc; +import org.apache.accumulo.minicluster.impl.MiniAccumuloClusterImpl; +import org.apache.accumulo.minicluster.impl.MiniAccumuloConfigImpl; +import org.apache.accumulo.proxy.Proxy; +import org.apache.accumulo.proxy.ProxyServer; +import org.apache.accumulo.proxy.thrift.AccumuloProxy; +import org.apache.accumulo.proxy.thrift.AccumuloProxy.Client; +import org.apache.accumulo.proxy.thrift.AccumuloSecurityException; +import org.apache.accumulo.proxy.thrift.ColumnUpdate; +import org.apache.accumulo.proxy.thrift.Key; +import org.apache.accumulo.proxy.thrift.KeyValue; +import org.apache.accumulo.proxy.thrift.ScanOptions; +import org.apache.accumulo.proxy.thrift.ScanResult; +import org.apache.accumulo.proxy.thrift.TimeType; +import org.apache.accumulo.proxy.thrift.WriterOptions; +import org.apache.accumulo.server.util.PortUtils; ++import org.apache.accumulo.test.categories.MiniClusterOnlyTest; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.thrift.protocol.TCompactProtocol; +import org.apache.thrift.transport.TSaslClientTransport; +import org.apache.thrift.transport.TSocket; +import org.apache.thrift.transport.TTransportException; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; ++import org.junit.experimental.categories.Category; +import org.junit.rules.ExpectedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests impersonation of clients by the proxy over SASL + */ ++@Category(MiniClusterOnlyTest.class) +public class KerberosProxyIT extends AccumuloITBase { + private static final Logger log = LoggerFactory.getLogger(KerberosProxyIT.class); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private static TestingKdc kdc; + private static String krbEnabledForITs = null; + private static File proxyKeytab; + private static String hostname, proxyPrimary, proxyPrincipal; + + @Override + protected int defaultTimeoutSeconds() { + return 60 * 5; + } + + @BeforeClass + public static void startKdc() throws Exception { + kdc = new TestingKdc(); + kdc.start(); + krbEnabledForITs = System.getProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION); + if (null == krbEnabledForITs || !Boolean.parseBoolean(krbEnabledForITs)) { + System.setProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION, "true"); + } + + // Create a principal+keytab for the proxy + proxyKeytab = new File(kdc.getKeytabDir(), "proxy.keytab"); + hostname = InetAddress.getLocalHost().getCanonicalHostName(); + // Set the primary because the client needs to know it + proxyPrimary = "proxy"; + // Qualify with an instance + proxyPrincipal = proxyPrimary + "/" + hostname; + kdc.createPrincipal(proxyKeytab, proxyPrincipal); + // Tack on the realm too + proxyPrincipal = kdc.qualifyUser(proxyPrincipal); + } + + @AfterClass + public static void stopKdc() throws Exception { + if (null != kdc) { + kdc.stop(); + } + if (null != krbEnabledForITs) { + System.setProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION, krbEnabledForITs); + } + UserGroupInformation.setConfiguration(new Configuration(false)); + } + + private MiniAccumuloClusterImpl mac; + private Process proxyProcess; + private int proxyPort; + + @Before + public void startMac() throws Exception { + MiniClusterHarness harness = new MiniClusterHarness(); + mac = harness.create(getClass().getName(), testName.getMethodName(), new PasswordToken("unused"), new MiniClusterConfigurationCallback() { + + @Override + public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) { + cfg.setNumTservers(1); + Map<String,String> siteCfg = cfg.getSiteConfig(); + // Allow the proxy to impersonate the client user, but no one else + siteCfg.put(Property.INSTANCE_RPC_SASL_ALLOWED_USER_IMPERSONATION.getKey(), proxyPrincipal + ":" + kdc.getRootUser().getPrincipal()); + siteCfg.put(Property.INSTANCE_RPC_SASL_ALLOWED_HOST_IMPERSONATION.getKey(), "*"); + cfg.setSiteConfig(siteCfg); + } + + }, kdc); + + mac.start(); + MiniAccumuloConfigImpl cfg = mac.getConfig(); + + // Generate Proxy configuration and start the proxy + proxyProcess = startProxy(cfg); + + // Enabled kerberos auth + Configuration conf = new Configuration(false); + conf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); + UserGroupInformation.setConfiguration(conf); + + boolean success = false; + ClusterUser rootUser = kdc.getRootUser(); + // Rely on the junit timeout rule + while (!success) { + UserGroupInformation ugi; + try { + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + } catch (IOException ex) { + log.info("Login as root is failing", ex); + Thread.sleep(3000); + continue; + } + + TSocket socket = new TSocket(hostname, proxyPort); + log.info("Connecting to proxy with server primary '" + proxyPrimary + "' running on " + hostname); + TSaslClientTransport transport = new TSaslClientTransport("GSSAPI", null, proxyPrimary, hostname, Collections.singletonMap("javax.security.sasl.qop", + "auth"), null, socket); + + final UGIAssumingTransport ugiTransport = new UGIAssumingTransport(transport, ugi); + + try { + // UGI transport will perform the doAs for us + ugiTransport.open(); + success = true; + } catch (TTransportException e) { + Throwable cause = e.getCause(); + if (null != cause && cause instanceof ConnectException) { + log.info("Proxy not yet up, waiting"); + Thread.sleep(3000); + proxyProcess = checkProxyAndRestart(proxyProcess, cfg); + continue; + } + } finally { + if (null != ugiTransport) { + ugiTransport.close(); + } + } + } + + assertTrue("Failed to connect to the proxy repeatedly", success); + } + + /** + * Starts the thrift proxy using the given MAConfig. + * + * @param cfg + * configuration for MAC + * @return Process for the thrift proxy + */ + private Process startProxy(MiniAccumuloConfigImpl cfg) throws IOException { + File proxyPropertiesFile = generateNewProxyConfiguration(cfg); + return mac.exec(Proxy.class, "-p", proxyPropertiesFile.getCanonicalPath()); + } + + /** + * Generates a proxy configuration file for the MAC instance. Implicitly updates {@link #proxyPort} when choosing the port the proxy will listen on. + * + * @param cfg + * The MAC configuration + * @return The proxy's configuration file + */ + private File generateNewProxyConfiguration(MiniAccumuloConfigImpl cfg) throws IOException { + // Chooses a new port for the proxy as side-effect + proxyPort = PortUtils.getRandomFreePort(); + + // Proxy configuration + File proxyPropertiesFile = new File(cfg.getConfDir(), "proxy.properties"); + if (proxyPropertiesFile.exists()) { + assertTrue("Failed to delete proxy.properties file", proxyPropertiesFile.delete()); + } + Properties proxyProperties = new Properties(); + proxyProperties.setProperty("useMockInstance", "false"); + proxyProperties.setProperty("useMiniAccumulo", "false"); + proxyProperties.setProperty("protocolFactory", TCompactProtocol.Factory.class.getName()); + proxyProperties.setProperty("tokenClass", KerberosToken.class.getName()); + proxyProperties.setProperty("port", Integer.toString(proxyPort)); + proxyProperties.setProperty("maxFrameSize", "16M"); + proxyProperties.setProperty("instance", mac.getInstanceName()); + proxyProperties.setProperty("zookeepers", mac.getZooKeepers()); + proxyProperties.setProperty("thriftServerType", "sasl"); + proxyProperties.setProperty("kerberosPrincipal", proxyPrincipal); + proxyProperties.setProperty("kerberosKeytab", proxyKeytab.getCanonicalPath()); + + // Write out the proxy.properties file + FileWriter writer = new FileWriter(proxyPropertiesFile); + proxyProperties.store(writer, "Configuration for Accumulo proxy"); + writer.close(); + + log.info("Created configuration for proxy listening on {}", proxyPort); + + return proxyPropertiesFile; + } + + /** + * Restarts the thrift proxy if the previous instance is no longer running. If the proxy is still running, this method does nothing. + * + * @param proxy + * The thrift proxy process + * @param cfg + * The MAC configuration + * @return The process for the Proxy, either the previous instance or a new instance. + */ + private Process checkProxyAndRestart(Process proxy, MiniAccumuloConfigImpl cfg) throws IOException { + try { + // Get the return code + proxy.exitValue(); + } catch (IllegalThreadStateException e) { + log.info("Proxy is still running"); + // OK, process is still running, don't restart + return proxy; + } + + log.info("Restarting proxy because it is no longer alive"); + + // We got a return code which means the proxy exited. We'll assume this is because it failed + // to bind the port due to the known race condition between choosing a port and having the + // proxy bind it. + return startProxy(cfg); + } + + @After + public void stopMac() throws Exception { + if (null != proxyProcess) { + log.info("Destroying proxy process"); + proxyProcess.destroy(); + log.info("Waiting for proxy termination"); + proxyProcess.waitFor(); + log.info("Proxy terminated"); + } + if (null != mac) { + mac.stop(); + } + } + + @Test + public void testProxyClient() throws Exception { + ClusterUser rootUser = kdc.getRootUser(); + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + + TSocket socket = new TSocket(hostname, proxyPort); + log.info("Connecting to proxy with server primary '" + proxyPrimary + "' running on " + hostname); + TSaslClientTransport transport = new TSaslClientTransport("GSSAPI", null, proxyPrimary, hostname, Collections.singletonMap("javax.security.sasl.qop", + "auth"), null, socket); + + final UGIAssumingTransport ugiTransport = new UGIAssumingTransport(transport, ugi); + + // UGI transport will perform the doAs for us + ugiTransport.open(); + + AccumuloProxy.Client.Factory factory = new AccumuloProxy.Client.Factory(); + Client client = factory.getClient(new TCompactProtocol(ugiTransport), new TCompactProtocol(ugiTransport)); + + // Will fail if the proxy can impersonate the client + ByteBuffer login = client.login(rootUser.getPrincipal(), Collections.<String,String> emptyMap()); + + // For all of the below actions, the proxy user doesn't have permission to do any of them, but the client user does. + // The fact that any of them actually run tells us that impersonation is working. + + // Create a table + String table = "table"; + if (!client.tableExists(login, table)) { + client.createTable(login, table, true, TimeType.MILLIS); + } + + // Write two records to the table + String writer = client.createWriter(login, table, new WriterOptions()); + Map<ByteBuffer,List<ColumnUpdate>> updates = new HashMap<>(); + ColumnUpdate update = new ColumnUpdate(ByteBuffer.wrap("cf1".getBytes(UTF_8)), ByteBuffer.wrap("cq1".getBytes(UTF_8))); + update.setValue(ByteBuffer.wrap("value1".getBytes(UTF_8))); + updates.put(ByteBuffer.wrap("row1".getBytes(UTF_8)), Collections.<ColumnUpdate> singletonList(update)); + update = new ColumnUpdate(ByteBuffer.wrap("cf2".getBytes(UTF_8)), ByteBuffer.wrap("cq2".getBytes(UTF_8))); + update.setValue(ByteBuffer.wrap("value2".getBytes(UTF_8))); + updates.put(ByteBuffer.wrap("row2".getBytes(UTF_8)), Collections.<ColumnUpdate> singletonList(update)); + client.update(writer, updates); + + // Flush and close the writer + client.flush(writer); + client.closeWriter(writer); + + // Open a scanner to the table + String scanner = client.createScanner(login, table, new ScanOptions()); + ScanResult results = client.nextK(scanner, 10); + assertEquals(2, results.getResults().size()); + + // Check the first key-value + KeyValue kv = results.getResults().get(0); + Key k = kv.key; + ByteBuffer v = kv.value; + assertEquals(ByteBuffer.wrap("row1".getBytes(UTF_8)), k.row); + assertEquals(ByteBuffer.wrap("cf1".getBytes(UTF_8)), k.colFamily); + assertEquals(ByteBuffer.wrap("cq1".getBytes(UTF_8)), k.colQualifier); + assertEquals(ByteBuffer.wrap(new byte[0]), k.colVisibility); + assertEquals(ByteBuffer.wrap("value1".getBytes(UTF_8)), v); + + // And then the second + kv = results.getResults().get(1); + k = kv.key; + v = kv.value; + assertEquals(ByteBuffer.wrap("row2".getBytes(UTF_8)), k.row); + assertEquals(ByteBuffer.wrap("cf2".getBytes(UTF_8)), k.colFamily); + assertEquals(ByteBuffer.wrap("cq2".getBytes(UTF_8)), k.colQualifier); + assertEquals(ByteBuffer.wrap(new byte[0]), k.colVisibility); + assertEquals(ByteBuffer.wrap("value2".getBytes(UTF_8)), v); + + // Close the scanner + client.closeScanner(scanner); + + ugiTransport.close(); + } + + @Test + public void testDisallowedClientForImpersonation() throws Exception { + String user = testName.getMethodName(); + File keytab = new File(kdc.getKeytabDir(), user + ".keytab"); + kdc.createPrincipal(keytab, user); + + // Login as the new user + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(user, keytab.getAbsolutePath()); + + log.info("Logged in as " + ugi); + + // Expect an AccumuloSecurityException + thrown.expect(AccumuloSecurityException.class); + // Error msg would look like: + // + // org.apache.accumulo.core.client.AccumuloSecurityException: Error BAD_CREDENTIALS for user Principal in credentials object should match kerberos + // principal. + // Expected 'proxy/hw10447.lo...@example.com' but was 'testdisallowedclientforimpersonat...@example.com' - Username or Password is Invalid) + thrown.expect(new ThriftExceptionMatchesPattern(".*Error BAD_CREDENTIALS.*")); + thrown.expect(new ThriftExceptionMatchesPattern(".*Expected '" + proxyPrincipal + "' but was '" + kdc.qualifyUser(user) + "'.*")); + + TSocket socket = new TSocket(hostname, proxyPort); + log.info("Connecting to proxy with server primary '" + proxyPrimary + "' running on " + hostname); + + // Should fail to open the tran + TSaslClientTransport transport = new TSaslClientTransport("GSSAPI", null, proxyPrimary, hostname, Collections.singletonMap("javax.security.sasl.qop", + "auth"), null, socket); + + final UGIAssumingTransport ugiTransport = new UGIAssumingTransport(transport, ugi); + + // UGI transport will perform the doAs for us + ugiTransport.open(); + + AccumuloProxy.Client.Factory factory = new AccumuloProxy.Client.Factory(); + Client client = factory.getClient(new TCompactProtocol(ugiTransport), new TCompactProtocol(ugiTransport)); + + // Will fail because the proxy can't impersonate this user (per the site configuration) + try { + client.login(kdc.qualifyUser(user), Collections.<String,String> emptyMap()); + } finally { + if (null != ugiTransport) { + ugiTransport.close(); + } + } + } + + @Test + public void testMismatchPrincipals() throws Exception { + ClusterUser rootUser = kdc.getRootUser(); + // Should get an AccumuloSecurityException and the given message + thrown.expect(AccumuloSecurityException.class); + thrown.expect(new ThriftExceptionMatchesPattern(ProxyServer.RPC_ACCUMULO_PRINCIPAL_MISMATCH_MSG)); + + // Make a new user + String user = testName.getMethodName(); + File keytab = new File(kdc.getKeytabDir(), user + ".keytab"); + kdc.createPrincipal(keytab, user); + + // Login as the new user + UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(user, keytab.getAbsolutePath()); + + log.info("Logged in as " + ugi); + + TSocket socket = new TSocket(hostname, proxyPort); + log.info("Connecting to proxy with server primary '" + proxyPrimary + "' running on " + hostname); + + // Should fail to open the tran + TSaslClientTransport transport = new TSaslClientTransport("GSSAPI", null, proxyPrimary, hostname, Collections.singletonMap("javax.security.sasl.qop", + "auth"), null, socket); + + final UGIAssumingTransport ugiTransport = new UGIAssumingTransport(transport, ugi); + + // UGI transport will perform the doAs for us + ugiTransport.open(); + + AccumuloProxy.Client.Factory factory = new AccumuloProxy.Client.Factory(); + Client client = factory.getClient(new TCompactProtocol(ugiTransport), new TCompactProtocol(ugiTransport)); + + // The proxy needs to recognize that the requested principal isn't the same as the SASL principal and fail + // Accumulo should let this through -- we need to rely on the proxy to dump me before talking to accumulo + try { + client.login(rootUser.getPrincipal(), Collections.<String,String> emptyMap()); + } finally { + if (null != ugiTransport) { + ugiTransport.close(); + } + } + } + + private static class ThriftExceptionMatchesPattern extends TypeSafeMatcher<AccumuloSecurityException> { + private String pattern; + + public ThriftExceptionMatchesPattern(String pattern) { + this.pattern = pattern; + } + + @Override + protected boolean matchesSafely(AccumuloSecurityException item) { + return item.isSetMsg() && item.msg.matches(pattern); + } + + @Override + public void describeTo(Description description) { + description.appendText("matches pattern ").appendValue(pattern); + } + + @Override + protected void describeMismatchSafely(AccumuloSecurityException item, Description mismatchDescription) { + mismatchDescription.appendText("does not match"); + } + } +} http://git-wip-us.apache.org/repos/asf/accumulo/blob/d28a3ee3/test/src/main/java/org/apache/accumulo/test/functional/KerberosRenewalIT.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/functional/KerberosRenewalIT.java index 142a8bb,0000000..0e60501 mode 100644,000000..100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/KerberosRenewalIT.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/KerberosRenewalIT.java @@@ -1,188 -1,0 +1,191 @@@ +/* + * 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.test.functional; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.accumulo.cluster.ClusterUser; +import org.apache.accumulo.core.client.AccumuloException; +import org.apache.accumulo.core.client.AccumuloSecurityException; +import org.apache.accumulo.core.client.BatchWriter; +import org.apache.accumulo.core.client.BatchWriterConfig; +import org.apache.accumulo.core.client.Connector; +import org.apache.accumulo.core.client.Scanner; +import org.apache.accumulo.core.client.TableExistsException; +import org.apache.accumulo.core.client.TableNotFoundException; +import org.apache.accumulo.core.client.admin.CompactionConfig; +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.core.data.Key; +import org.apache.accumulo.core.data.Mutation; +import org.apache.accumulo.core.data.PartialKey; +import org.apache.accumulo.core.data.Value; +import org.apache.accumulo.core.security.Authorizations; +import org.apache.accumulo.harness.AccumuloITBase; +import org.apache.accumulo.harness.MiniClusterConfigurationCallback; +import org.apache.accumulo.harness.MiniClusterHarness; +import org.apache.accumulo.harness.TestingKdc; +import org.apache.accumulo.minicluster.impl.MiniAccumuloClusterImpl; +import org.apache.accumulo.minicluster.impl.MiniAccumuloConfigImpl; ++import org.apache.accumulo.test.categories.MiniClusterOnlyTest; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.minikdc.MiniKdc; +import org.apache.hadoop.security.UserGroupInformation; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; ++import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Iterables; + +/** + * MAC test which uses {@link MiniKdc} to simulate ta secure environment. Can be used as a sanity check for Kerberos/SASL testing. + */ ++@Category(MiniClusterOnlyTest.class) +public class KerberosRenewalIT extends AccumuloITBase { + private static final Logger log = LoggerFactory.getLogger(KerberosRenewalIT.class); + + private static TestingKdc kdc; + private static String krbEnabledForITs = null; + private static ClusterUser rootUser; + + private static final long TICKET_LIFETIME = 6 * 60 * 1000; // Anything less seems to fail when generating the ticket + private static final long TICKET_TEST_LIFETIME = 8 * 60 * 1000; // Run a test for 8 mins + private static final long TEST_DURATION = 9 * 60 * 1000; // The test should finish within 9 mins + + @BeforeClass + public static void startKdc() throws Exception { + // 30s renewal time window + kdc = new TestingKdc(TestingKdc.computeKdcDir(), TestingKdc.computeKeytabDir(), TICKET_LIFETIME); + kdc.start(); + krbEnabledForITs = System.getProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION); + if (null == krbEnabledForITs || !Boolean.parseBoolean(krbEnabledForITs)) { + System.setProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION, "true"); + } + rootUser = kdc.getRootUser(); + } + + @AfterClass + public static void stopKdc() throws Exception { + if (null != kdc) { + kdc.stop(); + } + if (null != krbEnabledForITs) { + System.setProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION, krbEnabledForITs); + } + } + + @Override + public int defaultTimeoutSeconds() { + return (int) TEST_DURATION / 1000; + } + + private MiniAccumuloClusterImpl mac; + + @Before + public void startMac() throws Exception { + MiniClusterHarness harness = new MiniClusterHarness(); + mac = harness.create(this, new PasswordToken("unused"), kdc, new MiniClusterConfigurationCallback() { + + @Override + public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) { + Map<String,String> site = cfg.getSiteConfig(); + site.put(Property.INSTANCE_ZK_TIMEOUT.getKey(), "15s"); + // Reduce the period just to make sure we trigger renewal fast + site.put(Property.GENERAL_KERBEROS_RENEWAL_PERIOD.getKey(), "5s"); + cfg.setSiteConfig(site); + } + + }); + + mac.getConfig().setNumTservers(1); + mac.start(); + // Enabled kerberos auth + Configuration conf = new Configuration(false); + conf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); + UserGroupInformation.setConfiguration(conf); + } + + @After + public void stopMac() throws Exception { + if (null != mac) { + mac.stop(); + } + } + + // Intentially setting the Test annotation timeout. We do not want to scale the timeout. + @Test(timeout = TEST_DURATION) + public void testReadAndWriteThroughTicketLifetime() throws Exception { + // Attempt to use Accumulo for a duration of time that exceeds the Kerberos ticket lifetime. + // This is a functional test to verify that Accumulo services renew their ticket. + // If the test doesn't finish on its own, this signifies that Accumulo services failed + // and the test should fail. If Accumulo services renew their ticket, the test case + // should exit gracefully on its own. + + // Login as the "root" user + UserGroupInformation.loginUserFromKeytab(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + log.info("Logged in as {}", rootUser.getPrincipal()); + + Connector conn = mac.getConnector(rootUser.getPrincipal(), new KerberosToken()); + log.info("Created connector as {}", rootUser.getPrincipal()); + assertEquals(rootUser.getPrincipal(), conn.whoami()); + + long duration = 0; + long last = System.currentTimeMillis(); + // Make sure we have a couple renewals happen + while (duration < TICKET_TEST_LIFETIME) { + // Create a table, write a record, compact, read the record, drop the table. + createReadWriteDrop(conn); + // Wait a bit after + Thread.sleep(5000); + + // Update the duration + long now = System.currentTimeMillis(); + duration += now - last; + last = now; + } + } + + /** + * Creates a table, adds a record to it, and then compacts the table. A simple way to make sure that the system user exists (since the master does an RPC to + * the tserver which will create the system user if it doesn't already exist). + */ + private void createReadWriteDrop(Connector conn) throws TableNotFoundException, AccumuloSecurityException, AccumuloException, TableExistsException { + final String table = testName.getMethodName() + "_table"; + conn.tableOperations().create(table); + BatchWriter bw = conn.createBatchWriter(table, new BatchWriterConfig()); + Mutation m = new Mutation("a"); + m.put("b", "c", "d"); + bw.addMutation(m); + bw.close(); + conn.tableOperations().compact(table, new CompactionConfig().setFlush(true).setWait(true)); + Scanner s = conn.createScanner(table, Authorizations.EMPTY); + Entry<Key,Value> entry = Iterables.getOnlyElement(s); + assertEquals("Did not find the expected key", 0, new Key("a", "b", "c").compareTo(entry.getKey(), PartialKey.ROW_COLFAM_COLQUAL)); + assertEquals("d", entry.getValue().toString()); + conn.tableOperations().delete(table); + } +}