calcite-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From els...@apache.org
Subject [1/3] calcite git commit: [CALCITE-1159] Kerberos-based client authentication via SPNEGO
Date Thu, 24 Mar 2016 18:49:23 GMT
Repository: calcite
Updated Branches:
  refs/heads/master 5dfa3f1ec -> 48d8ebf57


http://git-wip-us.apache.org/repos/asf/calcite/blob/406372f1/avatica/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java b/avatica/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java
new file mode 100644
index 0000000..5c5b1ef
--- /dev/null
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java
@@ -0,0 +1,208 @@
+/*
+ * 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;
+
+import org.apache.calcite.avatica.remote.Service.RpcMetadataResponse;
+import org.apache.calcite.avatica.server.AvaticaHandler;
+
+import org.apache.kerby.kerberos.kerb.KrbException;
+import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
+
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.handler.DefaultHandler;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.security.AccessController;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+
+import javax.security.auth.login.Configuration;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Utility class for setting up SPNEGO
+ */
+public class SpnegoTestUtil {
+
+  public static final String JGSS_KERBEROS_TICKET_OID = "1.2.840.113554.1.2.2";
+
+  public static final String REALM = "EXAMPLE.COM";
+  public static final String KDC_HOST = "localhost";
+  public static final String CLIENT_PRINCIPAL = "client@" + REALM;
+  public static final String SERVER_PRINCIPAL = "HTTP/" + KDC_HOST + "@" + REALM;
+
+  private SpnegoTestUtil() {}
+
+  public static int getFreePort() throws IOException {
+    ServerSocket s = new ServerSocket(0);
+    try {
+      s.setReuseAddress(true);
+      int port = s.getLocalPort();
+      return port;
+    } finally {
+      if (null != s) {
+        s.close();
+      }
+    }
+  }
+
+  public static void setupUser(SimpleKdcServer kdc, File keytab, String principal)
+      throws KrbException {
+    kdc.createPrincipal(principal);
+    kdc.exportPrincipal(principal, keytab);
+  }
+
+  /**
+   * Recursively deletes a {@link File}.
+   */
+  public static void deleteRecursively(File d) {
+    if (d.isDirectory()) {
+      for (String name : d.list()) {
+        File child = new File(d, name);
+        if (child.isFile()) {
+          child.delete();
+        } else {
+          deleteRecursively(d);
+        }
+      }
+    }
+    d.delete();
+  }
+
+  /**
+   * Creates the SPNEGO JAAS configuration file for the Jetty server
+   */
+  public static void writeSpnegoConf(File configFile, File serverKeytab)
+      throws Exception {
+    try (BufferedWriter writer = new BufferedWriter(new FileWriter(configFile))) {
+      // Server login
+      writer.write("com.sun.security.jgss.accept {\n");
+      writer.write(" com.sun.security.auth.module.Krb5LoginModule required\n");
+      writer.write(" principal=\"" + SERVER_PRINCIPAL + "\"\n");
+      writer.write(" useKeyTab=true\n");
+      writer.write(" keyTab=\"" + serverKeytab + "\"\n");
+      writer.write(" storeKey=true \n");
+      // Some extra debug information from JAAS
+      //writer.write(" debug=true\n");
+      writer.write(" isInitiator=false;\n");
+      writer.write("};\n");
+    }
+  }
+
+  public static void refreshJaasConfiguration() {
+    // This is *extremely* important to make sure we get the right Configuration instance.
+    // Configuration keeps a static instance of Configuration that it will return once it
+    // has been initialized. We need to nuke that static instance to make sure our
+    // serverSpnegoConfigFile gets read.
+    AccessController.doPrivileged(new PrivilegedAction<Configuration>() {
+      public Configuration run() {
+        return Configuration.getConfiguration();
+      }
+    }).refresh();
+  }
+
+  /**
+   * A simple handler which returns "OK " with the client's authenticated name and HTTP/200
or
+   * HTTP/401 and the message "Not authenticated!".
+   */
+  public static class AuthenticationRequiredAvaticaHandler implements AvaticaHandler {
+    private final Handler handler = new DefaultHandler();
+
+    @Override public void handle(String target, Request baseRequest, HttpServletRequest request,
+        HttpServletResponse response) throws IOException, ServletException {
+      Authentication auth = baseRequest.getAuthentication();
+      if (Authentication.UNAUTHENTICATED == auth) {
+        throw new AssertionError("Unauthenticated users should not reach here!");
+      }
+
+      baseRequest.setHandled(true);
+      UserAuthentication userAuth = (UserAuthentication) auth;
+      UserIdentity userIdentity = userAuth.getUserIdentity();
+      Principal userPrincipal = userIdentity.getUserPrincipal();
+
+      response.getWriter().print("OK " + userPrincipal.getName());
+      response.setStatus(200);
+    }
+
+    @Override public void setServer(Server server) {
+      handler.setServer(server);
+    }
+
+    @Override public Server getServer() {
+      return handler.getServer();
+    }
+
+    @Override public void destroy() {
+      handler.destroy();
+    }
+
+    @Override public void start() throws Exception {
+      handler.start();
+    }
+
+    @Override public void stop() throws Exception {
+      handler.stop();
+    }
+
+    @Override public boolean isRunning() {
+      return handler.isRunning();
+    }
+
+    @Override public boolean isStarted() {
+      return handler.isStarted();
+    }
+
+    @Override public boolean isStarting() {
+      return handler.isStarting();
+    }
+
+    @Override public boolean isStopping() {
+      return handler.isStopping();
+    }
+
+    @Override public boolean isStopped() {
+      return handler.isStopped();
+    }
+
+    @Override public boolean isFailed() {
+      return handler.isFailed();
+    }
+
+    @Override public void addLifeCycleListener(Listener listener) {
+      handler.addLifeCycleListener(listener);
+    }
+
+    @Override public void removeLifeCycleListener(Listener listener) {
+      handler.removeLifeCycleListener(listener);
+    }
+
+    @Override public void setServerRpcMetadata(RpcMetadataResponse metadata) {}
+  }
+}
+
+// End SpnegoTestUtil.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/406372f1/avatica/server/src/test/java/org/apache/calcite/avatica/server/AbstractAvaticaHandlerTest.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/server/AbstractAvaticaHandlerTest.java
b/avatica/server/src/test/java/org/apache/calcite/avatica/server/AbstractAvaticaHandlerTest.java
new file mode 100644
index 0000000..0260a74
--- /dev/null
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/server/AbstractAvaticaHandlerTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.server;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.HttpURLConnection;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for logic common to all {@link AvaticaHandler}'s.
+ */
+public class AbstractAvaticaHandlerTest {
+
+  private AbstractAvaticaHandler handler;
+  private AvaticaServerConfiguration config;
+  private HttpServletRequest request;
+  private HttpServletResponse response;
+
+  @Before public void setup() throws Exception {
+    handler = mock(AbstractAvaticaHandler.class);
+    config = mock(AvaticaServerConfiguration.class);
+    request = mock(HttpServletRequest.class);
+    response = mock(HttpServletResponse.class);
+    when(handler.isUserPermitted(config, request, response)).thenCallRealMethod();
+  }
+
+  @Test public void disallowUnauthenticatedUsers() throws Exception {
+    ServletOutputStream os = mock(ServletOutputStream.class);
+
+    when(config.getAuthenticationType()).thenReturn(AuthenticationType.SPNEGO);
+    when(request.getRemoteUser()).thenReturn(null);
+    when(response.getOutputStream()).thenReturn(os);
+
+    assertFalse(handler.isUserPermitted(config, request, response));
+
+    verify(response).setStatus(HttpURLConnection.HTTP_UNAUTHORIZED);
+    // Make sure that the serialized ErrorMessage looks reasonable
+    verify(os).write(argThat(new BaseMatcher<byte[]>() {
+      @Override public void describeTo(Description description) {
+        String desc = "A serialized ErrorMessage which contains 'User is not authenticated'";
+        description.appendText(desc);
+      }
+
+      @Override public boolean matches(Object item) {
+        String msg = new String((byte[]) item);
+        return msg.contains("User is not authenticated");
+      }
+
+      @Override public void describeMismatch(Object item, Description mismatchDescription)
{
+        mismatchDescription.appendText("The message should contain 'User is not authenticated'");
+      }
+    }));
+  }
+
+  @Test public void allowAuthenticatedUsers() throws Exception {
+    when(config.getAuthenticationType()).thenReturn(AuthenticationType.SPNEGO);
+    when(request.getRemoteUser()).thenReturn("user1");
+    assertTrue(handler.isUserPermitted(config, request, response));
+  }
+
+  @Test public void allowAllUsersWhenNoAuthenticationIsNeeded() throws Exception {
+    when(config.getAuthenticationType()).thenReturn(AuthenticationType.NONE);
+    when(request.getRemoteUser()).thenReturn(null);
+    assertTrue(handler.isUserPermitted(config, request, response));
+
+    when(request.getRemoteUser()).thenReturn("user1");
+    assertTrue(handler.isUserPermitted(config, request, response));
+  }
+}
+
+// End AbstractAvaticaHandlerTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/406372f1/avatica/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithJaasTest.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithJaasTest.java
b/avatica/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithJaasTest.java
new file mode 100644
index 0000000..e9c5299
--- /dev/null
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithJaasTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.server;
+
+import org.apache.calcite.avatica.SpnegoTestUtil;
+import org.apache.calcite.avatica.remote.AvaticaCommonsHttpClientSpnegoImpl;
+
+import org.apache.kerby.kerberos.kerb.KrbException;
+import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
+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.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosTicket;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for SPNEGO with Kerberos. Purely testing SPNEGO, not the Avatica "protocol"
on top
+ * of that HTTP. This variant of the test requires that the user use JAAS configuration to
+ * perform server-side login.
+ */
+public class HttpServerSpnegoWithJaasTest {
+  private static final Logger LOG = LoggerFactory.getLogger(HttpServerSpnegoWithJaasTest.class);
+
+  private static SimpleKdcServer kdc;
+  private static HttpServer httpServer;
+
+  private static KrbConfig clientConfig;
+
+  private static int kdcPort;
+
+  private static File clientKeytab;
+  private static File serverKeytab;
+
+  private static File serverSpnegoConfigFile;
+
+  private static boolean isKdcStarted = false;
+  private static boolean isHttpServerStarted = false;
+
+  private static URL httpServerUrl;
+
+  @BeforeClass public static void setupKdc() throws Exception {
+    kdc = new SimpleKdcServer();
+    File target = new File(System.getProperty("user.dir"), "target");
+    assertTrue(target.exists());
+
+    File kdcDir = new File(target, HttpServerSpnegoWithJaasTest.class.getSimpleName());
+    if (kdcDir.exists()) {
+      SpnegoTestUtil.deleteRecursively(kdcDir);
+    }
+    kdcDir.mkdirs();
+    kdc.setWorkDir(kdcDir);
+
+    kdc.setKdcHost(SpnegoTestUtil.KDC_HOST);
+    kdcPort = SpnegoTestUtil.getFreePort();
+    kdc.setAllowTcp(true);
+    kdc.setAllowUdp(false);
+    kdc.setKdcTcpPort(kdcPort);
+
+    LOG.info("Starting KDC server at {}:{}", SpnegoTestUtil.KDC_HOST, kdcPort);
+
+    kdc.init();
+    kdc.start();
+    isKdcStarted = true;
+
+    File keytabDir = new File(target, HttpServerSpnegoWithJaasTest.class.getSimpleName()
+        + "_keytabs");
+    if (keytabDir.exists()) {
+      SpnegoTestUtil.deleteRecursively(keytabDir);
+    }
+    keytabDir.mkdirs();
+    setupUsers(keytabDir);
+
+    clientConfig = new KrbConfig();
+    clientConfig.setString(KrbConfigKey.KDC_HOST, SpnegoTestUtil.KDC_HOST);
+    clientConfig.setInt(KrbConfigKey.KDC_TCP_PORT, kdcPort);
+    clientConfig.setString(KrbConfigKey.DEFAULT_REALM, SpnegoTestUtil.REALM);
+
+    serverSpnegoConfigFile = new File(kdcDir, "server-spnego.conf");
+    SpnegoTestUtil.writeSpnegoConf(serverSpnegoConfigFile, serverKeytab);
+
+    // Kerby sets "java.security.krb5.conf" for us!
+    System.setProperty("java.security.auth.login.config", serverSpnegoConfigFile.toString());
+    // http://docs.oracle.com/javase/7/docs/technotes/guides/security/jgss/...
+    //    tutorials/BasicClientServer.html#useSub
+    System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
+    //System.setProperty("sun.security.spnego.debug", "true");
+    //System.setProperty("sun.security.krb5.debug", "true");
+
+    // Create and start an HTTP server configured only to allow SPNEGO requests
+    // We're not using `withAutomaticLogin(File)` which means we're relying on JAAS to log
the
+    // server in.
+    httpServer = new HttpServer.Builder()
+        .withPort(0)
+        .withSpnego(SpnegoTestUtil.SERVER_PRINCIPAL, SpnegoTestUtil.REALM)
+        .withHandler(new SpnegoTestUtil.AuthenticationRequiredAvaticaHandler())
+        .build();
+    httpServer.start();
+    isHttpServerStarted = true;
+
+    httpServerUrl = new URL("http://" + SpnegoTestUtil.KDC_HOST + ":" + httpServer.getPort());
+    LOG.info("HTTP server running at {}", httpServerUrl);
+
+    SpnegoTestUtil.refreshJaasConfiguration();
+  }
+
+  @AfterClass public static void stopKdc() throws Exception {
+    if (isHttpServerStarted) {
+      LOG.info("Stopping HTTP server at {}", httpServerUrl);
+      httpServer.stop();
+    }
+
+    if (isKdcStarted) {
+      LOG.info("Stopping KDC on {}", kdcPort);
+      kdc.stop();
+    }
+  }
+
+  private static void setupUsers(File keytabDir) throws KrbException {
+    String clientPrincipal = SpnegoTestUtil.CLIENT_PRINCIPAL.substring(0,
+        SpnegoTestUtil.CLIENT_PRINCIPAL.indexOf('@'));
+    clientKeytab = new File(keytabDir, clientPrincipal.replace('/', '_') + ".keytab");
+    if (clientKeytab.exists()) {
+      SpnegoTestUtil.deleteRecursively(clientKeytab);
+    }
+    LOG.info("Creating {} with keytab {}", clientPrincipal, clientKeytab);
+    SpnegoTestUtil.setupUser(kdc, clientKeytab, clientPrincipal);
+
+    String serverPrincipal = SpnegoTestUtil.SERVER_PRINCIPAL.substring(0,
+        SpnegoTestUtil.SERVER_PRINCIPAL.indexOf('@'));
+    serverKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
+    if (serverKeytab.exists()) {
+      SpnegoTestUtil.deleteRecursively(serverKeytab);
+    }
+    LOG.info("Creating {} with keytab {}", SpnegoTestUtil.SERVER_PRINCIPAL, serverKeytab);
+    SpnegoTestUtil.setupUser(kdc, serverKeytab, SpnegoTestUtil.SERVER_PRINCIPAL);
+  }
+
+  @Test public void testNormalClientsDisallowed() throws Exception {
+    LOG.info("Connecting to {}", httpServerUrl.toString());
+    HttpURLConnection conn = (HttpURLConnection) httpServerUrl.openConnection();
+    conn.setRequestMethod("GET");
+    // Authentication should fail because we didn't provide anything
+    assertEquals(401, conn.getResponseCode());
+  }
+
+  @Test public void testAuthenticatedClientsAllowed() throws Exception {
+    // Create the subject for the client
+    final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(SpnegoTestUtil.CLIENT_PRINCIPAL,
+        clientKeytab);
+    final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
+    // Make sure the subject has a principal
+    assertFalse(clientPrincipals.isEmpty());
+
+    // Get a TGT for the subject (might have many, different encryption types). The first
should
+    // be the default encryption type.
+    Set<KerberosTicket> privateCredentials =
+            clientSubject.getPrivateCredentials(KerberosTicket.class);
+    assertFalse(privateCredentials.isEmpty());
+    KerberosTicket tgt = privateCredentials.iterator().next();
+    assertNotNull(tgt);
+    LOG.info("Using TGT with etype: {}", tgt.getSessionKey().getAlgorithm());
+
+    // The name of the principal
+    final String principalName = clientPrincipals.iterator().next().getName();
+
+    // Run this code, logged in as the subject (the client)
+    byte[] response = Subject.doAs(clientSubject, new PrivilegedExceptionAction<byte[]>()
{
+      @Override public byte[] run() throws Exception {
+        // Logs in with Kerberos via GSS
+        GSSManager gssManager = GSSManager.getInstance();
+        Oid oid = new Oid(SpnegoTestUtil.JGSS_KERBEROS_TICKET_OID);
+        GSSName gssClient = gssManager.createName(principalName, GSSName.NT_USER_NAME);
+        GSSCredential credential = gssManager.createCredential(gssClient,
+            GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
+
+        // Passes the GSSCredential into the HTTP client implementation
+        final AvaticaCommonsHttpClientSpnegoImpl httpClient =
+            new AvaticaCommonsHttpClientSpnegoImpl(httpServerUrl, credential);
+
+        return httpClient.send(new byte[0]);
+      }
+    });
+
+    // We should get a response which is "OK" with our client's name
+    assertNotNull(response);
+    assertEquals("OK " + SpnegoTestUtil.CLIENT_PRINCIPAL, new String(response));
+  }
+}
+
+// End HttpServerSpnegoWithJaasTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/406372f1/avatica/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java
b/avatica/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java
new file mode 100644
index 0000000..1f234d2
--- /dev/null
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.server;
+
+import org.apache.calcite.avatica.SpnegoTestUtil;
+import org.apache.calcite.avatica.remote.AvaticaCommonsHttpClientSpnegoImpl;
+
+import org.apache.kerby.kerberos.kerb.KrbException;
+import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
+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.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosTicket;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for SPNEGO with Kerberos. Purely testing SPNEGO, not the Avatica "protocol"
on top
+ * of that HTTP. This variant of the test relies on the "feature" Avatica provides to not
require
+ * JAAS configuration by the user.
+ */
+public class HttpServerSpnegoWithoutJaasTest {
+  private static final Logger LOG = LoggerFactory.getLogger(HttpServerSpnegoWithoutJaasTest.class);
+
+  private static SimpleKdcServer kdc;
+  private static HttpServer httpServer;
+
+  private static KrbConfig clientConfig;
+
+  private static int kdcPort;
+
+  private static File clientKeytab;
+  private static File serverKeytab;
+
+  private static boolean isKdcStarted = false;
+  private static boolean isHttpServerStarted = false;
+
+  private static URL httpServerUrl;
+
+  @BeforeClass public static void setupKdc() throws Exception {
+    kdc = new SimpleKdcServer();
+    File target = new File(System.getProperty("user.dir"), "target");
+    assertTrue(target.exists());
+
+    File kdcDir = new File(target, HttpServerSpnegoWithoutJaasTest.class.getSimpleName());
+    if (kdcDir.exists()) {
+      SpnegoTestUtil.deleteRecursively(kdcDir);
+    }
+    kdcDir.mkdirs();
+    kdc.setWorkDir(kdcDir);
+
+    kdc.setKdcHost(SpnegoTestUtil.KDC_HOST);
+    kdcPort = SpnegoTestUtil.getFreePort();
+    kdc.setAllowTcp(true);
+    kdc.setAllowUdp(false);
+    kdc.setKdcTcpPort(kdcPort);
+
+    LOG.info("Starting KDC server at {}:{}", SpnegoTestUtil.KDC_HOST, kdcPort);
+
+    kdc.init();
+    kdc.start();
+    isKdcStarted = true;
+
+    File keytabDir = new File(target, HttpServerSpnegoWithoutJaasTest.class.getSimpleName()
+        + "_keytabs");
+    if (keytabDir.exists()) {
+      SpnegoTestUtil.deleteRecursively(keytabDir);
+    }
+    keytabDir.mkdirs();
+    setupUsers(keytabDir);
+
+    clientConfig = new KrbConfig();
+    clientConfig.setString(KrbConfigKey.KDC_HOST, SpnegoTestUtil.KDC_HOST);
+    clientConfig.setInt(KrbConfigKey.KDC_TCP_PORT, kdcPort);
+    clientConfig.setString(KrbConfigKey.DEFAULT_REALM, SpnegoTestUtil.REALM);
+
+    // Kerby sets "java.security.krb5.conf" for us!
+    System.clearProperty("java.security.auth.login.config");
+    System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
+    //System.setProperty("sun.security.spnego.debug", "true");
+    //System.setProperty("sun.security.krb5.debug", "true");
+
+    // Create and start an HTTP server configured only to allow SPNEGO requests
+    // We use `withAutomaticLogin(File)` here which should invalidate the need to do JAAS
config
+    httpServer = new HttpServer.Builder()
+        .withPort(0)
+        .withAutomaticLogin(serverKeytab)
+        .withSpnego(SpnegoTestUtil.SERVER_PRINCIPAL, SpnegoTestUtil.REALM)
+        .withHandler(new SpnegoTestUtil.AuthenticationRequiredAvaticaHandler())
+        .build();
+    httpServer.start();
+    isHttpServerStarted = true;
+
+    httpServerUrl = new URL("http://" + SpnegoTestUtil.KDC_HOST + ":" + httpServer.getPort());
+    LOG.info("HTTP server running at {}", httpServerUrl);
+  }
+
+  @AfterClass public static void stopKdc() throws Exception {
+    if (isHttpServerStarted) {
+      LOG.info("Stopping HTTP server at {}", httpServerUrl);
+      httpServer.stop();
+    }
+
+    if (isKdcStarted) {
+      LOG.info("Stopping KDC on {}", kdcPort);
+      kdc.stop();
+    }
+  }
+
+  private static void setupUsers(File keytabDir) throws KrbException {
+    String clientPrincipal = SpnegoTestUtil.CLIENT_PRINCIPAL.substring(0,
+        SpnegoTestUtil.CLIENT_PRINCIPAL.indexOf('@'));
+    clientKeytab = new File(keytabDir, clientPrincipal.replace('/', '_') + ".keytab");
+    if (clientKeytab.exists()) {
+      SpnegoTestUtil.deleteRecursively(clientKeytab);
+    }
+    LOG.info("Creating {} with keytab {}", clientPrincipal, clientKeytab);
+    SpnegoTestUtil.setupUser(kdc, clientKeytab, clientPrincipal);
+
+    String serverPrincipal = SpnegoTestUtil.SERVER_PRINCIPAL.substring(0,
+        SpnegoTestUtil.SERVER_PRINCIPAL.indexOf('@'));
+    serverKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
+    if (serverKeytab.exists()) {
+      SpnegoTestUtil.deleteRecursively(serverKeytab);
+    }
+    LOG.info("Creating {} with keytab {}", SpnegoTestUtil.SERVER_PRINCIPAL, serverKeytab);
+    SpnegoTestUtil.setupUser(kdc, serverKeytab, SpnegoTestUtil.SERVER_PRINCIPAL);
+  }
+
+  @Test public void testNormalClientsDisallowed() throws Exception {
+    LOG.info("Connecting to {}", httpServerUrl.toString());
+    HttpURLConnection conn = (HttpURLConnection) httpServerUrl.openConnection();
+    conn.setRequestMethod("GET");
+    // Authentication should fail because we didn't provide anything
+    assertEquals(401, conn.getResponseCode());
+  }
+
+  @Test public void testAuthenticatedClientsAllowed() throws Exception {
+    // Create the subject for the client
+    final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(SpnegoTestUtil.CLIENT_PRINCIPAL,
+        clientKeytab);
+    final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
+    // Make sure the subject has a principal
+    assertFalse(clientPrincipals.isEmpty());
+
+    // Get a TGT for the subject (might have many, different encryption types). The first
should
+    // be the default encryption type.
+    Set<KerberosTicket> privateCredentials =
+            clientSubject.getPrivateCredentials(KerberosTicket.class);
+    assertFalse(privateCredentials.isEmpty());
+    KerberosTicket tgt = privateCredentials.iterator().next();
+    assertNotNull(tgt);
+    LOG.info("Using TGT with etype: {}", tgt.getSessionKey().getAlgorithm());
+
+    // The name of the principal
+    final String principalName = clientPrincipals.iterator().next().getName();
+
+    // Run this code, logged in as the subject (the client)
+    byte[] response = Subject.doAs(clientSubject, new PrivilegedExceptionAction<byte[]>()
{
+      @Override public byte[] run() throws Exception {
+        // Logs in with Kerberos via GSS
+        GSSManager gssManager = GSSManager.getInstance();
+        Oid oid = new Oid(SpnegoTestUtil.JGSS_KERBEROS_TICKET_OID);
+        GSSName gssClient = gssManager.createName(principalName, GSSName.NT_USER_NAME);
+        GSSCredential credential = gssManager.createCredential(gssClient,
+            GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
+
+        // Passes the GSSCredential into the HTTP client implementation
+        final AvaticaCommonsHttpClientSpnegoImpl httpClient =
+            new AvaticaCommonsHttpClientSpnegoImpl(httpServerUrl, credential);
+
+        return httpClient.send(new byte[0]);
+      }
+    });
+
+    // We should get a response which is "OK" with our client's name
+    assertNotNull(response);
+    assertEquals("OK " + SpnegoTestUtil.CLIENT_PRINCIPAL, new String(response));
+  }
+}
+
+// End HttpServerSpnegoWithoutJaasTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/406372f1/avatica/server/src/test/resources/log4j.properties
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/resources/log4j.properties b/avatica/server/src/test/resources/log4j.properties
index 834e2db..662858e 100644
--- a/avatica/server/src/test/resources/log4j.properties
+++ b/avatica/server/src/test/resources/log4j.properties
@@ -22,3 +22,7 @@ log4j.appender.A1=org.apache.log4j.ConsoleAppender
 # Set the pattern for each log message
 log4j.appender.A1.layout=org.apache.log4j.PatternLayout
 log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p - %m%n
+
+# Debug for JGSS and Jetty's security (Kerberos/SPNEGO debugging)
+#log4j.logger.sun.security.jgss=DEBUG
+#log4j.logger.org.eclipse.jetty.security=DEBUG
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/calcite/blob/406372f1/avatica/site/_data/docs.yml
----------------------------------------------------------------------
diff --git a/avatica/site/_data/docs.yml b/avatica/site/_data/docs.yml
index 8b1af11..787588d 100644
--- a/avatica/site/_data/docs.yml
+++ b/avatica/site/_data/docs.yml
@@ -22,9 +22,11 @@
 
 - title: Reference
   docs:
+  - client_reference
   - json_reference
   - protobuf_reference
   - howto
+  - security
 
 - title: Meta
   docs:

http://git-wip-us.apache.org/repos/asf/calcite/blob/406372f1/avatica/site/_docs/client_reference.md
----------------------------------------------------------------------
diff --git a/avatica/site/_docs/client_reference.md b/avatica/site/_docs/client_reference.md
new file mode 100644
index 0000000..f17b22c
--- /dev/null
+++ b/avatica/site/_docs/client_reference.md
@@ -0,0 +1,108 @@
+---
+layout: docs
+title: Client Reference
+sidebar_title: Client Reference
+permalink: /docs/client_reference.html
+---
+
+<!--
+{% comment %}
+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.
+{% endcomment %}
+-->
+
+Avatica provides a reference-implementation client in the form of a Java
+JDBC client that interacts with the Avatica server over HTTP. This client
+can be used just as any other JDBC driver. There are a number of options
+that are available for clients to specify via the JDBC connection URL.
+
+As a reminder, the JDBC connection URL for Avatica is:
+
+  `jdbc:avatica:remote:[option=value[;option=value]]`
+
+The following are a list of supported options:
+
+**url**
+
+: _Description_: This property is a URL which refers to the location of the
+  Avatica Server which the driver will communicate with.
+
+: _Default_: This property's default value is `null`. It is required that the
+  user provides a value for this property.
+
+: _Required_: Yes.
+
+
+**serialization**
+
+: _Description_: Avatica supports multiple types of serialization mechanisms
+  to format data between the client and server. This property is used to ensure
+  that the client and server both use the same serialization mechanism. Valid
+  values presently include `json` and `protobuf`.
+
+: _Default_: `json` is the default value.
+
+: _Required_: No.
+
+
+**authentication**
+
+: _Description_: Avatica clients can specify the means in which it authenticates
+  with the Avatica server. Presently, the only form of authentication is SPNEGO
+  which enables Kerberos authentication. Clients who want to use a specific form
+  of authentication should specify the appropriate value in this property.
+
+: _Default_: `null` (implying "no authentication").
+
+: _Required_: No.
+
+
+**timeZone**
+
+: _Description_: The timezone that will be used for dates and times. Valid values for this
+  property are defined by [RFC 822](https://www.ietf.org/rfc/rfc0822.txt), for
+  example: `GMT`, `GMT-3`, `EST` or `PDT`.
+
+: _Default_: This property's default value is `null` which will cause the Avatica Driver
to
+  use the default timezone as specified by the JVM, commonly overriden by the
+  `user.timezone` system property.
+
+: _Required_: No.
+
+
+**httpclient_factory**
+
+: _Description_: The Avatica client is a "fancy" HTTP client. As such, there are
+  many libraries and APIs available for making HTTP calls. To determine which implementation
+  should be used, there is an interface `AvaticaHttpClientFactory` which can be provided
+  to control how the `AvaticaHttpClient` implementation is chosen.
+
+: _Default_: `AvaticaHttpClientFactoryImpl`.
+
+: _Required_: No.
+
+
+**httpclient_impl**
+
+: _Description_: When using the default `AvaticaHttpClientFactoryImpl` HTTP client factory
+  implementation, this factory should choose the correct client implementation for the
+  given client configuration. This property can be used to override the specific HTTP
+  client implementation. If it is not provided, the `AvaticaHttpClientFactoryImpl` will
+  automatically choose the HTTP client implementation.
+
+: _Default_: `null`.
+
+: _Required_: No.

http://git-wip-us.apache.org/repos/asf/calcite/blob/406372f1/avatica/site/_docs/security.md
----------------------------------------------------------------------
diff --git a/avatica/site/_docs/security.md b/avatica/site/_docs/security.md
new file mode 100644
index 0000000..c98f0a5
--- /dev/null
+++ b/avatica/site/_docs/security.md
@@ -0,0 +1,145 @@
+---
+layout: docs
+title: Security
+sidebar_title: Security
+permalink: /docs/security.html
+---
+<!--
+{% comment %}
+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.
+{% endcomment %}
+-->
+
+Security is an important topic between clients and the Avatica server. Most JDBC
+drivers and databases implement some level of authentication and authorization
+for limit what actions clients are allowed to perform.
+
+Similarly, Avatica must limit what users are allowed to connect and interact
+with the server. Avatica must primarily deal with authentication while authorization
+is deferred to the underlying database. By default, Avatica provides no authentication.
+Avatica does have the ability to perform client authentication using Kerberos.
+
+## Kerberos-based authentication
+
+Because Avatica operates over an HTTP interface, the simple and protected GSSAPI
+negotiation mechanism ([SPNEGO](https://en.wikipedia.org/wiki/SPNEGO)) is a logical
+choice. This mechanism makes use of the "HTTP Negotiate" authentication extension to
+communicate with the Kerberos Key Distribution Center (KDC) to authenticate a client.
+
+## Enabling SPNEGO/Kerberos Authentication in servers
+
+The Avatica server can operate either by performing the login using
+a JAAS configuration file or login programmatically. By default, authenticated clients
+will have queries executed as the Avatica server's kerberos user. [Impersonation](#impersonation)
+is the feature which enables actions to be run in the server as the actual end-user.
+
+As a note, it is required that the Kerberos principal in use by the Avatica server
+**must** have an primary of `HTTP` (where Kerberos principals are of the form
+`primary[/instance]@REALM`). This is specified by [RFC-4559](https://tools.ietf.org/html/rfc4559).
+
+### Programmatic Login
+
+This approach requires no external file configurations and only requires a
+keytab file for the principal.
+
+{% highlight java %}
+HttpServer server = new HttpServer.Builder()
+    .withPort(8765)
+    .withHandler(new LocalService(), Driver.Serialization.PROTOBUF)
+    .withSpnego("HTTP/host.domain.com@DOMAIN.COM")
+    .withAutomaticLogin(
+        new File("/etc/security/keytabs/avatica.spnego.keytab"))
+    .build();
+{% endhighlight %}
+
+### JAAS Configuration File Login
+
+A JAAS configuration file can be set via the system property `java.security.auth.login.config`.
+The user must set this property when launching their Java application invoking the Avatica
server.
+The presence of this file will automatically perform login as necessary in the first use
+of the Avatica server. The invocation is nearly the same as the programmatic login.
+
+{% highlight java %}
+HttpServer server = new HttpServer.Builder()
+    .withPort(8765)
+    .withHandler(new LocalService(), Driver.Serialization.PROTOBUF)
+    .withSpnego("HTTP/host.domain.com@DOMAIN.COM")
+    .build();
+{% endhighlight %}
+
+The contents of the JAAS configuration file are very specific:
+
+{% highlight java %}
+com.sun.security.jgss.accept  {
+  com.sun.security.auth.module.Krb5LoginModule required
+  storeKey=true
+  useKeyTab=true
+  keyTab=/etc/security/keytabs/avatica.spnego.keyTab
+  principal=HTTP/host.domain.com@DOMAIN.COM;
+};
+{% endhighlight %}
+
+Ensure the `keyTab` and `principal` attributes are set correctly for your system.
+
+## Impersonation
+
+Impersonation is a feature of the Avatica server which allows the Avatica clients
+to execute the server-side calls (e.g. the underlying JDBC calls). Because the details
+on what it means to execute such an operation are dependent on the actual system, a
+callback is exposed for downstream integrators to implement.
+
+For example, the following is an example for creating an Apache Hadoop `UserGroupInformation`
+"proxy user". This example takes a `UserGroupInformation` object representing the Avatica
server's
+identity, creates a "proxy user" with the client's username, and performs the action as that
+client but using the server's identity.
+
+{% highlight java %}
+public class PhoenixDoAsCallback implements DoAsRemoteUserCallback {
+  private final UserGroupInformation serverUgi;
+
+  public PhoenixDoAsCallback(UserGroupInformation serverUgi) {
+    this.serverUgi = Objects.requireNonNull(serverUgi);
+  }
+
+  @Override
+  public <T> T doAsRemoteUser(String remoteUserName, String remoteAddress, final Callable<T>
action) throws Exception {
+    // Proxy this user on top of the server's user (the real user)
+    UserGroupInformation proxyUser = UserGroupInformation.createProxyUser(remoteUserName,
serverUgi);
+
+    // Check if this user is allowed to be impersonated.
+    // Will throw AuthorizationException if the impersonation as this user is not allowed
+    ProxyUsers.authorize(proxyUser, remoteAddress);
+
+    // Execute the actual call as this proxy user
+    return proxyUser.doAs(new PrivilegedExceptionAction<T>() {
+      @Override
+      public T run() throws Exception {
+        return action.call();
+      }
+    });
+  }
+}
+{% endhighlight %}
+
+## Client implementation
+
+Many HTTP client libraries, such as [Apache Commons HttpComponents](https://hc.apache.org/),
already have
+support for performing SPNEGO authentication. When in doubt, refer to one of
+these implementations as it is likely correct.
+
+For information on building this by hand, consult [RFC-4559](https://tools.ietf.org/html/rfc4559)
+which describes how the authentication handshake, through use of the "WWW-authenticate"
+HTTP header, is used to authenticate a client.


Mime
View raw message