You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by el...@apache.org on 2016/04/29 21:12:13 UTC

calcite git commit: [CALCITE-1214] Support for automatic Kerberos login in thin driver

Repository: calcite
Updated Branches:
  refs/heads/master 4255767d9 -> b7fbb35b3


[CALCITE-1214] Support for automatic Kerberos login in thin driver

When the kerberos principal and keytab are provided in the URL, the
driver can login and automatically launch a thread to renew the
Kerberos ticket.

Closes apache/calcite#223


Project: http://git-wip-us.apache.org/repos/asf/calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/b7fbb35b
Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/b7fbb35b
Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/b7fbb35b

Branch: refs/heads/master
Commit: b7fbb35b3883d5bbead0892c6898711253ddac24
Parents: 4255767
Author: Josh Elser <el...@apache.org>
Authored: Wed Apr 27 15:56:30 2016 -0400
Committer: Josh Elser <el...@apache.org>
Committed: Fri Apr 29 13:02:21 2016 -0400

----------------------------------------------------------------------
 .../calcite/avatica/AvaticaConnection.java      |  24 ++
 .../avatica/BuiltInConnectionProperty.java      |   8 +-
 .../calcite/avatica/ConnectionConfig.java       |   6 +
 .../calcite/avatica/ConnectionConfigImpl.java   |  18 +
 .../remote/AvaticaHttpClientFactory.java        |   2 +-
 .../remote/AvaticaHttpClientFactoryImpl.java    |   6 +-
 .../avatica/remote/DoAsAvaticaHttpClient.java   |  46 +++
 .../apache/calcite/avatica/remote/Driver.java   |  28 +-
 .../avatica/remote/KerberosConnection.java      | 400 +++++++++++++++++++
 .../remote/AvaticaHttpClientFactoryTest.java    |   4 +-
 .../avatica/remote/KerberosConnectionTest.java  | 142 +++++++
 .../calcite/avatica/AvaticaSpnegoTest.java      |  43 +-
 .../remote/AlternatingRemoteMetaTest.java       |   4 +-
 avatica/site/_docs/client_reference.md          |  22 +
 14 files changed, 730 insertions(+), 23 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java b/avatica/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
index 1b4f758..69a60ec 100644
--- a/avatica/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
@@ -18,6 +18,8 @@ package org.apache.calcite.avatica;
 
 import org.apache.calcite.avatica.Meta.ExecuteBatchResult;
 import org.apache.calcite.avatica.Meta.MetaResultSet;
+import org.apache.calcite.avatica.remote.KerberosConnection;
+import org.apache.calcite.avatica.remote.Service;
 import org.apache.calcite.avatica.remote.Service.ErrorResponse;
 import org.apache.calcite.avatica.remote.Service.OpenConnectionRequest;
 import org.apache.calcite.avatica.remote.TypedValue;
@@ -73,6 +75,8 @@ public abstract class AvaticaConnection implements Connection {
   private boolean closed;
   private int holdability;
   private int networkTimeout;
+  private KerberosConnection kerberosConnection;
+  private Service service;
 
   public final String id;
   public final Meta.ConnectionHandle handle;
@@ -194,6 +198,9 @@ public abstract class AvaticaConnection implements Connection {
       try {
         meta.closeConnection(handle);
         driver.handler.onConnectionClose(this);
+        if (null != kerberosConnection) {
+          kerberosConnection.stopRenewalThread();
+        }
       } catch (RuntimeException e) {
         throw helper.createException("While closing connection", e);
       }
@@ -721,6 +728,23 @@ public abstract class AvaticaConnection implements Connection {
       throw new IllegalStateException();
     }
   }
+
+  public void setKerberosConnection(KerberosConnection kerberosConnection) {
+    this.kerberosConnection = Objects.requireNonNull(kerberosConnection);
+  }
+
+  public KerberosConnection getKerberosConnection() {
+    return this.kerberosConnection;
+  }
+
+  public Service getService() {
+    assert null != service;
+    return service;
+  }
+
+  public void setService(Service service) {
+    this.service = Objects.requireNonNull(service);
+  }
 }
 
 // End AvaticaConnection.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java b/avatica/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java
index 930b3f4..0533e7b 100644
--- a/avatica/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java
@@ -60,7 +60,13 @@ public enum BuiltInConnectionProperty implements ConnectionProperty {
       AvaticaHttpClientFactoryImpl.class.getName(), false),
 
   /** HttpClient implementation class name. */
-  HTTP_CLIENT_IMPL("httpclient_impl", Type.STRING, null, false);
+  HTTP_CLIENT_IMPL("httpclient_impl", Type.STRING, null, false),
+
+  /** Principal to use to perform Kerberos login. */
+  PRINCIPAL("principal", Type.STRING, null, false),
+
+  /** Keytab to use to perform Kerberos login. */
+  KEYTAB("keytab", Type.STRING, null, false);
 
   private final String camelName;
   private final Type type;

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java b/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java
index 9856452..cd48243 100644
--- a/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java
@@ -19,6 +19,8 @@ package org.apache.calcite.avatica;
 import org.apache.calcite.avatica.remote.AvaticaHttpClientFactory;
 import org.apache.calcite.avatica.remote.Service;
 
+import java.io.File;
+
 /**
  * Connection configuration.
  */
@@ -41,6 +43,10 @@ public interface ConnectionConfig {
   String avaticaPassword();
   AvaticaHttpClientFactory httpClientFactory();
   String httpClientClass();
+  /** @see BuiltInConnectionProperty#PRINCIPAL */
+  String kerberosPrincipal();
+  /** @see BuiltInConnectionProperty#KEYTAB */
+  File kerberosKeytab();
 }
 
 // End ConnectionConfig.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java b/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java
index 065bab9..ee47ebe 100644
--- a/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java
@@ -19,6 +19,7 @@ package org.apache.calcite.avatica;
 import org.apache.calcite.avatica.remote.AvaticaHttpClientFactory;
 import org.apache.calcite.avatica.remote.Service;
 
+import java.io.File;
 import java.math.BigDecimal;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -74,6 +75,23 @@ public class ConnectionConfigImpl implements ConnectionConfig {
     return BuiltInConnectionProperty.HTTP_CLIENT_IMPL.wrap(properties).getString();
   }
 
+  public String kerberosPrincipal() {
+    return BuiltInConnectionProperty.PRINCIPAL.wrap(properties).getString();
+  }
+
+  public File kerberosKeytab() {
+    String keytabPath = BuiltInConnectionProperty.KEYTAB.wrap(properties).getString();
+    if (null == keytabPath) {
+      return null;
+    }
+    File keytab = new File(keytabPath);
+    if (!keytab.exists() || !keytab.isFile()) {
+      throw new RuntimeException("The " + BuiltInConnectionProperty.KEYTAB.name() + " does not "
+          + " reference a normal, existent file: " + keytabPath);
+    }
+    return keytab;
+  }
+
   /** Converts a {@link Properties} object containing (name, value)
    * pairs into a map whose keys are
    * {@link org.apache.calcite.avatica.InternalProperty} objects.

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactory.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactory.java b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactory.java
index b5d213a..efb3c49 100644
--- a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactory.java
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactory.java
@@ -32,7 +32,7 @@ public interface AvaticaHttpClientFactory {
    * @param config Configuration to use when constructing the implementation.
    * @return An instance of {@link AvaticaHttpClient}.
    */
-  AvaticaHttpClient getClient(URL url, ConnectionConfig config);
+  AvaticaHttpClient getClient(URL url, ConnectionConfig config, KerberosConnection kerberosUtil);
 
 }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java
index 1d3ccec..cc83b00 100644
--- a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java
@@ -52,7 +52,8 @@ public class AvaticaHttpClientFactoryImpl implements AvaticaHttpClientFactory {
     return INSTANCE;
   }
 
-  @Override public AvaticaHttpClient getClient(URL url, ConnectionConfig config) {
+  @Override public AvaticaHttpClient getClient(URL url, ConnectionConfig config,
+      KerberosConnection kerberosUtil) {
     String className = config.httpClientClass();
     if (null == className) {
       // Provide an implementation that works with SPNEGO if that's the authentication is use.
@@ -64,6 +65,9 @@ public class AvaticaHttpClientFactoryImpl implements AvaticaHttpClientFactory {
     }
 
     AvaticaHttpClient client = instantiateClient(className, url);
+    if (null != kerberosUtil) {
+      client = new DoAsAvaticaHttpClient(client, kerberosUtil);
+    }
 
     if (client instanceof UsernamePasswordAuthenticateable) {
       // Shortcircuit quickly if authentication wasn't provided (implies NONE)

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/DoAsAvaticaHttpClient.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/DoAsAvaticaHttpClient.java b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/DoAsAvaticaHttpClient.java
new file mode 100644
index 0000000..e06760d
--- /dev/null
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/DoAsAvaticaHttpClient.java
@@ -0,0 +1,46 @@
+/*
+ * 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.calcite.avatica.remote;
+
+import java.security.PrivilegedAction;
+import java.util.Objects;
+
+import javax.security.auth.Subject;
+
+/**
+ * HTTP client implementation which invokes the wrapped HTTP client in a doAs with the provided
+ * Subject.
+ */
+public class DoAsAvaticaHttpClient implements AvaticaHttpClient {
+  private final AvaticaHttpClient wrapped;
+  private final KerberosConnection kerberosUtil;
+
+  public DoAsAvaticaHttpClient(AvaticaHttpClient wrapped, KerberosConnection kerberosUtil) {
+    this.wrapped = Objects.requireNonNull(wrapped);
+    this.kerberosUtil = Objects.requireNonNull(kerberosUtil);
+  }
+
+  @Override public byte[] send(final byte[] request) {
+    return Subject.doAs(kerberosUtil.getSubject(), new PrivilegedAction<byte[]>() {
+      @Override public byte[] run() {
+        return wrapped.send(request);
+      }
+    });
+  }
+}
+
+// End DoAsAvaticaHttpClient.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/Driver.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/Driver.java b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/Driver.java
index e98c486..86f0814 100644
--- a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/Driver.java
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/Driver.java
@@ -42,6 +42,7 @@ import java.util.Properties;
  */
 public class Driver extends UnregisteredDriver {
   private static final Logger LOG = LoggerFactory.getLogger(Driver.class);
+
   public static final String CONNECT_STRING_PREFIX = "jdbc:avatica:remote:";
 
   static {
@@ -83,10 +84,28 @@ public class Driver extends UnregisteredDriver {
 
   @Override public Meta createMeta(AvaticaConnection connection) {
     final ConnectionConfig config = connection.config();
+
+    // Perform the login and launch the renewal thread if necessary
+    final KerberosConnection kerberosUtil = createKerberosUtility(config);
+    if (null != kerberosUtil) {
+      kerberosUtil.login();
+      connection.setKerberosConnection(kerberosUtil);
+    }
+
+    // Create a single Service and set it on the Connection instance
     final Service service = createService(connection, config);
+    connection.setService(service);
     return new RemoteMeta(connection, service);
   }
 
+  KerberosConnection createKerberosUtility(ConnectionConfig config) {
+    final String principal = config.kerberosPrincipal();
+    if (null != principal) {
+      return new KerberosConnection(principal, config.kerberosKeytab());
+    }
+    return null;
+  }
+
   /**
    * Creates a {@link Service} with the given {@link AvaticaConnection} and configuration.
    *
@@ -137,7 +156,7 @@ public class Driver extends UnregisteredDriver {
 
     AvaticaHttpClientFactory httpClientFactory = config.httpClientFactory();
 
-    return httpClientFactory.getClient(url, config);
+    return httpClientFactory.getClient(url, config, connection.getKerberosConnection());
   }
 
   @Override public Connection connect(String url, Properties info)
@@ -148,9 +167,10 @@ public class Driver extends UnregisteredDriver {
       return null;
     }
 
-    // Create the corresponding remote connection
-    ConnectionConfig config = conn.config();
-    Service service = createService(conn, config);
+    Service service = conn.getService();
+
+    // super.connect(...) should be creating a service and setting it in the AvaticaConnection
+    assert null != service;
 
     service.apply(
         new Service.OpenConnectionRequest(conn.id,

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/KerberosConnection.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/KerberosConnection.java b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/KerberosConnection.java
new file mode 100644
index 0000000..da701dc
--- /dev/null
+++ b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/KerberosConnection.java
@@ -0,0 +1,400 @@
+/*
+ * 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.calcite.avatica.remote;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.security.Principal;
+import java.util.AbstractMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.kerberos.KerberosTicket;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+/**
+ * A utility to perform Kerberos logins and renewals.
+ */
+public class KerberosConnection {
+  private static final Logger LOG = LoggerFactory.getLogger(KerberosConnection.class);
+
+  private static final String IBM_KRB5_LOGIN_MODULE =
+      "com.ibm.security.auth.module.Krb5LoginModule";
+  private static final String SUN_KRB5_LOGIN_MODULE =
+      "com.sun.security.auth.module.Krb5LoginModule";
+  private static final String JAAS_CONF_NAME = "AvaticaKeytabConf";
+  private static final String RENEWAL_THREAD_NAME = "Avatica Kerberos Renewal Thread";
+
+  /** The percentage of the Kerberos ticket's lifetime which we should start trying to renew it */
+  public static final float PERCENT_OF_LIFETIME_TO_RENEW = 0.80f;
+  /** How long should we sleep between checks to renew the Kerberos ticket */
+  public static final long RENEWAL_PERIOD = 30L;
+
+  private final String principal;
+  private final Configuration jaasConf;
+  private Subject subject;
+  private RenewalTask renewalTask;
+  private Thread renewalThread;
+
+  /**
+   * Constructs an instance.
+   *
+   * @param principal The Kerberos principal
+   * @param keytab The keytab containing keys for the Kerberos principal
+   */
+  public KerberosConnection(String principal, File keytab) {
+    this.principal = Objects.requireNonNull(principal);
+    this.jaasConf = new KeytabJaasConf(principal, Objects.requireNonNull(keytab));
+  }
+
+  public synchronized Subject getSubject() {
+    return this.subject;
+  }
+
+  /**
+   * Perform a Kerberos login and launch a daemon thread to periodically perfrom renewals of that
+   * Kerberos login. Exceptions are intentionally caught and rethrown as unchecked exceptions as
+   * there is nothing Avatica itself can do if the Kerberos login fails.
+   *
+   * @throws RuntimeException If the Kerberos login fails
+   */
+  public synchronized void login() {
+    final Entry<LoginContext, Subject> securityMaterial = performKerberosLogin();
+    subject = securityMaterial.getValue();
+    // Launch a thread to periodically perform renewals
+    final Entry<RenewalTask, Thread> renewalMaterial = createRenewalThread(
+        securityMaterial.getKey(), subject, KerberosConnection.RENEWAL_PERIOD);
+    renewalTask = renewalMaterial.getKey();
+    renewalThread = renewalMaterial.getValue();
+    renewalThread.start();
+  }
+
+  /**
+   * Performs a Kerberos login given the {@code principal} and {@code keytab}.
+   *
+   * @return The {@code Subject} and {@code LoginContext} from the successful login.
+   * @throws RuntimeException if the login failed
+   */
+  Entry<LoginContext, Subject> performKerberosLogin() {
+    // Loosely based on Apache Kerby's JaasKrbUtil class
+    // Synchronized by the caller
+
+    // Create a KerberosPrincipal given the principal.
+    final Set<Principal> principals = new HashSet<Principal>();
+    principals.add(new KerberosPrincipal(principal));
+
+    final Subject subject = new Subject(false, principals, new HashSet<Object>(),
+        new HashSet<Object>());
+
+    try {
+      return login(null, jaasConf, subject);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to perform Kerberos login");
+    }
+  }
+
+  /**
+   * Performs a kerberos login, possibly logging out first.
+   *
+   * @param prevContext The LoginContext from the previous login, or null
+   * @param conf JAAS Configuration object
+   * @param subject The JAAS Subject
+   * @return The context and subject from the login
+   * @throws LoginException If the login failed.
+   */
+  Entry<LoginContext, Subject> login(LoginContext prevContext, Configuration conf,
+      Subject subject) throws LoginException {
+    // Is synchronized by the caller
+
+    // If a context was provided, perform a logout first
+    if (null != prevContext) {
+      prevContext.logout();
+    }
+
+    // Create a LoginContext given the Configuration and Subject
+    LoginContext loginContext = createLoginContext(conf);
+    // Invoke the login
+    loginContext.login();
+    // Get the Subject from the context and verify it's non-null (null would imply failure)
+    Subject loggedInSubject = loginContext.getSubject();
+    if (null == loggedInSubject) {
+      throw new RuntimeException("Failed to perform Kerberos login");
+    }
+
+    // Send it back to the caller to use with launchRenewalThread
+    return new AbstractMap.SimpleEntry<>(loginContext, loggedInSubject);
+  }
+
+  // Enables mocking for unit tests
+  LoginContext createLoginContext(Configuration conf) throws LoginException {
+    return new LoginContext(JAAS_CONF_NAME, subject, null, conf);
+  }
+
+  /**
+   * Launches a thread to periodically check the current ticket's lifetime and perform a relogin
+   * as necessary.
+   *
+   * @param originalContext The original login's context.
+   * @param originalSubject The original login's subject.
+   * @param renewalPeriod The amount of time to sleep inbetween checks to renew
+   */
+  Entry<RenewalTask, Thread> createRenewalThread(LoginContext originalContext,
+      Subject originalSubject, long renewalPeriod) {
+    RenewalTask task = new RenewalTask(this, originalContext, originalSubject, jaasConf,
+        renewalPeriod);
+    Thread t = new Thread(task);
+
+    // Don't prevent the JVM from existing
+    t.setDaemon(true);
+    // Log an error message if this thread somehow dies
+    t.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+      @Override public void uncaughtException(Thread t, Throwable e) {
+        LOG.error("Uncaught exception from Kerberos credential renewal thread", e);
+      }
+    });
+    t.setName(RENEWAL_THREAD_NAME);
+
+    return new AbstractMap.SimpleEntry<>(task, t);
+  }
+
+  /**
+   * Stops the Kerberos renewal thread if it is still running. If the thread was already started
+   * or never started, this method does nothing.
+   */
+  public void stopRenewalThread() {
+    if (null != renewalTask && null != renewalThread) {
+      LOG.debug("Informing RenewalTask to gracefully stop and interrupting the renewal thread.");
+      renewalTask.asyncStop();
+
+      long now = System.currentTimeMillis();
+      long until = now + 5000;
+      while (now < until) {
+        if (renewalThread.isAlive()) {
+          try {
+            Thread.sleep(500);
+          } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return;
+          }
+
+          now = System.currentTimeMillis();
+        } else {
+          break;
+        }
+      }
+
+      if (renewalThread.isAlive()) {
+        LOG.warn("Renewal thread failed to gracefully stop, interrupting it");
+        renewalThread.interrupt();
+        try {
+          renewalThread.join(5000);
+        } catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+        }
+      }
+
+      // What more could we do?
+      if (renewalThread.isAlive()) {
+        LOG.warn("Renewal thread failed to gracefully and ungracefully stop, proceeding.");
+      }
+
+      renewalTask = null;
+      renewalThread = null;
+    } else {
+      LOG.warn("Renewal thread was never started or already stopped.");
+    }
+  }
+
+  /**
+   * Runnable for performing Kerberos renewals.
+   */
+  static class RenewalTask implements Runnable {
+    private static final Logger RENEWAL_LOG = LoggerFactory.getLogger(RenewalTask.class);
+    // Mutable variables -- change as re-login occurs
+    private LoginContext context;
+    private Subject subject;
+    private final KerberosConnection utilInstance;
+    private final Configuration conf;
+    private final long renewalPeriod;
+    private final AtomicBoolean keepRunning = new AtomicBoolean(true);
+
+    public RenewalTask(KerberosConnection utilInstance, LoginContext context, Subject subject,
+        Configuration conf, long renewalPeriod) {
+      this.utilInstance = Objects.requireNonNull(utilInstance);
+      this.context = Objects.requireNonNull(context);
+      this.subject = Objects.requireNonNull(subject);
+      this.conf = Objects.requireNonNull(conf);
+      this.renewalPeriod = renewalPeriod;
+    }
+
+    @Override public void run() {
+      while (keepRunning.get() && !Thread.currentThread().isInterrupted()) {
+        RENEWAL_LOG.debug("Checking if Kerberos ticket should be renewed");
+        // The current time
+        final long now = System.currentTimeMillis();
+
+        // Find the TGT in the Subject for the principal we were given.
+        Set<KerberosTicket> tickets = subject.getPrivateCredentials(KerberosTicket.class);
+        KerberosTicket activeTicket = null;
+        for (KerberosTicket ticket : tickets) {
+          if (isTGSPrincipal(ticket.getServer())) {
+            activeTicket = ticket;
+            break;
+          }
+        }
+
+        // If we have no active ticket, immediately renew and check again to make sure we have
+        // a valid ticket now.
+        if (null == activeTicket) {
+          RENEWAL_LOG.debug("There is no active Kerberos ticket, renewing now");
+          renew();
+          continue;
+        }
+
+        // Only renew when we hit a certain threshold of the current ticket's lifetime.
+        // We want to limit the number of renewals we have to invoke.
+        if (shouldRenew(activeTicket.getStartTime().getTime(),
+            activeTicket.getEndTime().getTime(), now)) {
+          RENEWAL_LOG.debug("The current ticket should be renewed now");
+          renew();
+        }
+
+        // Sleep until we check again
+        waitForNextCheck(renewalPeriod);
+      }
+    }
+
+    /**
+     * Computes whether or not the ticket should be renewed based on the lifetime of the ticket
+     * and the current time.
+     *
+     * @param start The start time of the ticket's validity in millis
+     * @param end The end time of the ticket's validity in millis
+     * @param now Milliseconds since the epoch
+     * @return True if renewal should occur, false otherwise
+     */
+    boolean shouldRenew(final long start, final long end, long now) {
+      final long lifetime = end - start;
+      final long renewAfter = start + (long) (lifetime * PERCENT_OF_LIFETIME_TO_RENEW);
+      return now >= renewAfter;
+    }
+
+    /**
+     * Logout and log back in with the Kerberos identity.
+     */
+    void renew() {
+      try {
+        // Lock on the instance of KerberosUtil
+        synchronized (utilInstance) {
+          Entry<LoginContext, Subject> pair = utilInstance.login(context, conf, subject);
+          context = pair.getKey();
+          subject = pair.getValue();
+        }
+      } catch (Exception e) {
+        throw new RuntimeException("Failed to perform kerberos login");
+      }
+    }
+
+    /**
+     * Wait the given amount of time.
+     *
+     * @param renewalPeriod The number of milliseconds to wait
+     */
+    void waitForNextCheck(long renewalPeriod) {
+      try {
+        Thread.sleep(renewalPeriod);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        return;
+      }
+    }
+
+    void asyncStop() {
+      keepRunning.set(false);
+    }
+  }
+
+  /**
+   * Computes if the given {@code principal} is the ticket-granting system's principal ("krbtgt").
+   *
+   * @param principal A {@link KerberosPrincipal}.
+   * @return True if {@code principal} is the TGS principal, false otherwise.
+   */
+  static boolean isTGSPrincipal(KerberosPrincipal principal) {
+    if (principal == null) {
+      return false;
+    }
+
+    if (principal.getName().equals("krbtgt/" + principal.getRealm() + "@" + principal.getRealm())) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Javax Configuration for performing a keytab-based Kerberos login.
+   */
+  static class KeytabJaasConf extends Configuration {
+    private String principal;
+    private File keytabFile;
+
+    KeytabJaasConf(String principal, File keytab) {
+      this.principal = principal;
+      this.keytabFile = keytab;
+    }
+
+    @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+      HashMap<String, String> options = new HashMap<String, String>();
+      options.put("keyTab", keytabFile.getAbsolutePath());
+      options.put("principal", principal);
+      options.put("useKeyTab", "true");
+      options.put("storeKey", "true");
+      options.put("doNotPrompt", "true");
+      options.put("renewTGT", "false");
+      options.put("refreshKrb5Config", "true");
+      options.put("isInitiator", "true");
+
+      return new AppConfigurationEntry[] {new AppConfigurationEntry(getKrb5LoginModuleName(),
+          AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)};
+    }
+  }
+
+  /**
+   * Returns the KRB5 LoginModule implementation. This is JVM-vendor dependent.
+   *
+   * @return The class name of the KRB5 LoginModule
+   */
+  static String getKrb5LoginModuleName() {
+    return System.getProperty("java.vendor").contains("IBM") ? IBM_KRB5_LOGIN_MODULE
+        : SUN_KRB5_LOGIN_MODULE;
+  }
+}
+
+// End KerberosConnection.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryTest.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryTest.java b/avatica/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryTest.java
index cd64329..8e1397c 100644
--- a/avatica/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryTest.java
+++ b/avatica/core/src/test/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryTest.java
@@ -38,7 +38,7 @@ public class AvaticaHttpClientFactoryTest {
     ConnectionConfig config = new ConnectionConfigImpl(props);
     AvaticaHttpClientFactory httpClientFactory = new AvaticaHttpClientFactoryImpl();
 
-    AvaticaHttpClient client = httpClientFactory.getClient(url, config);
+    AvaticaHttpClient client = httpClientFactory.getClient(url, config, null);
     assertTrue("Client was an instance of " + client.getClass(),
         client instanceof AvaticaCommonsHttpClientImpl);
   }
@@ -51,7 +51,7 @@ public class AvaticaHttpClientFactoryTest {
     ConnectionConfig config = new ConnectionConfigImpl(props);
     AvaticaHttpClientFactory httpClientFactory = new AvaticaHttpClientFactoryImpl();
 
-    AvaticaHttpClient client = httpClientFactory.getClient(url, config);
+    AvaticaHttpClient client = httpClientFactory.getClient(url, config, null);
     assertTrue("Client was an instance of " + client.getClass(),
         client instanceof AvaticaHttpClientImpl);
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/core/src/test/java/org/apache/calcite/avatica/remote/KerberosConnectionTest.java
----------------------------------------------------------------------
diff --git a/avatica/core/src/test/java/org/apache/calcite/avatica/remote/KerberosConnectionTest.java b/avatica/core/src/test/java/org/apache/calcite/avatica/remote/KerberosConnectionTest.java
new file mode 100644
index 0000000..e27676b
--- /dev/null
+++ b/avatica/core/src/test/java/org/apache/calcite/avatica/remote/KerberosConnectionTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.calcite.avatica.remote;
+
+import org.apache.calcite.avatica.remote.KerberosConnection.RenewalTask;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.util.Map.Entry;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+
+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.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test case for KerberosConnection
+ */
+public class KerberosConnectionTest {
+
+  @Test(expected = NullPointerException.class) public void testNullArgs() {
+    new KerberosConnection(null, null);
+  }
+
+  @Test public void testThreadConfiguration() {
+    KerberosConnection krbUtil = new KerberosConnection("foo", new File("/bar.keytab"));
+    Subject subject = new Subject();
+    LoginContext context = Mockito.mock(LoginContext.class);
+
+    Entry<RenewalTask, Thread> entry = krbUtil.createRenewalThread(context, subject, 10);
+    assertNotNull("RenewalTask should not be null", entry.getKey());
+    Thread t = entry.getValue();
+    assertTrue("Thread name should contain 'Avatica', but is '" + t.getName() + "'",
+        t.getName().contains("Avatica"));
+    assertTrue(t.isDaemon());
+    assertNotNull(t.getUncaughtExceptionHandler());
+  }
+
+  @Test public void noPreviousContextOnLogin() throws Exception {
+    KerberosConnection krbUtil = mock(KerberosConnection.class);
+    Subject subject = new Subject();
+    Subject loggedInSubject = new Subject();
+    Configuration conf = mock(Configuration.class);
+    LoginContext context = mock(LoginContext.class);
+
+    // Call the real login(LoginContext, Configuration, Subject) method
+    when(krbUtil.login(any(LoginContext.class), any(Configuration.class), any(Subject.class)))
+        .thenCallRealMethod();
+    // Return a fake LoginContext
+    when(krbUtil.createLoginContext(conf)).thenReturn(context);
+    // Return a fake Subject from that fake LoginContext
+    when(context.getSubject()).thenReturn(loggedInSubject);
+
+    Entry<LoginContext, Subject> pair = krbUtil.login(null, conf, subject);
+
+    // Verify we get the fake LoginContext and Subject
+    assertEquals(context, pair.getKey());
+    assertEquals(loggedInSubject, pair.getValue());
+
+    // login should be called on the LoginContext
+    verify(context).login();
+  }
+
+  @Test public void previousContextLoggedOut() throws Exception {
+    KerberosConnection krbUtil = mock(KerberosConnection.class);
+    Subject subject = new Subject();
+    Subject loggedInSubject = new Subject();
+    Configuration conf = mock(Configuration.class);
+    LoginContext originalContext = mock(LoginContext.class);
+    LoginContext context = mock(LoginContext.class);
+
+    // Call the real login(LoginContext, Configuration, Subject) method
+    when(krbUtil.login(any(LoginContext.class), any(Configuration.class), any(Subject.class)))
+        .thenCallRealMethod();
+    // Return a fake LoginContext
+    when(krbUtil.createLoginContext(conf)).thenReturn(context);
+    // Return a fake Subject from that fake LoginContext
+    when(context.getSubject()).thenReturn(loggedInSubject);
+
+    Entry<LoginContext, Subject> pair = krbUtil.login(originalContext, conf, subject);
+
+    // Verify we get the fake LoginContext and Subject
+    assertEquals(context, pair.getKey());
+    assertEquals(loggedInSubject, pair.getValue());
+
+    verify(originalContext).logout();
+
+    // login should be called on the LoginContext
+    verify(context).login();
+  }
+
+  @Test public void testTicketRenewalTime() {
+    RenewalTask renewal = mock(RenewalTask.class);
+    when(renewal.shouldRenew(any(long.class), any(long.class), any(long.class)))
+        .thenCallRealMethod();
+
+    long start = 0;
+    long end = 200;
+    long now = 100;
+    assertFalse(renewal.shouldRenew(start, end, now));
+
+    // Renewal should happen at 80%
+    start = 0;
+    end = 100;
+    now = 80;
+    assertTrue(renewal.shouldRenew(start, end, now));
+
+    start = 5000;
+    // One day
+    end = start + 1000 * 60 * 60 * 24;
+    // Ten minutes prior to expiration
+    now = end - 1000 * 60 * 10;
+    assertTrue(String.format("start=%d, end=%d, now=%d", start, end, now),
+        renewal.shouldRenew(start, end, now));
+  }
+}
+
+// End KerberosConnectionTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java b/avatica/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java
index 39de6fe..573eb6e 100644
--- a/avatica/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java
@@ -27,7 +27,6 @@ import org.apache.kerby.kerberos.kerb.client.KrbConfig;
 import org.apache.kerby.kerberos.kerb.client.KrbConfigKey;
 import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
 
-import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -60,6 +59,7 @@ public class AvaticaSpnegoTest {
   private static final Logger LOG = LoggerFactory.getLogger(AvaticaSpnegoTest.class);
 
   private static final ConnectionSpec CONNECTION_SPEC = ConnectionSpec.HSQLDB;
+  private static final List<HttpServer> SERVERS_TO_STOP = new ArrayList<>();
 
   private static SimpleKdcServer kdc;
   private static KrbConfig clientConfig;
@@ -110,11 +110,15 @@ public class AvaticaSpnegoTest {
 
     // Kerby sets "java.security.krb5.conf" for us!
     System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
-    // System.setProperty("sun.security.spnego.debug", "true");
-    // System.setProperty("sun.security.krb5.debug", "true");
+    //System.setProperty("sun.security.spnego.debug", "true");
+    //System.setProperty("sun.security.krb5.debug", "true");
   }
 
   @AfterClass public static void stopKdc() throws Exception {
+    for (HttpServer server : SERVERS_TO_STOP) {
+      server.stop();
+    }
+
     if (isKdcStarted) {
       LOG.info("Stopping KDC on {}", kdcPort);
       kdc.stop();
@@ -164,31 +168,24 @@ public class AvaticaSpnegoTest {
           .withHandler(localService, serialization)
           .build();
       httpServer.start();
+      SERVERS_TO_STOP.add(httpServer);
 
       final String url = "jdbc:avatica:remote:url=http://" + SpnegoTestUtil.KDC_HOST + ":"
           + httpServer.getPort() + ";authentication=SPNEGO;serialization=" + serialization;
       LOG.info("JDBC URL {}", url);
 
-      parameters.add(new Object[] {httpServer, url});
+      parameters.add(new Object[] {url});
     }
 
     return parameters;
   }
 
-  private final HttpServer httpServer;
   private final String jdbcUrl;
 
-  public AvaticaSpnegoTest(HttpServer httpServer, String jdbcUrl) {
-    this.httpServer = Objects.requireNonNull(httpServer);
+  public AvaticaSpnegoTest(String jdbcUrl) {
     this.jdbcUrl = Objects.requireNonNull(jdbcUrl);
   }
 
-  @After public void stopHttpServer() {
-    if (null != httpServer) {
-      httpServer.stop();
-    }
-  }
-
   @Test public void testAuthenticatedClient() throws Exception {
     ConnectionSpec.getDatabaseLock().lock();
     try {
@@ -222,6 +219,26 @@ public class AvaticaSpnegoTest {
       ConnectionSpec.getDatabaseLock().unlock();
     }
   }
+
+  @Test public void testAutomaticLogin() throws Exception {
+    final String tableName = "automaticAllowedClients";
+    // Avatica should log in for us with this info
+    String url = jdbcUrl + ";principal=" + SpnegoTestUtil.CLIENT_PRINCIPAL + ";keytab="
+        + clientKeytab;
+    LOG.info("Updated JDBC url: {}", url);
+    try (Connection conn = DriverManager.getConnection(url);
+        Statement stmt = conn.createStatement()) {
+      assertFalse(stmt.execute("DROP TABLE IF EXISTS " + tableName));
+      assertFalse(stmt.execute("CREATE TABLE " + tableName + "(pk integer)"));
+      assertEquals(1, stmt.executeUpdate("INSERT INTO " + tableName + " VALUES(1)"));
+      assertEquals(1, stmt.executeUpdate("INSERT INTO " + tableName + " VALUES(2)"));
+      assertEquals(1, stmt.executeUpdate("INSERT INTO " + tableName + " VALUES(3)"));
+
+      ResultSet results = stmt.executeQuery("SELECT count(1) FROM " + tableName);
+      assertTrue(results.next());
+      assertEquals(3, results.getInt(1));
+    }
+  }
 }
 
 // End AvaticaSpnegoTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java b/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java
index 6f4c51e..d0c10c6 100644
--- a/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java
@@ -139,7 +139,9 @@ public class AlternatingRemoteMetaTest {
 
     @Override public Meta createMeta(AvaticaConnection connection) {
       final ConnectionConfig config = connection.config();
-      return new RemoteMeta(connection, new RemoteService(getHttpClient(connection, config)));
+      final Service service = new RemoteService(getHttpClient(connection, config));
+      connection.setService(service);
+      return new RemoteMeta(connection, service);
     }
 
     @Override AvaticaHttpClient getHttpClient(AvaticaConnection connection,

http://git-wip-us.apache.org/repos/asf/calcite/blob/b7fbb35b/avatica/site/_docs/client_reference.md
----------------------------------------------------------------------
diff --git a/avatica/site/_docs/client_reference.md b/avatica/site/_docs/client_reference.md
index 65cf6f4..3024ca6 100644
--- a/avatica/site/_docs/client_reference.md
+++ b/avatica/site/_docs/client_reference.md
@@ -132,3 +132,25 @@ on-hover images for the permalink, but oh well.
 : _Default_: `null`.
 
 : _Required_: No.
+
+<strong><a name="principal" href="#principal">principal</a></strong>
+
+: _Description_: The Kerberos principal which can be used by the Avatica JDBC Driver
+  to automatically perform a Kerberos login before attempting to contact the Avatica
+  server. If this property is provided, it is also expected that `keytab` is provided
+  and that the Avatica server is configured for SPNEGO authentication. Users can perform
+  their own Kerberos login; this option is provided only as a convenience.
+
+: _Default_: `null`.
+
+: _Required_: No.
+
+<strong><a name="keytab" href="#keytab">keytab</a></strong>
+
+: _Description_: The Kerberos keytab which contains the secret material to perform
+  a Kerberos login with the `principal`. The value should be a path on the local
+  filesystem to a regular file.
+
+: _Default_: `null`.
+
+: _Required_: No.