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);
 +  }
 +}

Reply via email to