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/[email protected]") + .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/[email protected]") + .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/[email protected]; +}; +{% 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.
