ACCESS-208: Add interface for on failure hooks
diff --git a/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHook.java b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHook.java new file mode 100644 index 0000000..776eb6a --- /dev/null +++ b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHook.java
@@ -0,0 +1,38 @@ +/** + * 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.access.binding.hive; + +import org.apache.hadoop.hive.ql.hooks.Hook; + +/** + * + * AccessOnFailureHook allows Access to be extended + * with custom logic to be executed upon authorization failure. + * + */ +public interface AccessOnFailureHook extends Hook { + + /** + * + * @param context + * The hook context passed to each hook. + * @throws Exception + */ + void run(AccessOnFailureHookContext context) throws Exception; +}
diff --git a/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHookContext.java b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHookContext.java new file mode 100644 index 0000000..df9b0ba --- /dev/null +++ b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHookContext.java
@@ -0,0 +1,89 @@ +/* + * 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.access.binding.hive; + +import org.apache.access.core.AccessURI; +import org.apache.access.core.Database; +import org.apache.access.core.Table; +import org.apache.hadoop.hive.ql.exec.Task; +import org.apache.hadoop.hive.ql.hooks.ReadEntity; +import org.apache.hadoop.hive.ql.hooks.WriteEntity; +import org.apache.hadoop.hive.ql.metadata.AuthorizationException; + +import java.io.Serializable; +import java.util.List; +import java.util.Set; + +/** + * Context information provided by Access to implementations + * of AccessOnFailureHook + */ +public interface AccessOnFailureHookContext { + + /** + * @return the command attempted by user + */ + public String getCommand(); + + /** + * @return the set of read entities + */ + public Set<ReadEntity> getInputs(); + + /** + * @return the set of write entities + */ + public Set<WriteEntity> getOutputs(); + + /** + * @return the user name + */ + public String getUserName(); + + /** + * @return the ip address + */ + public String getIpAddress(); + + /** + * @return the database object + */ + public Database getDatabase(); + + /** + * @return the table object + */ + public Table getTable(); + + /** + * @return the udf URI + */ + public AccessURI getUdfURI(); + + /** + * @return the partition URI + */ + public AccessURI getPartitionURI(); + + /** + * @return the authorization failure exception + */ + public AuthorizationException getException(); + +} \ No newline at end of file
diff --git a/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHookContextImpl.java b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHookContextImpl.java new file mode 100644 index 0000000..8e4190f --- /dev/null +++ b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/AccessOnFailureHookContextImpl.java
@@ -0,0 +1,111 @@ +/* + * 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.access.binding.hive; + +import org.apache.access.core.AccessURI; +import org.apache.access.core.Database; +import org.apache.access.core.Table; +import org.apache.hadoop.hive.ql.exec.Task; +import org.apache.hadoop.hive.ql.hooks.ReadEntity; +import org.apache.hadoop.hive.ql.hooks.WriteEntity; +import org.apache.hadoop.hive.ql.metadata.AuthorizationException; + +import java.io.Serializable; +import java.util.List; +import java.util.Set; + +public class AccessOnFailureHookContextImpl implements AccessOnFailureHookContext { + + private final String command; + private final Set<ReadEntity> inputs; + private final Set<WriteEntity> outputs; + private final String userName; + private final String ipAddress; + private final Database database; + private final Table table; + private final AccessURI udfURI; + private final AccessURI partitionURI; + private final AuthorizationException authException; + + public AccessOnFailureHookContextImpl(String command, + Set<ReadEntity> inputs, Set<WriteEntity> outputs, Database db, + Table tab, AccessURI udfURI, AccessURI partitionURI, + String userName, String ipAddress, AuthorizationException e) { + this.command = command; + this.inputs = inputs; + this.outputs = outputs; + this.userName = userName; + this.ipAddress = ipAddress; + this.database = db; + this.table = tab; + this.udfURI = udfURI; + this.partitionURI = partitionURI; + this.authException = e; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public Set<ReadEntity> getInputs() { + return inputs; + } + + @Override + public Set<WriteEntity> getOutputs() { + return outputs; + } + + @Override + public String getUserName() { + return userName; + } + + @Override + public String getIpAddress() { + return ipAddress; + } + + @Override + public Database getDatabase() { + return database; + } + + @Override + public Table getTable() { + return table; + } + + @Override + public AccessURI getUdfURI() { + return udfURI; + } + + @Override + public AccessURI getPartitionURI() { + return partitionURI; + } + + @Override + public AuthorizationException getException() { + return authException; + } +} \ No newline at end of file
diff --git a/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/HiveAuthzBindingHook.java b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/HiveAuthzBindingHook.java index 681f3aa..7a224ea 100644 --- a/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/HiveAuthzBindingHook.java +++ b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/HiveAuthzBindingHook.java
@@ -42,6 +42,7 @@ import org.apache.access.core.Database; import org.apache.access.core.Subject; import org.apache.access.core.Table; +import org.apache.hadoop.hive.common.JavaUtils; import org.apache.hadoop.hive.conf.HiveConf; import org.apache.hadoop.hive.conf.HiveConf.ConfVars; import org.apache.hadoop.hive.ql.HiveDriverFilterHook; @@ -51,6 +52,7 @@ import org.apache.hadoop.hive.ql.exec.Task; import org.apache.hadoop.hive.ql.hooks.Entity; import org.apache.hadoop.hive.ql.hooks.Entity.Type; +import org.apache.hadoop.hive.ql.hooks.Hook; import org.apache.hadoop.hive.ql.hooks.ReadEntity; import org.apache.hadoop.hive.ql.hooks.WriteEntity; import org.apache.hadoop.hive.ql.metadata.AuthorizationException; @@ -270,11 +272,27 @@ try { authorizeWithHiveBindings(context, stmtAuthObject, stmtOperation); } catch (AuthorizationException e) { + executeOnFailureHooks(context, e); throw new SemanticException("No valid privileges", e); } hiveAuthzBinding.set(context.getConf()); } + private void executeOnFailureHooks(HiveSemanticAnalyzerHookContext context, + AuthorizationException e) { + AccessOnFailureHookContext hookCtx = new AccessOnFailureHookContextImpl( + context.getCommand(), context.getInputs(), context.getOutputs(), + currDB, currTab, udfURI, partitionURI, context.getUserName(), + context.getIpAddress(), e); + try { + for (Hook aofh : getHooks(HiveAuthzConf.AuthzConfVars.AUTHZ_ONFAILURE_HOOKS)) { + ((AccessOnFailureHook)aofh).run(hookCtx); + } + } catch (Exception ex) { + LOG.error("Error executing hook:", ex); + } + } + /** * Convert the input/output entities into authorizables. generate * authorizables for cases like Database and metadata operations where the @@ -626,4 +644,57 @@ return false; } } -} + + /** + * Returns a set of hooks specified in a configuration variable. + * + * See getHooks(HiveAuthzConf.AuthzConfVars hookConfVar, Class<T> clazz) + * @param hookConfVar + * @return + * @throws Exception + */ + private List<Hook> getHooks(HiveAuthzConf.AuthzConfVars hookConfVar) throws Exception { + return getHooks(hookConfVar, Hook.class); + } + + /** + * Returns the hooks specified in a configuration variable. The hooks are returned in a list in + * the order they were specified in the configuration variable. + * + * @param hookConfVar The configuration variable specifying a comma separated list of the hook + * class names. + * @param clazz The super type of the hooks. + * @return A list of the hooks cast as the type specified in clazz, in the order + * they are listed in the value of hookConfVar + * @throws Exception + */ + private <T extends Hook> List<T> getHooks(HiveAuthzConf.AuthzConfVars hookConfVar, Class<T> clazz) + throws Exception { + + List<T> hooks = new ArrayList<T>(); + String csHooks = authzConf.get(hookConfVar.getVar(), ""); + if (csHooks == null) { + return hooks; + } + + csHooks = csHooks.trim(); + if (csHooks.equals("")) { + return hooks; + } + + String[] hookClasses = csHooks.split(","); + + for (String hookClass : hookClasses) { + try { + T hook = + (T) Class.forName(hookClass.trim(), true, JavaUtils.getClassLoader()).newInstance(); + hooks.add(hook); + } catch (ClassNotFoundException e) { + LOG.error(hookConfVar.getVar() + " Class not found:" + e.getMessage()); + throw e; + } + } + + return hooks; + } +} \ No newline at end of file
diff --git a/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/conf/HiveAuthzConf.java b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/conf/HiveAuthzConf.java index 59650bf..dd536f6 100644 --- a/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/conf/HiveAuthzConf.java +++ b/access-binding/access-binding-hive/src/main/java/org/apache/access/binding/hive/conf/HiveAuthzConf.java
@@ -46,6 +46,7 @@ ACCESS_TESTING_MODE("hive.access.testing.mode", "false"), AUTHZ_UDF_WHITELIST("hive.access.udf.whitelist", HIVE_UDF_WHITE_LIST), AUTHZ_ALLOW_HIVE_IMPERSONATION("hive.access.allow.hive.impersonation", "false"), + AUTHZ_ONFAILURE_HOOKS("hive.access.failure.hooks", ""), ; private final String varName; @@ -138,4 +139,4 @@ } return retVal; } -} \ No newline at end of file +}
diff --git a/access-tests/src/test/java/org/apache/access/tests/e2e/DummyAccessOnFailureHook.java b/access-tests/src/test/java/org/apache/access/tests/e2e/DummyAccessOnFailureHook.java new file mode 100644 index 0000000..6cfbe84 --- /dev/null +++ b/access-tests/src/test/java/org/apache/access/tests/e2e/DummyAccessOnFailureHook.java
@@ -0,0 +1,32 @@ +/* + * 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.access.tests.e2e; + +import org.apache.access.binding.hive.AccessOnFailureHook; +import org.apache.access.binding.hive.AccessOnFailureHookContext; + +public class DummyAccessOnFailureHook implements AccessOnFailureHook { + + static boolean invoked = false; + + @Override + public void run(AccessOnFailureHookContext failureHookContext) + throws Exception { + invoked = true; + } +}
diff --git a/access-tests/src/test/java/org/apache/access/tests/e2e/TestAccessOnFailureHookLoading.java b/access-tests/src/test/java/org/apache/access/tests/e2e/TestAccessOnFailureHookLoading.java new file mode 100644 index 0000000..d6a70f5 --- /dev/null +++ b/access-tests/src/test/java/org/apache/access/tests/e2e/TestAccessOnFailureHookLoading.java
@@ -0,0 +1,133 @@ +/* + * 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.access.tests.e2e; + +import com.google.common.io.Resources; +import org.apache.access.binding.hive.conf.HiveAuthzConf; +import org.apache.access.tests.e2e.hiveserver.HiveServerFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import java.io.File; +import java.io.FileOutputStream; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; +import junit.framework.Assert; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +public class TestAccessOnFailureHookLoading extends AbstractTestWithHiveServer { + + private Context context; + Map<String, String > testProperties; + private static final String SINGLE_TYPE_DATA_FILE_NAME = "kv1.dat"; + + @Before + public void setup() throws Exception { + testProperties = new HashMap<String, String>(); + testProperties.put(HiveAuthzConf.AuthzConfVars.AUTHZ_ONFAILURE_HOOKS.getVar(), + DummyAccessOnFailureHook.class.getName()); + } + + @After + public void teardown() throws Exception { + if (context != null) { + context.close(); + } + } + + /* Admin creates database DB_2 + * USER_1 tries to drop DB_2, but it has permissions for DB_1. + */ + @Test + public void testOnFailureHookLoading() throws Exception { + + // Do not run this test if run with external HiveServer2 + // This test checks for a static member, which will not + // be set if HiveServer2 and the test run in different JVMs + String hiveServer2Type = System.getProperty( + HiveServerFactory.HIVESERVER2_TYPE); + if (hiveServer2Type != null && + HiveServerFactory.HiveServer2Type.valueOf(hiveServer2Type.trim()) != + HiveServerFactory.HiveServer2Type.InternalHiveServer2) { + return; + } + + context = createContext(testProperties); + + File policyFile = context.getPolicyFile(); + File dataDir = context.getDataDir(); + //copy data file to test dir + File dataFile = new File(dataDir, SINGLE_TYPE_DATA_FILE_NAME); + FileOutputStream to = new FileOutputStream(dataFile); + Resources.copy(Resources.getResource(SINGLE_TYPE_DATA_FILE_NAME), to); + to.close(); + //delete existing policy file; create new policy file + assertTrue("Could not delete " + policyFile, context.deletePolicyFile()); + // groups : role -> group + context.append("[groups]"); + context.append("admin = all_server"); + context.append("user_group1 = all_db1, load_data"); + // roles: privileges -> role + context.append("[roles]"); + context.append("all_server = server=server1"); + context.append("all_db1 = server=server1->db=DB_1"); + // users: users -> groups + context.append("[users]"); + context.append("hive = admin"); + context.append("user_1 = user_group1"); + // setup db objects needed by the test + Connection connection = context.createConnection("hive", "hive"); + Statement statement = context.createStatement(connection); + statement.execute("DROP DATABASE IF EXISTS DB_1 CASCADE"); + statement.execute("DROP DATABASE IF EXISTS DB_2 CASCADE"); + statement.execute("CREATE DATABASE DB_1"); + statement.execute("CREATE DATABASE DB_2"); + statement.close(); + connection.close(); + + // test execution + connection = context.createConnection("user_1", "password"); + statement = context.createStatement(connection); + + //negative test case: user can't drop another user's database + assertFalse(DummyAccessOnFailureHook.invoked); + try { + statement.execute("DROP DATABASE DB_2 CASCADE"); + Assert.fail("Expected SQL exception"); + } catch (SQLException e) { + assertTrue(DummyAccessOnFailureHook.invoked); + } + + statement.close(); + connection.close(); + + //test cleanup + connection = context.createConnection("hive", "hive"); + statement = context.createStatement(connection); + statement.execute("DROP DATABASE DB_1 CASCADE"); + statement.execute("DROP DATABASE DB_2 CASCADE"); + statement.close(); + connection.close(); + context.close(); + } +}
diff --git a/access-tests/src/test/java/org/apache/access/tests/e2e/hiveserver/HiveServerFactory.java b/access-tests/src/test/java/org/apache/access/tests/e2e/hiveserver/HiveServerFactory.java index 873176c..08b0fd9 100644 --- a/access-tests/src/test/java/org/apache/access/tests/e2e/hiveserver/HiveServerFactory.java +++ b/access-tests/src/test/java/org/apache/access/tests/e2e/hiveserver/HiveServerFactory.java
@@ -24,6 +24,7 @@ import java.net.URL; import java.util.Map; +import com.google.common.annotations.VisibleForTesting; import org.apache.access.binding.hive.conf.HiveAuthzConf; import org.apache.access.provider.file.LocalGroupResourceAuthorizationProvider; import org.apache.hadoop.fs.FileSystem; @@ -196,7 +197,8 @@ return port; } - private static enum HiveServer2Type { + @VisibleForTesting + public static enum HiveServer2Type { EmbeddedHiveServer2, // Embedded HS2, directly executed by JDBC, without thrift InternalHiveServer2, // Start a thrift HS2 in the same process ExternalHiveServer2, // start a remote thrift HS2