You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@phoenix.apache.org by ka...@apache.org on 2018/11/15 22:50:30 UTC

phoenix git commit: PHOENIX-5000 Make SecureUserConnectionsTest as Integration test

Repository: phoenix
Updated Branches:
  refs/heads/master 3690c6323 -> cd31ed5e8


PHOENIX-5000 Make SecureUserConnectionsTest as Integration test


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

Branch: refs/heads/master
Commit: cd31ed5e8cef8ad8b6d20f7417974c47002f4297
Parents: 3690c63
Author: Karan Mehta <ka...@gmail.com>
Authored: Tue Oct 30 12:40:00 2018 -0700
Committer: Karan Mehta <ka...@gmail.com>
Committed: Thu Nov 15 14:50:05 2018 -0800

----------------------------------------------------------------------
 .../phoenix/jdbc/SecureUserConnectionsIT.java   | 459 ++++++++++++++++++
 .../phoenix/jdbc/SecureUserConnectionsTest.java | 460 -------------------
 2 files changed, 459 insertions(+), 460 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/phoenix/blob/cd31ed5e/phoenix-core/src/it/java/org/apache/phoenix/jdbc/SecureUserConnectionsIT.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/jdbc/SecureUserConnectionsIT.java b/phoenix-core/src/it/java/org/apache/phoenix/jdbc/SecureUserConnectionsIT.java
new file mode 100644
index 0000000..eaf981b
--- /dev/null
+++ b/phoenix-core/src/it/java/org/apache/phoenix/jdbc/SecureUserConnectionsIT.java
@@ -0,0 +1,459 @@
+/*
+ * 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.phoenix.jdbc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.CommonConfigurationKeys;
+import org.apache.hadoop.hbase.security.User;
+import org.apache.hadoop.minikdc.MiniKdc;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.apache.phoenix.jdbc.PhoenixEmbeddedDriver.ConnectionInfo;
+import org.apache.phoenix.query.ConfigurationFactory;
+import org.apache.phoenix.util.InstanceResolver;
+import org.apache.phoenix.util.PhoenixRuntime;
+import org.apache.phoenix.util.ReadOnlyProps;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests ConnectionQueryServices caching when Kerberos authentication is enabled. It's not
+ * trivial to directly test this, so we exploit the knowledge that the caching is driven by
+ * a ConcurrentHashMap. We can use a HashSet to determine when instances of ConnectionInfo
+ * collide and when they do not.
+ */
+public class SecureUserConnectionsIT {
+    private static final Log LOG = LogFactory.getLog(SecureUserConnectionsIT.class);
+    private static final int KDC_START_ATTEMPTS = 10;
+
+    private static final File TEMP_DIR = new File(getClassTempDir());
+    private static final File KEYTAB_DIR = new File(TEMP_DIR, "keytabs");
+    private static final File KDC_DIR = new File(TEMP_DIR, "kdc");
+    private static final List<File> USER_KEYTAB_FILES = new ArrayList<>();
+    private static final List<File> SERVICE_KEYTAB_FILES = new ArrayList<>();
+    private static final int NUM_USERS = 3;
+    private static final Properties EMPTY_PROPERTIES = new Properties();
+    private static final String BASE_URL = PhoenixRuntime.JDBC_PROTOCOL + ":localhost:2181";
+
+    private static MiniKdc KDC;
+
+    @BeforeClass
+    public static void setupKdc() throws Exception {
+        ensureIsEmptyDirectory(KDC_DIR);
+        ensureIsEmptyDirectory(KEYTAB_DIR);
+        // Create and start the KDC. MiniKDC appears to have a race condition in how it does
+        // port allocation (with apache-ds). See PHOENIX-3287.
+        boolean started = false;
+        for (int i = 0; !started && i < KDC_START_ATTEMPTS; i++) {
+            Properties kdcConf = MiniKdc.createConf();
+            kdcConf.put(MiniKdc.DEBUG, true);
+            KDC = new MiniKdc(kdcConf, KDC_DIR);
+            try {
+                KDC.start();
+                started = true;
+            } catch (Exception e) {
+                LOG.warn("PHOENIX-3287: Failed to start KDC, retrying..", e);
+            }
+        }
+        assertTrue("The embedded KDC failed to start successfully after " + KDC_START_ATTEMPTS
+                + " attempts.", started);
+
+        createUsers(NUM_USERS);
+        createServiceUsers(NUM_USERS);
+
+        final Configuration conf = new Configuration(false);
+        conf.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, "kerberos");
+        conf.set(User.HBASE_SECURITY_CONF_KEY, "kerberos");
+        conf.setBoolean(User.HBASE_SECURITY_AUTHORIZATION_CONF_KEY, true);
+        UserGroupInformation.setConfiguration(conf);
+
+        // Clear the cached singletons so we can inject our own.
+        InstanceResolver.clearSingletons();
+        // Make sure the ConnectionInfo doesn't try to pull a default Configuration
+        InstanceResolver.getSingleton(ConfigurationFactory.class, new ConfigurationFactory() {
+            @Override
+            public Configuration getConfiguration() {
+                return conf;
+            }
+            @Override
+            public Configuration getConfiguration(Configuration confToClone) {
+                Configuration copy = new Configuration(conf);
+                copy.addResource(confToClone);
+                return copy;
+            }
+        });
+        updateDefaultRealm();
+    }
+
+    private static void updateDefaultRealm() throws Exception {
+        // (at least) one other phoenix test triggers the caching of this field before the KDC is up
+        // which causes principal parsing to fail.
+        Field f = KerberosName.class.getDeclaredField("defaultRealm");
+        f.setAccessible(true);
+        // Default realm for MiniKDC
+        f.set(null, "EXAMPLE.COM");
+    }
+
+    @AfterClass
+    public static void stopKdc() throws Exception {
+        // Remove our custom ConfigurationFactory for future tests
+        InstanceResolver.clearSingletons();
+        if (null != KDC) {
+            KDC.stop();
+            KDC = null;
+        }
+    }
+
+    private static String getClassTempDir() {
+        StringBuilder sb = new StringBuilder(32);
+        sb.append(System.getProperty("user.dir")).append(File.separator);
+        sb.append("target").append(File.separator);
+        sb.append(SecureUserConnectionsIT.class.getSimpleName());
+        return sb.toString();
+    }
+
+    private static void ensureIsEmptyDirectory(File f) throws IOException {
+        if (f.exists()) {
+            if (f.isDirectory()) {
+                FileUtils.deleteDirectory(f);
+            } else {
+                assertTrue("Failed to delete keytab directory", f.delete());
+            }
+        }
+        assertTrue("Failed to create keytab directory", f.mkdirs());
+    }
+
+    private static void createUsers(int numUsers) throws Exception {
+        assertNotNull("KDC is null, was setup method called?", KDC);
+        for (int i = 1; i <= numUsers; i++) {
+            String principal = "user" + i;
+            File keytabFile = new File(KEYTAB_DIR, principal + ".keytab");
+            KDC.createPrincipal(keytabFile, principal);
+            USER_KEYTAB_FILES.add(keytabFile);
+        }
+    }
+
+    private static void createServiceUsers(int numUsers) throws Exception {
+        assertNotNull("KDC is null, was setup method called?", KDC);
+        for (int i = 1; i <= numUsers; i++) {
+            String principal = "user" + i + "/localhost";
+            File keytabFile = new File(KEYTAB_DIR, "user" + i + ".service.keytab");
+            KDC.createPrincipal(keytabFile, principal);
+            SERVICE_KEYTAB_FILES.add(keytabFile);
+        }
+    }
+
+    /**
+     * Returns the principal for a user.
+     *
+     * @param offset The "number" user to return, based on one, not zero.
+     */
+    private static String getUserPrincipal(int offset) {
+        return "user" + offset + "@" + KDC.getRealm();
+    }
+
+    private static String getServicePrincipal(int offset) {
+        return "user" + offset + "/localhost@" + KDC.getRealm();
+    }
+
+    /**
+     * Returns the keytab file for the corresponding principal with the same {@code offset}.
+     * Requires {@link #createUsers(int)} to have been called with a value greater than {@code offset}.
+     *
+     * @param offset The "number" for the principal whose keytab should be returned. One-based, not zero-based.
+     */
+    public static File getUserKeytabFile(int offset) {
+        return getKeytabFile(offset, USER_KEYTAB_FILES);
+    }
+
+    public static File getServiceKeytabFile(int offset) {
+        return getKeytabFile(offset, SERVICE_KEYTAB_FILES);
+    }
+
+    private static File getKeytabFile(int offset, List<File> keytabs) {
+        assertTrue("Invalid offset: " + offset, (offset - 1) >= 0 && (offset - 1) < keytabs.size());
+        return keytabs.get(offset - 1);
+    }
+
+    private String joinUserAuthentication(String origUrl, String principal, File keytab) {
+        StringBuilder sb = new StringBuilder(64);
+        // Knock off the trailing terminator if one exists
+        if (origUrl.charAt(origUrl.length() - 1) == PhoenixRuntime.JDBC_PROTOCOL_TERMINATOR) {
+            sb.append(origUrl, 0, origUrl.length() - 1);
+        } else {
+            sb.append(origUrl);
+        }
+
+        sb.append(PhoenixRuntime.JDBC_PROTOCOL_SEPARATOR).append(principal);
+        sb.append(PhoenixRuntime.JDBC_PROTOCOL_SEPARATOR).append(keytab.getPath());
+        return sb.append(PhoenixRuntime.JDBC_PROTOCOL_TERMINATOR).toString();
+    }
+
+    @Test
+    public void testMultipleInvocationsBySameUserAreEquivalent() throws Exception {
+        final HashSet<ConnectionInfo> connections = new HashSet<>();
+        final String princ1 = getUserPrincipal(1);
+        final File keytab1 = getUserKeytabFile(1);
+
+        UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ1, keytab1.getPath());
+
+        PrivilegedExceptionAction<Void> callable = new PrivilegedExceptionAction<Void>() {
+            public Void run() throws Exception {
+                String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
+                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+                return null;
+            }
+        };
+
+        // Using the same UGI should result in two equivalent ConnectionInfo objects
+        ugi.doAs(callable);
+        assertEquals(1, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        ugi.doAs(callable);
+        assertEquals(1, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+    }
+
+    @Test
+    public void testMultipleUniqueUGIInstancesAreDisjoint() throws Exception {
+        final HashSet<ConnectionInfo> connections = new HashSet<>();
+        final String princ1 = getUserPrincipal(1);
+        final File keytab1 = getUserKeytabFile(1);
+
+        UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ1, keytab1.getPath());
+
+        PrivilegedExceptionAction<Void> callable = new PrivilegedExceptionAction<Void>() {
+            public Void run() throws Exception {
+                String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
+                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+                return null;
+            }
+        };
+
+        ugi.doAs(callable);
+        assertEquals(1, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // A second, but equivalent, call from the same "real" user but a different UGI instance
+        // is expected functionality (programmer error).
+        UserGroupInformation ugiCopy = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ1, keytab1.getPath());
+        ugiCopy.doAs(callable);
+        assertEquals(2, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+    }
+
+    @Test
+    public void testAlternatingLogins() throws Exception {
+        final HashSet<ConnectionInfo> connections = new HashSet<>();
+        final String princ1 = getUserPrincipal(1);
+        final File keytab1 = getUserKeytabFile(1);
+        final String princ2 = getUserPrincipal(2);
+        final File keytab2 = getUserKeytabFile(2);
+
+        UserGroupInformation ugi1 = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ1, keytab1.getPath());
+        UserGroupInformation ugi2 = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ2, keytab2.getPath());
+
+        // Using the same UGI should result in two equivalent ConnectionInfo objects
+        ugi1.doAs(new PrivilegedExceptionAction<Void>() {
+            public Void run() throws Exception {
+                String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
+                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+                return null;
+            }
+        });
+        assertEquals(1, connections.size());
+        // Sanity check
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        ugi2.doAs(new PrivilegedExceptionAction<Void>() {
+            public Void run() throws Exception {
+                String url = joinUserAuthentication(BASE_URL, princ2, keytab2);
+                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+                return null;
+            }
+        });
+        assertEquals(2, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        ugi1.doAs(new PrivilegedExceptionAction<Void>() {
+            public Void run() throws Exception {
+                String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
+                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+                return null;
+            }
+        });
+        assertEquals(2, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+    }
+
+    @Test
+    public void testAlternatingDestructiveLogins() throws Exception {
+        final HashSet<ConnectionInfo> connections = new HashSet<>();
+        final String princ1 = getUserPrincipal(1);
+        final File keytab1 = getUserKeytabFile(1);
+        final String princ2 = getUserPrincipal(2);
+        final File keytab2 = getUserKeytabFile(2);
+        final String url1 = joinUserAuthentication(BASE_URL, princ1, keytab1);
+        final String url2 = joinUserAuthentication(BASE_URL, princ2, keytab2);
+
+        UserGroupInformation.loginUserFromKeytab(princ1, keytab1.getPath());
+        // Using the same UGI should result in two equivalent ConnectionInfo objects
+        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(1, connections.size());
+        // Sanity check
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        UserGroupInformation.loginUserFromKeytab(princ2, keytab2.getPath());
+        connections.add(ConnectionInfo.create(url2).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(2, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Because the UGI instances are unique, so are the connections
+        UserGroupInformation.loginUserFromKeytab(princ1, keytab1.getPath());
+        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(3, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+    }
+
+    @Test
+    public void testMultipleConnectionsAsSameUser() throws Exception {
+        final HashSet<ConnectionInfo> connections = new HashSet<>();
+        final String princ1 = getUserPrincipal(1);
+        final File keytab1 = getUserKeytabFile(1);
+        final String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
+
+        UserGroupInformation.loginUserFromKeytab(princ1, keytab1.getPath());
+        // Using the same UGI should result in two equivalent ConnectionInfo objects
+        connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(1, connections.size());
+        // Sanity check
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Because the UGI instances are unique, so are the connections
+        connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(1, connections.size());
+    }
+
+    @Test
+    public void testMultipleConnectionsAsSameUserWithoutLogin() throws Exception {
+        final HashSet<ConnectionInfo> connections = new HashSet<>();
+        final String princ1 = getUserPrincipal(1);
+        final File keytab1 = getUserKeytabFile(1);
+
+        // Using the same UGI should result in two equivalent ConnectionInfo objects
+        final String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
+        connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(1, connections.size());
+        // Sanity check
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Because the UGI instances are unique, so are the connections
+        connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(1, connections.size());
+    }
+
+    @Test
+    public void testAlternatingConnectionsWithoutLogin() throws Exception {
+        final HashSet<ConnectionInfo> connections = new HashSet<>();
+        final String princ1 = getUserPrincipal(1);
+        final File keytab1 = getUserKeytabFile(1);
+        final String princ2 = getUserPrincipal(2);
+        final File keytab2 = getUserKeytabFile(2);
+        final String url1 = joinUserAuthentication(BASE_URL, princ1, keytab1);
+        final String url2 = joinUserAuthentication(BASE_URL, princ2, keytab2);
+
+        // Using the same UGI should result in two equivalent ConnectionInfo objects
+        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(1, connections.size());
+        // Sanity check
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Because the UGI instances are unique, so are the connections
+        connections.add(ConnectionInfo.create(url2).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(2, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Using the same UGI should result in two equivalent ConnectionInfo objects
+        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(3, connections.size());
+        // Sanity check
+        verifyAllConnectionsAreKerberosBased(connections);
+    }
+
+    @Test
+    public void testHostSubstitutionInUrl() throws Exception {
+        final HashSet<ConnectionInfo> connections = new HashSet<>();
+        final String princ1 = getServicePrincipal(1);
+        final File keytab1 = getServiceKeytabFile(1);
+        final String princ2 = getServicePrincipal(2);
+        final File keytab2 = getServiceKeytabFile(2);
+        final String url1 = joinUserAuthentication(BASE_URL, princ1, keytab1);
+        final String url2 = joinUserAuthentication(BASE_URL, princ2, keytab2);
+
+        // Using the same UGI should result in two equivalent ConnectionInfo objects
+        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(1, connections.size());
+        // Sanity check
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Logging in as the same user again should not duplicate connections
+        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(1, connections.size());
+        // Sanity check
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Add a second one.
+        connections.add(ConnectionInfo.create(url2).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(2, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Again, verify this user is not duplicated
+        connections.add(ConnectionInfo.create(url2).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(2, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+
+        // Because the UGI instances are unique, so are the connections
+        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
+        assertEquals(3, connections.size());
+        verifyAllConnectionsAreKerberosBased(connections);
+    }
+
+    private void verifyAllConnectionsAreKerberosBased(Collection<ConnectionInfo> connections) {
+        for (ConnectionInfo cnxnInfo : connections) {
+            assertTrue("ConnectionInfo does not have kerberos credentials: " + cnxnInfo, cnxnInfo.getUser().getUGI().hasKerberosCredentials());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/phoenix/blob/cd31ed5e/phoenix-core/src/test/java/org/apache/phoenix/jdbc/SecureUserConnectionsTest.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/jdbc/SecureUserConnectionsTest.java b/phoenix-core/src/test/java/org/apache/phoenix/jdbc/SecureUserConnectionsTest.java
deleted file mode 100644
index 2bed452..0000000
--- a/phoenix-core/src/test/java/org/apache/phoenix/jdbc/SecureUserConnectionsTest.java
+++ /dev/null
@@ -1,460 +0,0 @@
-/*
- * 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.phoenix.jdbc;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.security.PrivilegedExceptionAction;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Properties;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.CommonConfigurationKeys;
-import org.apache.hadoop.hbase.security.User;
-import org.apache.hadoop.minikdc.MiniKdc;
-import org.apache.hadoop.security.UserGroupInformation;
-import org.apache.hadoop.security.authentication.util.KerberosName;
-import org.apache.phoenix.jdbc.PhoenixEmbeddedDriver.ConnectionInfo;
-import org.apache.phoenix.query.ConfigurationFactory;
-import org.apache.phoenix.util.InstanceResolver;
-import org.apache.phoenix.util.PhoenixRuntime;
-import org.apache.phoenix.util.ReadOnlyProps;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Ignore;
-import org.junit.Test;
-
-/**
- * Tests ConnectionQueryServices caching when Kerberos authentication is enabled. It's not
- * trivial to directly test this, so we exploit the knowledge that the caching is driven by
- * a ConcurrentHashMap. We can use a HashSet to determine when instances of ConnectionInfo
- * collide and when they do not.
- */
-public class SecureUserConnectionsTest {
-    private static final Log LOG = LogFactory.getLog(SecureUserConnectionsTest.class); 
-    private static final int KDC_START_ATTEMPTS = 10;
-
-    private static final File TEMP_DIR = new File(getClassTempDir());
-    private static final File KEYTAB_DIR = new File(TEMP_DIR, "keytabs");
-    private static final File KDC_DIR = new File(TEMP_DIR, "kdc");
-    private static final List<File> USER_KEYTAB_FILES = new ArrayList<>();
-    private static final List<File> SERVICE_KEYTAB_FILES = new ArrayList<>();
-    private static final int NUM_USERS = 3;
-    private static final Properties EMPTY_PROPERTIES = new Properties();
-    private static final String BASE_URL = PhoenixRuntime.JDBC_PROTOCOL + ":localhost:2181";
-
-    private static MiniKdc KDC;
-
-    @BeforeClass
-    public static void setupKdc() throws Exception {
-        ensureIsEmptyDirectory(KDC_DIR);
-        ensureIsEmptyDirectory(KEYTAB_DIR);
-        // Create and start the KDC. MiniKDC appears to have a race condition in how it does
-        // port allocation (with apache-ds). See PHOENIX-3287.
-        boolean started = false;
-        for (int i = 0; !started && i < KDC_START_ATTEMPTS; i++) {
-            Properties kdcConf = MiniKdc.createConf();
-            kdcConf.put(MiniKdc.DEBUG, true);
-            KDC = new MiniKdc(kdcConf, KDC_DIR);
-            try {
-                KDC.start();
-                started = true;
-            } catch (Exception e) {
-                LOG.warn("PHOENIX-3287: Failed to start KDC, retrying..", e);
-            }
-        }
-        assertTrue("The embedded KDC failed to start successfully after " + KDC_START_ATTEMPTS
-                + " attempts.", started);
-
-        createUsers(NUM_USERS);
-        createServiceUsers(NUM_USERS);
-
-        final Configuration conf = new Configuration(false);
-        conf.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, "kerberos");
-        conf.set(User.HBASE_SECURITY_CONF_KEY, "kerberos");
-        conf.setBoolean(User.HBASE_SECURITY_AUTHORIZATION_CONF_KEY, true);
-        UserGroupInformation.setConfiguration(conf);
-
-        // Clear the cached singletons so we can inject our own.
-        InstanceResolver.clearSingletons();
-        // Make sure the ConnectionInfo doesn't try to pull a default Configuration
-        InstanceResolver.getSingleton(ConfigurationFactory.class, new ConfigurationFactory() {
-            @Override
-            public Configuration getConfiguration() {
-                return conf;
-            }
-            @Override
-            public Configuration getConfiguration(Configuration confToClone) {
-                Configuration copy = new Configuration(conf);
-                copy.addResource(confToClone);
-                return copy;
-            }
-        });
-        updateDefaultRealm();
-    }
-
-    private static void updateDefaultRealm() throws Exception {
-        // (at least) one other phoenix test triggers the caching of this field before the KDC is up
-        // which causes principal parsing to fail.
-        Field f = KerberosName.class.getDeclaredField("defaultRealm");
-        f.setAccessible(true);
-        // Default realm for MiniKDC
-        f.set(null, "EXAMPLE.COM");
-    }
-
-    @AfterClass
-    public static void stopKdc() throws Exception {
-        // Remove our custom ConfigurationFactory for future tests
-        InstanceResolver.clearSingletons();
-        if (null != KDC) {
-            KDC.stop();
-            KDC = null;
-        }
-    }
-
-    private static String getClassTempDir() {
-        StringBuilder sb = new StringBuilder(32);
-        sb.append(System.getProperty("user.dir")).append(File.separator);
-        sb.append("target").append(File.separator);
-        sb.append(SecureUserConnectionsTest.class.getSimpleName());
-        return sb.toString();
-    }
-
-    private static void ensureIsEmptyDirectory(File f) throws IOException {
-        if (f.exists()) {
-            if (f.isDirectory()) {
-                FileUtils.deleteDirectory(f);
-            } else {
-                assertTrue("Failed to delete keytab directory", f.delete());
-            }
-        }
-        assertTrue("Failed to create keytab directory", f.mkdirs());
-    }
-
-    private static void createUsers(int numUsers) throws Exception {
-        assertNotNull("KDC is null, was setup method called?", KDC);
-        for (int i = 1; i <= numUsers; i++) {
-            String principal = "user" + i;
-            File keytabFile = new File(KEYTAB_DIR, principal + ".keytab");
-            KDC.createPrincipal(keytabFile, principal);
-            USER_KEYTAB_FILES.add(keytabFile);
-        }
-    }
-
-    private static void createServiceUsers(int numUsers) throws Exception {
-        assertNotNull("KDC is null, was setup method called?", KDC);
-        for (int i = 1; i <= numUsers; i++) {
-            String principal = "user" + i + "/localhost";
-            File keytabFile = new File(KEYTAB_DIR, "user" + i + ".service.keytab");
-            KDC.createPrincipal(keytabFile, principal);
-            SERVICE_KEYTAB_FILES.add(keytabFile);
-        }
-    }
-
-    /**
-     * Returns the principal for a user.
-     *
-     * @param offset The "number" user to return, based on one, not zero.
-     */
-    private static String getUserPrincipal(int offset) {
-        return "user" + offset + "@" + KDC.getRealm();
-    }
-
-    private static String getServicePrincipal(int offset) {
-        return "user" + offset + "/localhost@" + KDC.getRealm();
-    }
-
-    /**
-     * Returns the keytab file for the corresponding principal with the same {@code offset}.
-     * Requires {@link #createUsers(int)} to have been called with a value greater than {@code offset}.
-     *
-     * @param offset The "number" for the principal whose keytab should be returned. One-based, not zero-based.
-     */
-    public static File getUserKeytabFile(int offset) {
-        return getKeytabFile(offset, USER_KEYTAB_FILES);
-    }
-
-    public static File getServiceKeytabFile(int offset) {
-        return getKeytabFile(offset, SERVICE_KEYTAB_FILES);
-    }
-
-    private static File getKeytabFile(int offset, List<File> keytabs) {
-        assertTrue("Invalid offset: " + offset, (offset - 1) >= 0 && (offset - 1) < keytabs.size());
-        return keytabs.get(offset - 1);
-    }
-
-    private String joinUserAuthentication(String origUrl, String principal, File keytab) {
-        StringBuilder sb = new StringBuilder(64);
-        // Knock off the trailing terminator if one exists
-        if (origUrl.charAt(origUrl.length() - 1) == PhoenixRuntime.JDBC_PROTOCOL_TERMINATOR) {
-            sb.append(origUrl, 0, origUrl.length() - 1);
-        } else {
-            sb.append(origUrl);
-        }
-
-        sb.append(PhoenixRuntime.JDBC_PROTOCOL_SEPARATOR).append(principal);
-        sb.append(PhoenixRuntime.JDBC_PROTOCOL_SEPARATOR).append(keytab.getPath());
-        return sb.append(PhoenixRuntime.JDBC_PROTOCOL_TERMINATOR).toString();
-    }
-
-    @Test
-    public void testMultipleInvocationsBySameUserAreEquivalent() throws Exception {
-        final HashSet<ConnectionInfo> connections = new HashSet<>();
-        final String princ1 = getUserPrincipal(1);
-        final File keytab1 = getUserKeytabFile(1);
-
-        UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ1, keytab1.getPath());
-
-        PrivilegedExceptionAction<Void> callable = new PrivilegedExceptionAction<Void>() {
-            public Void run() throws Exception {
-                String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
-                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-                return null;
-            }
-        };
-
-        // Using the same UGI should result in two equivalent ConnectionInfo objects
-        ugi.doAs(callable);
-        assertEquals(1, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        ugi.doAs(callable);
-        assertEquals(1, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-    }
-
-    @Test
-    public void testMultipleUniqueUGIInstancesAreDisjoint() throws Exception {
-        final HashSet<ConnectionInfo> connections = new HashSet<>();
-        final String princ1 = getUserPrincipal(1);
-        final File keytab1 = getUserKeytabFile(1);
-
-        UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ1, keytab1.getPath());
-
-        PrivilegedExceptionAction<Void> callable = new PrivilegedExceptionAction<Void>() {
-            public Void run() throws Exception {
-                String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
-                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-                return null;
-            }
-        };
-
-        ugi.doAs(callable);
-        assertEquals(1, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // A second, but equivalent, call from the same "real" user but a different UGI instance
-        // is expected functionality (programmer error).
-        UserGroupInformation ugiCopy = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ1, keytab1.getPath());
-        ugiCopy.doAs(callable);
-        assertEquals(2, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-    }
-
-    @Test
-    public void testAlternatingLogins() throws Exception {
-        final HashSet<ConnectionInfo> connections = new HashSet<>();
-        final String princ1 = getUserPrincipal(1);
-        final File keytab1 = getUserKeytabFile(1);
-        final String princ2 = getUserPrincipal(2);
-        final File keytab2 = getUserKeytabFile(2);
-
-        UserGroupInformation ugi1 = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ1, keytab1.getPath());
-        UserGroupInformation ugi2 = UserGroupInformation.loginUserFromKeytabAndReturnUGI(princ2, keytab2.getPath());
-
-        // Using the same UGI should result in two equivalent ConnectionInfo objects
-        ugi1.doAs(new PrivilegedExceptionAction<Void>() {
-            public Void run() throws Exception {
-                String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
-                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-                return null;
-            }
-        });
-        assertEquals(1, connections.size());
-        // Sanity check
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        ugi2.doAs(new PrivilegedExceptionAction<Void>() {
-            public Void run() throws Exception {
-                String url = joinUserAuthentication(BASE_URL, princ2, keytab2);
-                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-                return null;
-            }
-        });
-        assertEquals(2, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        ugi1.doAs(new PrivilegedExceptionAction<Void>() {
-            public Void run() throws Exception {
-                String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
-                connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-                return null;
-            }
-        });
-        assertEquals(2, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-    }
-
-    @Test
-    public void testAlternatingDestructiveLogins() throws Exception {
-        final HashSet<ConnectionInfo> connections = new HashSet<>();
-        final String princ1 = getUserPrincipal(1);
-        final File keytab1 = getUserKeytabFile(1);
-        final String princ2 = getUserPrincipal(2);
-        final File keytab2 = getUserKeytabFile(2);
-        final String url1 = joinUserAuthentication(BASE_URL, princ1, keytab1);
-        final String url2 = joinUserAuthentication(BASE_URL, princ2, keytab2);
-
-        UserGroupInformation.loginUserFromKeytab(princ1, keytab1.getPath());
-        // Using the same UGI should result in two equivalent ConnectionInfo objects
-        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(1, connections.size());
-        // Sanity check
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        UserGroupInformation.loginUserFromKeytab(princ2, keytab2.getPath());
-        connections.add(ConnectionInfo.create(url2).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(2, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Because the UGI instances are unique, so are the connections
-        UserGroupInformation.loginUserFromKeytab(princ1, keytab1.getPath());
-        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(3, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-    }
-
-    @Test
-    public void testMultipleConnectionsAsSameUser() throws Exception {
-        final HashSet<ConnectionInfo> connections = new HashSet<>();
-        final String princ1 = getUserPrincipal(1);
-        final File keytab1 = getUserKeytabFile(1);
-        final String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
-
-        UserGroupInformation.loginUserFromKeytab(princ1, keytab1.getPath());
-        // Using the same UGI should result in two equivalent ConnectionInfo objects
-        connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(1, connections.size());
-        // Sanity check
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Because the UGI instances are unique, so are the connections
-        connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(1, connections.size());
-    }
-
-    @Test
-    public void testMultipleConnectionsAsSameUserWithoutLogin() throws Exception {
-        final HashSet<ConnectionInfo> connections = new HashSet<>();
-        final String princ1 = getUserPrincipal(1);
-        final File keytab1 = getUserKeytabFile(1);
-
-        // Using the same UGI should result in two equivalent ConnectionInfo objects
-        final String url = joinUserAuthentication(BASE_URL, princ1, keytab1);
-        connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(1, connections.size());
-        // Sanity check
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Because the UGI instances are unique, so are the connections
-        connections.add(ConnectionInfo.create(url).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(1, connections.size());
-    }
-
-    @Test
-    public void testAlternatingConnectionsWithoutLogin() throws Exception {
-        final HashSet<ConnectionInfo> connections = new HashSet<>();
-        final String princ1 = getUserPrincipal(1);
-        final File keytab1 = getUserKeytabFile(1);
-        final String princ2 = getUserPrincipal(2);
-        final File keytab2 = getUserKeytabFile(2);
-        final String url1 = joinUserAuthentication(BASE_URL, princ1, keytab1);
-        final String url2 = joinUserAuthentication(BASE_URL, princ2, keytab2);
-
-        // Using the same UGI should result in two equivalent ConnectionInfo objects
-        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(1, connections.size());
-        // Sanity check
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Because the UGI instances are unique, so are the connections
-        connections.add(ConnectionInfo.create(url2).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(2, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Using the same UGI should result in two equivalent ConnectionInfo objects
-        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(3, connections.size());
-        // Sanity check
-        verifyAllConnectionsAreKerberosBased(connections);
-    }
-
-    @Test
-    public void testHostSubstitutionInUrl() throws Exception {
-        final HashSet<ConnectionInfo> connections = new HashSet<>();
-        final String princ1 = getServicePrincipal(1);
-        final File keytab1 = getServiceKeytabFile(1);
-        final String princ2 = getServicePrincipal(2);
-        final File keytab2 = getServiceKeytabFile(2);
-        final String url1 = joinUserAuthentication(BASE_URL, princ1, keytab1);
-        final String url2 = joinUserAuthentication(BASE_URL, princ2, keytab2);
-
-        // Using the same UGI should result in two equivalent ConnectionInfo objects
-        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(1, connections.size());
-        // Sanity check
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Logging in as the same user again should not duplicate connections
-        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(1, connections.size());
-        // Sanity check
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Add a second one.
-        connections.add(ConnectionInfo.create(url2).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(2, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Again, verify this user is not duplicated
-        connections.add(ConnectionInfo.create(url2).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(2, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-
-        // Because the UGI instances are unique, so are the connections
-        connections.add(ConnectionInfo.create(url1).normalize(ReadOnlyProps.EMPTY_PROPS, EMPTY_PROPERTIES));
-        assertEquals(3, connections.size());
-        verifyAllConnectionsAreKerberosBased(connections);
-    }
-
-    private void verifyAllConnectionsAreKerberosBased(Collection<ConnectionInfo> connections) {
-        for (ConnectionInfo cnxnInfo : connections) {
-            assertTrue("ConnectionInfo does not have kerberos credentials: " + cnxnInfo, cnxnInfo.getUser().getUGI().hasKerberosCredentials());
-        }
-    }
-}