blob: aeb63a16ce59b22908f2d8eaebeffc3bcdcac620 [file] [log] [blame]
/*
* 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.Driver;
import org.apache.calcite.avatica.server.AvaticaJaasKrbUtil;
import org.apache.calcite.avatica.server.HttpServer;
import org.apache.kerby.kerberos.kerb.KrbException;
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.AfterClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.security.PrivilegedExceptionAction;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import javax.security.auth.Subject;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* End to end test case for SPNEGO with Avatica.
*
* <p>The following system properties are useful for debugging problems around SPNEGO.</p>
* <ul>
* <li>sun.security.krb5.debug</li>
* <li>sun.security.jgss.debug</li>
* <li>sun.security.spnego.debug</li>
* <li>java.security.debug</li>
* </ul>
*/
@RunWith(Parameterized.class)
public class AvaticaSpnegoTest extends HttpBaseTest {
private static final Logger LOG = LoggerFactory.getLogger(AvaticaSpnegoTest.class);
private static SimpleKdcServer kdc;
private static KrbConfig clientConfig;
private static File keytabDir;
private static int kdcPort;
private static File clientKeytab;
private static File serverKeytab;
private static boolean isKdcStarted = false;
private static void setupKdc() throws Exception {
if (isKdcStarted) {
return;
}
if (System.getProperty("avatica.http.spnego.use_canonical_hostname") == null) {
System.setProperty("avatica.http.spnego.use_canonical_hostname", "false");
}
kdc = new SimpleKdcServer();
File target = SpnegoTestUtil.TARGET_DIR;
assertTrue(target.exists());
File kdcDir = new File(target, AvaticaSpnegoTest.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;
keytabDir = new File(target, AvaticaSpnegoTest.class.getSimpleName()
+ "_keytabs");
if (keytabDir.exists()) {
SpnegoTestUtil.deleteRecursively(keytabDir);
}
keytabDir.mkdirs();
setupServerUser(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!
// useSubjectCredsOnly is only important when we expect JAAS to go "looking"
// for credentials on our behalf.
System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
}
@AfterClass public static void stopKdc() throws KrbException {
if (isKdcStarted) {
LOG.info("Stopping KDC on {}", kdcPort);
kdc.stop();
}
}
private static void setupServerUser(File keytabDir) throws KrbException {
// Create the client user
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);
// Create the server user
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);
}
@Parameters public static List<Object[]> parameters() throws Exception {
final ArrayList<Object[]> parameters = new ArrayList<>();
setupClass();
// Start the KDC
setupKdc();
for (boolean tls : new Boolean[] {false, true}) {
for (Driver.Serialization serialization : new Driver.Serialization[] {
Driver.Serialization.JSON, Driver.Serialization.PROTOBUF}) {
if (tls && System.getProperty("java.vendor").contains("IBM")) {
// Skip TLS testing on IBM Java due the combination of:
// - Jetty 9.4.12+ ignores SSL_* ciphers due to security - eclipse/jetty.project#2807
// - IBM uses SSL_* cipher names for ALL ciphers not following RFC cipher names
// See eclipse/jetty.project#2807 for details
LOG.info("Skipping HTTPS test on IBM Java");
parameters.add(new Object[] {null});
continue;
}
// Build and start the server
HttpServer.Builder httpServerBuilder = new HttpServer.Builder();
if (tls) {
httpServerBuilder = httpServerBuilder
.withTLS(KEYSTORE, KEYSTORE_PASSWORD, KEYSTORE, KEYSTORE_PASSWORD);
}
HttpServer httpServer = httpServerBuilder
.withPort(0)
.withAutomaticLogin(serverKeytab)
.withSpnego(SpnegoTestUtil.SERVER_PRINCIPAL, SpnegoTestUtil.REALM)
.withHandler(localService, serialization)
.build();
httpServer.start();
SERVERS_TO_STOP.add(httpServer);
String url = "jdbc:avatica:remote:url=" + (tls ? "https://" : "http://")
+ SpnegoTestUtil.KDC_HOST + ":" + httpServer.getPort()
+ ";authentication=SPNEGO;serialization=" + serialization;
if (tls) {
url += ";truststore=" + KEYSTORE.getAbsolutePath()
+ ";truststore_password=" + KEYSTORE_PASSWORD;
}
LOG.info("JDBC URL {}", url);
parameters.add(new Object[] {url});
}
}
return parameters;
}
public AvaticaSpnegoTest(String jdbcUrl) {
super(jdbcUrl);
}
@Test public void testAuthenticatedClient() throws Exception {
ConnectionSpec.getDatabaseLock().lock();
try {
final String tableName = "allowed_clients";
// Create the subject for the client
final Subject clientSubject = AvaticaJaasKrbUtil.loginUsingKeytab(
SpnegoTestUtil.CLIENT_PRINCIPAL, clientKeytab);
// The name of the principal
// Run this code, logged in as the subject (the client)
Subject.doAs(clientSubject, new PrivilegedExceptionAction<Void>() {
@Override public Void run() throws Exception {
try (Connection conn = DriverManager.getConnection(jdbcUrl)) {
try (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));
}
}
return null;
}
});
} finally {
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