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.