You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@phoenix.apache.org by pb...@apache.org on 2018/11/27 15:18:28 UTC
[18/28] phoenix git commit: PHOENIX-5000 Make
SecureUserConnectionsTest as Integration test
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/60c19250
Tree: http://git-wip-us.apache.org/repos/asf/phoenix/tree/60c19250
Diff: http://git-wip-us.apache.org/repos/asf/phoenix/diff/60c19250
Branch: refs/heads/4.x-cdh5.15
Commit: 60c19250116d378a5f6f725d9dde9a8284d86ef5
Parents: 1c65619
Author: Karan Mehta <ka...@gmail.com>
Authored: Tue Oct 30 19:40:00 2018 +0000
Committer: Pedro Boado <pb...@apache.org>
Committed: Tue Nov 27 15:11:56 2018 +0000
----------------------------------------------------------------------
.../phoenix/jdbc/SecureUserConnectionsIT.java | 459 +++++++++++++++++++
.../phoenix/jdbc/SecureUserConnectionsTest.java | 459 -------------------
2 files changed, 459 insertions(+), 459 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/phoenix/blob/60c19250/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/60c19250/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 5a99b69..0000000
--- a/phoenix-core/src/test/java/org/apache/phoenix/jdbc/SecureUserConnectionsTest.java
+++ /dev/null
@@ -1,459 +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.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());
- }
- }
-}