This is an automated email from the ASF dual-hosted git repository.

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 8a7348963b [#12480] improvement(server): Add table-like existence 
probe privilege (#12482)
8a7348963b is described below

commit 8a7348963b3b031791220c121cbe440c58cabf5d
Author: jarred0214 <[email protected]>
AuthorDate: Mon Aug 24 09:45:35 2026 +0800

    [#12480] improvement(server): Add table-like existence probe privilege 
(#12482)
    
    ### What changes were proposed in this pull request?
    
    This PR updates the Flink connector to preserve Gravitino table
    authorization failures when loading tables.
    
    - `BaseCatalog#getTable` now wraps `ForbiddenException` as
    `CatalogException` instead of `TableNotExistException`.
    - `GravitinoHiveCatalog#getTable` applies the same behavior for Hive,
    which overrides the base implementation.
    - Added regression tests for both the common `BaseCatalog` path and the
    Hive-specific path.
    
    ### Why are the changes needed?
    
    When Gravitino server returns `403 Forbidden` for `loadTable`, the Flink
    connector currently converts it to `TableNotExistException`. Flink then
    reports a misleading error saying the table cannot be found in any
    catalog.
    
    The table exists; the real problem is missing permission.
    
    Fix: #12480
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Flink users will now see a permission-related catalog error when
    table authorization fails, instead of a misleading table-not-found
    error.
    
    ### How was this patch tested?
    
    Added unit tests:
    
    - `TestBaseCatalog#testGetTableThrowsCatalogExceptionWhenForbidden`
    -
    `TestGravitinoHiveCatalog#testGetTableThrowsCatalogExceptionWhenForbidden`
    
    I could not run Gradle locally because this machine only has Java 8
    installed, while the project requires JDK 17.
---
 .../apache/gravitino/authorization/Privilege.java  |   4 +-
 .../apache/gravitino/authorization/Privileges.java |  42 ++++++
 .../test/authorization/TableAuthorizationIT.java   |   4 +-
 .../authorization/AuthorizationUtils.java          |   5 +-
 docs/open-api/roles.yaml                           |   1 +
 docs/security/access-control.md                    |  47 +++---
 .../flink/connector/catalog/BaseCatalog.java       |   7 +-
 .../flink/connector/hive/GravitinoHiveCatalog.java |   2 +-
 .../flink/connector/catalog/TestBaseCatalog.java   |  38 +++++
 .../connector/hive/TestGravitinoHiveCatalog.java   |  84 +++++++++++
 .../AuthorizationExpressionConstants.java          |  15 ++
 .../AuthorizationExpressionConverter.java          |   5 +
 .../web/filter/GravitinoInterceptionService.java   |   3 +-
 .../authorization/AuthorizeExecutorFactory.java    |   6 +-
 .../LoadTableAuthorizationExecutor.java            |  28 +++-
 .../gravitino/server/web/rest/TableOperations.java |   4 +-
 .../filter/TestGravitinoInterceptionService.java   | 167 +++++++++++++++++++++
 .../TestLoadTableAuthorizationExecutor.java        |   3 +-
 .../TestTableAuthorizationExpression.java          |  98 ++++++++----
 19 files changed, 499 insertions(+), 64 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/Privilege.java 
b/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
index 69e1392d69..9122a8d25b 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
@@ -151,7 +151,9 @@ public interface Privilege {
     /** The privilege to execute (invoke) a function. */
     EXECUTE_FUNCTION(0L, 1L << 31),
     /** The privilege to alter a function's metadata. */
-    MODIFY_FUNCTION(0L, 1L << 32);
+    MODIFY_FUNCTION(0L, 1L << 32),
+    /** The privilege to probe whether a table-like object exists. */
+    PROBE_TABLE_LIKE(0L, 1L << 33);
 
     private final long highBits;
     private final long lowBits;
diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/Privileges.java 
b/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
index 393252cf15..5c217bc5cd 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
@@ -32,6 +32,13 @@ public class Privileges {
           MetadataObject.Type.CATALOG,
           MetadataObject.Type.SCHEMA,
           MetadataObject.Type.TABLE);
+  private static final Set<MetadataObject.Type> TABLE_LIKE_SUPPORTED_TYPES =
+      Sets.immutableEnumSet(
+          MetadataObject.Type.METALAKE,
+          MetadataObject.Type.CATALOG,
+          MetadataObject.Type.SCHEMA,
+          MetadataObject.Type.TABLE,
+          MetadataObject.Type.VIEW);
 
   private static final Set<MetadataObject.Type> MODEL_SUPPORTED_TYPES =
       Sets.immutableEnumSet(
@@ -127,6 +134,8 @@ public class Privileges {
         return ModifyTable.allow();
       case SELECT_TABLE:
         return SelectTable.allow();
+      case PROBE_TABLE_LIKE:
+        return ProbeTableLike.allow();
 
         // Fileset
       case CREATE_FILESET:
@@ -249,6 +258,8 @@ public class Privileges {
         return ModifyTable.deny();
       case SELECT_TABLE:
         return SelectTable.deny();
+      case PROBE_TABLE_LIKE:
+        return ProbeTableLike.deny();
 
         // Fileset
       case CREATE_FILESET:
@@ -598,6 +609,37 @@ public class Privileges {
     }
   }
 
+  /** The privilege to probe whether a table-like object exists. */
+  public static class ProbeTableLike extends GenericPrivilege<ProbeTableLike> {
+    private static final ProbeTableLike ALLOW_INSTANCE =
+        new ProbeTableLike(Condition.ALLOW, Name.PROBE_TABLE_LIKE);
+    private static final ProbeTableLike DENY_INSTANCE =
+        new ProbeTableLike(Condition.DENY, Name.PROBE_TABLE_LIKE);
+
+    private ProbeTableLike(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /**
+     * @return The instance with allow condition of the privilege.
+     */
+    public static ProbeTableLike allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /**
+     * @return The instance with deny condition of the privilege.
+     */
+    public static ProbeTableLike deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return TABLE_LIKE_SUPPORTED_TYPES.contains(type);
+    }
+  }
+
   /** The privilege to create a fileset. */
   public static class CreateFileset extends GenericPrivilege<CreateFileset> {
     private static final CreateFileset ALLOW_INSTANCE =
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/TableAuthorizationIT.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/TableAuthorizationIT.java
index af12862609..b244fad6eb 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/TableAuthorizationIT.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/TableAuthorizationIT.java
@@ -219,7 +219,9 @@ public class TableAuthorizationIT extends 
BaseRestApiAuthorizationIT {
     NameIdentifier[] tablesListNormalUser = 
tableCatalogNormalUser.listTables(Namespace.of(SCHEMA));
     assertArrayEquals(
         new NameIdentifier[] {
-          NameIdentifier.of(SCHEMA, "table2"), NameIdentifier.of(SCHEMA, 
"table3")
+          NameIdentifier.of(SCHEMA, "table1"),
+          NameIdentifier.of(SCHEMA, "table2"),
+          NameIdentifier.of(SCHEMA, "table3")
         },
         tablesListNormalUser);
   }
diff --git 
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java 
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
index 76f77770e8..8ca88fb5da 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
@@ -111,7 +111,10 @@ public class AuthorizationUtils {
           Privilege.Name.CREATE_FILESET, Privilege.Name.WRITE_FILESET, 
Privilege.Name.READ_FILESET);
   private static final Set<Privilege.Name> TABLE_PRIVILEGES =
       Sets.immutableEnumSet(
-          Privilege.Name.CREATE_TABLE, Privilege.Name.MODIFY_TABLE, 
Privilege.Name.SELECT_TABLE);
+          Privilege.Name.CREATE_TABLE,
+          Privilege.Name.MODIFY_TABLE,
+          Privilege.Name.SELECT_TABLE,
+          Privilege.Name.PROBE_TABLE_LIKE);
   private static final Set<Privilege.Name> TOPIC_PRIVILEGES =
       Sets.immutableEnumSet(
           Privilege.Name.CREATE_TOPIC, Privilege.Name.PRODUCE_TOPIC, 
Privilege.Name.CONSUME_TOPIC);
diff --git a/docs/open-api/roles.yaml b/docs/open-api/roles.yaml
index 046b8071c9..888dc966ab 100644
--- a/docs/open-api/roles.yaml
+++ b/docs/open-api/roles.yaml
@@ -199,6 +199,7 @@ components:
             - CREATE_SCHEMA
             - USE_SCHEMA
             - CREATE_TABLE
+            - PROBE_TABLE_LIKE
             - MODIFY_TABLE
             - SELECT_TABLE
             - CREATE_FILESET
diff --git a/docs/security/access-control.md b/docs/security/access-control.md
index 969d8a359c..dc6ddad78c 100755
--- a/docs/security/access-control.md
+++ b/docs/security/access-control.md
@@ -165,29 +165,30 @@ sets the scope of the grant. Binding a privilege to a 
type not listed for it is
 
 ### Data Object Privileges
 
-| Privilege            | Grantable On                        | What It Allows  
                                                   |
-|----------------------|-------------------------------------|--------------------------------------------------------------------|
-| `CREATE_CATALOG`     | Metalake                            | Create catalogs 
                                                   |
-| `USE_CATALOG`        | Metalake, Catalog                   | Use any catalog 
in scope, and reach the objects inside it          |
-| `CREATE_SCHEMA`      | Metalake, Catalog, Schema           | Create schemas 
or nested schemas in scope                          |
-| `USE_SCHEMA`         | Metalake, Catalog, Schema           | Use any schema 
in scope, and reach the objects inside it           |
-| `CREATE_TABLE`       | Metalake, Catalog, Schema           | Create tables 
in any schema in scope                               |
-| `SELECT_TABLE`       | Metalake, Catalog, Schema, Table    | Read any table 
in scope                                            |
-| `MODIFY_TABLE`       | Metalake, Catalog, Schema, Table    | Read and write 
to, and alter the schema of, any table in scope     |
-| `CREATE_VIEW`        | Metalake, Catalog, Schema           | Create views in 
any schema in scope                                |
-| `SELECT_VIEW`        | Metalake, Catalog, Schema, View     | Read any view 
in scope                                             |
-| `CREATE_TOPIC`       | Metalake, Catalog, Schema           | Create topics 
in any schema in scope                               |
-| `CONSUME_TOPIC`      | Metalake, Catalog, Schema, Topic    | Consume from 
any topic in scope                                    |
-| `PRODUCE_TOPIC`      | Metalake, Catalog, Schema, Topic    | Consume from, 
produce to, and alter any topic in scope             |
-| `CREATE_FILESET`     | Metalake, Catalog, Schema           | Create filesets 
in any schema in scope                             |
-| `READ_FILESET`       | Metalake, Catalog, Schema, Fileset  | Read any 
fileset in scope                                          |
-| `WRITE_FILESET`      | Metalake, Catalog, Schema, Fileset  | Read, write, 
and alter any fileset in scope                        |
-| `REGISTER_MODEL`     | Metalake, Catalog, Schema           | Register models 
in any schema in scope                             |
-| `LINK_MODEL_VERSION` | Metalake, Catalog, Schema, Model    | Link versions 
to any model in scope                                |
-| `USE_MODEL`          | Metalake, Catalog, Schema, Model    | Read the 
metadata of, and download versions of, any model in scope |
-| `REGISTER_FUNCTION`  | Metalake, Catalog, Schema           | Register 
functions in any schema in scope                          |
-| `EXECUTE_FUNCTION`   | Metalake, Catalog, Schema, Function | Read the 
metadata of, and execute, any function in scope           |
-| `MODIFY_FUNCTION`    | Metalake, Catalog, Schema, Function | Alter or drop 
any function in scope                                |
+| Privilege            | Grantable On                           | What It 
Allows                                                     |
+|----------------------|----------------------------------------|--------------------------------------------------------------------|
+| `CREATE_CATALOG`     | Metalake                               | Create 
catalogs                                                    |
+| `USE_CATALOG`        | Metalake, Catalog                      | Use any 
catalog in scope, and reach the objects inside it          |
+| `CREATE_SCHEMA`      | Metalake, Catalog, Schema              | Create 
schemas or nested schemas in scope                          |
+| `USE_SCHEMA`         | Metalake, Catalog, Schema              | Use any 
schema in scope, and reach the objects inside it           |
+| `CREATE_TABLE`       | Metalake, Catalog, Schema              | Create 
tables in any schema in scope                               |
+| `PROBE_TABLE_LIKE`   | Metalake, Catalog, Schema, Table, View | Probe 
whether a table-like object exists without reading its data  |
+| `SELECT_TABLE`       | Metalake, Catalog, Schema, Table       | Read any 
table in scope                                            |
+| `MODIFY_TABLE`       | Metalake, Catalog, Schema, Table       | Read and 
write to, and alter the schema of, any table in scope     |
+| `CREATE_VIEW`        | Metalake, Catalog, Schema              | Create views 
in any schema in scope                                |
+| `SELECT_VIEW`        | Metalake, Catalog, Schema, View        | Read any 
view in scope                                             |
+| `CREATE_TOPIC`       | Metalake, Catalog, Schema              | Create 
topics in any schema in scope                               |
+| `CONSUME_TOPIC`      | Metalake, Catalog, Schema, Topic       | Consume from 
any topic in scope                                    |
+| `PRODUCE_TOPIC`      | Metalake, Catalog, Schema, Topic       | Consume 
from, produce to, and alter any topic in scope             |
+| `CREATE_FILESET`     | Metalake, Catalog, Schema              | Create 
filesets in any schema in scope                             |
+| `READ_FILESET`       | Metalake, Catalog, Schema, Fileset     | Read any 
fileset in scope                                          |
+| `WRITE_FILESET`      | Metalake, Catalog, Schema, Fileset     | Read, write, 
and alter any fileset in scope                        |
+| `REGISTER_MODEL`     | Metalake, Catalog, Schema              | Register 
models in any schema in scope                             |
+| `LINK_MODEL_VERSION` | Metalake, Catalog, Schema, Model       | Link 
versions to any model in scope                                |
+| `USE_MODEL`          | Metalake, Catalog, Schema, Model       | Read the 
metadata of, and download versions of, any model in scope |
+| `REGISTER_FUNCTION`  | Metalake, Catalog, Schema              | Register 
functions in any schema in scope                          |
+| `EXECUTE_FUNCTION`   | Metalake, Catalog, Schema, Function    | Read the 
metadata of, and execute, any function in scope           |
+| `MODIFY_FUNCTION`    | Metalake, Catalog, Schema, Function    | Alter or 
drop any function in scope                                |
 
 Either `SELECT_TABLE` or `MODIFY_TABLE` is enough to load a table's metadata, 
and the same pairing
 holds for views, topics, and filesets.
diff --git 
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/catalog/BaseCatalog.java
 
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/catalog/BaseCatalog.java
index b116e93c69..bc9d87eac6 100644
--- 
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/catalog/BaseCatalog.java
+++ 
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/catalog/BaseCatalog.java
@@ -271,10 +271,7 @@ public abstract class BaseCatalog extends AbstractCatalog {
     } catch (NoSuchTableException e) {
       // Fall through to check views.
     } catch (ForbiddenException e) {
-      // Flink/Calcite speculatively probes tables during multi-part 
identifier resolution.
-      // Treat authorization failure as table-not-exist to allow Calcite to 
fall back to
-      // alternative resolution paths (e.g., treating the name as a schema).
-      throw new TableNotExistException(catalogName(), tablePath, e);
+      throw new CatalogException(e);
     } catch (CatalogException e) {
       throw e;
     } catch (Exception e) {
@@ -294,7 +291,7 @@ public abstract class BaseCatalog extends AbstractCatalog {
         return true;
       }
     } catch (ForbiddenException e) {
-      return false;
+      throw new CatalogException(e);
     } catch (Exception e) {
       throw new CatalogException(e);
     }
diff --git 
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/hive/GravitinoHiveCatalog.java
 
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/hive/GravitinoHiveCatalog.java
index feb6362fae..5a3b9971f4 100644
--- 
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/hive/GravitinoHiveCatalog.java
+++ 
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/hive/GravitinoHiveCatalog.java
@@ -207,7 +207,7 @@ public class GravitinoHiveCatalog extends BaseCatalog {
     } catch (NoSuchTableException e) {
       // Fall through to check views.
     } catch (ForbiddenException e) {
-      throw new TableNotExistException(catalogName(), tablePath, e);
+      throw new CatalogException(e);
     } catch (Exception e) {
       LOG.warn("Failed to load table {} from catalog {}", tablePath, 
catalogName(), e);
       throw new CatalogException(e);
diff --git 
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/catalog/TestBaseCatalog.java
 
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/catalog/TestBaseCatalog.java
index 5b3e44d90b..96e9215ba5 100644
--- 
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/catalog/TestBaseCatalog.java
+++ 
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/catalog/TestBaseCatalog.java
@@ -33,20 +33,24 @@ import org.apache.flink.table.catalog.CatalogDatabase;
 import org.apache.flink.table.catalog.CatalogDatabaseImpl;
 import org.apache.flink.table.catalog.CatalogView;
 import org.apache.flink.table.catalog.Column;
+import org.apache.flink.table.catalog.ObjectPath;
 import org.apache.flink.table.catalog.ResolvedCatalogView;
 import org.apache.flink.table.catalog.ResolvedSchema;
 import org.apache.flink.table.catalog.TableChange;
+import org.apache.flink.table.catalog.exceptions.CatalogException;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.SchemaChange;
 import org.apache.gravitino.catalog.lakehouse.paimon.PaimonConstants;
+import org.apache.gravitino.exceptions.ForbiddenException;
 import org.apache.gravitino.flink.connector.PartitionConverter;
 import org.apache.gravitino.flink.connector.SchemaAndTablePropertiesConverter;
 import org.apache.gravitino.flink.connector.utils.DefaultCatalogCompat;
 import org.apache.gravitino.rel.Dialects;
 import org.apache.gravitino.rel.Representation;
 import org.apache.gravitino.rel.SQLRepresentation;
+import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.gravitino.rel.ViewChange;
 import org.apache.gravitino.rel.expressions.distributions.Distributions;
@@ -179,6 +183,40 @@ public class TestBaseCatalog {
     Assertions.assertEquals(ImmutableList.of("v1", "v2"), views);
   }
 
+  @Test
+  public void testGetTableThrowsCatalogExceptionWhenForbidden() throws 
Exception {
+    Catalog gravitinoCatalog = Mockito.mock(Catalog.class);
+    TableCatalog tableCatalog = Mockito.mock(TableCatalog.class);
+    ForbiddenException forbiddenException = new ForbiddenException("denied");
+    Mockito.when(gravitinoCatalog.asTableCatalog()).thenReturn(tableCatalog);
+    
Mockito.when(tableCatalog.loadTable(Mockito.any())).thenThrow(forbiddenException);
+    BaseCatalog catalog =
+        new TestableBaseCatalog(Mockito.mock(AbstractCatalog.class), 
gravitinoCatalog);
+
+    CatalogException catalogException =
+        Assertions.assertThrows(
+            CatalogException.class, () -> catalog.getTable(new 
ObjectPath("db", "tbl")));
+
+    Assertions.assertSame(forbiddenException, catalogException.getCause());
+  }
+
+  @Test
+  public void testTableExistsThrowsCatalogExceptionWhenForbidden() throws 
Exception {
+    Catalog gravitinoCatalog = Mockito.mock(Catalog.class);
+    TableCatalog tableCatalog = Mockito.mock(TableCatalog.class);
+    ForbiddenException forbiddenException = new ForbiddenException("denied");
+    Mockito.when(gravitinoCatalog.asTableCatalog()).thenReturn(tableCatalog);
+    
Mockito.when(tableCatalog.tableExists(Mockito.any())).thenThrow(forbiddenException);
+    BaseCatalog catalog =
+        new TestableBaseCatalog(Mockito.mock(AbstractCatalog.class), 
gravitinoCatalog);
+
+    CatalogException catalogException =
+        Assertions.assertThrows(
+            CatalogException.class, () -> catalog.tableExists(new 
ObjectPath("db", "tbl")));
+
+    Assertions.assertSame(forbiddenException, catalogException.getCause());
+  }
+
   @Test
   public void testGetGravitinoViewChangesSetAndRemoveProperty() {
     List<TableChange> tableChanges =
diff --git 
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/hive/TestGravitinoHiveCatalog.java
 
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/hive/TestGravitinoHiveCatalog.java
new file mode 100644
index 0000000000..64cd45641b
--- /dev/null
+++ 
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/hive/TestGravitinoHiveCatalog.java
@@ -0,0 +1,84 @@
+/*
+ * 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.gravitino.flink.connector.hive;
+
+import java.util.Collections;
+import org.apache.flink.table.catalog.AbstractCatalog;
+import org.apache.flink.table.catalog.ObjectPath;
+import org.apache.flink.table.catalog.exceptions.CatalogException;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.flink.connector.PartitionConverter;
+import org.apache.gravitino.flink.connector.SchemaAndTablePropertiesConverter;
+import org.apache.gravitino.rel.TableCatalog;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class TestGravitinoHiveCatalog {
+
+  @Test
+  public void testGetTableThrowsCatalogExceptionWhenForbidden() throws 
Exception {
+    Catalog gravitinoCatalog = Mockito.mock(Catalog.class);
+    TableCatalog tableCatalog = Mockito.mock(TableCatalog.class);
+    ForbiddenException forbiddenException = new ForbiddenException("denied");
+    Mockito.when(gravitinoCatalog.asTableCatalog()).thenReturn(tableCatalog);
+    
Mockito.when(tableCatalog.loadTable(Mockito.any())).thenThrow(forbiddenException);
+    TestableGravitinoHiveCatalog catalog = new 
TestableGravitinoHiveCatalog(gravitinoCatalog);
+
+    CatalogException catalogException =
+        Assertions.assertThrows(
+            CatalogException.class, () -> catalog.getTable(new 
ObjectPath("db", "tbl")));
+
+    Assertions.assertSame(forbiddenException, catalogException.getCause());
+  }
+
+  private static class TestableGravitinoHiveCatalog extends 
GravitinoHiveCatalog {
+    private final Catalog gravitinoCatalog;
+
+    TestableGravitinoHiveCatalog(Catalog gravitinoCatalog) {
+      super(
+          "test",
+          "default",
+          Collections.emptyMap(),
+          Mockito.mock(SchemaAndTablePropertiesConverter.class),
+          Mockito.mock(PartitionConverter.class),
+          hiveConf(),
+          null);
+      this.gravitinoCatalog = gravitinoCatalog;
+    }
+
+    @Override
+    protected AbstractCatalog realCatalog() {
+      return Mockito.mock(AbstractCatalog.class);
+    }
+
+    @Override
+    protected Catalog catalog() {
+      return gravitinoCatalog;
+    }
+
+    private static HiveConf hiveConf() {
+      HiveConf hiveConf = new HiveConf();
+      hiveConf.set("hive.metastore.uris", "thrift://localhost:9083");
+      return hiveConf;
+    }
+  }
+}
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
index c8fd3d07d1..7086f7e125 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
@@ -40,6 +40,21 @@ public class AuthorizationExpressionConstants {
                   ANY_USE_CATALOG && ANY_USE_SCHEMA  && (TABLE::OWNER || 
ANY_SELECT_TABLE || ANY_MODIFY_TABLE)
                   """;
 
+  public static final String PROBE_TABLE_LIKE_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA &&
+                  (ANY_PROBE_TABLE_LIKE || ANY_SELECT_TABLE || 
ANY_MODIFY_TABLE ||
+                  ANY_CREATE_TABLE || ANY_CREATE_VIEW)
+                  """;
+
+  public static final String LIST_TABLE_LIKE_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY(OWNER, METALAKE, CATALOG, SCHEMA, TABLE) ||
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA &&
+                  (ANY_PROBE_TABLE_LIKE || ANY_SELECT_TABLE || 
ANY_MODIFY_TABLE ||
+                  ANY_CREATE_TABLE || ANY_CREATE_VIEW)
+                  """;
+
   //  Adding ANY_CREATE_TABLE here as Spark calls tableExists before creating 
a table.
   public static final String ICEBERG_LOAD_TABLE_AUTHORIZATION_EXPRESSION =
       """
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
index f98ea1ea02..968f40bbf2 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
@@ -261,6 +261,11 @@ public class AuthorizationExpressionConverter {
             "ANY_CREATE_TABLE",
             "((ANY(CREATE_TABLE, METALAKE, CATALOG, SCHEMA, TABLE)) "
                 + "&& !(ANY(DENY_CREATE_TABLE, METALAKE, CATALOG, SCHEMA, 
TABLE)))");
+    expression =
+        expression.replaceAll(
+            "ANY_PROBE_TABLE_LIKE",
+            "((ANY(PROBE_TABLE_LIKE, METALAKE, CATALOG, SCHEMA, TABLE)) "
+                + "&& !(ANY(DENY_PROBE_TABLE_LIKE, METALAKE, CATALOG, SCHEMA, 
TABLE)))");
     expression =
         expression.replaceAll(
             "ANY_SELECT_VIEW",
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
index c23818cc17..ae67980587 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
@@ -234,7 +234,8 @@ public class GravitinoInterceptionService implements 
InterceptionService {
                     parameters,
                     args,
                     secondaryExpression,
-                    secondaryExpressionCondition);
+                    secondaryExpressionCondition,
+                    expressionAnnotation.allowCheckExistence());
             boolean authorizeResult = 
executor.execute(authorizationRequestContext);
             if (!authorizeResult) {
               MetadataObject.Type type = 
expressionAnnotation.accessMetadataType();
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
index 4d2f4c63aa..4b9f43d599 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
@@ -36,7 +36,8 @@ public class AuthorizeExecutorFactory {
       Parameter[] parameters,
       Object[] args,
       String secondaryExpression,
-      ExpressionCondition secondaryExpressionCondition) {
+      ExpressionCondition secondaryExpressionCondition,
+      String allowCheckExistenceExpression) {
     return switch (requestType) {
       case COMMON -> new CommonAuthorizerExecutor(
           expression, metadataContext, pathParams, entityType);
@@ -54,7 +55,8 @@ public class AuthorizeExecutorFactory {
           pathParams,
           entityType,
           secondaryExpression,
-          secondaryExpressionCondition);
+          secondaryExpressionCondition,
+          allowCheckExistenceExpression);
       case CREATE_SCHEMA -> new CreateSchemaAuthorizationExecutor(
           parameters, args, expression, metadataContext, pathParams, 
entityType);
     };
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadTableAuthorizationExecutor.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadTableAuthorizationExecutor.java
index 5b87979518..a0a99b94e4 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadTableAuthorizationExecutor.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadTableAuthorizationExecutor.java
@@ -22,7 +22,9 @@ import java.util.Map;
 import java.util.Optional;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Entity;
+import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.authorization.Privilege;
 import 
org.apache.gravitino.server.authorization.annotations.ExpressionCondition;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
@@ -42,6 +44,8 @@ import org.apache.gravitino.server.web.filter.ParameterUtil;
  * Legacy clients without the `privileges` parameter will use default 
authorization.
  */
 public class LoadTableAuthorizationExecutor extends CommonAuthorizerExecutor {
+  private final AuthorizationExpressionEvaluator allowCheckExistenceEvaluator;
+
   public LoadTableAuthorizationExecutor(
       Parameter[] parameters,
       Object[] args,
@@ -50,8 +54,13 @@ public class LoadTableAuthorizationExecutor extends 
CommonAuthorizerExecutor {
       Map<String, Object> pathParams,
       Optional<String> entityType,
       String secondaryExpression,
-      ExpressionCondition secondaryExpressionCondition) {
+      ExpressionCondition secondaryExpressionCondition,
+      String allowCheckExistenceExpression) {
     super(expression, metadataContext, pathParams, entityType);
+    this.allowCheckExistenceEvaluator =
+        StringUtils.isBlank(allowCheckExistenceExpression)
+            ? null
+            : new 
AuthorizationExpressionEvaluator(allowCheckExistenceExpression);
 
     if (!shouldCheckModifyTablePrivilege(secondaryExpression, 
secondaryExpressionCondition)) {
       return;
@@ -65,6 +74,23 @@ public class LoadTableAuthorizationExecutor extends 
CommonAuthorizerExecutor {
     }
   }
 
+  @Override
+  public boolean execute(AuthorizationRequestContext 
authorizationRequestContext) throws Exception {
+    if (super.execute(authorizationRequestContext)) {
+      return true;
+    }
+
+    if (allowCheckExistenceEvaluator == null
+        || !allowCheckExistenceEvaluator.evaluate(
+            metadataContext, pathParams, authorizationRequestContext, 
entityType)) {
+      return false;
+    }
+
+    NameIdentifier tableIdentifier = 
metadataContext.get(Entity.EntityType.TABLE);
+    return tableIdentifier != null
+        && 
!GravitinoEnv.getInstance().tableDispatcher().tableExists(tableIdentifier);
+  }
+
   private static boolean shouldCheckModifyTablePrivilege(
       String secondaryExpression, ExpressionCondition 
secondaryExpressionCondition) {
     return StringUtils.isNotBlank(secondaryExpression)
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TableOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/TableOperations.java
index fa89ee0259..b44f7b1408 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TableOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/TableOperations.java
@@ -99,7 +99,7 @@ public class TableOperations {
             idents =
                 MetadataAuthzHelper.filterByExpression(
                     metalake,
-                    
AuthorizationExpressionConstants.FILTER_TABLE_AUTHORIZATION_EXPRESSION,
+                    
AuthorizationExpressionConstants.LIST_TABLE_LIKE_AUTHORIZATION_EXPRESSION,
                     Entity.EntityType.TABLE,
                     idents);
             Response response = Utils.ok(new EntityListResponse(idents));
@@ -167,6 +167,8 @@ public class TableOperations {
   @ResponseMetered(name = "load-table", absolute = true)
   @AuthorizationExpression(
       expression = 
AuthorizationExpressionConstants.LOAD_TABLE_AUTHORIZATION_EXPRESSION,
+      allowCheckExistence =
+          
AuthorizationExpressionConstants.PROBE_TABLE_LIKE_AUTHORIZATION_EXPRESSION,
       secondaryExpression = 
AuthorizationExpressionConstants.MODIFY_TABLE_AUTHORIZATION_EXPRESSION,
       secondaryExpressionCondition = 
ExpressionCondition.REQUIRED_MODIFY_PRIVILEGES,
       accessMetadataType = MetadataObject.Type.TABLE)
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
 
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
index 5e9f60ac36..e3548d857c 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
@@ -18,6 +18,8 @@
 package org.apache.gravitino.server.web.filter;
 
 import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.CAN_ACCESS_METADATA_AND_TAG;
+import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.LOAD_TABLE_AUTHORIZATION_EXPRESSION;
+import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.PROBE_TABLE_LIKE_AUTHORIZATION_EXPRESSION;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
@@ -48,6 +50,7 @@ import 
org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.authorization.AuthorizationUtils;
 import org.apache.gravitino.authorization.GravitinoAuthorizer;
 import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.catalog.TableDispatcher;
 import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.exceptions.ForbiddenException;
@@ -515,6 +518,99 @@ public class TestGravitinoInterceptionService {
     }
   }
 
+  @Test
+  public void 
testLoadTableAuthorizationProceedsWhenProbeAllowedTableDoesNotExist()
+      throws Throwable {
+    try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);
+        MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+            mockStatic(GravitinoAuthorizerProvider.class);
+        MockedStatic<AuthorizationUtils> authUtilsMocked = 
mockStatic(AuthorizationUtils.class);
+        MockedStatic<GravitinoEnv> envMocked = mockStatic(GravitinoEnv.class)) 
{
+      principalUtilsMocked
+          .when(PrincipalUtils::getCurrentPrincipal)
+          .thenReturn(new UserPrincipal("tester"));
+      
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+
+      authUtilsMocked
+          .when(
+              () ->
+                  AuthorizationUtils.checkCurrentUser(
+                      ArgumentMatchers.any(), ArgumentMatchers.any(), 
ArgumentMatchers.any()))
+          .thenAnswer(invocation -> null);
+
+      GravitinoAuthorizerProvider mockedProvider = 
mock(GravitinoAuthorizerProvider.class);
+      GravitinoAuthorizer authorizer = tableProbeAuthorizer();
+      
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
+      when(mockedProvider.getGravitinoAuthorizer()).thenReturn(authorizer);
+
+      GravitinoEnv mockEnv = mock(GravitinoEnv.class);
+      TableDispatcher tableDispatcher = mock(TableDispatcher.class);
+      EventBus mockEventBus = mock(EventBus.class);
+      envMocked.when(GravitinoEnv::getInstance).thenReturn(mockEnv);
+      when(mockEnv.tableDispatcher()).thenReturn(tableDispatcher);
+      when(mockEnv.eventBus()).thenReturn(mockEventBus);
+      
when(tableDispatcher.tableExists(ArgumentMatchers.any())).thenReturn(false);
+
+      MethodInterceptor interceptor = tableLoadInterceptor();
+      MethodInvocation invocation = tableLoadInvocation();
+      Response notFound = Utils.notFound("NoSuchTableException", "Table does 
not exist");
+      when(invocation.proceed()).thenReturn(notFound);
+
+      Response response = (Response) interceptor.invoke(invocation);
+
+      assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response.getStatus());
+      verify(invocation).proceed();
+      verify(tableDispatcher).tableExists(ArgumentMatchers.any());
+      verify(mockEventBus, never()).dispatchEvent(ArgumentMatchers.any());
+    }
+  }
+
+  @Test
+  public void 
testLoadTableAuthorizationReturnsForbiddenWhenProbeAllowedTableExists()
+      throws Throwable {
+    try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);
+        MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+            mockStatic(GravitinoAuthorizerProvider.class);
+        MockedStatic<AuthorizationUtils> authUtilsMocked = 
mockStatic(AuthorizationUtils.class);
+        MockedStatic<GravitinoEnv> envMocked = mockStatic(GravitinoEnv.class)) 
{
+      principalUtilsMocked
+          .when(PrincipalUtils::getCurrentPrincipal)
+          .thenReturn(new UserPrincipal("tester"));
+      
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+
+      authUtilsMocked
+          .when(
+              () ->
+                  AuthorizationUtils.checkCurrentUser(
+                      ArgumentMatchers.any(), ArgumentMatchers.any(), 
ArgumentMatchers.any()))
+          .thenAnswer(invocation -> null);
+
+      GravitinoAuthorizerProvider mockedProvider = 
mock(GravitinoAuthorizerProvider.class);
+      GravitinoAuthorizer authorizer = tableProbeAuthorizer();
+      
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
+      when(mockedProvider.getGravitinoAuthorizer()).thenReturn(authorizer);
+
+      GravitinoEnv mockEnv = mock(GravitinoEnv.class);
+      TableDispatcher tableDispatcher = mock(TableDispatcher.class);
+      EventBus mockEventBus = spy(new EventBus(Collections.emptyList()));
+      envMocked.when(GravitinoEnv::getInstance).thenReturn(mockEnv);
+      when(mockEnv.tableDispatcher()).thenReturn(tableDispatcher);
+      when(mockEnv.eventBus()).thenReturn(mockEventBus);
+      
when(tableDispatcher.tableExists(ArgumentMatchers.any())).thenReturn(true);
+
+      MethodInterceptor interceptor = tableLoadInterceptor();
+      MethodInvocation invocation = tableLoadInvocation();
+
+      Response response = (Response) interceptor.invoke(invocation);
+
+      assertEquals(Response.Status.FORBIDDEN.getStatusCode(), 
response.getStatus());
+      verify(invocation, never()).proceed();
+      verify(tableDispatcher).tableExists(ArgumentMatchers.any());
+      verify(mockEventBus)
+          
.dispatchEvent(ArgumentMatchers.any(AuthorizationDenialFailureEvent.class));
+    }
+  }
+
   /**
    * When {@code checkCurrentUser} throws {@link NoSuchMetalakeException}, the 
403 is a
    * resource-not-found masquerading as forbidden — no {@link 
AuthorizationDenialFailureEvent} is
@@ -582,6 +678,23 @@ public class TestGravitinoInterceptionService {
     }
   }
 
+  public static class TestTableLoadOperations {
+
+    @AuthorizationExpression(
+        expression = LOAD_TABLE_AUTHORIZATION_EXPRESSION,
+        allowCheckExistence = PROBE_TABLE_LIKE_AUTHORIZATION_EXPRESSION,
+        accessMetadataType = MetadataObject.Type.TABLE)
+    public Response loadTable(
+        @AuthorizationMetadata(type = Entity.EntityType.METALAKE) String 
metalake,
+        @AuthorizationMetadata(type = Entity.EntityType.CATALOG) String 
catalog,
+        @AuthorizationMetadata(type = Entity.EntityType.SCHEMA) String schema,
+        @AuthorizationMetadata(type = Entity.EntityType.TABLE) String table,
+        @AuthorizationRequest(type = 
AuthorizationRequest.RequestType.LOAD_TABLE)
+            String requiredPrivileges) {
+      return Utils.ok("unused");
+    }
+  }
+
   public static class TestOperations {
 
     @AuthorizationExpression(
@@ -602,6 +715,60 @@ public class TestGravitinoInterceptionService {
     }
   }
 
+  private MethodInterceptor tableLoadInterceptor() throws 
NoSuchMethodException {
+    Method method =
+        TestTableLoadOperations.class.getMethod(
+            "loadTable", String.class, String.class, String.class, 
String.class, String.class);
+    return new 
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+  }
+
+  private MethodInvocation tableLoadInvocation() throws NoSuchMethodException {
+    Method method =
+        TestTableLoadOperations.class.getMethod(
+            "loadTable", String.class, String.class, String.class, 
String.class, String.class);
+    MethodInvocation invocation = mock(MethodInvocation.class);
+    when(invocation.getMethod()).thenReturn(method);
+    when(invocation.getArguments())
+        .thenReturn(
+            new Object[] {"testMetalake", "testCatalog", "testSchema", 
"missingTable", null});
+    return invocation;
+  }
+
+  private GravitinoAuthorizer tableProbeAuthorizer() {
+    GravitinoAuthorizer authorizer = mock(GravitinoAuthorizer.class);
+    when(authorizer.authorize(
+            ArgumentMatchers.any(),
+            ArgumentMatchers.eq("testMetalake"),
+            ArgumentMatchers.argThat(
+                metadataObject ->
+                    metadataObject.type() == MetadataObject.Type.CATALOG
+                        && "testCatalog".equals(metadataObject.name())),
+            ArgumentMatchers.eq(Privilege.Name.USE_CATALOG),
+            ArgumentMatchers.any()))
+        .thenReturn(true);
+    when(authorizer.authorize(
+            ArgumentMatchers.any(),
+            ArgumentMatchers.eq("testMetalake"),
+            ArgumentMatchers.argThat(
+                metadataObject ->
+                    metadataObject.type() == MetadataObject.Type.SCHEMA
+                        && "testSchema".equals(metadataObject.name())),
+            ArgumentMatchers.eq(Privilege.Name.USE_SCHEMA),
+            ArgumentMatchers.any()))
+        .thenReturn(true);
+    when(authorizer.authorize(
+            ArgumentMatchers.any(),
+            ArgumentMatchers.eq("testMetalake"),
+            ArgumentMatchers.argThat(
+                metadataObject ->
+                    metadataObject.type() == MetadataObject.Type.METALAKE
+                        && "testMetalake".equals(metadataObject.name())),
+            ArgumentMatchers.eq(Privilege.Name.PROBE_TABLE_LIKE),
+            ArgumentMatchers.any()))
+        .thenReturn(true);
+    return authorizer;
+  }
+
   private static class MockGravitinoAuthorizer implements GravitinoAuthorizer {
 
     @Override
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
 
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
index fcd0f273e7..15bc76ce8f 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
@@ -74,7 +74,8 @@ public class TestLoadTableAuthorizationExecutor {
         Collections.emptyMap(),
         Optional.empty(),
         SECONDARY_EXPRESSION,
-        condition);
+        condition,
+        "");
   }
 
   private static String expression(LoadTableAuthorizationExecutor executor) 
throws Exception {
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestTableAuthorizationExpression.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestTableAuthorizationExpression.java
index f115695416..779fe6062f 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestTableAuthorizationExpression.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestTableAuthorizationExpression.java
@@ -82,47 +82,59 @@ public class TestTableAuthorizationExpression {
   }
 
   @Test
-  public void testListTable() throws IllegalAccessException, OgnlException, 
NoSuchFieldException {
-    Field loadTableAuthorizationExpressionField =
+  public void testListTable() throws NoSuchMethodException, OgnlException {
+    Method method =
+        TableOperations.class.getMethod("listTables", String.class, 
String.class, String.class);
+    AuthorizationExpression authorizationExpressionAnnotation =
+        method.getAnnotation(AuthorizationExpression.class);
+    String expression = authorizationExpressionAnnotation.expression();
+    MockAuthorizationExpressionEvaluator mockEvaluator =
+        new MockAuthorizationExpressionEvaluator(expression);
+    assertFalse(mockEvaluator.getResult(ImmutableSet.of()));
+    assertTrue(mockEvaluator.getResult(ImmutableSet.of("METALAKE::OWNER")));
+    assertTrue(mockEvaluator.getResult(ImmutableSet.of("CATALOG::OWNER")));
+    assertTrue(
+        mockEvaluator.getResult(ImmutableSet.of("SCHEMA::USE_SCHEMA", 
"CATALOG::USE_CATALOG")));
+  }
+
+  @Test
+  public void testListTableFilter()
+      throws IllegalAccessException, OgnlException, NoSuchFieldException {
+    Field listTableLikeAuthorizationExpressionField =
         AuthorizationExpressionConstants.class.getDeclaredField(
-            "LOAD_TABLE_AUTHORIZATION_EXPRESSION");
-    loadTableAuthorizationExpressionField.setAccessible(true);
-    String loadTableAuthExpression = (String) 
loadTableAuthorizationExpressionField.get(null);
+            "LIST_TABLE_LIKE_AUTHORIZATION_EXPRESSION");
+    listTableLikeAuthorizationExpressionField.setAccessible(true);
+    String expression = (String) 
listTableLikeAuthorizationExpressionField.get(null);
     MockAuthorizationExpressionEvaluator mockEvaluator =
-        new MockAuthorizationExpressionEvaluator(loadTableAuthExpression);
+        new MockAuthorizationExpressionEvaluator(expression);
     assertFalse(mockEvaluator.getResult(ImmutableSet.of()));
     assertTrue(mockEvaluator.getResult(ImmutableSet.of("METALAKE::OWNER")));
     assertTrue(mockEvaluator.getResult(ImmutableSet.of("CATALOG::OWNER")));
-    assertFalse(mockEvaluator.getResult(ImmutableSet.of("SCHEMA::OWNER")));
-    assertTrue(mockEvaluator.getResult(ImmutableSet.of("SCHEMA::OWNER", 
"CATALOG::USE_CATALOG")));
-    assertTrue(mockEvaluator.getResult(ImmutableSet.of("SCHEMA::OWNER", 
"METALAKE::USE_CATALOG")));
-    
assertFalse(mockEvaluator.getResult(ImmutableSet.of("SCHEMA::CREATE_TABLE")));
-    assertFalse(
-        mockEvaluator.getResult(ImmutableSet.of("SCHEMA::CREATE_TABLE", 
"SCHEMA::USE_SCHEMA")));
+    assertTrue(mockEvaluator.getResult(ImmutableSet.of("SCHEMA::OWNER")));
+    assertTrue(mockEvaluator.getResult(ImmutableSet.of("TABLE::OWNER")));
     assertFalse(
         mockEvaluator.getResult(
-            ImmutableSet.of("SCHEMA::CREATE_TABLE", "SCHEMA::USE_SCHEMA", 
"CATALOG::USE_CATALOG")));
-    
assertFalse(mockEvaluator.getResult(ImmutableSet.of("SCHEMA::SELECT_TABLE")));
-    assertFalse(
-        mockEvaluator.getResult(ImmutableSet.of("SCHEMA::SELECT_TABLE", 
"SCHEMA::USE_SCHEMA")));
+            ImmutableSet.of(
+                "METALAKE::PROBE_TABLE_LIKE",
+                "CATALOG::USE_CATALOG",
+                "SCHEMA::USE_SCHEMA",
+                "CATALOG::DENY_PROBE_TABLE_LIKE")));
+    assertTrue(
+        mockEvaluator.getResult(
+            ImmutableSet.of(
+                "METALAKE::PROBE_TABLE_LIKE", "CATALOG::USE_CATALOG", 
"SCHEMA::USE_SCHEMA")));
     assertTrue(
         mockEvaluator.getResult(
             ImmutableSet.of("SCHEMA::SELECT_TABLE", "SCHEMA::USE_SCHEMA", 
"CATALOG::USE_CATALOG")));
     assertTrue(
         mockEvaluator.getResult(
-            ImmutableSet.of(
-                "CATALOG::SELECT_TABLE", "CATALOG::USE_SCHEMA", 
"CATALOG::USE_CATALOG")));
+            ImmutableSet.of("SCHEMA::MODIFY_TABLE", "CATALOG::USE_CATALOG", 
"SCHEMA::USE_SCHEMA")));
     assertTrue(
         mockEvaluator.getResult(
-            ImmutableSet.of(
-                "METALAKE::SELECT_TABLE", "METALAKE::USE_SCHEMA", 
"METALAKE::USE_CATALOG")));
-    assertFalse(
+            ImmutableSet.of("SCHEMA::CREATE_TABLE", "CATALOG::USE_CATALOG", 
"SCHEMA::USE_SCHEMA")));
+    assertTrue(
         mockEvaluator.getResult(
-            ImmutableSet.of(
-                "METALAKE::SELECT_TABLE",
-                "CATALOG::DENY_SELECT_TABLE",
-                "METALAKE::USE_SCHEMA",
-                "METALAKE::USE_CATALOG")));
+            ImmutableSet.of("SCHEMA::CREATE_VIEW", "CATALOG::USE_CATALOG", 
"SCHEMA::USE_SCHEMA")));
   }
 
   @Test
@@ -170,6 +182,40 @@ public class TestTableAuthorizationExpression {
                 "METALAKE::USE_CATALOG")));
   }
 
+  @Test
+  public void testLoadTableExistenceProbe() throws NoSuchMethodException, 
OgnlException {
+    Method method =
+        TableOperations.class.getMethod(
+            "loadTable", String.class, String.class, String.class, 
String.class, String.class);
+    AuthorizationExpression authorizationExpressionAnnotation =
+        method.getAnnotation(AuthorizationExpression.class);
+    String expression = 
authorizationExpressionAnnotation.allowCheckExistence();
+    MockAuthorizationExpressionEvaluator mockEvaluator =
+        new MockAuthorizationExpressionEvaluator(expression);
+    assertFalse(mockEvaluator.getResult(ImmutableSet.of()));
+    assertFalse(mockEvaluator.getResult(ImmutableSet.of("METALAKE::OWNER")));
+    assertFalse(mockEvaluator.getResult(ImmutableSet.of("CATALOG::OWNER")));
+    assertFalse(mockEvaluator.getResult(ImmutableSet.of("SCHEMA::OWNER")));
+    assertFalse(mockEvaluator.getResult(ImmutableSet.of("TABLE::OWNER")));
+    assertFalse(
+        mockEvaluator.getResult(
+            ImmutableSet.of(
+                "METALAKE::PROBE_TABLE_LIKE",
+                "CATALOG::USE_CATALOG",
+                "SCHEMA::USE_SCHEMA",
+                "CATALOG::DENY_PROBE_TABLE_LIKE")));
+    assertTrue(
+        mockEvaluator.getResult(
+            ImmutableSet.of(
+                "METALAKE::PROBE_TABLE_LIKE", "CATALOG::USE_CATALOG", 
"SCHEMA::USE_SCHEMA")));
+    assertTrue(
+        mockEvaluator.getResult(
+            ImmutableSet.of("SCHEMA::SELECT_TABLE", "CATALOG::USE_CATALOG", 
"SCHEMA::USE_SCHEMA")));
+    assertTrue(
+        mockEvaluator.getResult(
+            ImmutableSet.of("SCHEMA::MODIFY_TABLE", "CATALOG::USE_CATALOG", 
"SCHEMA::USE_SCHEMA")));
+  }
+
   @Test
   public void testAlterTable() throws NoSuchMethodException, OgnlException {
     Method method =

Reply via email to