ACCESS-217. Support for URIs in per DB policy file
diff --git a/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/DatabaseRequiredInRole.java b/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/DatabaseRequiredInRole.java
index 7a3c42a..630d81f 100644
--- a/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/DatabaseRequiredInRole.java
+++ b/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/DatabaseRequiredInRole.java
@@ -18,6 +18,7 @@
 
 import javax.annotation.Nullable;
 
+import org.apache.access.core.AccessURI;
 import org.apache.access.core.Authorizable;
 import org.apache.access.core.Database;
 import org.apache.shiro.config.ConfigurationException;
@@ -33,16 +34,34 @@
       Iterable<Authorizable> authorizables = parseRole(role);
       /*
        * Each permission in a non-global file must have a database
-       * object.
+       * object except for URIs.
+       *
+       * We allow URIs to be specified in the per DB policy file for
+       * ease of mangeability. URIs will contain to remain server scope
+       * objects.
        */
       boolean foundDatabaseInAuthorizables = false;
+      boolean foundURIInAuthorizables = false;
+      boolean allowURIInAuthorizables = false;
+
+      if ("true".equalsIgnoreCase(
+          System.getProperty(SimplePolicyEngine.ACCESS_ALLOW_URI_PER_DB_POLICYFILE))) {
+        allowURIInAuthorizables = true;
+      }
+
       for(Authorizable authorizable : authorizables) {
         if(authorizable instanceof Database) {
           foundDatabaseInAuthorizables = true;
-          break;
+        }
+        if (authorizable instanceof AccessURI) {
+          if (foundDatabaseInAuthorizables) {
+            String msg = "URI object is specified at DB scope in " + role;
+            throw new ConfigurationException(msg);
+          }
+          foundURIInAuthorizables = true;
         }
       }
-      if(!foundDatabaseInAuthorizables) {
+      if(!foundDatabaseInAuthorizables && !(foundURIInAuthorizables && allowURIInAuthorizables)) {
         String msg = "Missing database object in " + role;
         throw new ConfigurationException(msg);
       }
diff --git a/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/Roles.java b/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/Roles.java
index 71ccdcd..e43cb6f 100644
--- a/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/Roles.java
+++ b/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/Roles.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.io.Resources;
 
 public class Roles {
   private static final Logger LOGGER = LoggerFactory
@@ -41,14 +42,27 @@
     this.globalRoles = globalRoles;
     this.perDatabaseRoles = perDatabaseRoles;
   }
-  public ImmutableSet<String> getRoles(@Nullable String database, String group) {
+  public ImmutableSet<String> getRoles(@Nullable String database, String group, Boolean isURI) {
     ImmutableSet.Builder<String> resultBuilder = ImmutableSet.builder();
+    String allowURIPerDbFile = 
+        System.getProperty(SimplePolicyEngine.ACCESS_ALLOW_URI_PER_DB_POLICYFILE);
+    Boolean consultPerDbRolesForURI = isURI && ("true".equalsIgnoreCase(allowURIPerDbFile));
+
     if(database != null) {
       ImmutableSetMultimap<String, String> dbPolicies =  perDatabaseRoles.get(database);
       if(dbPolicies != null && dbPolicies.containsKey(group)) {
         resultBuilder.addAll(dbPolicies.get(group));
       }
     }
+    if (consultPerDbRolesForURI) {
+      for(String db:perDatabaseRoles.keySet()) {
+        ImmutableSetMultimap<String, String> dbPolicies =  perDatabaseRoles.get(db);
+        if(dbPolicies != null && dbPolicies.containsKey(group)) {
+          resultBuilder.addAll(dbPolicies.get(group));
+        }
+      }
+    }
+
     if(globalRoles.containsKey(group)) {
       resultBuilder.addAll(globalRoles.get(group));
     }
diff --git a/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/SimplePolicyEngine.java b/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/SimplePolicyEngine.java
index 8e68dc0..91f0953 100644
--- a/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/SimplePolicyEngine.java
+++ b/access-provider/access-provider-file/src/main/java/org/apache/access/provider/file/SimplePolicyEngine.java
@@ -33,6 +33,7 @@
 
 import javax.annotation.Nullable;
 
+import org.apache.access.core.AccessURI;
 import org.apache.access.core.Authorizable;
 import org.apache.access.core.Database;
 import org.apache.hadoop.conf.Configuration;
@@ -67,6 +68,7 @@
   private final String serverName;
   private final List<Path> perDbResources = Lists.newArrayList();
   private final AtomicReference<Roles> rolesReference;
+  public final static String ACCESS_ALLOW_URI_PER_DB_POLICYFILE = "access.allow.uri.db.policyfile";
 
   public SimplePolicyEngine(String resourcePath, String serverName) throws IOException {
     this(new Configuration(), new Path(resourcePath), serverName);
@@ -243,17 +245,22 @@
   public ImmutableSetMultimap<String, String> getPermissions(List<Authorizable> authorizables, List<String> groups) {
     Roles roles = rolesReference.get();
     String database = null;
+    Boolean isURI = false;
     for(Authorizable authorizable : authorizables) {
       if(authorizable instanceof Database) {
         database = authorizable.getName();
       }
+      if (authorizable instanceof AccessURI) {
+        isURI = true;
+      }
     }
+
     if(LOGGER.isDebugEnabled()) {
       LOGGER.debug("Getting permissions for {} via {}", groups, database);
     }
     ImmutableSetMultimap.Builder<String, String> resultBuilder = ImmutableSetMultimap.builder();
     for(String group : groups) {
-      resultBuilder.putAll(group, roles.getRoles(database, group));
+      resultBuilder.putAll(group, roles.getRoles(database, group, isURI));
     }
     ImmutableSetMultimap<String, String> result = resultBuilder.build();
     if(LOGGER.isDebugEnabled()) {
diff --git a/access-provider/access-provider-file/src/test/java/org/apache/access/provider/file/TestDatabaseRequiredInRole.java b/access-provider/access-provider-file/src/test/java/org/apache/access/provider/file/TestDatabaseRequiredInRole.java
new file mode 100644
index 0000000..8757c05
--- /dev/null
+++ b/access-provider/access-provider-file/src/test/java/org/apache/access/provider/file/TestDatabaseRequiredInRole.java
@@ -0,0 +1,48 @@
+/*
+ * 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.provider.file;
+
+import junit.framework.Assert;
+
+import org.apache.shiro.config.ConfigurationException;
+import org.junit.Test;
+
+public class TestDatabaseRequiredInRole {
+
+  @Test
+  public void testURIInPerDbPolicyFile() throws Exception {
+    DatabaseRequiredInRole dbRequiredInRole = new DatabaseRequiredInRole();
+    System.setProperty("access.allow.uri.db.policyfile", "true");
+    dbRequiredInRole.validate("db1",
+      "server=server1->URI=file:///user/hive/warehouse/tab1");
+    System.setProperty("access.allow.uri.db.policyfile", "false");
+  }
+
+  @Test
+  public void testURIWithDBInPerDbPolicyFile() throws Exception {
+    DatabaseRequiredInRole dbRequiredInRole = new DatabaseRequiredInRole();
+    try {
+      dbRequiredInRole.validate("db1",
+        "server=server1->db=db1->URI=file:///user/hive/warehouse/tab1");
+      Assert.fail("Expected ConfigurationException");
+    } catch (ConfigurationException e) {
+      ;
+    }
+  }
+}
\ No newline at end of file
diff --git a/access-tests/src/test/java/org/apache/access/tests/e2e/TestPerDBConfiguration.java b/access-tests/src/test/java/org/apache/access/tests/e2e/TestPerDBConfiguration.java
index 442f7b3..32fc188 100644
--- a/access-tests/src/test/java/org/apache/access/tests/e2e/TestPerDBConfiguration.java
+++ b/access-tests/src/test/java/org/apache/access/tests/e2e/TestPerDBConfiguration.java
@@ -26,6 +26,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
+import org.apache.access.provider.file.SimplePolicyEngine;
 import org.junit.After;
 import org.junit.Test;
 
@@ -299,6 +300,114 @@
     connection.close();
   }
 
+  @Test
+  public void testPerDBPolicyFileWithURI() throws Exception {
+    context = createContext();
+    File policyFile = context.getPolicyFile();
+    File db2PolicyFile = new File(policyFile.getParent(), DB2_POLICY_FILE);
+    File dataDir = context.getDataDir();
+    //copy data file to test dir
+    File dataFile = new File(dataDir, MULTI_TYPE_DATA_FILE_NAME);
+    FileOutputStream to = new FileOutputStream(dataFile);
+    Resources.copy(Resources.getResource(MULTI_TYPE_DATA_FILE_NAME), to);
+    to.close();
+    //delete existing policy file; create new policy file
+    assertTrue("Could not delete " + policyFile, context.deletePolicyFile());
+    assertTrue("Could not delete " + db2PolicyFile,!db2PolicyFile.exists() || db2PolicyFile.delete());
+
+    String[] policyFileContents = {
+        // groups : role -> group
+        "[groups]",
+        "admin = all_server",
+        "user_group1 = select_tbl1",
+        "user_group2 = select_tbl2",
+        // roles: privileges -> role
+        "[roles]",
+        "all_server = server=server1",
+        "select_tbl1 = server=server1->db=db1->table=tbl1->action=select",
+        // users: users -> groups
+        "[users]",
+        "hive = admin",
+        "user_1 = user_group1",
+        "user_2 = user_group2",
+        "[databases]",
+        "db2 = " + db2PolicyFile.getPath(),
+    };
+    context.makeNewPolicy(policyFileContents);
+
+    String[] db2PolicyFileContents = {
+        "[groups]",
+        "user_group2 = select_tbl2, data_read, insert_tbl2",
+        "[roles]",
+        "select_tbl2 = server=server1->db=db2->table=tbl2->action=select",
+        "insert_tbl2 = server=server1->db=db2->table=tbl2->action=insert",
+        "data_read = server=server1->URI=file://" + dataFile
+    };
+    Files.write(Joiner.on("\n").join(db2PolicyFileContents), db2PolicyFile, Charsets.UTF_8);
+    // ugly hack: needs to go away once this becomes a config property. Note that this property
+    // will not be set with external HS and this test will fail. Hope is this fix will go away
+    // by then.
+    System.setProperty(SimplePolicyEngine.ACCESS_ALLOW_URI_PER_DB_POLICYFILE, "true");
+    // setup db objects needed by the test
+    Connection connection = context.createConnection("hive", "hive");
+    Statement statement = context.createStatement(connection);
+
+    statement.execute("DROP DATABASE IF EXISTS db1 CASCADE");
+    statement.execute("DROP DATABASE IF EXISTS db2 CASCADE");
+    statement.execute("CREATE DATABASE db1");
+    statement.execute("USE db1");
+    statement.execute("CREATE TABLE tbl1(B INT, A STRING) " +
+                      " row format delimited fields terminated by '|'  stored as textfile");
+    statement.execute("LOAD DATA LOCAL INPATH '" + dataFile.getPath() + "' INTO TABLE tbl1");
+    statement.execute("DROP DATABASE IF EXISTS db2 CASCADE");
+    statement.execute("CREATE DATABASE db2");
+    statement.execute("USE db2");
+    statement.execute("CREATE TABLE tbl2(B INT, A STRING) " +
+                      " row format delimited fields terminated by '|'  stored as textfile");
+    statement.execute("LOAD DATA LOCAL INPATH '" + dataFile.getPath() + "' INTO TABLE tbl2");
+    statement.close();
+    connection.close();
+
+    // test execution
+    connection = context.createConnection("user_1", "password");
+    statement = context.createStatement(connection);
+    statement.execute("USE db1");
+    // test user1 can execute query on tbl1
+    verifyCount(statement, "SELECT COUNT(*) FROM tbl1");
+
+    // user1 cannot query db2.tbl2
+    context.assertAuthzException(statement, "USE db2");
+    context.assertAuthzException(statement, "SELECT COUNT(*) FROM db2.tbl2");
+    statement.close();
+    connection.close();
+
+    // test per-db file for db2
+    connection = context.createConnection("user_2", "password");
+    statement = context.createStatement(connection);
+    statement.execute("USE db2");
+    // test user2 can execute query on tbl2
+    verifyCount(statement, "SELECT COUNT(*) FROM tbl2");
+
+    // verify user2 can execute LOAD
+    statement.execute("LOAD DATA LOCAL INPATH '" + dataFile.getPath() + "' INTO TABLE tbl2");
+
+    // user2 cannot query db1.tbl1
+    context.assertAuthzException(statement, "SELECT COUNT(*) FROM db1.tbl1");
+    context.assertAuthzException(statement, "USE db1");
+
+    statement.close();
+    connection.close();
+
+    //test cleanup
+    connection = context.createConnection("hive", "hive");
+    statement = context.createStatement(connection);
+    statement.execute("DROP DATABASE db1 CASCADE");
+    statement.execute("DROP DATABASE db2 CASCADE");
+    statement.close();
+    connection.close();
+    System.setProperty(SimplePolicyEngine.ACCESS_ALLOW_URI_PER_DB_POLICYFILE, "false");
+  }
+
   private void verifyCount(Statement statement, String query) throws SQLException {
     ResultSet resultSet = statement.executeQuery(query);
     int count = 0;