| /* |
| * 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.drill.exec.impersonation; |
| |
| import org.apache.drill.categories.SecurityTest; |
| import org.apache.drill.categories.SlowTest; |
| import org.apache.drill.categories.UnlikelyTest; |
| import org.apache.drill.common.exceptions.UserException; |
| import org.apache.drill.common.exceptions.UserRemoteException; |
| import org.apache.drill.exec.dotdrill.DotDrillType; |
| import org.apache.drill.exec.rpc.user.QueryDataBatch; |
| import org.apache.drill.exec.store.StoragePluginRegistry.PluginException; |
| import org.apache.drill.exec.store.dfs.WorkspaceConfig; |
| import org.apache.drill.shaded.guava.com.google.common.base.Joiner; |
| import org.apache.drill.shaded.guava.com.google.common.collect.Maps; |
| import org.apache.drill.test.ClientFixture; |
| import org.apache.hadoop.fs.FileStatus; |
| import org.apache.hadoop.fs.FileSystem; |
| import org.apache.hadoop.fs.Path; |
| import org.apache.hadoop.fs.permission.FsPermission; |
| import org.apache.hadoop.security.UserGroupInformation; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.experimental.categories.Category; |
| import org.junit.rules.ExpectedException; |
| |
| import java.util.List; |
| import java.util.Map; |
| |
| import static org.hamcrest.core.StringContains.containsString; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| |
| /** |
| * Tests impersonation on metadata related queries as SHOW FILES, SHOW TABLES, CREATE VIEW, CREATE TABLE and DROP TABLE |
| */ |
| @Category({SlowTest.class, SecurityTest.class}) |
| public class TestImpersonationMetadata extends BaseTestImpersonation { |
| private static final String user1 = "drillTestUser1"; |
| private static final String user2 = "drillTestUser2"; |
| |
| private static final String group0 = "drill_test_grp_0"; |
| private static final String group1 = "drill_test_grp_1"; |
| |
| static { |
| UserGroupInformation.createUserForTesting(user1, new String[]{ group1, group0 }); |
| UserGroupInformation.createUserForTesting(user2, new String[]{ group1 }); |
| } |
| |
| @Rule |
| public ExpectedException thrown = ExpectedException.none(); |
| |
| @Before |
| public void setup() throws Exception { |
| startMiniDfsCluster(TestImpersonationMetadata.class.getSimpleName()); |
| startDrillCluster(true); |
| addMiniDfsBasedStorage(createTestWorkspaces()); |
| } |
| |
| private static Map<String, WorkspaceConfig> createTestWorkspaces() throws Exception { |
| // Create "/tmp" folder and set permissions to "777" |
| final Path tmpPath = new Path("/tmp"); |
| fs.delete(tmpPath, true); |
| FileSystem.mkdirs(fs, tmpPath, new FsPermission((short)0777)); |
| |
| Map<String, WorkspaceConfig> workspaces = Maps.newHashMap(); |
| |
| // Create /drill_test_grp_0_700 directory with permissions 700 (owned by user running the tests) |
| createAndAddWorkspace("drill_test_grp_0_700", "/drill_test_grp_0_700", (short)0700, processUser, group0, workspaces); |
| |
| // Create /drill_test_grp_0_750 directory with permissions 750 (owned by user running the tests) |
| createAndAddWorkspace("drill_test_grp_0_750", "/drill_test_grp_0_750", (short)0750, processUser, group0, workspaces); |
| |
| // Create /drill_test_grp_0_755 directory with permissions 755 (owned by user running the tests) |
| createAndAddWorkspace("drill_test_grp_0_755", "/drill_test_grp_0_755", (short)0755, processUser, group0, workspaces); |
| |
| // Create /drill_test_grp_0_770 directory with permissions 770 (owned by user running the tests) |
| createAndAddWorkspace("drill_test_grp_0_770", "/drill_test_grp_0_770", (short)0770, processUser, group0, workspaces); |
| |
| // Create /drill_test_grp_0_777 directory with permissions 777 (owned by user running the tests) |
| createAndAddWorkspace("drill_test_grp_0_777", "/drill_test_grp_0_777", (short)0777, processUser, group0, workspaces); |
| |
| // Create /drill_test_grp_1_700 directory with permissions 700 (owned by user1) |
| createAndAddWorkspace("drill_test_grp_1_700", "/drill_test_grp_1_700", (short)0700, user1, group1, workspaces); |
| |
| // create /user2_workspace1 with 775 permissions (owner by user1) |
| createAndAddWorkspace("user2_workspace1", "/user2_workspace1", (short)0775, user2, group1, workspaces); |
| |
| // create /user2_workspace with 755 permissions (owner by user1) |
| createAndAddWorkspace("user2_workspace2", "/user2_workspace2", (short)0755, user2, group1, workspaces); |
| |
| return workspaces; |
| } |
| |
| @Test |
| public void testDropTable() throws Exception { |
| |
| // create tables as user2 |
| try (ClientFixture client = cluster.client(user2, "")) { |
| client.run("use `%s.user2_workspace1`", MINI_DFS_STORAGE_PLUGIN_NAME); |
| // create a table that can be dropped by another user in a different group |
| client.run("create table parquet_table_775 as select * from cp.`employee.json`"); |
| |
| // create a table that cannot be dropped by another user |
| client.run("use `%s.user2_workspace2`", MINI_DFS_STORAGE_PLUGIN_NAME); |
| client.run("create table parquet_table_700 as select * from cp.`employee.json`"); |
| } |
| |
| // Drop tables as user1 |
| try (ClientFixture client = cluster.client(user1, "")) { |
| client.run("use `%s.user2_workspace1`", MINI_DFS_STORAGE_PLUGIN_NAME); |
| client.testBuilder() |
| .sqlQuery("drop table parquet_table_775") |
| .unOrdered() |
| .baselineColumns("ok", "summary") |
| .baselineValues(true, String.format("Table [%s] dropped", "parquet_table_775")) |
| .go(); |
| |
| client.run("use `%s.user2_workspace2`", MINI_DFS_STORAGE_PLUGIN_NAME); |
| boolean dropFailed = false; |
| try { |
| client.run("drop table parquet_table_700"); |
| } catch (UserException e) { |
| assertTrue(e.getMessage().contains("PERMISSION ERROR")); |
| dropFailed = true; |
| } |
| assertTrue("Permission checking failed during drop table", dropFailed); |
| } |
| } |
| |
| @Test // DRILL-3037 |
| @Category(UnlikelyTest.class) |
| public void testImpersonatingProcessUser() throws Exception { |
| try (ClientFixture client = cluster.client(processUser, "")) { |
| |
| // Process user start the mini dfs, he has read/write permissions by default |
| final String viewName = String.format("%s.drill_test_grp_0_700.testView", MINI_DFS_STORAGE_PLUGIN_NAME); |
| try { |
| client.run("CREATE VIEW " + viewName + " AS SELECT * FROM cp.`region.json`"); |
| client.run("SELECT * FROM " + viewName + " LIMIT 2"); |
| } finally { |
| client.run("DROP VIEW " + viewName); |
| } |
| } |
| } |
| |
| @Test |
| public void testShowFilesInWSWithUserAndGroupPermissionsForQueryUser() throws Exception { |
| try (ClientFixture client = cluster.client(user1, "")) { |
| { |
| // Try show tables in schema "drill_test_grp_1_700" which is owned by |
| // "user1" |
| List<QueryDataBatch> results = client |
| .queryBuilder() |
| .sql("SHOW FILES IN %s.drill_test_grp_1_700", MINI_DFS_STORAGE_PLUGIN_NAME) |
| .results(); |
| assertTrue(client.countResults(results) > 0); |
| results.forEach(r -> r.release()); |
| } |
| { |
| // Try show tables in schema "drill_test_grp_0_750" which is owned by |
| // "processUser" and has group permissions for "user1" |
| List<QueryDataBatch> results = client |
| .queryBuilder() |
| .sql("SHOW FILES IN %s.drill_test_grp_0_750", MINI_DFS_STORAGE_PLUGIN_NAME) |
| .results(); |
| assertTrue(client.countResults(results) > 0); |
| results.forEach(r -> r.release()); |
| } |
| } |
| } |
| |
| @Test |
| public void testShowFilesInWSWithOtherPermissionsForQueryUser() throws Exception { |
| try (ClientFixture client = cluster.client(user2, "")) { |
| // Try show tables in schema "drill_test_grp_0_755" which is owned by "processUser" and group0. "user2" is not part of the "group0" |
| List<QueryDataBatch> results = client.queryBuilder().sql( |
| String.format("SHOW FILES IN %s.drill_test_grp_0_755", MINI_DFS_STORAGE_PLUGIN_NAME)).results(); |
| assertTrue(client.countResults(results) > 0); |
| results.forEach(r -> r.release()); |
| } |
| } |
| |
| @Test |
| public void testShowFilesInWSWithNoPermissionsForQueryUser() throws Exception { |
| try (ClientFixture client = cluster.client(user2, "")) { |
| // Try show tables in schema "drill_test_grp_1_700" which is owned by "user1" |
| List<QueryDataBatch> results = client.queryBuilder().sql( |
| String.format("SHOW FILES IN %s.drill_test_grp_1_700", MINI_DFS_STORAGE_PLUGIN_NAME)).results(); |
| assertEquals(0, client.countResults(results)); |
| results.forEach(r -> r.release()); |
| } |
| } |
| |
| @Test |
| public void testShowSchemasAsUser1() throws Exception { |
| // "user1" is part of "group0" and has access to following workspaces |
| // drill_test_grp_1_700 (through ownership) |
| // drill_test_grp_0_750, drill_test_grp_0_770 (through "group" category permissions) |
| // drill_test_grp_0_755, drill_test_grp_0_777 (through "others" category permissions) |
| try (ClientFixture client = cluster.client(user1, "")) { |
| client.testBuilder() |
| .sqlQuery("SHOW SCHEMAS LIKE '%drill_test%'") |
| .unOrdered() |
| .baselineColumns("SCHEMA_NAME") |
| .baselineValues(String.format("%s.drill_test_grp_0_750", MINI_DFS_STORAGE_PLUGIN_NAME)) |
| .baselineValues(String.format("%s.drill_test_grp_0_755", MINI_DFS_STORAGE_PLUGIN_NAME)) |
| .baselineValues(String.format("%s.drill_test_grp_0_770", MINI_DFS_STORAGE_PLUGIN_NAME)) |
| .baselineValues(String.format("%s.drill_test_grp_0_777", MINI_DFS_STORAGE_PLUGIN_NAME)) |
| .baselineValues(String.format("%s.drill_test_grp_1_700", MINI_DFS_STORAGE_PLUGIN_NAME)) |
| .go(); |
| } |
| } |
| |
| @Test |
| public void testShowSchemasAsUser2() throws Exception { |
| // "user2" is part of "group0", but part of "group1" and has access to following workspaces |
| // drill_test_grp_0_755, drill_test_grp_0_777 (through "others" category permissions) |
| try (ClientFixture client = cluster.client(user2, "")) { |
| client.testBuilder() |
| .sqlQuery("SHOW SCHEMAS LIKE '%drill_test%'") |
| .unOrdered() |
| .baselineColumns("SCHEMA_NAME") |
| .baselineValues(String.format("%s.drill_test_grp_0_755", MINI_DFS_STORAGE_PLUGIN_NAME)) |
| .baselineValues(String.format("%s.drill_test_grp_0_777", MINI_DFS_STORAGE_PLUGIN_NAME)) |
| .go(); |
| } |
| } |
| |
| @Test |
| public void testCreateViewInDirWithUserPermissionsForQueryUser() throws Exception { |
| final String viewSchema = MINI_DFS_STORAGE_PLUGIN_NAME + ".drill_test_grp_1_700"; // Workspace dir owned by "user1" |
| testCreateViewTestHelper(user1, viewSchema, "view1"); |
| } |
| |
| @Test |
| public void testCreateViewInDirWithGroupPermissionsForQueryUser() throws Exception { |
| // Workspace dir owned by "processUser", workspace group is "group0" and "user1" is part of "group0" |
| final String viewSchema = MINI_DFS_STORAGE_PLUGIN_NAME + ".drill_test_grp_0_770"; |
| testCreateViewTestHelper(user1, viewSchema, "view1"); |
| } |
| |
| @Test |
| public void testCreateViewInDirWithOtherPermissionsForQueryUser() throws Exception { |
| // Workspace dir owned by "processUser", workspace group is "group0" and "user2" is not part of "group0" |
| final String viewSchema = MINI_DFS_STORAGE_PLUGIN_NAME + ".drill_test_grp_0_777"; |
| testCreateViewTestHelper(user2, viewSchema, "view1"); |
| } |
| |
| private static void testCreateViewTestHelper(String user, String viewSchema, |
| String viewName) throws Exception { |
| try (ClientFixture client = cluster.client(user, "")) { |
| try { |
| client.run("USE " + viewSchema); |
| client.run("CREATE VIEW " + viewName + " AS SELECT " + |
| "c_custkey, c_nationkey FROM cp.`tpch/customer.parquet` ORDER BY c_custkey"); |
| |
| client.testBuilder() |
| .sqlQuery("SHOW TABLES") |
| .unOrdered() |
| .baselineColumns("TABLE_SCHEMA", "TABLE_NAME") |
| .baselineValues(viewSchema, viewName) |
| .go(); |
| |
| client.run("SHOW FILES"); |
| |
| client.testBuilder() |
| .sqlQuery("SELECT * FROM " + viewName + " LIMIT 1") |
| .ordered() |
| .baselineColumns("c_custkey", "c_nationkey") |
| .baselineValues(1, 15) |
| .go(); |
| |
| } finally { |
| client.run("DROP VIEW " + viewSchema + "." + viewName); |
| } |
| } |
| } |
| |
| @Test |
| public void testCreateViewInWSWithNoPermissionsForQueryUser() throws Exception { |
| // Workspace dir owned by "processUser", workspace group is "group0" and "user2" is not part of "group0" |
| final String tableWS = "drill_test_grp_0_755"; |
| final String viewSchema = MINI_DFS_STORAGE_PLUGIN_NAME + "." + tableWS; |
| final String viewName = "view1"; |
| |
| try (ClientFixture client = cluster.client(user2, "")) { |
| |
| client.run("USE " + viewSchema); |
| |
| String expErrorMsg = "PERMISSION ERROR: Permission denied: user=drillTestUser2, access=WRITE, inode=\"/" + tableWS; |
| thrown.expect(UserRemoteException.class); |
| thrown.expectMessage(containsString(expErrorMsg)); |
| |
| client.run("CREATE VIEW %s AS" + |
| " SELECT c_custkey, c_nationkey FROM cp.`tpch/customer.parquet` ORDER BY c_custkey", viewName); |
| |
| // SHOW TABLES is expected to return no records as view creation fails above. |
| client.testBuilder() |
| .sqlQuery("SHOW TABLES") |
| .expectsEmptyResultSet() |
| .go(); |
| |
| client.run("SHOW FILES"); |
| } |
| } |
| |
| @Test |
| public void testCreateTableInDirWithUserPermissionsForQueryUser() throws Exception { |
| final String tableWS = "drill_test_grp_1_700"; // Workspace dir owned by "user1" |
| testCreateTableTestHelper(user1, tableWS, "table1"); |
| } |
| |
| @Test |
| public void testCreateTableInDirWithGroupPermissionsForQueryUser() throws Exception { |
| // Workspace dir owned by "processUser", workspace group is "group0" and "user1" is part of "group0" |
| final String tableWS = "drill_test_grp_0_770"; |
| testCreateTableTestHelper(user1, tableWS, "table1"); |
| } |
| |
| @Test |
| public void testCreateTableInDirWithOtherPermissionsForQueryUser() throws Exception { |
| // Workspace dir owned by "processUser", workspace group is "group0" and "user2" is not part of "group0" |
| final String tableWS = "drill_test_grp_0_777"; |
| testCreateTableTestHelper(user2, tableWS, "table1"); |
| } |
| |
| private static void testCreateTableTestHelper(String user, String tableWS, |
| String tableName) throws Exception { |
| try { |
| try (ClientFixture client = cluster.client(user, "")) { |
| |
| client.run("USE " + Joiner.on(".").join(MINI_DFS_STORAGE_PLUGIN_NAME, tableWS)); |
| |
| client.run("CREATE TABLE " + tableName + " AS SELECT " + |
| "c_custkey, c_nationkey FROM cp.`tpch/customer.parquet` ORDER BY c_custkey"); |
| |
| client.run("SHOW FILES"); |
| |
| client.testBuilder() |
| .sqlQuery("SELECT * FROM " + tableName + " LIMIT 1") |
| .ordered() |
| .baselineColumns("c_custkey", "c_nationkey") |
| .baselineValues(1, 15) |
| .go(); |
| } |
| |
| } finally { |
| // There is no drop table, we need to delete the table directory through FileSystem object |
| final Path tablePath = new Path(Path.SEPARATOR + tableWS + Path.SEPARATOR + tableName); |
| if (fs.exists(tablePath)) { |
| fs.delete(tablePath, true); |
| } |
| } |
| } |
| |
| @Test |
| public void testCreateTableInWSWithNoPermissionsForQueryUser() throws Exception { |
| // Workspace dir owned by "processUser", workspace group is "group0" and "user2" is not part of "group0" |
| String tableWS = "drill_test_grp_0_755"; |
| String tableName = "table1"; |
| |
| try (ClientFixture client = cluster.client(user2, "")) { |
| client.run("use %s.`%s`", MINI_DFS_STORAGE_PLUGIN_NAME, tableWS); |
| |
| thrown.expect(UserRemoteException.class); |
| thrown.expectMessage(containsString("Permission denied: user=drillTestUser2, " + |
| "access=WRITE, inode=\"/" + tableWS)); |
| |
| client.run("CREATE TABLE %s AS SELECT c_custkey, c_nationkey " + |
| "FROM cp.`tpch/customer.parquet` ORDER BY c_custkey", tableName); |
| } |
| } |
| |
| @Test |
| public void testRefreshMetadata() throws Exception { |
| final String tableName = "nation1"; |
| final String tableWS = "drill_test_grp_1_700"; |
| |
| try (ClientFixture client = cluster.client(user1, "")) { |
| client.run("USE " + Joiner.on(".").join(MINI_DFS_STORAGE_PLUGIN_NAME, tableWS)); |
| |
| client.run("CREATE TABLE " + tableName + " partition by (n_regionkey) AS SELECT * " + |
| "FROM cp.`tpch/nation.parquet`"); |
| |
| client.run( "refresh table metadata " + tableName); |
| |
| client.run("SELECT * FROM " + tableName); |
| |
| final Path tablePath = new Path(Path.SEPARATOR + tableWS + Path.SEPARATOR + tableName); |
| assertTrue ( fs.exists(tablePath) && fs.isDirectory(tablePath)); |
| fs.mkdirs(new Path(tablePath, "tmp5")); |
| |
| client.run("SELECT * from " + tableName); |
| } |
| } |
| |
| @Test |
| public void testAnalyzeTable() throws Exception { |
| final String tableName = "nation1_stats"; |
| final String tableWS = "drill_test_grp_1_700"; |
| |
| try (ClientFixture client = cluster.client(user1, "")) { |
| client.run("USE " + Joiner.on(".").join(MINI_DFS_STORAGE_PLUGIN_NAME, tableWS)); |
| client.run("ALTER SESSION SET `store.format` = 'parquet'"); |
| client.run("CREATE TABLE " + tableName + " AS SELECT * FROM cp.`tpch/nation.parquet`"); |
| client.run("ANALYZE TABLE " + tableName + " COMPUTE STATISTICS"); |
| client.run("SELECT * FROM " + tableName); |
| |
| final Path statsFilePath = new Path(Path.SEPARATOR + tableWS + Path.SEPARATOR + tableName |
| + Path.SEPARATOR + DotDrillType.STATS.getEnding()); |
| assertTrue (fs.exists(statsFilePath) && fs.isDirectory(statsFilePath)); |
| FileStatus status = fs.getFileStatus(statsFilePath); |
| // Verify process user is the directory owner |
| assert(processUser.equalsIgnoreCase(status.getOwner())); |
| |
| fs.mkdirs(new Path(statsFilePath, "tmp5")); |
| |
| client.run("SELECT * from " + tableName); |
| client.run("DROP TABLE " + tableName); |
| } |
| } |
| |
| @After |
| public void removeMiniDfsBasedStorage() throws PluginException { |
| cluster.storageRegistry().remove(MINI_DFS_STORAGE_PLUGIN_NAME); |
| stopMiniDfsCluster(); |
| } |
| } |