Java tutorial
/* * 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 static org.junit.Assert.fail; import java.io.IOException; import java.lang.reflect.UndeclaredThrowableException; 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.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.AuthUtil; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.LocalHBaseCluster; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.security.AccessDeniedException; import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.access.AccessControlClient; import org.apache.hadoop.hbase.security.access.Permission; import org.apache.phoenix.jdbc.PhoenixConnection; import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData; import org.apache.phoenix.jdbc.PhoenixStatement; import org.apache.phoenix.query.BaseTest; import org.apache.phoenix.query.QueryConstants; import org.apache.phoenix.query.QueryServices; import org.apache.phoenix.util.PhoenixRuntime; import org.junit.After; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import com.google.common.base.Joiner; import com.google.common.base.Throwables; @RunWith(Parameterized.class) public class BasePermissionsIT extends BaseTest { private static final Log LOG = LogFactory.getLog(BasePermissionsIT.class); static String SUPERUSER; static HBaseTestingUtility testUtil; static final Set<String> PHOENIX_SYSTEM_TABLES = new HashSet<>( Arrays.asList("SYSTEM.CATALOG", "SYSTEM.SEQUENCE", "SYSTEM.STATS", "SYSTEM.FUNCTION", "SYSTEM.MUTEX")); static final Set<String> PHOENIX_SYSTEM_TABLES_IDENTIFIERS = new HashSet<>(Arrays.asList("SYSTEM.\"CATALOG\"", "SYSTEM.\"SEQUENCE\"", "SYSTEM.\"STATS\"", "SYSTEM.\"FUNCTION\"", "SYSTEM.\"MUTEX\"")); static final String SYSTEM_SEQUENCE_IDENTIFIER = QueryConstants.SYSTEM_SCHEMA_NAME + "." + "\"" + PhoenixDatabaseMetaData.SYSTEM_SEQUENCE_TABLE + "\""; static final String SYSTEM_MUTEX_IDENTIFIER = QueryConstants.SYSTEM_SCHEMA_NAME + "." + "\"" + PhoenixDatabaseMetaData.SYSTEM_MUTEX_TABLE_NAME + "\""; static final Set<String> PHOENIX_NAMESPACE_MAPPED_SYSTEM_TABLES = new HashSet<>( Arrays.asList("SYSTEM:CATALOG", "SYSTEM:SEQUENCE", "SYSTEM:STATS", "SYSTEM:FUNCTION", "SYSTEM:MUTEX")); // Create Multiple users so that we can use Hadoop UGI to run tasks as various users // Permissions can be granted or revoke by superusers and admins only // DON'T USE HADOOP UserGroupInformation class to create testing users since HBase misses some of its functionality // Instead use org.apache.hadoop.hbase.security.User class for testing purposes. // Super User has all the access User superUser1 = null; User superUser2 = null; // Regular users are granted and revoked permissions as needed User regularUser1 = null; User regularUser2 = null; User regularUser3 = null; User regularUser4 = null; // Group User is equivalent of regular user but inside a group // Permissions can be granted to group should affect this user static final String GROUP_SYSTEM_ACCESS = "group_system_access"; User groupUser = null; // Unpriviledged User doesn't have any access and is denied for every action User unprivilegedUser = null; static final int NUM_RECORDS = 5; boolean isNamespaceMapped; public BasePermissionsIT(final boolean isNamespaceMapped) throws Exception { this.isNamespaceMapped = isNamespaceMapped; } @BeforeClass public static void doSetup() throws Exception { SUPERUSER = System.getProperty("user.name"); } void startNewMiniCluster() throws Exception { startNewMiniCluster(new Configuration()); } void startNewMiniCluster(Configuration overrideConf) throws Exception { if (null != testUtil) { testUtil.shutdownMiniCluster(); testUtil = null; } testUtil = new HBaseTestingUtility(); Configuration config = testUtil.getConfiguration(); enablePhoenixHBaseAuthorization(config); configureNamespacesOnServer(config); config.setBoolean(LocalHBaseCluster.ASSIGN_RANDOM_PORTS, true); if (overrideConf != null) { config.addResource(overrideConf); } testUtil.startMiniCluster(1); initializeUsers(testUtil.getConfiguration()); } private void initializeUsers(Configuration configuration) { superUser1 = User.createUserForTesting(configuration, SUPERUSER, new String[0]); superUser2 = User.createUserForTesting(configuration, "superUser2", new String[0]); regularUser1 = User.createUserForTesting(configuration, "regularUser1", new String[0]); regularUser2 = User.createUserForTesting(configuration, "regularUser2", new String[0]); regularUser3 = User.createUserForTesting(configuration, "regularUser3", new String[0]); regularUser4 = User.createUserForTesting(configuration, "regularUser4", new String[0]); groupUser = User.createUserForTesting(testUtil.getConfiguration(), "groupUser", new String[] { GROUP_SYSTEM_ACCESS }); unprivilegedUser = User.createUserForTesting(configuration, "unprivilegedUser", new String[0]); } private void configureRandomHMasterPort(Configuration config) { // Avoid multiple clusters trying to bind the master's info port (16010) config.setInt(HConstants.MASTER_INFO_PORT, 0); } void enablePhoenixHBaseAuthorization(Configuration config) { config.set("hbase.superuser", SUPERUSER + "," + "superUser2"); config.set("hbase.security.authorization", Boolean.TRUE.toString()); config.set("hbase.security.exec.permission.checks", Boolean.TRUE.toString()); config.set("hbase.coprocessor.master.classes", "org.apache.hadoop.hbase.security.access.AccessController"); config.set("hbase.coprocessor.region.classes", "org.apache.hadoop.hbase.security.access.AccessController"); config.set("hbase.coprocessor.regionserver.classes", "org.apache.hadoop.hbase.security.access.AccessController"); config.set(QueryServices.PHOENIX_ACLS_ENABLED, "true"); config.set("hbase.regionserver.wal.codec", "org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec"); } void configureNamespacesOnServer(Configuration conf) { conf.set(QueryServices.IS_NAMESPACE_MAPPING_ENABLED, Boolean.toString(isNamespaceMapped)); } @Parameterized.Parameters(name = "isNamespaceMapped={0}") // name is used by failsafe as file name in reports public static Collection<Boolean> data() { return Arrays.asList(false, true); } @After public void cleanup() throws Exception { if (testUtil != null) { testUtil.shutdownMiniCluster(); testUtil = null; } } public static HBaseTestingUtility getUtility() { return testUtil; } // Utility functions to grant permissions with HBase API void grantPermissions(String toUser, Set<String> tablesToGrant, Permission.Action... actions) throws Throwable { for (String table : tablesToGrant) { AccessControlClient.grant(getUtility().getConnection(), TableName.valueOf(table), toUser, null, null, actions); } } void grantPermissions(String toUser, String namespace, Permission.Action... actions) throws Throwable { AccessControlClient.grant(getUtility().getConnection(), namespace, toUser, actions); } void grantPermissions(String groupEntry, Permission.Action... actions) throws IOException, Throwable { AccessControlClient.grant(getUtility().getConnection(), groupEntry, actions); } // Utility functions to revoke permissions with HBase API void revokeAll() throws Throwable { AccessControlClient.revoke(getUtility().getConnection(), AuthUtil.toGroupEntry(GROUP_SYSTEM_ACCESS), Permission.Action.values()); AccessControlClient.revoke(getUtility().getConnection(), regularUser1.getShortName(), Permission.Action.values()); AccessControlClient.revoke(getUtility().getConnection(), unprivilegedUser.getShortName(), Permission.Action.values()); } Properties getClientProperties(String tenantId) { Properties props = new Properties(); if (tenantId != null) { props.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId); } props.setProperty(QueryServices.IS_NAMESPACE_MAPPING_ENABLED, Boolean.toString(isNamespaceMapped)); return props; } public Connection getConnection() throws SQLException { return getConnection(null); } public Connection getConnection(String tenantId) throws SQLException { return DriverManager.getConnection(getUrl(), getClientProperties(tenantId)); } protected static String getUrl() { return "jdbc:phoenix:localhost:" + testUtil.getZkCluster().getClientPort() + ":/hbase"; } static Set<String> getHBaseTables() throws IOException { Set<String> tables = new HashSet<>(); for (TableName tn : testUtil.getHBaseAdmin().listTableNames()) { tables.add(tn.getNameAsString()); } return tables; } // UG Object // 1. Instance of String --> represents GROUP name // 2. Instance of User --> represents HBase user AccessTestAction grantPermissions(final String actions, final Object ug, final String tableOrSchemaList, final boolean isSchema) throws SQLException { return grantPermissions(actions, ug, Collections.singleton(tableOrSchemaList), isSchema); } AccessTestAction grantPermissions(final String actions, final Object ug, final Set<String> tableOrSchemaList, final boolean isSchema) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { for (String tableOrSchema : tableOrSchemaList) { String grantStmtSQL = "GRANT '" + actions + "' ON " + (isSchema ? " SCHEMA " : " TABLE ") + tableOrSchema + " TO " + ((ug instanceof String) ? (" GROUP " + "'" + ug + "'") : ("'" + ((User) ug).getShortName() + "'")); LOG.info("Grant Permissions SQL: " + grantStmtSQL); assertFalse(stmt.execute(grantStmtSQL)); } } return null; } }; } AccessTestAction grantPermissions(final String actions, final User user) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { String grantStmtSQL = "GRANT '" + actions + "' TO " + " '" + user.getShortName() + "'"; LOG.info("Grant Permissions SQL: " + grantStmtSQL); assertFalse(stmt.execute(grantStmtSQL)); } return null; } }; } AccessTestAction revokePermissions(final Object ug, final String tableOrSchemaList, final boolean isSchema) throws SQLException { return revokePermissions(ug, Collections.singleton(tableOrSchemaList), isSchema); } AccessTestAction revokePermissions(final Object ug, final Set<String> tableOrSchemaList, final boolean isSchema) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { for (String tableOrSchema : tableOrSchemaList) { String revokeStmtSQL = "REVOKE ON " + (isSchema ? " SCHEMA " : " TABLE ") + tableOrSchema + " FROM " + ((ug instanceof String) ? (" GROUP " + "'" + ug + "'") : ("'" + ((User) ug).getShortName() + "'")); LOG.info("Revoke Permissions SQL: " + revokeStmtSQL); assertFalse(stmt.execute(revokeStmtSQL)); } } return null; } }; } AccessTestAction revokePermissions(final Object ug) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { String revokeStmtSQL = "REVOKE FROM " + ((ug instanceof String) ? (" GROUP " + "'" + ug + "'") : ("'" + ((User) ug).getShortName() + "'")); LOG.info("Revoke Permissions SQL: " + revokeStmtSQL); assertFalse(stmt.execute(revokeStmtSQL)); } return null; } }; } // Attempts to get a Phoenix Connection // New connections could create SYSTEM tables if appropriate perms are granted AccessTestAction getConnectionAction() throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection();) { } return null; } }; } AccessTestAction createSchema(final String schemaName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { if (isNamespaceMapped) { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("CREATE SCHEMA " + schemaName)); } } return null; } }; } AccessTestAction dropSchema(final String schemaName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { if (isNamespaceMapped) { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("DROP SCHEMA " + schemaName)); } } return null; } }; } AccessTestAction createTable(final String tableName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("CREATE TABLE " + tableName + "(pk INTEGER not null primary key, data VARCHAR, val integer)")); try (PreparedStatement pstmt = conn .prepareStatement("UPSERT INTO " + tableName + " values(?, ?, ?)")) { for (int i = 0; i < NUM_RECORDS; i++) { pstmt.setInt(1, i); pstmt.setString(2, Integer.toString(i)); pstmt.setInt(3, i); assertEquals(1, pstmt.executeUpdate()); } } conn.commit(); } return null; } }; } AccessTestAction createMultiTenantTable(final String tableName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("CREATE TABLE " + tableName + "(ORG_ID VARCHAR NOT NULL, PREFIX CHAR(3) NOT NULL, DATA VARCHAR, VAL INTEGER CONSTRAINT PK PRIMARY KEY (ORG_ID, PREFIX)) MULTI_TENANT=TRUE")); try (PreparedStatement pstmt = conn .prepareStatement("UPSERT INTO " + tableName + " values(?, ?, ?, ?)")) { for (int i = 0; i < NUM_RECORDS; i++) { pstmt.setString(1, "o" + i); pstmt.setString(2, "pr" + i); pstmt.setString(3, Integer.toString(i)); pstmt.setInt(4, i); assertEquals(1, pstmt.executeUpdate()); } } conn.commit(); } return null; } }; } AccessTestAction dropTable(final String tableName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute(String.format("DROP TABLE IF EXISTS %s CASCADE", tableName))); } return null; } }; } // Attempts to read given table without verifying data // AccessDeniedException is only triggered when ResultSet#next() method is called // The first call triggers HBase Scan object // The Statement#executeQuery() method returns an iterator and doesn't interact with HBase API at all AccessTestAction readTableWithoutVerification(final String tableName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); assertNotNull(rs); while (rs.next()) { } } return null; } }; } AccessTestAction readTable(final String tableName) throws SQLException { return readTable(tableName, null); } AccessTestAction readTable(final String tableName, final String indexName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { String readTableSQL = "SELECT " + (indexName != null ? "/*+ INDEX(" + tableName + " " + indexName + ")*/" : "") + " pk, data, val FROM " + tableName + " where data >= '0'"; ResultSet rs = stmt.executeQuery(readTableSQL); assertNotNull(rs); int i = 0; while (rs.next()) { assertEquals(i, rs.getInt(1)); assertEquals(Integer.toString(i), rs.getString(2)); assertEquals(i, rs.getInt(3)); i++; } assertEquals(NUM_RECORDS, i); } return null; } }; } AccessTestAction readMultiTenantTableWithoutIndex(final String tableName) throws SQLException { return readMultiTenantTableWithoutIndex(tableName, null); } AccessTestAction readMultiTenantTableWithoutIndex(final String tableName, final String tenantId) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(tenantId); Statement stmt = conn.createStatement()) { // Accessing all the data from the table avoids the use of index String readTableSQL = "SELECT data, val FROM " + tableName; ResultSet rs = stmt.executeQuery(readTableSQL); assertNotNull(rs); int i = 0; String explainPlan = Joiner.on(" ") .join(((PhoenixStatement) stmt).getQueryPlan().getExplainPlan().getPlanSteps()); rs = stmt.executeQuery(readTableSQL); if (tenantId != null) { rs.next(); assertFalse(explainPlan.contains("_IDX_")); assertEquals(((PhoenixConnection) conn).getTenantId().toString(), tenantId); // For tenant ID "o3", the value in table will be 3 assertEquals(Character.toString(tenantId.charAt(1)), rs.getString(1)); // Only 1 record is inserted per Tenant assertFalse(rs.next()); } else { while (rs.next()) { assertEquals(Integer.toString(i), rs.getString(1)); assertEquals(i, rs.getInt(2)); i++; } assertEquals(NUM_RECORDS, i); } } return null; } }; } AccessTestAction readMultiTenantTableWithIndex(final String tableName) throws SQLException { return readMultiTenantTableWithIndex(tableName, null); } AccessTestAction readMultiTenantTableWithIndex(final String tableName, final String tenantId) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(tenantId); Statement stmt = conn.createStatement()) { // Accessing only the 'data' from the table uses index since index tables are built on 'data' column String readTableSQL = "SELECT data FROM " + tableName; ResultSet rs = stmt.executeQuery(readTableSQL); assertNotNull(rs); int i = 0; String explainPlan = Joiner.on(" ") .join(((PhoenixStatement) stmt).getQueryPlan().getExplainPlan().getPlanSteps()); assertTrue(explainPlan.contains("_IDX_")); rs = stmt.executeQuery(readTableSQL); if (tenantId != null) { rs.next(); assertEquals(((PhoenixConnection) conn).getTenantId().toString(), tenantId); // For tenant ID "o3", the value in table will be 3 assertEquals(Character.toString(tenantId.charAt(1)), rs.getString(1)); // Only 1 record is inserted per Tenant assertFalse(rs.next()); } else { while (rs.next()) { assertEquals(Integer.toString(i), rs.getString(1)); i++; } assertEquals(NUM_RECORDS, i); } } return null; } }; } AccessTestAction addProperties(final String tableName, final String property, final String value) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("ALTER TABLE " + tableName + " SET " + property + "=" + value)); } return null; } }; } AccessTestAction addColumn(final String tableName, final String columnName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("ALTER TABLE " + tableName + " ADD " + columnName + " varchar")); } return null; } }; } AccessTestAction dropColumn(final String tableName, final String columnName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("ALTER TABLE " + tableName + " DROP COLUMN " + columnName)); } return null; } }; } AccessTestAction createIndex(final String indexName, final String dataTable) throws SQLException { return createIndex(indexName, dataTable, null); } AccessTestAction createIndex(final String indexName, final String dataTable, final String tenantId) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(tenantId); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("CREATE INDEX " + indexName + " on " + dataTable + "(data)")); } return null; } }; } AccessTestAction createLocalIndex(final String indexName, final String dataTable) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("CREATE LOCAL INDEX " + indexName + " on " + dataTable + "(data)")); } return null; } }; } AccessTestAction dropIndex(final String indexName, final String dataTable) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("DROP INDEX " + indexName + " on " + dataTable)); } return null; } }; } AccessTestAction rebuildIndex(final String indexName, final String dataTable) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute("ALTER INDEX " + indexName + " on " + dataTable + " DISABLE")); assertFalse(stmt.execute("ALTER INDEX " + indexName + " on " + dataTable + " REBUILD")); } return null; } }; } AccessTestAction dropView(final String viewName) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(); Statement stmt = conn.createStatement();) { assertFalse(stmt.execute(String.format("DROP VIEW %s CASCADE", viewName))); } return null; } }; } AccessTestAction createView(final String viewName, final String dataTable) throws SQLException { return createView(viewName, dataTable, null); } AccessTestAction createView(final String viewName, final String dataTable, final String tenantId) throws SQLException { return new AccessTestAction() { @Override public Object run() throws Exception { try (Connection conn = getConnection(tenantId); Statement stmt = conn.createStatement();) { String viewStmtSQL = "CREATE VIEW " + viewName + " AS SELECT * FROM " + dataTable; assertFalse(stmt.execute(viewStmtSQL)); } return null; } }; } static interface AccessTestAction extends PrivilegedExceptionAction<Object> { } /** This fails only in case of ADE or empty list for any of the users. */ void verifyAllowed(AccessTestAction action, User... users) throws Exception { if (users.length == 0) { throw new Exception("Action needs at least one user to run"); } for (User user : users) { verifyAllowed(user, action); } } void verifyAllowed(User user, TableDDLPermissionsIT.AccessTestAction... actions) throws Exception { for (TableDDLPermissionsIT.AccessTestAction action : actions) { try { Object obj = user.runAs(action); if (obj != null && obj instanceof List<?>) { List<?> results = (List<?>) obj; if (results != null && results.isEmpty()) { fail("Empty non null results from action for user '" + user.getShortName() + "'"); } } } catch (AccessDeniedException ade) { fail("Expected action to pass for user '" + user.getShortName() + "' but was denied"); } } } /** This passes only if desired exception is caught for all users. */ <T> void verifyDenied(AccessTestAction action, Class<T> exception, User... users) throws Exception { if (users.length == 0) { throw new Exception("Action needs at least one user to run"); } for (User user : users) { verifyDenied(user, exception, action); } } /** This passes only if desired exception is caught for all users. */ <T> void verifyDenied(User user, Class<T> exception, TableDDLPermissionsIT.AccessTestAction... actions) throws Exception { for (TableDDLPermissionsIT.AccessTestAction action : actions) { try { user.runAs(action); fail("Expected exception was not thrown for user '" + user.getShortName() + "'"); } catch (IOException e) { fail("Expected exception was not thrown for user '" + user.getShortName() + "'"); } catch (UndeclaredThrowableException ute) { Throwable ex = ute.getUndeclaredThrowable(); // HBase AccessDeniedException(ADE) is handled in different ways in different parts of code // 1. Wrap HBase ADE in PhoenixIOException (Mostly for create, delete statements) // 2. Wrap HBase ADE in ExecutionException (Mostly for scans) // 3. Directly throwing HBase ADE or custom msg with HBase ADE // Thus we iterate over the chain of throwables and find ADE for (Throwable throwable : Throwables.getCausalChain(ex)) { if (exception.equals(throwable.getClass())) { if (throwable instanceof AccessDeniedException) { validateAccessDeniedException((AccessDeniedException) throwable); } return; } } } catch (RuntimeException ex) { // This can occur while accessing tabledescriptors from client by the unprivileged user if (ex.getCause() instanceof AccessDeniedException) { // expected result validateAccessDeniedException((AccessDeniedException) ex.getCause()); return; } } fail("Expected exception was not thrown for user '" + user.getShortName() + "'"); } } String surroundWithDoubleQuotes(String input) { return "\"" + input + "\""; } void validateAccessDeniedException(AccessDeniedException ade) { String msg = ade.getMessage(); assertTrue("Exception contained unexpected message: '" + msg + "'", !msg.contains("is not the scanner owner")); } }