/**
* 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 com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import org.apache.drill.common.exceptions.UserException;
import org.apache.drill.common.exceptions.UserRemoteException;
import org.apache.drill.exec.store.dfs.WorkspaceConfig;
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.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Map;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Tests impersonation on metadata related queries as SHOW FILES, SHOW TABLES, CREATE VIEW, CREATE TABLE and DROP TABLE
*/
public class TestImpersonationMetadata extends BaseTestImpersonation {
private static final String user1 = "drillTestUser1";
private static final String user2 = "drillTestUser2";
private static final String group0 = "drillTestGrp0";
private static final String group1 = "drillTestGrp1";
static {
UserGroupInformation.createUserForTesting(user1, new String[]{ group1, group0 });
UserGroupInformation.createUserForTesting(user2, new String[]{ group1 });
}
@BeforeClass
public static 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 /drillTestGrp0_700 directory with permissions 700 (owned by user running the tests)
createAndAddWorkspace("drillTestGrp0_700", "/drillTestGrp0_700", (short)0700, processUser, group0, workspaces);
// Create /drillTestGrp0_750 directory with permissions 750 (owned by user running the tests)
createAndAddWorkspace("drillTestGrp0_750", "/drillTestGrp0_750", (short)0750, processUser, group0, workspaces);
// Create /drillTestGrp0_755 directory with permissions 755 (owned by user running the tests)
createAndAddWorkspace("drillTestGrp0_755", "/drillTestGrp0_755", (short)0755, processUser, group0, workspaces);
// Create /drillTestGrp0_770 directory with permissions 770 (owned by user running the tests)
createAndAddWorkspace("drillTestGrp0_770", "/drillTestGrp0_770", (short)0770, processUser, group0, workspaces);
// Create /drillTestGrp0_777 directory with permissions 777 (owned by user running the tests)
createAndAddWorkspace("drillTestGrp0_777", "/drillTestGrp0_777", (short)0777, processUser, group0, workspaces);
// Create /drillTestGrp1_700 directory with permissions 700 (owned by user1)
createAndAddWorkspace("drillTestGrp1_700", "/drillTestGrp1_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
updateClient(user2);
test(String.format("use `%s.user2_workspace1`", MINIDFS_STORAGE_PLUGIN_NAME));
// create a table that can be dropped by another user in a different group
test("create table parquet_table_775 as select * from cp.`employee.json`");
// create a table that cannot be dropped by another user
test(String.format("use `%s.user2_workspace2`", MINIDFS_STORAGE_PLUGIN_NAME));
test("create table parquet_table_700 as select * from cp.`employee.json`");
// Drop tables as user1
updateClient(user1);
test(String.format("use `%s.user2_workspace1`", MINIDFS_STORAGE_PLUGIN_NAME));
testBuilder()
.sqlQuery("drop table parquet_table_775")
.unOrdered()
.baselineColumns("ok", "summary")
.baselineValues(true, String.format("Table [%s] dropped", "parquet_table_775"))
.go();
test(String.format("use `%s.user2_workspace2`", MINIDFS_STORAGE_PLUGIN_NAME));
boolean dropFailed = false;
try {
test("drop table parquet_table_700");
} catch (UserException e) {
Assert.assertTrue(e.getMessage().contains("PERMISSION ERROR"));
dropFailed = true;
}
Assert.assertTrue("Permission checking failed during drop table", dropFailed);
}
@Test // DRILL-3037
public void testImpersonatingProcessUser() throws Exception {
updateClient(processUser);
// Process user start the mini dfs, he has read/write permissions by default
final String viewName = String.format("%s.drillTestGrp0_700.testView", MINIDFS_STORAGE_PLUGIN_NAME);
try {
test("CREATE VIEW " + viewName + " AS SELECT * FROM cp.`region.json`");
test("SELECT * FROM " + viewName + " LIMIT 2");
} finally {
test("DROP VIEW " + viewName);
}
}
@Test
public void testShowFilesInWSWithUserAndGroupPermissionsForQueryUser() throws Exception {
updateClient(user1);
// Try show tables in schema "drillTestGrp1_700" which is owned by "user1"
test(String.format("SHOW FILES IN %s.drillTestGrp1_700", MINIDFS_STORAGE_PLUGIN_NAME));
// Try show tables in schema "drillTestGrp0_750" which is owned by "processUser" and has group permissions for
// "user1"
test(String.format("SHOW FILES IN %s.drillTestGrp0_750", MINIDFS_STORAGE_PLUGIN_NAME));
}
@Test
public void testShowFilesInWSWithOtherPermissionsForQueryUser() throws Exception {
updateClient(user2);
// Try show tables in schema "drillTestGrp0_755" which is owned by "processUser" and group0. "user2" is not part
// of the "group0"
test(String.format("SHOW FILES IN %s.drillTestGrp0_755", MINIDFS_STORAGE_PLUGIN_NAME));
}
@Test
public void testShowFilesInWSWithNoPermissionsForQueryUser() throws Exception {
UserRemoteException ex = null;
updateClient(user2);
try {
// Try show tables in schema "drillTestGrp1_700" which is owned by "user1"
test(String.format("SHOW FILES IN %s.drillTestGrp1_700", MINIDFS_STORAGE_PLUGIN_NAME));
} catch(UserRemoteException e) {
ex = e;
}
assertNotNull("UserRemoteException is expected", ex);
assertThat(ex.getMessage(),
containsString("Permission denied: user=drillTestUser2, " +
"access=READ_EXECUTE, inode=\"/drillTestGrp1_700\":drillTestUser1:drillTestGrp1:drwx------"));
}
@Test
public void testShowSchemasAsUser1() throws Exception {
// "user1" is part of "group0" and has access to following workspaces
// drillTestGrp1_700 (through ownership)
// drillTestGrp0_750, drillTestGrp0_770 (through "group" category permissions)
// drillTestGrp0_755, drillTestGrp0_777 (through "others" category permissions)
updateClient(user1);
testBuilder()
.sqlQuery("SHOW SCHEMAS LIKE '%drillTest%'")
.unOrdered()
.baselineColumns("SCHEMA_NAME")
.baselineValues(String.format("%s.drillTestGrp0_750", MINIDFS_STORAGE_PLUGIN_NAME))
.baselineValues(String.format("%s.drillTestGrp0_755", MINIDFS_STORAGE_PLUGIN_NAME))
.baselineValues(String.format("%s.drillTestGrp0_770", MINIDFS_STORAGE_PLUGIN_NAME))
.baselineValues(String.format("%s.drillTestGrp0_777", MINIDFS_STORAGE_PLUGIN_NAME))
.baselineValues(String.format("%s.drillTestGrp1_700", MINIDFS_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
// drillTestGrp0_755, drillTestGrp0_777 (through "others" category permissions)
updateClient(user2);
testBuilder()
.sqlQuery("SHOW SCHEMAS LIKE '%drillTest%'")
.unOrdered()
.baselineColumns("SCHEMA_NAME")
.baselineValues(String.format("%s.drillTestGrp0_755", MINIDFS_STORAGE_PLUGIN_NAME))
.baselineValues(String.format("%s.drillTestGrp0_777", MINIDFS_STORAGE_PLUGIN_NAME))
.go();
}
@Test
public void testCreateViewInDirWithUserPermissionsForQueryUser() throws Exception {
final String viewSchema = MINIDFS_STORAGE_PLUGIN_NAME + ".drillTestGrp1_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 = MINIDFS_STORAGE_PLUGIN_NAME + ".drillTestGrp0_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 = MINIDFS_STORAGE_PLUGIN_NAME + ".drillTestGrp0_777";
testCreateViewTestHelper(user2, viewSchema, "view1");
}
private static void testCreateViewTestHelper(String user, String viewSchema,
String viewName) throws Exception {
try {
updateClient(user);
test("USE " + viewSchema);
test("CREATE VIEW " + viewName + " AS SELECT " +
"c_custkey, c_nationkey FROM cp.`tpch/customer.parquet` ORDER BY c_custkey;");
testBuilder()
.sqlQuery("SHOW TABLES")
.unOrdered()
.baselineColumns("TABLE_SCHEMA", "TABLE_NAME")
.baselineValues(viewSchema, viewName)
.go();
test("SHOW FILES");
testBuilder()
.sqlQuery("SELECT * FROM " + viewName + " LIMIT 1")
.ordered()
.baselineColumns("c_custkey", "c_nationkey")
.baselineValues(1, 15)
.go();
} finally {
test("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 viewSchema = MINIDFS_STORAGE_PLUGIN_NAME + ".drillTestGrp0_755";
final String viewName = "view1";
updateClient(user2);
test("USE " + viewSchema);
final String query = "CREATE VIEW " + viewName + " AS SELECT " +
"c_custkey, c_nationkey FROM cp.`tpch/customer.parquet` ORDER BY c_custkey;";
final String expErrorMsg = "PERMISSION ERROR: Permission denied: user=drillTestUser2, access=WRITE, inode=\"/drillTestGrp0_755/";
errorMsgTestHelper(query, expErrorMsg);
// SHOW TABLES is expected to return no records as view creation fails above.
testBuilder()
.sqlQuery("SHOW TABLES")
.expectsEmptyResultSet()
.go();
test("SHOW FILES");
}
@Test
public void testCreateTableInDirWithUserPermissionsForQueryUser() throws Exception {
final String tableWS = "drillTestGrp1_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 = "drillTestGrp0_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 = "drillTestGrp0_777";
testCreateTableTestHelper(user2, tableWS, "table1");
}
private static void testCreateTableTestHelper(String user, String tableWS,
String tableName) throws Exception {
try {
updateClient(user);
test("USE " + Joiner.on(".").join(MINIDFS_STORAGE_PLUGIN_NAME, tableWS));
test("CREATE TABLE " + tableName + " AS SELECT " +
"c_custkey, c_nationkey FROM cp.`tpch/customer.parquet` ORDER BY c_custkey;");
test("SHOW FILES");
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"
final String tableWS = "drillTestGrp0_755";
final String tableName = "table1";
UserRemoteException ex = null;
try {
updateClient(user2);
test("USE " + Joiner.on(".").join(MINIDFS_STORAGE_PLUGIN_NAME, tableWS));
test("CREATE TABLE " + tableName + " AS SELECT " +
"c_custkey, c_nationkey FROM cp.`tpch/customer.parquet` ORDER BY c_custkey;");
} catch(UserRemoteException e) {
ex = e;
}
assertNotNull("UserRemoteException is expected", ex);
assertThat(ex.getMessage(),
containsString("SYSTEM ERROR: RemoteException: Permission denied: user=drillTestUser2, access=WRITE, inode=\"/drillTestGrp0_755/"));
}
@Test
public void testRefreshMetadata() throws Exception {
final String tableName = "nation1";
final String tableWS = "drillTestGrp1_700";
updateClient(user1);
test("USE " + Joiner.on(".").join(MINIDFS_STORAGE_PLUGIN_NAME, tableWS));
test("CREATE TABLE " + tableName + " partition by (n_regionkey) AS SELECT * " +
"FROM cp.`tpch/nation.parquet`;");
test( "refresh table metadata " + tableName + ";");
test("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"));
test("SELECT * from " + tableName + ";");
}
@AfterClass
public static void removeMiniDfsBasedStorage() throws Exception {
getDrillbitContext().getStorage().deletePlugin(MINIDFS_STORAGE_PLUGIN_NAME);
stopMiniDfsCluster();
}
}