PHOENIX-3756 Handle users lacking ADMIN for the SYSTEM namespace
Project: http://git-wip-us.apache.org/repos/asf/phoenix/repo Commit: http://git-wip-us.apache.org/repos/asf/phoenix/commit/943ddfa6 Tree: http://git-wip-us.apache.org/repos/asf/phoenix/tree/943ddfa6 Diff: http://git-wip-us.apache.org/repos/asf/phoenix/diff/943ddfa6 Branch: refs/heads/4.x-HBase-0.98 Commit: 943ddfa6cbd2dc6e409469180ad3a9de6ef3f2fe Parents: 12fd0fa Author: Josh Elser <els...@apache.org> Authored: Thu Mar 30 15:13:57 2017 -0400 Committer: Josh Elser <els...@apache.org> Committed: Wed Apr 5 18:36:44 2017 -0400 ---------------------------------------------------------------------- .../end2end/SystemTablePermissionsIT.java | 263 +++++++++++++++++++ .../phoenix/jdbc/PhoenixDatabaseMetaData.java | 3 + .../query/ConnectionQueryServicesImpl.java | 51 +++- .../query/ConnectionQueryServicesImplTest.java | 73 +++++ 4 files changed, 380 insertions(+), 10 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/phoenix/blob/943ddfa6/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemTablePermissionsIT.java ---------------------------------------------------------------------- diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemTablePermissionsIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemTablePermissionsIT.java new file mode 100644 index 0000000..e99f322 --- /dev/null +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemTablePermissionsIT.java @@ -0,0 +1,263 @@ +/* + * 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.end2end; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.security.PrivilegedExceptionAction; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.security.access.AccessControlClient; +import org.apache.hadoop.hbase.security.access.Permission.Action; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.phoenix.query.QueryServices; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Test that verifies a user can read Phoenix tables with a minimal set of permissions. + */ +public class SystemTablePermissionsIT { + private static String SUPERUSER; + + private static final Set<String> PHOENIX_SYSTEM_TABLES = new HashSet<>(Arrays.asList( + "SYSTEM.CATALOG", "SYSTEM.SEQUENCE", "SYSTEM.STATS", "SYSTEM.FUNCTION", + "SYSTEM.MUTEX")); + // PHOENIX-XXXX SYSTEM.MUTEX isn't being created in the SYSTEM namespace as it should be. + private static final Set<String> PHOENIX_NAMESPACE_MAPPED_SYSTEM_TABLES = new HashSet<>( + Arrays.asList("SYSTEM:CATALOG", "SYSTEM:SEQUENCE", "SYSTEM:STATS", "SYSTEM:FUNCTION", + "SYSTEM.MUTEX")); + + private static final String TABLE_NAME = + SystemTablePermissionsIT.class.getSimpleName().toUpperCase(); + private static final int NUM_RECORDS = 5; + + private HBaseTestingUtility testUtil = null; + private Properties clientProperties = null; + + @BeforeClass + public static void setup() throws Exception { + SUPERUSER = System.getProperty("user.name"); + } + + private static void setCommonConfigProperties(Configuration conf) { + conf.set("hbase.coprocessor.master.classes", + "org.apache.hadoop.hbase.security.access.AccessController"); + conf.set("hbase.coprocessor.region.classes", + "org.apache.hadoop.hbase.security.access.AccessController"); + conf.set("hbase.coprocessor.regionserver.classes", + "org.apache.hadoop.hbase.security.access.AccessController"); + conf.set("hbase.security.exec.permission.checks", "true"); + conf.set("hbase.security.authorization", "true"); + conf.set("hbase.superuser", SUPERUSER); + } + + @After + public void cleanup() throws Exception { + if (null != testUtil) { + testUtil.shutdownMiniCluster(); + testUtil = null; + } + } + + @Test + public void testSystemTablePermissions() throws Exception { + testUtil = new HBaseTestingUtility(); + clientProperties = new Properties(); + Configuration conf = testUtil.getConfiguration(); + setCommonConfigProperties(conf); + conf.set(QueryServices.IS_NAMESPACE_MAPPING_ENABLED, "false"); + clientProperties.setProperty(QueryServices.IS_NAMESPACE_MAPPING_ENABLED, "false"); + testUtil.startMiniCluster(1); + final UserGroupInformation superUser = UserGroupInformation.createUserForTesting( + SUPERUSER, new String[0]); + final UserGroupInformation regularUser = UserGroupInformation.createUserForTesting( + "user", new String[0]); + + superUser.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + createTable(); + readTable(); + return null; + } + }); + + Set<String> tables = getHBaseTables(); + assertTrue("HBase tables do not include expected Phoenix tables: " + tables, + tables.containsAll(PHOENIX_SYSTEM_TABLES)); + + // Grant permission to the system tables for the unprivileged user + superUser.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + try { + grantPermissions(regularUser.getShortUserName(), PHOENIX_SYSTEM_TABLES, + Action.EXEC, Action.READ); + grantPermissions(regularUser.getShortUserName(), + Collections.singleton(TABLE_NAME), Action.READ); + } catch (Throwable e) { + if (e instanceof Exception) { + throw (Exception) e; + } else { + throw new Exception(e); + } + } + return null; + } + }); + + // Make sure that the unprivileged user can read the table + regularUser.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + // We expect this to not throw an error + readTable(); + return null; + } + }); + } + + @Test + public void testNamespaceMappedSystemTables() throws Exception { + testUtil = new HBaseTestingUtility(); + clientProperties = new Properties(); + Configuration conf = testUtil.getConfiguration(); + setCommonConfigProperties(conf); + testUtil.getConfiguration().set(QueryServices.IS_NAMESPACE_MAPPING_ENABLED, "true"); + clientProperties.setProperty(QueryServices.IS_NAMESPACE_MAPPING_ENABLED, "true"); + testUtil.startMiniCluster(1); + final UserGroupInformation superUser = + UserGroupInformation.createUserForTesting(SUPERUSER, new String[0]); + final UserGroupInformation regularUser = + UserGroupInformation.createUserForTesting("user", new String[0]); + + superUser.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + createTable(); + readTable(); + return null; + } + }); + + Set<String> tables = getHBaseTables(); + assertTrue("HBase tables do not include expected Phoenix tables: " + tables, + tables.containsAll(PHOENIX_NAMESPACE_MAPPED_SYSTEM_TABLES)); + + // Grant permission to the system tables for the unprivileged user + // An unprivileged user should only need to be able to Read and eXecute on them. + superUser.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + try { + grantPermissions(regularUser.getShortUserName(), + PHOENIX_NAMESPACE_MAPPED_SYSTEM_TABLES, Action.EXEC, Action.READ); + grantPermissions(regularUser.getShortUserName(), + Collections.singleton(TABLE_NAME), Action.READ); + } catch (Throwable e) { + if (e instanceof Exception) { + throw (Exception) e; + } else { + throw new Exception(e); + } + } + return null; + } + }); + + regularUser.doAs(new PrivilegedExceptionAction<Void>() { + @Override + public Void run() throws Exception { + // We expect this to not throw an error + readTable(); + return null; + } + }); + } + + private String getJdbcUrl() { + return "jdbc:phoenix:localhost:" + testUtil.getZkCluster().getClientPort() + ":/hbase"; + } + + private void createTable() throws SQLException { + try (Connection conn = DriverManager.getConnection(getJdbcUrl(), clientProperties); + Statement stmt = conn.createStatement();) { + assertFalse(stmt.execute("DROP TABLE IF EXISTS " + TABLE_NAME)); + assertFalse(stmt.execute("CREATE TABLE " + TABLE_NAME + + "(pk INTEGER not null primary key, data VARCHAR)")); + try (PreparedStatement pstmt = conn.prepareStatement("UPSERT INTO " + + TABLE_NAME + " values(?, ?)")) { + for (int i = 0; i < NUM_RECORDS; i++) { + pstmt.setInt(1, i); + pstmt.setString(2, Integer.toString(i)); + assertEquals(1, pstmt.executeUpdate()); + } + } + conn.commit(); + } + } + + private void readTable() throws SQLException { + try (Connection conn = DriverManager.getConnection(getJdbcUrl(), clientProperties); + Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery("SELECT pk, data FROM " + TABLE_NAME); + assertNotNull(rs); + int i = 0; + while (rs.next()) { + assertEquals(i, rs.getInt(1)); + assertEquals(Integer.toString(i), rs.getString(2)); + i++; + } + assertEquals(NUM_RECORDS, i); + } + } + + private void grantPermissions(String toUser, Set<String> tablesToGrant, Action... actions) + throws Throwable { + for (String table : tablesToGrant) { + AccessControlClient.grant(testUtil.getConfiguration(), TableName.valueOf(table), toUser, + null, null, actions); + } + } + + private Set<String> getHBaseTables() throws IOException { + Set<String> tables = new HashSet<>(); + for (TableName tn : testUtil.getHBaseAdmin().listTableNames()) { + tables.add(tn.getNameAsString()); + } + return tables; + } +} http://git-wip-us.apache.org/repos/asf/phoenix/blob/943ddfa6/phoenix-core/src/main/java/org/apache/phoenix/jdbc/PhoenixDatabaseMetaData.java ---------------------------------------------------------------------- diff --git a/phoenix-core/src/main/java/org/apache/phoenix/jdbc/PhoenixDatabaseMetaData.java b/phoenix-core/src/main/java/org/apache/phoenix/jdbc/PhoenixDatabaseMetaData.java index e09f5ba..195727b 100644 --- a/phoenix-core/src/main/java/org/apache/phoenix/jdbc/PhoenixDatabaseMetaData.java +++ b/phoenix-core/src/main/java/org/apache/phoenix/jdbc/PhoenixDatabaseMetaData.java @@ -30,6 +30,7 @@ import java.util.List; import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.KeyValue; +import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.util.Bytes; import org.apache.phoenix.compile.ColumnProjector; @@ -97,6 +98,7 @@ public class PhoenixDatabaseMetaData implements DatabaseMetaData { public static final String SYSTEM_CATALOG = SYSTEM_CATALOG_SCHEMA + ".\"" + SYSTEM_CATALOG_TABLE + "\""; public static final String SYSTEM_CATALOG_NAME = SchemaUtil.getTableName(SYSTEM_CATALOG_SCHEMA, SYSTEM_CATALOG_TABLE); + public static final TableName SYSTEM_CATALOG_HBASE_TABLE_NAME = TableName.valueOf(SYSTEM_CATALOG_NAME); public static final byte[] SYSTEM_CATALOG_NAME_BYTES = Bytes.toBytes(SYSTEM_CATALOG_NAME); public static final String SYSTEM_STATS_TABLE = "STATS"; public static final String SYSTEM_STATS_NAME = SchemaUtil.getTableName(SYSTEM_CATALOG_SCHEMA, SYSTEM_STATS_TABLE); @@ -305,6 +307,7 @@ public class PhoenixDatabaseMetaData implements DatabaseMetaData { public static final String SYSTEM_MUTEX_TABLE_NAME = "MUTEX"; public static final String SYSTEM_MUTEX_NAME = SchemaUtil.getTableName(QueryConstants.SYSTEM_SCHEMA_NAME, SYSTEM_MUTEX_TABLE_NAME); + public static final TableName SYSTEM_MUTEX_HBASE_TABLE_NAME = TableName.valueOf(SYSTEM_MUTEX_NAME); public static final byte[] SYSTEM_MUTEX_NAME_BYTES = Bytes.toBytes(SYSTEM_MUTEX_NAME); public static final byte[] SYSTEM_MUTEX_FAMILY_NAME_BYTES = TABLE_FAMILY_BYTES; http://git-wip-us.apache.org/repos/asf/phoenix/blob/943ddfa6/phoenix-core/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java ---------------------------------------------------------------------- diff --git a/phoenix-core/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java b/phoenix-core/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java index 9cd6a29..7d65d5a 100644 --- a/phoenix-core/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java +++ b/phoenix-core/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java @@ -121,6 +121,7 @@ import org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactory; import org.apache.hadoop.hbase.ipc.ServerRpcController; import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.MutationProto; import org.apache.hadoop.hbase.regionserver.IndexHalfStoreFileReaderGenerator; +import org.apache.hadoop.hbase.security.AccessDeniedException; import org.apache.hadoop.hbase.util.ByteStringer; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.Pair; @@ -253,6 +254,7 @@ import com.google.common.base.Throwables; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -1015,7 +1017,7 @@ public class ConnectionQueryServicesImpl extends DelegateQueryServices implement QueryServicesOptions.DEFAULT_ALLOW_ONLINE_TABLE_SCHEMA_UPDATE); } - private NamespaceDescriptor ensureNamespaceCreated(String schemaName) throws SQLException { + void ensureNamespaceCreated(String schemaName) throws SQLException { SQLException sqlE = null; try (HBaseAdmin admin = getAdmin()) { NamespaceDescriptor namespaceDescriptor = null; @@ -1028,13 +1030,12 @@ public class ConnectionQueryServicesImpl extends DelegateQueryServices implement namespaceDescriptor = NamespaceDescriptor.create(schemaName).build(); admin.createNamespace(namespaceDescriptor); } - return namespaceDescriptor; + return; } catch (IOException e) { sqlE = ServerUtil.parseServerException(e); } finally { if (sqlE != null) { throw sqlE; } } - return null; // will never make it here } /** @@ -2441,6 +2442,16 @@ public class ConnectionQueryServicesImpl extends DelegateQueryServices implement if (currentServerSideTableTimeStamp < MIN_SYSTEM_TABLE_TIMESTAMP) { ConnectionQueryServicesImpl.this.upgradeRequired.set(true); } + } catch (PhoenixIOException e) { + if (!Iterables.isEmpty(Iterables.filter(Throwables.getCausalChain(e), AccessDeniedException.class))) { + // Pass + logger.warn("Could not check for Phoenix SYSTEM tables, assuming they exist and are properly configured"); + checkClientServerCompatibility(SchemaUtil.getPhysicalName(SYSTEM_CATALOG_NAME_BYTES, getProps()).getName()); + success = true; + } else { + initializationException = e; + } + return null; } if (!ConnectionQueryServicesImpl.this.upgradeRequired.get()) { createOtherSystemTables(metaConnection); @@ -2493,8 +2504,13 @@ public class ConnectionQueryServicesImpl extends DelegateQueryServices implement private void createSysMutexTable(HBaseAdmin admin) throws IOException, SQLException { try { - HTableDescriptor tableDesc = new HTableDescriptor( - TableName.valueOf(PhoenixDatabaseMetaData.SYSTEM_MUTEX_NAME_BYTES)); + final String mutexTableName = PhoenixDatabaseMetaData.SYSTEM_MUTEX_NAME; + List<String> systemTables = getSystemTableNames(admin); + if (systemTables.contains(mutexTableName)) { + logger.debug("System mutex table already appears to exist, not creating it"); + return; + } + HTableDescriptor tableDesc = new HTableDescriptor(mutexTableName); HColumnDescriptor columnDesc = new HColumnDescriptor( PhoenixDatabaseMetaData.SYSTEM_MUTEX_FAMILY_NAME_BYTES); columnDesc.setTimeToLive(TTL_FOR_MUTEX); // Let mutex expire after some time @@ -2512,6 +2528,10 @@ public class ConnectionQueryServicesImpl extends DelegateQueryServices implement } } + List<String> getSystemTableNames(HBaseAdmin admin) throws IOException { + return Lists.newArrayList(admin.getTableNames(QueryConstants.SYSTEM_SCHEMA_NAME + "\\..*")); + } + private void createOtherSystemTables(PhoenixConnection metaConnection) throws SQLException { try { metaConnection.createStatement().execute(QueryConstants.CREATE_SEQUENCE_METADATA); @@ -3078,23 +3098,34 @@ public class ConnectionQueryServicesImpl extends DelegateQueryServices implement } } } - - private void ensureSystemTablesUpgraded(ReadOnlyProps props) + + void ensureSystemTablesUpgraded(ReadOnlyProps props) throws SQLException, IOException, IllegalArgumentException, InterruptedException { if (!SchemaUtil.isNamespaceMappingEnabled(PTableType.SYSTEM, props)) { return; } HTableInterface metatable = null; try (HBaseAdmin admin = getAdmin()) { - ensureNamespaceCreated(QueryConstants.SYSTEM_SCHEMA_NAME); + // Namespace-mapping is enabled at this point. + try { + ensureNamespaceCreated(QueryConstants.SYSTEM_SCHEMA_NAME); + } catch (PhoenixIOException e) { + // We could either: + // 1) Not access the NS descriptor. The NS may or may not exist at this point. + // 2) We could not create the NS + // Regardless of the case 1 or 2, if the NS does not exist, we will error expectedly + // below. If the NS does exist and is mapped, the below check will exit gracefully. + } - List<String> tableNames = Lists.newArrayList(admin.getTableNames(QueryConstants.SYSTEM_SCHEMA_NAME + "\\..*")); + List<String> tableNames = getSystemTableNames(admin); + // No tables exist matching "SYSTEM\..*", they are all already in "SYSTEM:.*" if (tableNames.size() == 0) { return; } + // Try to move any remaining tables matching "SYSTEM\..*" into "SYSTEM:" if (tableNames.size() > 5) { logger.warn("Expected 5 system tables but found " + tableNames.size() + ":" + tableNames); } byte[] mappedSystemTable = SchemaUtil .getPhysicalName(PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME_BYTES, props).getName(); metatable = getTable(mappedSystemTable); - if (tableNames.contains(PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME)) { + if (tableNames.contains(PhoenixDatabaseMetaData.SYSTEM_CATALOG_HBASE_TABLE_NAME)) { if (!admin.tableExists(mappedSystemTable)) { UpgradeUtil.mapTableToNamespace(admin, metatable, PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME, props, null, PTableType.SYSTEM, http://git-wip-us.apache.org/repos/asf/phoenix/blob/943ddfa6/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java ---------------------------------------------------------------------- diff --git a/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java b/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java new file mode 100644 index 0000000..f7676d7 --- /dev/null +++ b/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java @@ -0,0 +1,73 @@ +/* + * 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.query; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.client.HBaseAdmin; +import org.apache.hadoop.hbase.client.Mutation; +import org.apache.phoenix.exception.PhoenixIOException; +import org.apache.phoenix.util.ReadOnlyProps; +import org.junit.Test; + +public class ConnectionQueryServicesImplTest { + private static final PhoenixIOException PHOENIX_IO_EXCEPTION = new PhoenixIOException(new Exception("Test exception")); + + @SuppressWarnings("unchecked") + @Test + public void testExceptionHandlingOnSystemNamespaceCreation() throws Exception { + ConnectionQueryServicesImpl cqs = mock(ConnectionQueryServicesImpl.class); + // Invoke the real methods for these two calls + when(cqs.createSchema(any(List.class), anyString())).thenCallRealMethod(); + doCallRealMethod().when(cqs).ensureSystemTablesUpgraded(any(ReadOnlyProps.class)); + + // Spoof out this call so that ensureSystemTablesUpgrade() will return-fast. + when(cqs.getSystemTableNames(any(HBaseAdmin.class))).thenReturn(Collections.<String> emptyList()); + + // Throw a special exception to check on later + doThrow(PHOENIX_IO_EXCEPTION).when(cqs).ensureNamespaceCreated(anyString()); + + // Make sure that ensureSystemTablesUpgraded will try to migrate the system tables. + Map<String,String> props = new HashMap<>(); + props.put(QueryServices.IS_NAMESPACE_MAPPING_ENABLED, "true"); + cqs.ensureSystemTablesUpgraded(new ReadOnlyProps(props)); + + // Should be called after upgradeSystemTables() + // Proves that execution proceeded + verify(cqs).getSystemTableNames(any(HBaseAdmin.class)); + + try { + // Verifies that the exception is propagated back to the caller + cqs.createSchema(Collections.<Mutation> emptyList(), ""); + } catch (PhoenixIOException e) { + assertEquals(PHOENIX_IO_EXCEPTION, e); + } + } +}