Repository: pgjdbc/pgjdbc
Pull request #1630 (2019-11-29 20:01:04)

feat: WIP Filter DatabaseMetaData using privileges for the user
This replaces PR#647 as it required considerable work to merge.
Original work done by Scott Langley @slangley
adds a connection parameter: hideUnprivilegedObjects defaults to false



codecov-io: # [Codecov](https://codecov.io/gh/pgjdbc/pgjdbc/pull/1630?src=pr&el=h1) Report
> Merging [#1630](https://codecov.io/gh/pgjdbc/pgjdbc/pull/1630?src=pr&el=desc) into [master](https://codecov.io/gh/pgjdbc/pgjdbc/commit/97d32caad1f72c11d3e89ffaf16a17a22c6b9790?src=pr&el=desc) will **decrease** coverage by `0.03%`.
> The diff coverage is `31.57%`.



```diff
@@             Coverage Diff              @@
##             master    #1630      +/-   ##
============================================
- Coverage     69.02%   68.98%   -0.04%     
- Complexity     4068     4072       +4     
============================================
  Files           181      181              
  Lines         16861    16880      +19     
  Branches       2760     2767       +7     
============================================
+ Hits          11638    11645       +7     
- Misses         3947     3954       +7     
- Partials       1276     1281       +5
```

davecramer: And it passes ... because I didn't add it to a suite :(

Patch:
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index 42729c95..be805c47 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -425,6 +425,13 @@ public enum PGProperty {
   REWRITE_BATCHED_INSERTS("reWriteBatchedInserts", "false",
       "Enable optimization to rewrite and collapse compatible INSERT statements that are batched."),
 
+  /**
+   * Enable mode to filter out the names of database objects for which the current user has no privileges
+   * granted from appearing in the DatabaseMetaData returned by the driver.
+   */
+  HIDE_UNPRIVILEGED_OBJECTS("hideUnprivilegedObjects", "false",
+    "Enable hiding of database objects for which the current user has no privileges granted from the DatabaseMetaData"),
+
   /**
    * <p>Connection parameter passed in the startup message. This parameter accepts two values; "true"
    * and "database". Passing "true" tells the backend to go into walsender mode, wherein a small set
diff --git a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
index e1a6df35..e72aac5e 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -1495,6 +1495,22 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
     PGProperty.REWRITE_BATCHED_INSERTS.set(properties, reWrite);
   }
 
+  /**
+   * @return boolean indicating property is enabled or not.
+   * @see PGProperty#HIDE_UNPRIVILEGED_OBJECTS
+   */
+  public boolean getHideUnprivilegedObjects() {
+    return PGProperty.HIDE_UNPRIVILEGED_OBJECTS.getBoolean(properties);
+  }
+
+  /**
+   * @param hideUnprivileged boolean value to set the property in the properties collection
+   * @see PGProperty#HIDE_UNPRIVILEGED_OBJECTS
+   */
+  public void setHideUnprivilegedObjects(boolean hideUnprivileged) {
+    PGProperty.HIDE_UNPRIVILEGED_OBJECTS.set(properties, hideUnprivileged);
+  }
+
   //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
   public java.util.logging.Logger getParentLogger() {
     return Logger.getLogger("org.postgresql");
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index 2e7b9f2c..3b8d86f3 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
@@ -134,7 +134,8 @@ public class PgConnection implements BaseConnection {
   private boolean autoCommit = true;
   // Connection's readonly state.
   private boolean readOnly = false;
-
+  // Filter out database objects for which the current user has no privileges granted from the DatabaseMetaData
+  private boolean  hideUnprivilegedObjects ;
   // Bind String to UNSPECIFIED or VARCHAR?
   private final boolean bindStringAsVarchar;
 
@@ -221,6 +222,8 @@ public class PgConnection implements BaseConnection {
       setReadOnly(true);
     }
 
+    this.hideUnprivilegedObjects = PGProperty.HIDE_UNPRIVILEGED_OBJECTS.getBoolean(info);
+
     Set<Integer> binaryOids = getBinaryOids(info);
 
     // split for receive and send for better control
@@ -948,6 +951,10 @@ public class PgConnection implements BaseConnection {
     return queryExecutor.getDatabase();
   }
 
+  public boolean getHideUnprivilegedObjects() {
+    return hideUnprivilegedObjects;
+  }
+
   /**
    * <p>Overrides finalize(). If called, it closes the connection.</p>
    *
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
index 08823012..80201a89 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
@@ -1045,6 +1045,9 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
     if (procedureNamePattern != null && !procedureNamePattern.isEmpty()) {
       sql += " AND p.proname LIKE " + escapeQuotes(procedureNamePattern);
     }
+    if (connection.getHideUnprivilegedObjects()) {
+      sql += " AND has_function_privilege(p.oid,'EXECUTE')";
+    }
     sql += " ORDER BY PROCEDURE_SCHEM, PROCEDURE_NAME, p.oid::text ";
 
     return createMetaDataStatement().executeQuery(sql);
@@ -1303,6 +1306,10 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
     if (schemaPattern != null && !schemaPattern.isEmpty()) {
       select += " AND n.nspname LIKE " + escapeQuotes(schemaPattern);
     }
+    if (connection.getHideUnprivilegedObjects()) {
+      select += " AND has_table_privilege(c.oid, "
+        + " 'SELECT, INSERT, UPDATE, DELETE, RULE, REFERENCES, TRIGGER')";
+    }
     orderby = " ORDER BY TABLE_TYPE,TABLE_SCHEM,TABLE_NAME ";
 
     if (tableNamePattern != null && !tableNamePattern.isEmpty()) {
@@ -1419,6 +1426,9 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
     if (schemaPattern != null && !schemaPattern.isEmpty()) {
       sql += " AND nspname LIKE " + escapeQuotes(schemaPattern);
     }
+    if (connection.getHideUnprivilegedObjects()) {
+      sql += " AND has_schema_privilege(nspname, 'USAGE, CREATE')";
+    }
     sql += " ORDER BY TABLE_SCHEM";
 
     return createMetaDataStatement().executeQuery(sql);
@@ -2266,6 +2276,10 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
           + " AND "
           + " (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))";
 
+    if (connection.getHideUnprivilegedObjects() && connection.haveMinimumServerVersion(ServerVersion.v9_2)) {
+      sql += " AND has_type_privilege(t.oid, 'USAGE')";
+    }
+
     Statement stmt = connection.createStatement();
     ResultSet rs = stmt.executeQuery(sql);
     // cache some results, this will keep memory usage down, and speed
@@ -2584,6 +2598,12 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
       toAdd.append(" and n.nspname like ").append(escapeQuotes(schemaPattern));
     }
     sql += toAdd.toString();
+
+    if (connection.getHideUnprivilegedObjects()
+        && connection.haveMinimumServerVersion(ServerVersion.v9_2)) {
+      sql += " AND has_type_privilege(t.oid, 'USAGE')";
+    }
+
     sql += " order by data_type, type_schem, type_name";
     return createMetaDataStatement().executeQuery(sql);
   }
@@ -2695,6 +2715,9 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
     if (functionNamePattern != null && !functionNamePattern.isEmpty()) {
       sql += " AND p.proname LIKE " + escapeQuotes(functionNamePattern);
     }
+    if (connection.getHideUnprivilegedObjects()) {
+      sql += " AND has_function_privilege(p.oid,'EXECUTE')";
+    }
     sql += " ORDER BY FUNCTION_SCHEM, FUNCTION_NAME, p.oid::text ";
 
     return createMetaDataStatement().executeQuery(sql);


Test patch:
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/DatabaseMetaDataHideUnprivilegedObjectsTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/DatabaseMetaDataHideUnprivilegedObjectsTest.java
new file mode 100644
index 00000000..b021787e
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/DatabaseMetaDataHideUnprivilegedObjectsTest.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc4;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import org.postgresql.PGProperty;
+import org.postgresql.core.ServerVersion;
+import org.postgresql.jdbc.PgConnection;
+import org.postgresql.test.TestUtil;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+;
+
+/**
+ * Tests that database objects for which the current user has no privileges are filtered out from
+ * the DatabaseMetaData depending on whether the connection parameter hideUnprivilegedObjects is
+ * set to true.
+ */
+public class DatabaseMetaDataHideUnprivilegedObjectsTest {
+  public static final String COLUMNS = "digit int4, name text";
+  private static Connection hidingCon;
+  private static Connection nonhidingCon;
+  private static Connection privilegedCon;
+  private static PgConnection pgConnection;
+  private static DatabaseMetaData hidingDatabaseMetaData;
+  private static DatabaseMetaData nonhidingDatabaseMetaData;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    Properties props = new Properties();
+    privilegedCon = TestUtil.openPrivilegedDB();
+    pgConnection = privilegedCon.unwrap(PgConnection.class);
+    Statement stmt = privilegedCon.createStatement();
+
+    createTestDataObjectsWithRangeOfPrivilegesInSchema("high_privileges_schema");
+    // Grant Test User ALL privileges on schema.
+    stmt.executeUpdate("GRANT ALL ON SCHEMA high_privileges_schema TO " + TestUtil.getUser());
+    stmt.executeUpdate("REVOKE ALL ON SCHEMA high_privileges_schema FROM public");
+
+    createTestDataObjectsWithRangeOfPrivilegesInSchema("low_privileges_schema");
+    // Grant Test User USAGE privileges on schema.
+    stmt.executeUpdate("GRANT USAGE ON SCHEMA low_privileges_schema TO " + TestUtil.getUser());
+    stmt.executeUpdate("REVOKE ALL ON SCHEMA low_privileges_schema FROM public");
+
+    createTestDataObjectsWithRangeOfPrivilegesInSchema("no_privileges_schema");
+    // Revoke ALL privileges from Test User USAGE on schema.
+    stmt.executeUpdate("REVOKE ALL ON SCHEMA no_privileges_schema FROM " + TestUtil.getUser());
+    stmt.executeUpdate("REVOKE ALL ON SCHEMA no_privileges_schema FROM public");
+
+    stmt.close();
+
+    nonhidingDatabaseMetaData = getNonHidingDatabaseMetaData(props);
+    hidingDatabaseMetaData = getHidingDatabaseMetaData(props);
+  }
+
+  private static DatabaseMetaData getHidingDatabaseMetaData(Properties props) throws Exception {
+    PGProperty.HIDE_UNPRIVILEGED_OBJECTS.set(props, true);
+    hidingCon = TestUtil.openDB(props);
+    if (isSuperUser(hidingCon)) {
+      fail("Test for hiding database objects will not work while:" + TestUtil.getUser()
+          + " has a SUPERUSER role.");
+    }
+    return hidingCon.getMetaData();
+  }
+
+  private static DatabaseMetaData getNonHidingDatabaseMetaData(Properties props) throws Exception {
+    nonhidingCon = TestUtil.openDB(props);
+    return nonhidingCon.getMetaData();
+  }
+
+  private static void createTestDataObjectsWithRangeOfPrivilegesInSchema(String schema)
+      throws SQLException {
+    TestUtil.createSchema(privilegedCon, schema);
+    createSimpleTablesInSchema(schema,
+        new String[]{"owned_table", "all_grants_table", "insert_granted_table",
+          "select_granted_table", "no_grants_table"});
+
+    Statement stmt = privilegedCon.createStatement();
+    stmt.executeUpdate(
+        "CREATE FUNCTION " + schema + "."
+          + "execute_granted_add_function(integer, integer) RETURNS integer AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE  RETURNS NULL ON NULL INPUT");
+    stmt.executeUpdate(
+        "CREATE FUNCTION " + schema + "."
+          + "no_grants_add_function(integer, integer) RETURNS integer AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE  RETURNS NULL ON NULL INPUT");
+
+    if (pgConnection.haveMinimumServerVersion(ServerVersion.v11)) {
+      stmt.executeUpdate(
+          "CREATE PROCEDURE " + schema + "."
+          + "execute_granted_insert_procedure( a integer, b integer) LANGUAGE SQL AS 'select $1 + $2;'");
+      stmt.executeUpdate(
+          "CREATE PROCEDURE " + schema + "."
+          + "no_grants_insert_procedure( a integer, b integer) LANGUAGE SQL AS 'select $1 + $2;'");
+
+    }
+    stmt.executeUpdate(
+        "CREATE OR REPLACE VIEW " + schema + "." + "select_granted_view AS SELECT name FROM "
+          + schema + "." + "select_granted_table");
+    stmt.executeUpdate(
+        "CREATE OR REPLACE VIEW " + schema + "." + "no_grants_view AS SELECT name FROM " + schema
+          + "." + "owned_table");
+    stmt.executeUpdate(
+        "CREATE TYPE " + schema + "." + "usage_granted_composite_type AS (f1 int, f2 text)");
+    stmt.executeUpdate(
+        "CREATE TYPE " + schema + "." + "no_grants_composite_type AS (f1 int, f2 text)");
+    stmt.executeUpdate(
+        "CREATE DOMAIN " + schema + "." + "usage_granted_us_postal_code_domain CHAR(5) NOT NULL");
+    stmt.executeUpdate(
+        "CREATE DOMAIN " + schema + "." + "no_grants_us_postal_code_domain AS CHAR(5) NOT NULL");
+
+    if (pgConnection.haveMinimumServerVersion(ServerVersion.v9_2)) {
+      stmt.executeUpdate(
+          "REVOKE ALL ON TYPE " + schema + "."
+          + "usage_granted_composite_type FROM public RESTRICT");
+      stmt.executeUpdate(
+          "REVOKE ALL ON TYPE " + schema + "." + "no_grants_composite_type FROM public RESTRICT");
+      stmt.executeUpdate("GRANT USAGE on TYPE " + schema + "." + "usage_granted_composite_type TO "
+          + TestUtil.getUser());
+      stmt.executeUpdate(
+          "REVOKE ALL ON TYPE " + schema + "."
+            + "usage_granted_us_postal_code_domain FROM public RESTRICT");
+      stmt.executeUpdate(
+          "REVOKE ALL ON TYPE " + schema + "."
+            + "no_grants_us_postal_code_domain FROM public RESTRICT");
+      stmt.executeUpdate(
+          "GRANT USAGE on TYPE " + schema + "." + "usage_granted_us_postal_code_domain TO "
+            + TestUtil.getUser());
+    }
+    revokeAllOnFunctions(schema, new String[]{"execute_granted_add_function(integer, integer)",
+        "no_grants_add_function(integer, integer)"});
+
+    revokeAllOnTables(schema,
+        new String[]{"owned_table", "all_grants_table", "insert_granted_table",
+          "select_granted_table", "no_grants_table", "select_granted_view", "no_grants_view"});
+
+    stmt.executeUpdate(
+        "GRANT ALL ON FUNCTION " + schema + "."
+          + "execute_granted_add_function(integer, integer) TO "
+          + TestUtil.getUser());
+
+    if (pgConnection.haveMinimumServerVersion(ServerVersion.v11)) {
+      revokeAllOnProcedures(schema, new String[]{"execute_granted_insert_procedure(integer, integer)",
+          "no_grants_insert_procedure(integer, integer)"});
+      stmt.executeUpdate(
+          "GRANT ALL ON PROCEDURE " + schema + "."
+            + "execute_granted_insert_procedure(integer, integer) TO "
+            + TestUtil.getUser());
+
+    }
+    stmt.executeUpdate(
+          "ALTER TABLE " + schema + "." + "owned_table OWNER TO " + TestUtil.getUser());
+    stmt.executeUpdate(
+        "GRANT ALL ON TABLE " + schema + "." + "all_grants_table TO " + TestUtil.getUser());
+    stmt.executeUpdate("GRANT INSERT ON TABLE " + schema + "." + "insert_granted_table TO "
+        + TestUtil.getUser());
+    stmt.executeUpdate("GRANT SELECT ON TABLE " + schema + "." + "select_granted_table TO "
+        + TestUtil.getUser());
+    stmt.executeUpdate("GRANT SELECT ON TABLE " + schema + "." + "select_granted_view TO "
+        + TestUtil.getUser());
+    stmt.close();
+  }
+
+  private static void revokeAllOnProcedures(String schema, String[] procedures
+  ) throws SQLException {
+    Statement stmt = privilegedCon.createStatement();
+    for (String procedure : procedures) {
+      stmt.executeUpdate(
+          "REVOKE ALL ON PROCEDURE " + schema + "." + procedure + " FROM public RESTRICT");
+      stmt.executeUpdate(
+          "REVOKE ALL ON PROCEDURE  " + schema + "." + procedure + " FROM " + TestUtil.getUser()
+            + " RESTRICT");
+    }
+    stmt.close();
+  }
+
+  private static void revokeAllOnFunctions(String schema, String[] functions
+  ) throws SQLException {
+    Statement stmt = privilegedCon.createStatement();
+    for (String function : functions) {
+      stmt.executeUpdate(
+          "REVOKE ALL ON FUNCTION " + schema + "." + function + " FROM public RESTRICT");
+      stmt.executeUpdate("REVOKE ALL ON FUNCTION  " + schema + "."
+            + function + " FROM " + TestUtil.getUser()
+            + " RESTRICT");
+    }
+    stmt.close();
+  }
+
+  private static void revokeAllOnTables(String schema, String[] tables
+  ) throws SQLException {
+    Statement stmt = privilegedCon.createStatement();
+    for (String table : tables) {
+      stmt.executeUpdate("REVOKE ALL ON TABLE " + schema + "." + table + " FROM public RESTRICT");
+      stmt.executeUpdate(
+          "REVOKE ALL ON TABLE  " + schema + "." + table + " FROM " + TestUtil.getUser()
+          +   " RESTRICT");
+    }
+    stmt.close();
+  }
+
+  private static void createSimpleTablesInSchema(String schema, String[] tables
+  ) throws SQLException {
+    for (String tableName : tables) {
+      TestUtil.createTable(privilegedCon, schema + "." + tableName, COLUMNS);
+    }
+  }
+
+  @AfterClass
+  public static void tearDown() throws SQLException {
+    TestUtil.closeDB(hidingCon);
+    TestUtil.closeDB(nonhidingCon);
+    TestUtil.dropSchema(privilegedCon, "high_privileges_schema");
+    TestUtil.dropSchema(privilegedCon, "low_privileges_schema");
+    TestUtil.dropSchema(privilegedCon, "no_privileges_schema");
+    TestUtil.closeDB(privilegedCon);
+  }
+
+  private static boolean isSuperUser(Connection connection) throws SQLException {
+    // Check if we're operating as a superuser.
+    Statement st = connection.createStatement();
+    st.executeQuery("SHOW is_superuser;");
+    ResultSet rs = st.getResultSet();
+    rs.next(); // One row is guaranteed
+    boolean connIsSuper = rs.getString(1).equalsIgnoreCase("on");
+    st.close();
+    return connIsSuper;
+  }
+
+  @Test
+  public void testGetSchemas() throws SQLException {
+    List<String> schemasWithHiding = getSchemaNames(hidingDatabaseMetaData);
+    assertThat(schemasWithHiding,
+        hasItems("pg_catalog", "information_schema",
+        "high_privileges_schema", "low_privileges_schema"));
+    assertThat(schemasWithHiding,
+        not(hasItem("no_privileges_schema")));
+
+    List<String> schemasWithNoHiding = getSchemaNames(nonhidingDatabaseMetaData);
+    assertThat(schemasWithNoHiding,
+        hasItems("pg_catalog", "information_schema",
+        "high_privileges_schema", "low_privileges_schema", "no_privileges_schema"));
+  }
+
+  List<String> getSchemaNames(DatabaseMetaData databaseMetaData) throws SQLException {
+    List<String> schemaNames = new ArrayList<String>();
+    ResultSet rs = databaseMetaData.getSchemas();
+    while (rs.next()) {
+      schemaNames.add(rs.getString("TABLE_SCHEM"));
+    }
+    return schemaNames;
+  }
+
+  @Test
+  public void testGetTables() throws SQLException {
+    List<String> tablesWithHiding = getTableNames(hidingDatabaseMetaData, "high_privileges_schema");
+
+    assertThat(tablesWithHiding,
+        hasItems(
+        "owned_table",
+        "all_grants_table",
+        "insert_granted_table",
+        "select_granted_table"));
+    assertThat(tablesWithHiding,
+        not(hasItem("no_grants_table")));
+
+    List<String> tablesWithNoHiding =
+        getTableNames(nonhidingDatabaseMetaData, "high_privileges_schema");
+    assertThat(tablesWithNoHiding,
+        hasItems(
+        "owned_table",
+        "all_grants_table",
+        "insert_granted_table",
+        "select_granted_table",
+        "no_grants_table"));
+
+    tablesWithHiding = getTableNames(hidingDatabaseMetaData, "low_privileges_schema");
+
+    assertThat(tablesWithHiding,
+        hasItems(
+        "owned_table",
+        "all_grants_table",
+        "insert_granted_table",
+        "select_granted_table"));
+    assertThat(tablesWithHiding,
+        not(hasItem("no_grants_table")));
+
+    tablesWithNoHiding =
+        getTableNames(nonhidingDatabaseMetaData, "low_privileges_schema");
+    assertThat(tablesWithNoHiding,
+        hasItems(
+        "owned_table",
+        "all_grants_table",
+        "insert_granted_table",
+        "select_granted_table",
+        "no_grants_table"));
+
+    // Or should the the tables names not be returned because the schema is not visible?
+    tablesWithHiding = getTableNames(hidingDatabaseMetaData, "no_privileges_schema");
+
+    assertThat(tablesWithHiding,
+        hasItems(
+        "owned_table",
+        "all_grants_table",
+        "insert_granted_table",
+        "select_granted_table"));
+    assertThat(tablesWithHiding,
+        not(hasItem("no_grants_table")));
+
+    tablesWithNoHiding =
+        getTableNames(nonhidingDatabaseMetaData, "no_privileges_schema");
+    assertThat(tablesWithNoHiding,
+        hasItems(
+        "owned_table",
+        "all_grants_table",
+        "insert_granted_table",
+        "select_granted_table",
+        "no_grants_table"));
+
+  }
+
+  List<String> getTableNames(DatabaseMetaData databaseMetaData, String schemaPattern)
+      throws SQLException {
+    List<String> tableNames = new ArrayList<String>();
+    ResultSet rs = databaseMetaData.getTables(null, schemaPattern, null, new String[]{"TABLE"});
+    while (rs.next()) {
+      tableNames.add(rs.getString("TABLE_NAME"));
+    }
+    return tableNames;
+  }
+
+  @Test
+  public void testGetViews() throws SQLException {
+    List<String> viewsWithHiding = getViewNames(hidingDatabaseMetaData, "high_privileges_schema");
+
+    assertThat(viewsWithHiding,
+        hasItems(
+        "select_granted_view"));
+    assertThat(viewsWithHiding,
+        not(hasItem("no_grants_view")));
+
+    List<String> viewsWithNoHiding =
+        getViewNames(nonhidingDatabaseMetaData, "high_privileges_schema");
+    assertThat(viewsWithNoHiding,
+        hasItems(
+        "select_granted_view",
+        "no_grants_view"));
+
+    viewsWithHiding = getViewNames(hidingDatabaseMetaData, "low_privileges_schema");
+
+    assertThat(viewsWithHiding,
+        hasItems(
+        "select_granted_view"));
+    assertThat(viewsWithHiding,
+        not(hasItem("no_grants_view")));
+
+    viewsWithNoHiding =
+        getViewNames(nonhidingDatabaseMetaData, "low_privileges_schema");
+    assertThat(viewsWithNoHiding,
+        hasItems(
+        "select_granted_view",
+        "no_grants_view"));
+
+    // Or should the the view names not be returned because the schema is not visible?
+    viewsWithHiding = getViewNames(hidingDatabaseMetaData, "no_privileges_schema");
+
+    assertThat(viewsWithHiding,
+        hasItems(
+        "select_granted_view"));
+    assertThat(viewsWithHiding,
+        not(hasItem("no_grants_view")));
+
+    viewsWithNoHiding =
+        getViewNames(nonhidingDatabaseMetaData, "no_privileges_schema");
+    assertThat(viewsWithNoHiding,
+        hasItems(
+        "select_granted_view",
+        "no_grants_view"));
+
+  }
+
+  List<String> getViewNames(DatabaseMetaData databaseMetaData, String schemaPattern)
+      throws SQLException {
+    List<String> viewNames = new ArrayList<String>();
+    ResultSet rs = databaseMetaData.getTables(null, schemaPattern, null, new String[]{"VIEW"});
+    while (rs.next()) {
+      viewNames.add(rs.getString("TABLE_NAME"));
+    }
+    return viewNames;
+  }
+
+  @Test
+  public void testGetFunctions() throws SQLException {
+    List<String> functionsWithHiding =
+        getFunctionNames(hidingDatabaseMetaData, "high_privileges_schema");
+    assertThat(functionsWithHiding,
+        hasItem("execute_granted_add_function"));
+    assertThat(functionsWithHiding,
+        not(hasItem("no_grants_add_function")));
+
+    List<String> functionsWithNoHiding =
+        getFunctionNames(nonhidingDatabaseMetaData, "high_privileges_schema");
+    assertThat(functionsWithNoHiding,
+        hasItems("execute_granted_add_function", "no_grants_add_function"));
+
+    functionsWithHiding =
+        getFunctionNames(hidingDatabaseMetaData, "low_privileges_schema");
+    assertThat(functionsWithHiding,
+        hasItem("execute_granted_add_function"));
+    assertThat(functionsWithHiding,
+        not(hasItem("no_grants_add_function")));
+
+    functionsWithNoHiding =
+        getFunctionNames(nonhidingDatabaseMetaData, "low_privileges_schema");
+    assertThat(functionsWithNoHiding,
+        hasItems("execute_granted_add_function", "no_grants_add_function"));
+
+    // Or should the the function names not be returned because the schema is not visible?
+    functionsWithHiding =
+        getFunctionNames(hidingDatabaseMetaData, "no_privileges_schema");
+    assertThat(functionsWithHiding,
+        hasItem("execute_granted_add_function"));
+    assertThat(functionsWithHiding,
+        not(hasItem("no_grants_add_function")));
+
+    functionsWithNoHiding =
+        getFunctionNames(nonhidingDatabaseMetaData, "no_privileges_schema");
+    assertThat(functionsWithNoHiding,
+        hasItems("execute_granted_add_function", "no_grants_add_function"));
+  }
+
+  List<String> getFunctionNames(DatabaseMetaData databaseMetaData, String schemaPattern)
+      throws SQLException {
+    List<String> functionNames = new ArrayList<String>();
+    ResultSet rs = databaseMetaData.getFunctions(null, schemaPattern, null);
+    while (rs.next()) {
+      functionNames.add(rs.getString("FUNCTION_NAME"));
+    }
+    return functionNames;
+  }
+
+  @Test
+  public void testGetProcedures() throws SQLException {
+    List<String> proceduresWithHiding =
+        getProcedureNames(hidingDatabaseMetaData, "high_privileges_schema");
+    assertThat(proceduresWithHiding,
+        hasItem("execute_granted_add_function"));
+    assertThat(proceduresWithHiding,
+        not(hasItem("no_grants_add_function")));
+
+    List<String> proceduresWithNoHiding =
+        getProcedureNames(nonhidingDatabaseMetaData, "high_privileges_schema");
+    assertThat(proceduresWithNoHiding,
+        hasItems("execute_granted_add_function", "no_grants_add_function"));
+
+    proceduresWithHiding =
+        getProcedureNames(hidingDatabaseMetaData, "low_privileges_schema");
+    assertThat(proceduresWithHiding,
+        hasItem("execute_granted_add_function"));
+    assertThat(proceduresWithHiding,
+        not(hasItem("no_grants_add_function")));
+
+    proceduresWithNoHiding =
+        getProcedureNames(nonhidingDatabaseMetaData, "low_privileges_schema");
+    assertThat(proceduresWithNoHiding,
+        hasItems("execute_granted_add_function", "no_grants_add_function"));
+
+    // Or should the the function names not be returned because the schema is not visible?
+    proceduresWithHiding =
+        getProcedureNames(hidingDatabaseMetaData, "no_privileges_schema");
+    assertThat(proceduresWithHiding,
+        hasItem("execute_granted_add_function"));
+    assertThat(proceduresWithHiding,
+        not(hasItem("no_grants_add_function")));
+
+    proceduresWithNoHiding =
+        getProcedureNames(nonhidingDatabaseMetaData, "no_privileges_schema");
+    assertThat(proceduresWithNoHiding,
+        hasItems("execute_granted_add_function", "no_grants_add_function"));
+
+  }
+
+  List<String> getProcedureNames(DatabaseMetaData databaseMetaData, String schemaPattern)
+      throws SQLException {
+    List<String> procedureNames = new ArrayList<String>();
+    ResultSet rs = databaseMetaData.getProcedures(null, schemaPattern, null);
+    while (rs.next()) {
+      procedureNames.add(rs.getString("PROCEDURE_NAME"));
+    }
+    return procedureNames;
+  }
+
+  @Test
+  /*
+   *  According to the JDBC JavaDoc, the applicable UDTs are: JAVA_OBJECT, STRUCT, or DISTINCT.
+   */
+  public void testGetUDTs() throws SQLException {
+    if (pgConnection.haveMinimumServerVersion(ServerVersion.v9_2)) {
+      List<String> typesWithHiding = getTypeNames(hidingDatabaseMetaData, "high_privileges_schema");
+      assertThat(typesWithHiding,
+          hasItems("usage_granted_composite_type", "usage_granted_us_postal_code_domain"));
+      assertThat(typesWithHiding,
+          not(hasItems("no_grants_composite_type", "no_grants_us_postal_code_domain")));
+
+      typesWithHiding = getTypeNames(hidingDatabaseMetaData, "low_privileges_schema");
+      assertThat(typesWithHiding,
+          hasItems("usage_granted_composite_type", "usage_granted_us_postal_code_domain"));
+      assertThat(typesWithHiding,
+          not(hasItems("no_grants_composite_type", "no_grants_us_postal_code_domain")));
+
+      // Or should the the types names not be returned because the schema is not visible?
+      typesWithHiding = getTypeNames(hidingDatabaseMetaData, "no_privileges_schema");
+      assertThat(typesWithHiding,
+          hasItems("usage_granted_composite_type", "usage_granted_us_postal_code_domain"));
+      assertThat(typesWithHiding,
+          not(hasItems("no_grants_composite_type", "no_grants_us_postal_code_domain")));
+    }
+
+    List<String> typesWithNoHiding =
+        getTypeNames(nonhidingDatabaseMetaData, "high_privileges_schema");
+    assertThat(typesWithNoHiding,
+        hasItems("usage_granted_composite_type", "no_grants_composite_type",
+          "usage_granted_us_postal_code_domain", "no_grants_us_postal_code_domain"));
+
+    typesWithNoHiding =
+        getTypeNames(nonhidingDatabaseMetaData, "low_privileges_schema");
+    assertThat(typesWithNoHiding,
+        hasItems("usage_granted_composite_type", "no_grants_composite_type",
+          "usage_granted_us_postal_code_domain", "no_grants_us_postal_code_domain"));
+
+    typesWithNoHiding =
+        getTypeNames(nonhidingDatabaseMetaData, "no_privileges_schema");
+    assertThat(typesWithNoHiding,
+        hasItems("usage_granted_composite_type", "no_grants_composite_type",
+          "usage_granted_us_postal_code_domain", "no_grants_us_postal_code_domain"));
+  }
+
+  /*
+  From the Postgres JDBC driver source code, we are mapping the types:
+      java.sql.Types.DISTINCT to the Postgres type:  TYPTYPE_COMPOSITE  'c'   # composite (e.g., table's rowtype)
+      java.sql.Types.STRUCT   to the Postgres type:  TYPTYPE_DOMAIN     'd'   # domain over another type
+   */
+  List<String> getTypeNames(DatabaseMetaData databaseMetaData, String schemaPattern) throws SQLException {
+    List<String> typeNames = new ArrayList<String>();
+    ResultSet rs = databaseMetaData.getUDTs(null, schemaPattern, null, null);
+    while (rs.next()) {
+      typeNames.add(rs.getString("TYPE_NAME"));
+    }
+    return typeNames;
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java
index 29292445..7a7ac592 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java
@@ -19,6 +19,7 @@ import org.junit.runners.Suite;
     BlobTest.class,
     CharacterStreamTest.class,
     ClientInfoTest.class,
+    DatabaseMetaDataHideUnprivilegedObjectsTest.class,
     DatabaseMetaDataTest.class,
     IsValidTest.class,
     JsonbTest.class,