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