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 a2839877d3 [#11172] fix(audit): skip audit events for internal Iceberg 
REST metadata operations    (#11188)
a2839877d3 is described below

commit a2839877d3e833dc0b5ed852043c92504fe602bb
Author: Shane <[email protected]>
AuthorDate: Fri Jun 5 09:44:34 2026 +0800

    [#11172] fix(audit): skip audit events for internal Iceberg REST metadata 
operations    (#11188)
    
    ### What changes were proposed in this pull request?
    This PR prevents internal metadata operations triggered by Iceberg REST
    server and authorization infrastructure from being recorded as
    standalone audit events.
    The main changes are:
    1. Add internal dispatcher accessors in `GravitinoEnv` for catalog,
    schema, table, view, owner, and access control operations.
    These internal dispatchers keep the normalize/operation layer but bypass
    hook and event dispatchers.
    2. Use internal dispatchers for Iceberg REST metadata synchronization:
    - schema import after namespace creation
    - table import after table creation, staged create commit, and table
    registration
    - view import after view creation
    - owner synchronization for schemas, tables, and views
    3. Use internal access control dispatcher for authorization user lookups
    to avoid emitting `GET_USER` audit events during auth checks.
    4. Use internal catalog dispatcher in `DynamicIcebergConfigProvider`
    when running in auxiliary mode, so Iceberg REST catalog config lookups
    do not emit `LOAD_CATALOG` audit events.
    5. Inject schema dispatcher suppliers into table/view operation
    dispatchers so internal table/view imports also use the internal schema
    dispatcher for dependent schema loading.
    
    ### Why are the changes needed?
    Iceberg REST server performs several metadata operations internally as
    part of handling a single user request. For example, after creating a
    namespace/table/view in the underlying Iceberg catalog, Gravitino
    imports the corresponding schema/table/view metadata and
    synchronizes ownership.
    Before this change, those internal operations reused public dispatchers.
    As a result, they emitted audit events such as `LOAD_SCHEMA`,
    `LOAD_TABLE`, `LOAD_VIEW`, `SET_OWNER`, `GET_USER`, and `LOAD_CATALOG`,
    even though users did not directly invoke those operations.
    This caused noisy and misleading audit logs. In particular, dynamic
    Iceberg catalog config lookups could repeatedly produce `LOAD_CATALOG
    ... FAILURE` audit entries for missing catalogs.
    This PR separates internal infrastructure calls from user-facing API
    calls by routing them through internal dispatchers that bypass event and
    hook dispatchers.
    ### Fix
    Fix: #11172
    ### Does this PR introduce any user-facing change?
    No user-facing API or configuration change is introduced.
    The observable behavior change is limited to audit logs: internal
    Iceberg REST metadata synchronization, authorization user checks, and
    auxiliary-mode catalog config lookups are no longer recorded as
    standalone user audit events.
    User-facing API operations are still audited normally.
    ### How was this patch tested?
    Added and updated unit tests to verify internal dispatcher usage and
    prevent regressions:
    1. Iceberg table hook dispatcher tests verify that table import and
    owner synchronization use internal table/owner dispatchers and do not
    call public dispatchers.
    2. Iceberg namespace hook dispatcher tests verify that schema/table
    imports and owner synchronization use internal schema/table/owner
    dispatchers.
    3. Iceberg view hook dispatcher tests verify that view import and owner
    synchronization use internal view/owner dispatchers.
    4. Dynamic Iceberg config provider tests verify that auxiliary-mode
    catalog config lookup uses internal catalog dispatcher and does not call
    public catalog dispatcher.
    5. Authorization tests verify that PassThrough and Jcasbin authorizers
    use internal access control dispatcher for user lookup.
---
 .../java/org/apache/gravitino/GravitinoEnv.java    | 98 +++++++++++++++++++++
 .../catalog/TableOperationDispatcher.java          | 36 +++++++-
 .../gravitino/catalog/ViewOperationDispatcher.java | 35 +++++++-
 .../catalog/TestTableOperationDispatcher.java      | 38 +++++++++
 .../catalog/TestViewOperationDispatcher.java       | 36 ++++++++
 .../dispatcher/IcebergNamespaceHookDispatcher.java |  8 +-
 .../dispatcher/IcebergTableHookDispatcher.java     |  4 +-
 .../dispatcher/IcebergViewHookDispatcher.java      |  4 +-
 .../provider/DynamicIcebergConfigProvider.java     |  4 +-
 .../TestIcebergNamespaceHookDispatcher.java        | 99 +++++++++++++++++++---
 .../dispatcher/TestIcebergTableHookDispatcher.java | 69 ++++++++++++---
 .../dispatcher/TestIcebergViewHookDispatcher.java  | 41 +++++++--
 .../provider/TestDynamicIcebergConfigProvider.java | 64 ++++++++++----
 .../authorization/PassThroughAuthorizer.java       |  3 +-
 .../authorization/TestPassThroughAuthorizer.java   | 35 ++++++++
 .../jcasbin/TestJcasbinAuthorizer.java             |  6 ++
 16 files changed, 513 insertions(+), 67 deletions(-)

diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java 
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index df80fad2ca..c845c43c49 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -120,14 +120,17 @@ public class GravitinoEnv {
   private EntityStore entityStore;
 
   private CatalogDispatcher catalogDispatcher;
+  private CatalogDispatcher internalCatalogDispatcher;
 
   private CatalogManager catalogManager;
 
   private MetalakeManager metalakeManager;
 
   private SchemaDispatcher schemaDispatcher;
+  private SchemaDispatcher internalSchemaDispatcher;
 
   private TableDispatcher tableDispatcher;
+  private TableDispatcher internalTableDispatcher;
 
   private PartitionDispatcher partitionDispatcher;
 
@@ -140,6 +143,7 @@ public class GravitinoEnv {
   private FunctionDispatcher functionDispatcher;
 
   private ViewDispatcher viewDispatcher;
+  private ViewDispatcher internalViewDispatcher;
 
   private MetalakeDispatcher metalakeDispatcher;
 
@@ -150,6 +154,7 @@ public class GravitinoEnv {
   private PolicyDispatcher policyDispatcher;
 
   private AccessControlDispatcher accessControlDispatcher;
+  private AccessControlDispatcher internalAccessControlDispatcher;
 
   private IdGenerator idGenerator;
 
@@ -167,6 +172,7 @@ public class GravitinoEnv {
 
   private EventBus eventBus;
   private OwnerDispatcher ownerDispatcher;
+  private OwnerDispatcher internalOwnerDispatcher;
   private FutureGrantManager futureGrantManager;
   private GravitinoAuthorizer gravitinoAuthorizer;
   private StatisticDispatcher statisticDispatcher;
@@ -241,6 +247,19 @@ public class GravitinoEnv {
     return catalogDispatcher;
   }
 
+  /**
+   * Get the internal CatalogDispatcher associated with the Gravitino 
environment.
+   *
+   * <p>The internal dispatcher preserves normalization but skips hooks and 
event emission. It is
+   * intended for infrastructure catalog lookups that should not be recorded 
as user API audit
+   * events.
+   *
+   * @return The internal CatalogDispatcher instance.
+   */
+  public CatalogDispatcher internalCatalogDispatcher() {
+    return internalCatalogDispatcher;
+  }
+
   /**
    * Get the SchemaDispatcher associated with the Gravitino environment.
    *
@@ -250,6 +269,19 @@ public class GravitinoEnv {
     return schemaDispatcher;
   }
 
+  /**
+   * Get the internal SchemaDispatcher associated with the Gravitino 
environment.
+   *
+   * <p>The internal dispatcher preserves normalization but skips hooks and 
event emission. It is
+   * intended for infrastructure code that synchronizes metadata as part of 
another user-visible
+   * operation.
+   *
+   * @return The internal SchemaDispatcher instance.
+   */
+  public SchemaDispatcher internalSchemaDispatcher() {
+    return internalSchemaDispatcher;
+  }
+
   /**
    * Get the TableDispatcher associated with the Gravitino environment.
    *
@@ -259,6 +291,19 @@ public class GravitinoEnv {
     return tableDispatcher;
   }
 
+  /**
+   * Get the internal TableDispatcher associated with the Gravitino 
environment.
+   *
+   * <p>The internal dispatcher preserves normalization but skips hooks and 
event emission. It is
+   * intended for infrastructure code that synchronizes metadata as part of 
another user-visible
+   * operation.
+   *
+   * @return The internal TableDispatcher instance.
+   */
+  public TableDispatcher internalTableDispatcher() {
+    return internalTableDispatcher;
+  }
+
   /**
    * Get the ModelDispatcher associated with the Gravitino environment.
    *
@@ -286,6 +331,19 @@ public class GravitinoEnv {
     return viewDispatcher;
   }
 
+  /**
+   * Get the internal ViewDispatcher associated with the Gravitino environment.
+   *
+   * <p>The internal dispatcher preserves normalization but skips hook/event 
side effects from
+   * dependent schema lookups. It is intended for infrastructure code that 
synchronizes metadata as
+   * part of another user-visible operation.
+   *
+   * @return The internal ViewDispatcher instance.
+   */
+  public ViewDispatcher internalViewDispatcher() {
+    return internalViewDispatcher;
+  }
+
   /**
    * * Get the PartitionDispatcher associated with the Gravitino environment.
    *
@@ -382,6 +440,18 @@ public class GravitinoEnv {
     return accessControlDispatcher;
   }
 
+  /**
+   * Get the internal AccessControlDispatcher associated with the Gravitino 
environment.
+   *
+   * <p>The internal dispatcher skips hooks and event emission. It is intended 
for authorization
+   * infrastructure lookups that should not be recorded as user API audit 
events.
+   *
+   * @return The internal AccessControlDispatcher instance.
+   */
+  public AccessControlDispatcher internalAccessControlDispatcher() {
+    return internalAccessControlDispatcher;
+  }
+
   /**
    * Get the tagDispatcher associated with the Gravitino environment.
    *
@@ -409,6 +479,18 @@ public class GravitinoEnv {
     return ownerDispatcher;
   }
 
+  /**
+   * Get the internal OwnerDispatcher associated with the Gravitino 
environment.
+   *
+   * <p>The internal dispatcher skips event emission. It is intended for 
infrastructure ownership
+   * synchronization that happens as part of another user-visible operation.
+   *
+   * @return The internal OwnerDispatcher instance.
+   */
+  public OwnerDispatcher internalOwnerDispatcher() {
+    return internalOwnerDispatcher;
+  }
+
   /**
    * Get the FutureGrantManager associated with the Gravitino environment.
    *
@@ -569,6 +651,7 @@ public class GravitinoEnv {
     this.catalogManager = new CatalogManager(config, entityStore, idGenerator);
     CatalogNormalizeDispatcher catalogNormalizeDispatcher =
         new CatalogNormalizeDispatcher(catalogManager);
+    this.internalCatalogDispatcher = catalogNormalizeDispatcher;
     CatalogEventDispatcher catalogEventDispatcher =
         new CatalogEventDispatcher(eventBus, catalogNormalizeDispatcher);
     this.catalogDispatcher = new CatalogHookDispatcher(catalogEventDispatcher);
@@ -580,6 +663,7 @@ public class GravitinoEnv {
         new SchemaOperationDispatcher(catalogManager, entityStore, 
idGenerator);
     SchemaNormalizeDispatcher schemaNormalizeDispatcher =
         new SchemaNormalizeDispatcher(schemaOperationDispatcher, 
catalogManager);
+    this.internalSchemaDispatcher = schemaNormalizeDispatcher;
     SchemaEventDispatcher schemaEventDispatcher =
         new SchemaEventDispatcher(eventBus, schemaNormalizeDispatcher);
     this.schemaDispatcher = new SchemaHookDispatcher(schemaEventDispatcher);
@@ -588,6 +672,11 @@ public class GravitinoEnv {
         new TableOperationDispatcher(catalogManager, entityStore, idGenerator);
     TableNormalizeDispatcher tableNormalizeDispatcher =
         new TableNormalizeDispatcher(tableOperationDispatcher, catalogManager);
+    TableOperationDispatcher internalTableOperationDispatcher =
+        new TableOperationDispatcher(
+            catalogManager, entityStore, idGenerator, () -> 
internalSchemaDispatcher);
+    this.internalTableDispatcher =
+        new TableNormalizeDispatcher(internalTableOperationDispatcher, 
catalogManager);
     TableEventDispatcher tableEventDispatcher =
         new TableEventDispatcher(eventBus, tableNormalizeDispatcher);
     this.tableDispatcher = new TableHookDispatcher(tableEventDispatcher);
@@ -644,6 +733,11 @@ public class GravitinoEnv {
         new ViewOperationDispatcher(catalogManager, entityStore, idGenerator);
     ViewNormalizeDispatcher viewNormalizeDispatcher =
         new ViewNormalizeDispatcher(viewOperationDispatcher, catalogManager);
+    ViewOperationDispatcher internalViewOperationDispatcher =
+        new ViewOperationDispatcher(
+            catalogManager, entityStore, idGenerator, () -> 
internalSchemaDispatcher);
+    this.internalViewDispatcher =
+        new ViewNormalizeDispatcher(internalViewOperationDispatcher, 
catalogManager);
     ViewEventDispatcher viewEventDispatcher =
         new ViewEventDispatcher(eventBus, viewNormalizeDispatcher);
     this.viewDispatcher = viewEventDispatcher;
@@ -657,15 +751,19 @@ public class GravitinoEnv {
     if (enableAuthorization) {
       AccessControlManager accessControlManager =
           new AccessControlManager(entityStore, idGenerator, config);
+      this.internalAccessControlDispatcher = accessControlManager;
       AccessControlEventDispatcher accessControlEventDispatcher =
           new AccessControlEventDispatcher(eventBus, accessControlManager);
       this.accessControlDispatcher = new 
AccessControlHookDispatcher(accessControlEventDispatcher);
       OwnerDispatcher ownerManager = new OwnerManager(entityStore);
+      this.internalOwnerDispatcher = ownerManager;
       this.ownerDispatcher = new OwnerEventManager(eventBus, ownerManager);
       this.futureGrantManager = new FutureGrantManager(entityStore, 
ownerManager);
     } else {
       this.accessControlDispatcher = null;
+      this.internalAccessControlDispatcher = null;
       this.ownerDispatcher = null;
+      this.internalOwnerDispatcher = null;
       this.futureGrantManager = null;
     }
 
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
index 244edec88d..34677f3e31 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
@@ -26,6 +26,7 @@ import static 
org.apache.gravitino.utils.NameIdentifierUtil.getCatalogIdentifier
 import static 
org.apache.gravitino.utils.NameIdentifierUtil.getSchemaIdentifier;
 
 import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import java.time.Instant;
 import java.util.Arrays;
@@ -33,6 +34,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.apache.commons.lang3.tuple.Pair;
@@ -72,6 +74,8 @@ public class TableOperationDispatcher extends 
OperationDispatcher implements Tab
 
   private static final Logger LOG = 
LoggerFactory.getLogger(TableOperationDispatcher.class);
 
+  private final Supplier<SchemaDispatcher> schemaDispatcherSupplier;
+
   /**
    * Creates a new TableOperationDispatcher instance.
    *
@@ -81,7 +85,26 @@ public class TableOperationDispatcher extends 
OperationDispatcher implements Tab
    */
   public TableOperationDispatcher(
       CatalogManager catalogManager, EntityStore store, IdGenerator 
idGenerator) {
+    this(catalogManager, store, idGenerator, () -> 
GravitinoEnv.getInstance().schemaDispatcher());
+  }
+
+  /**
+   * Creates a new TableOperationDispatcher instance.
+   *
+   * @param catalogManager The CatalogManager instance to be used for table 
operations.
+   * @param store The EntityStore instance to be used for table operations.
+   * @param idGenerator The IdGenerator instance to be used for table 
operations.
+   * @param schemaDispatcherSupplier The SchemaDispatcher supplier to ensure 
schemas are imported.
+   */
+  public TableOperationDispatcher(
+      CatalogManager catalogManager,
+      EntityStore store,
+      IdGenerator idGenerator,
+      Supplier<SchemaDispatcher> schemaDispatcherSupplier) {
     super(catalogManager, store, idGenerator);
+    this.schemaDispatcherSupplier =
+        Preconditions.checkNotNull(
+            schemaDispatcherSupplier, "schemaDispatcherSupplier must not be 
null");
   }
 
   /**
@@ -118,7 +141,7 @@ public class TableOperationDispatcher extends 
OperationDispatcher implements Tab
 
     if (!entityCombinedTable.imported()) {
       // Load the schema to make sure the schema is imported.
-      SchemaDispatcher schemaDispatcher = 
GravitinoEnv.getInstance().schemaDispatcher();
+      SchemaDispatcher schemaDispatcher = getSchemaDispatcher();
       NameIdentifier schemaIdent = 
NameIdentifier.of(ident.namespace().levels());
       schemaDispatcher.loadSchema(schemaIdent);
 
@@ -180,7 +203,7 @@ public class TableOperationDispatcher extends 
OperationDispatcher implements Tab
       throws NoSuchSchemaException, TableAlreadyExistsException {
 
     // Load the schema to make sure the schema exists.
-    SchemaDispatcher schemaDispatcher = 
GravitinoEnv.getInstance().schemaDispatcher();
+    SchemaDispatcher schemaDispatcher = getSchemaDispatcher();
     NameIdentifier schemaIdent = NameIdentifier.of(ident.namespace().levels());
     schemaDispatcher.loadSchema(schemaIdent);
 
@@ -505,6 +528,15 @@ public class TableOperationDispatcher extends 
OperationDispatcher implements Tab
                 table.tableFromCatalog().properties()));
   }
 
+  private SchemaDispatcher getSchemaDispatcher() {
+    SchemaDispatcher schemaDispatcher = schemaDispatcherSupplier.get();
+    Preconditions.checkArgument(
+        schemaDispatcher != null,
+        "schemaDispatcherSupplier returned null. "
+            + "SchemaDispatcher must be available for table operations.");
+    return schemaDispatcher;
+  }
+
   private EntityCombinedTable internalLoadTable(NameIdentifier ident) {
     NameIdentifier catalogIdentifier = getCatalogIdentifier(ident);
     Table table =
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
index a7b9ab1416..5b3937ee48 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
@@ -29,6 +29,7 @@ import java.time.Instant;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Supplier;
 import javax.annotation.Nullable;
 import org.apache.gravitino.EntityAlreadyExistsException;
 import org.apache.gravitino.EntityStore;
@@ -60,6 +61,8 @@ public class ViewOperationDispatcher extends 
OperationDispatcher implements View
 
   private static final Logger LOG = 
LoggerFactory.getLogger(ViewOperationDispatcher.class);
 
+  private final Supplier<SchemaDispatcher> schemaDispatcherSupplier;
+
   /**
    * Creates a new ViewOperationDispatcher instance.
    *
@@ -69,7 +72,26 @@ public class ViewOperationDispatcher extends 
OperationDispatcher implements View
    */
   public ViewOperationDispatcher(
       CatalogManager catalogManager, EntityStore store, IdGenerator 
idGenerator) {
+    this(catalogManager, store, idGenerator, () -> 
GravitinoEnv.getInstance().schemaDispatcher());
+  }
+
+  /**
+   * Creates a new ViewOperationDispatcher instance.
+   *
+   * @param catalogManager The CatalogManager instance to be used for view 
operations.
+   * @param store The EntityStore instance to be used for view operations.
+   * @param idGenerator The IdGenerator instance to be used for view 
operations.
+   * @param schemaDispatcherSupplier The SchemaDispatcher supplier to ensure 
schemas are imported.
+   */
+  public ViewOperationDispatcher(
+      CatalogManager catalogManager,
+      EntityStore store,
+      IdGenerator idGenerator,
+      Supplier<SchemaDispatcher> schemaDispatcherSupplier) {
     super(catalogManager, store, idGenerator);
+    this.schemaDispatcherSupplier =
+        Preconditions.checkNotNull(
+            schemaDispatcherSupplier, "schemaDispatcherSupplier must not be 
null");
   }
 
   /**
@@ -107,7 +129,7 @@ public class ViewOperationDispatcher extends 
OperationDispatcher implements View
         TreeLockUtils.doWithTreeLock(ident, LockType.READ, () -> 
internalLoadView(ident));
 
     if (!entityCombinedView.imported()) {
-      SchemaDispatcher schemaDispatcher = 
GravitinoEnv.getInstance().schemaDispatcher();
+      SchemaDispatcher schemaDispatcher = getSchemaDispatcher();
       NameIdentifier schemaIdent = 
NameIdentifier.of(ident.namespace().levels());
       schemaDispatcher.loadSchema(schemaIdent);
 
@@ -148,7 +170,7 @@ public class ViewOperationDispatcher extends 
OperationDispatcher implements View
         "representations must not be null or empty");
 
     // Load the schema to make sure the schema exists.
-    SchemaDispatcher schemaDispatcher = 
GravitinoEnv.getInstance().schemaDispatcher();
+    SchemaDispatcher schemaDispatcher = getSchemaDispatcher();
     NameIdentifier schemaIdent = NameIdentifier.of(ident.namespace().levels());
     schemaDispatcher.loadSchema(schemaIdent);
 
@@ -527,6 +549,15 @@ public class ViewOperationDispatcher extends 
OperationDispatcher implements View
         .withImported(true);
   }
 
+  private SchemaDispatcher getSchemaDispatcher() {
+    SchemaDispatcher schemaDispatcher = schemaDispatcherSupplier.get();
+    Preconditions.checkArgument(
+        schemaDispatcher != null,
+        "schemaDispatcherSupplier returned null. "
+            + "SchemaDispatcher must be available for view operations.");
+    return schemaDispatcher;
+  }
+
   private ViewEntity applyChangesToEntity(
       ViewEntity current, View alteredView, ViewChange[] changes) {
     String name = alteredView.name();
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
index 9c44e46c7e..10b21dc41d 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
@@ -43,6 +43,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.apache.commons.lang3.reflect.FieldUtils;
@@ -258,6 +259,43 @@ public class TestTableOperationDispatcher extends 
TestOperationDispatcher {
     Assertions.assertEquals("test", loadedTable4.auditInfo().creator());
   }
 
+  @Test
+  public void 
testTableOperationDispatcherRejectsNullSchemaDispatcherSupplier() {
+    Assertions.assertThrows(
+        NullPointerException.class,
+        () -> new TableOperationDispatcher(catalogManager, entityStore, 
idGenerator, null));
+  }
+
+  @Test
+  public void 
testCreateTableFailsFastWhenSchemaDispatcherSupplierReturnsNull() throws 
IOException {
+    Namespace tableNs = Namespace.of(metalake, catalog, 
"schema-null-dispatcher");
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    
schemaOperationDispatcher.createSchema(NameIdentifier.of(tableNs.levels()), 
"comment", props);
+
+    Supplier<SchemaDispatcher> nullSchemaDispatcherSupplier = () -> null;
+    TableOperationDispatcher dispatcher =
+        new TableOperationDispatcher(
+            catalogManager, entityStore, idGenerator, 
nullSchemaDispatcherSupplier);
+    NameIdentifier tableIdent = NameIdentifier.of(tableNs, 
"table_null_dispatcher");
+    Column[] columns =
+        new Column[] {
+          TestColumn.builder()
+              .withName("col1")
+              .withPosition(0)
+              .withType(Types.StringType.get())
+              .build()
+        };
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () -> dispatcher.createTable(tableIdent, columns, "comment", 
props, new Transform[0]));
+    Assertions.assertEquals(
+        "schemaDispatcherSupplier returned null. "
+            + "SchemaDispatcher must be available for table operations.",
+        exception.getMessage());
+  }
+
   @Test
   public void testConcurrentImportTableReusesExistingEntity() throws 
IOException {
     Namespace tableNs = Namespace.of(metalake, catalog, "schema52");
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
index a3c4146e8a..5f28bcd20e 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
@@ -40,6 +40,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Entity;
@@ -198,6 +199,41 @@ public class TestViewOperationDispatcher extends 
TestOperationDispatcher {
     }
   }
 
+  @Test
+  public void testViewOperationDispatcherRejectsNullSchemaDispatcherSupplier() 
{
+    Assertions.assertThrows(
+        NullPointerException.class,
+        () -> new ViewOperationDispatcher(catalogManager, entityStore, 
idGenerator, null));
+  }
+
+  @Test
+  public void testCreateViewFailsFastWhenSchemaDispatcherSupplierReturnsNull() 
throws IOException {
+    Namespace viewNs = Namespace.of(metalake, catalog, 
"schema-null-view-dispatcher");
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    schemaOperationDispatcher.createSchema(NameIdentifier.of(viewNs.levels()), 
"comment", props);
+
+    Supplier<SchemaDispatcher> nullSchemaDispatcherSupplier = () -> null;
+    ViewOperationDispatcher dispatcher =
+        new ViewOperationDispatcher(
+            catalogManager, entityStore, idGenerator, 
nullSchemaDispatcherSupplier);
+    NameIdentifier viewIdent = NameIdentifier.of(viewNs, 
"view_null_dispatcher");
+    Representation[] representations =
+        new Representation[] {
+          SQLRepresentation.builder().withDialect("spark-sql").withSql("SELECT 
1").build()
+        };
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                dispatcher.createView(
+                    viewIdent, "comment", new Column[0], representations, 
null, null, props));
+    Assertions.assertEquals(
+        "schemaDispatcherSupplier returned null. "
+            + "SchemaDispatcher must be available for view operations.",
+        exception.getMessage());
+  }
+
   @Test
   public void testLoadViewAutoImportsIntoEntityStore() throws IOException {
     Namespace viewNs = Namespace.of(metalake, catalog, "schema63");
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java
index 505b5dc87e..e316352339 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java
@@ -96,7 +96,7 @@ public class IcebergNamespaceHookDispatcher implements 
IcebergNamespaceOperation
         catalogName,
         newlyOwned,
         context.userName(),
-        GravitinoEnv.getInstance().ownerDispatcher());
+        GravitinoEnv.getInstance().internalOwnerDispatcher());
     return createNamespaceResponse;
   }
 
@@ -228,13 +228,13 @@ public class IcebergNamespaceHookDispatcher implements 
IcebergNamespaceOperation
         namespace,
         registerTableRequest.name(),
         context.userName(),
-        GravitinoEnv.getInstance().ownerDispatcher());
+        GravitinoEnv.getInstance().internalOwnerDispatcher());
 
     return response;
   }
 
   private void importTable(String catalogName, Namespace namespace, String 
tableName) {
-    TableDispatcher tableDispatcher = 
GravitinoEnv.getInstance().tableDispatcher();
+    TableDispatcher tableDispatcher = 
GravitinoEnv.getInstance().internalTableDispatcher();
     if (tableDispatcher != null) {
       tableDispatcher.loadTable(
           IcebergIdentifierUtils.toGravitinoTableIdentifier(
@@ -246,7 +246,7 @@ public class IcebergNamespaceHookDispatcher implements 
IcebergNamespaceOperation
   }
 
   private void importSchema(String catalogName, Namespace namespace) {
-    SchemaDispatcher schemaDispatcher = 
GravitinoEnv.getInstance().schemaDispatcher();
+    SchemaDispatcher schemaDispatcher = 
GravitinoEnv.getInstance().internalSchemaDispatcher();
     if (schemaDispatcher != null) {
       schemaDispatcher.loadSchema(
           IcebergIdentifierUtils.toGravitinoSchemaIdentifier(
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java
index 9f081571f5..d39d93f1ae 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java
@@ -209,11 +209,11 @@ public class IcebergTableHookDispatcher implements 
IcebergTableOperationDispatch
         namespace,
         tableName,
         context.userName(),
-        GravitinoEnv.getInstance().ownerDispatcher());
+        GravitinoEnv.getInstance().internalOwnerDispatcher());
   }
 
   private void importTableEntity(String catalogName, Namespace namespace, 
String tableName) {
-    TableDispatcher tableDispatcher = 
GravitinoEnv.getInstance().tableDispatcher();
+    TableDispatcher tableDispatcher = 
GravitinoEnv.getInstance().internalTableDispatcher();
     if (tableDispatcher != null) {
       tableDispatcher.loadTable(
           IcebergIdentifierUtils.toGravitinoTableIdentifier(
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
index a6e35fef50..dcde32b92c 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
@@ -76,7 +76,7 @@ public class IcebergViewHookDispatcher implements 
IcebergViewOperationDispatcher
         namespace,
         createViewRequest.name(),
         context.userName(),
-        GravitinoEnv.getInstance().ownerDispatcher());
+        GravitinoEnv.getInstance().internalOwnerDispatcher());
 
     return response;
   }
@@ -181,7 +181,7 @@ public class IcebergViewHookDispatcher implements 
IcebergViewOperationDispatcher
    * @param viewName The name of the view.
    */
   private void importView(String catalogName, Namespace namespace, String 
viewName) {
-    ViewDispatcher viewDispatcher = 
GravitinoEnv.getInstance().viewDispatcher();
+    ViewDispatcher viewDispatcher = 
GravitinoEnv.getInstance().internalViewDispatcher();
     if (viewDispatcher != null) {
       try {
         viewDispatcher.loadView(
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/provider/DynamicIcebergConfigProvider.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/provider/DynamicIcebergConfigProvider.java
index 9194b4dc07..2bbefbade0 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/provider/DynamicIcebergConfigProvider.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/provider/DynamicIcebergConfigProvider.java
@@ -251,10 +251,10 @@ public class DynamicIcebergConfigProvider implements 
IcebergConfigProvider {
 
     InternalCatalogFetcher(String metalake) {
       this.metalake = metalake;
-      CatalogDispatcher dispatcher = 
GravitinoEnv.getInstance().catalogDispatcher();
+      CatalogDispatcher dispatcher = 
GravitinoEnv.getInstance().internalCatalogDispatcher();
       Preconditions.checkState(
           dispatcher != null,
-          "CatalogDispatcher is not available. "
+          "Internal CatalogDispatcher is not available. "
               + "Internal catalog fetcher requires running within Gravitino 
server.");
       this.catalogDispatcher = dispatcher;
     }
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergNamespaceHookDispatcher.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergNamespaceHookDispatcher.java
index eb212f8e4f..7d5e76922b 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergNamespaceHookDispatcher.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergNamespaceHookDispatcher.java
@@ -67,21 +67,65 @@ public class TestIcebergNamespaceHookDispatcher {
   private IcebergNamespaceHookDispatcher hookDispatcher;
   private IcebergNamespaceOperationDispatcher mockDispatcher;
   private OwnerDispatcher mockOwnerDispatcher;
+  private OwnerDispatcher mockInternalOwnerDispatcher;
+  private SchemaDispatcher mockSchemaDispatcher;
+  private SchemaDispatcher mockInternalSchemaDispatcher;
+  private TableDispatcher mockTableDispatcher;
+  private TableDispatcher mockInternalTableDispatcher;
   private EntityStore mockEntityStore;
   private LockManager mockLockManager;
   private IcebergRequestContext mockContext;
 
+  private OwnerDispatcher previousOwnerDispatcher;
+  private OwnerDispatcher previousInternalOwnerDispatcher;
+  private SchemaDispatcher previousSchemaDispatcher;
+  private SchemaDispatcher previousInternalSchemaDispatcher;
+  private TableDispatcher previousTableDispatcher;
+  private TableDispatcher previousInternalTableDispatcher;
+  private EntityStore previousEntityStore;
+  private LockManager previousLockManager;
+
   @BeforeEach
   public void setUp() throws IllegalAccessException {
     mockDispatcher = mock(IcebergNamespaceOperationDispatcher.class);
     mockOwnerDispatcher = mock(OwnerDispatcher.class);
-    SchemaDispatcher mockSchemaDispatcher = mock(SchemaDispatcher.class);
-    TableDispatcher mockTableDispatcher = mock(TableDispatcher.class);
+    mockInternalOwnerDispatcher = mock(OwnerDispatcher.class);
+    mockSchemaDispatcher = mock(SchemaDispatcher.class);
+    mockInternalSchemaDispatcher = mock(SchemaDispatcher.class);
+    mockTableDispatcher = mock(TableDispatcher.class);
+    mockInternalTableDispatcher = mock(TableDispatcher.class);
+
+    previousOwnerDispatcher =
+        (OwnerDispatcher) FieldUtils.readField(GravitinoEnv.getInstance(), 
"ownerDispatcher", true);
+    previousInternalOwnerDispatcher =
+        (OwnerDispatcher)
+            FieldUtils.readField(GravitinoEnv.getInstance(), 
"internalOwnerDispatcher", true);
+    previousSchemaDispatcher =
+        (SchemaDispatcher)
+            FieldUtils.readField(GravitinoEnv.getInstance(), 
"schemaDispatcher", true);
+    previousInternalSchemaDispatcher =
+        (SchemaDispatcher)
+            FieldUtils.readField(GravitinoEnv.getInstance(), 
"internalSchemaDispatcher", true);
+    previousTableDispatcher =
+        (TableDispatcher) FieldUtils.readField(GravitinoEnv.getInstance(), 
"tableDispatcher", true);
+    previousInternalTableDispatcher =
+        (TableDispatcher)
+            FieldUtils.readField(GravitinoEnv.getInstance(), 
"internalTableDispatcher", true);
+    previousEntityStore =
+        (EntityStore) FieldUtils.readField(GravitinoEnv.getInstance(), 
"entityStore", true);
+    previousLockManager =
+        (LockManager) FieldUtils.readField(GravitinoEnv.getInstance(), 
"lockManager", true);
 
     FieldUtils.writeField(GravitinoEnv.getInstance(), "ownerDispatcher", 
mockOwnerDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "internalOwnerDispatcher", 
mockInternalOwnerDispatcher, true);
     FieldUtils.writeField(
         GravitinoEnv.getInstance(), "schemaDispatcher", mockSchemaDispatcher, 
true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "internalSchemaDispatcher", 
mockInternalSchemaDispatcher, true);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "tableDispatcher", 
mockTableDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "internalTableDispatcher", 
mockInternalTableDispatcher, true);
 
     mockEntityStore = mock(EntityStore.class);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", 
mockEntityStore, true);
@@ -105,11 +149,29 @@ public class TestIcebergNamespaceHookDispatcher {
 
   @AfterEach
   public void tearDown() throws IllegalAccessException {
-    FieldUtils.writeField(GravitinoEnv.getInstance(), "ownerDispatcher", null, 
true);
-    FieldUtils.writeField(GravitinoEnv.getInstance(), "schemaDispatcher", 
null, true);
-    FieldUtils.writeField(GravitinoEnv.getInstance(), "tableDispatcher", null, 
true);
-    FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", null, 
true);
-    FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", null, 
true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "ownerDispatcher", 
previousOwnerDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "internalOwnerDispatcher",
+        previousInternalOwnerDispatcher,
+        true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "schemaDispatcher", 
previousSchemaDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "internalSchemaDispatcher",
+        previousInternalSchemaDispatcher,
+        true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "tableDispatcher", 
previousTableDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "internalTableDispatcher",
+        previousInternalTableDispatcher,
+        true);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", 
previousEntityStore, true);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", 
previousLockManager, true);
 
     Class<?> holderClass =
         Arrays.stream(IcebergRESTServerContext.class.getDeclaredClasses())
@@ -129,7 +191,7 @@ public class TestIcebergNamespaceHookDispatcher {
     when(mockDispatcher.createNamespace(mockContext, 
mockRequest)).thenReturn(mockResponse);
 
     doThrow(new RuntimeException("Set owner failed"))
-        .when(mockOwnerDispatcher)
+        .when(mockInternalOwnerDispatcher)
         .setOwners(any(), any(), any(), any());
 
     RuntimeException thrown =
@@ -137,6 +199,7 @@ public class TestIcebergNamespaceHookDispatcher {
             RuntimeException.class, () -> 
hookDispatcher.createNamespace(mockContext, mockRequest));
     Assertions.assertEquals("Set owner failed", thrown.getMessage());
     verify(mockDispatcher).createNamespace(mockContext, mockRequest);
+    verify(mockOwnerDispatcher, never()).setOwners(any(), any(), any(), any());
   }
 
   @Test
@@ -150,7 +213,7 @@ public class TestIcebergNamespaceHookDispatcher {
         .thenReturn(mockResponse);
 
     doThrow(new RuntimeException("Set owner failed"))
-        .when(mockOwnerDispatcher)
+        .when(mockInternalOwnerDispatcher)
         .setOwner(any(), any(), any(), any());
 
     RuntimeException thrown =
@@ -159,6 +222,7 @@ public class TestIcebergNamespaceHookDispatcher {
             () -> hookDispatcher.registerTable(mockContext, namespace, 
mockRequest));
     Assertions.assertEquals("Set owner failed", thrown.getMessage());
     verify(mockDispatcher).registerTable(mockContext, namespace, mockRequest);
+    verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
   }
 
   @Test
@@ -172,15 +236,18 @@ public class TestIcebergNamespaceHookDispatcher {
 
     // Schema import (loadSchema) throwing must propagate so the caller learns 
the namespace
     // exists in Iceberg but is not registered in Gravitino. setOwner is 
therefore unreachable.
-    SchemaDispatcher schemaDispatcher = 
GravitinoEnv.getInstance().schemaDispatcher();
-    doThrow(new RuntimeException("Import 
failed")).when(schemaDispatcher).loadSchema(any());
+    doThrow(new RuntimeException("Import failed"))
+        .when(mockInternalSchemaDispatcher)
+        .loadSchema(any());
 
     RuntimeException thrown =
         Assertions.assertThrows(
             RuntimeException.class, () -> 
hookDispatcher.createNamespace(mockContext, mockRequest));
 
     Assertions.assertEquals("Import failed", thrown.getMessage());
+    verify(mockInternalOwnerDispatcher, never()).setOwners(any(), any(), 
any(), any());
     verify(mockOwnerDispatcher, never()).setOwners(any(), any(), any(), any());
+    verify(mockSchemaDispatcher, never()).loadSchema(any());
   }
 
   @Test
@@ -195,8 +262,9 @@ public class TestIcebergNamespaceHookDispatcher {
 
     // Table import (loadTable) throwing must propagate so the caller learns 
the table exists in
     // Iceberg but is not registered in Gravitino. setOwner is therefore 
unreachable.
-    TableDispatcher tableDispatcher = 
GravitinoEnv.getInstance().tableDispatcher();
-    doThrow(new RuntimeException("Import 
failed")).when(tableDispatcher).loadTable(any());
+    doThrow(new RuntimeException("Import failed"))
+        .when(mockInternalTableDispatcher)
+        .loadTable(any());
 
     RuntimeException thrown =
         Assertions.assertThrows(
@@ -204,7 +272,9 @@ public class TestIcebergNamespaceHookDispatcher {
             () -> hookDispatcher.registerTable(mockContext, namespace, 
mockRequest));
 
     Assertions.assertEquals("Import failed", thrown.getMessage());
+    verify(mockInternalOwnerDispatcher, never()).setOwner(any(), any(), any(), 
any());
     verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
+    verify(mockTableDispatcher, never()).loadTable(any());
   }
 
   @Test
@@ -248,8 +318,9 @@ public class TestIcebergNamespaceHookDispatcher {
 
     @SuppressWarnings("unchecked")
     ArgumentCaptor<List<MetadataObject>> captor = 
ArgumentCaptor.forClass(List.class);
-    verify(mockOwnerDispatcher)
+    verify(mockInternalOwnerDispatcher)
         .setOwners(eq(TEST_METALAKE), captor.capture(), eq(TEST_USER), 
eq(Owner.Type.USER));
+    verify(mockOwnerDispatcher, never()).setOwners(any(), any(), any(), any());
     List<String> names =
         
captor.getValue().stream().map(MetadataObject::fullName).collect(Collectors.toList());
     Assertions.assertEquals(
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
index 2d4f2e125c..40a11ec015 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
@@ -76,13 +76,17 @@ public class TestIcebergTableHookDispatcher {
   private IcebergTableOperationDispatcher mockDispatcher;
   private EntityStore mockEntityStore;
   private TableDispatcher mockTableDispatcher;
+  private TableDispatcher mockInternalTableDispatcher;
   private OwnerDispatcher mockOwnerDispatcher;
+  private OwnerDispatcher mockInternalOwnerDispatcher;
   private IcebergRequestContext mockContext;
 
   private Config previousConfig;
   private EntityStore previousEntityStore;
   private TableDispatcher previousTableDispatcher;
+  private TableDispatcher previousInternalTableDispatcher;
   private OwnerDispatcher previousOwnerDispatcher;
+  private OwnerDispatcher previousInternalOwnerDispatcher;
 
   @BeforeEach
   public void setUp() throws IllegalAccessException {
@@ -92,7 +96,9 @@ public class TestIcebergTableHookDispatcher {
     // Mock GravitinoEnv components
     mockEntityStore = mock(EntityStore.class);
     mockTableDispatcher = mock(TableDispatcher.class);
+    mockInternalTableDispatcher = mock(TableDispatcher.class);
     mockOwnerDispatcher = mock(OwnerDispatcher.class);
+    mockInternalOwnerDispatcher = mock(OwnerDispatcher.class);
 
     Config mockConfig = mock(Config.class);
     when(mockConfig.get(Configs.SCHEMA_SEPARATOR)).thenReturn(":");
@@ -101,12 +107,22 @@ public class TestIcebergTableHookDispatcher {
         (EntityStore) FieldUtils.readField(GravitinoEnv.getInstance(), 
"entityStore", true);
     previousTableDispatcher =
         (TableDispatcher) FieldUtils.readField(GravitinoEnv.getInstance(), 
"tableDispatcher", true);
+    previousInternalTableDispatcher =
+        (TableDispatcher)
+            FieldUtils.readField(GravitinoEnv.getInstance(), 
"internalTableDispatcher", true);
     previousOwnerDispatcher =
         (OwnerDispatcher) FieldUtils.readField(GravitinoEnv.getInstance(), 
"ownerDispatcher", true);
+    previousInternalOwnerDispatcher =
+        (OwnerDispatcher)
+            FieldUtils.readField(GravitinoEnv.getInstance(), 
"internalOwnerDispatcher", true);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "config", mockConfig, 
true);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", 
mockEntityStore, true);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "tableDispatcher", 
mockTableDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "internalTableDispatcher", 
mockInternalTableDispatcher, true);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "ownerDispatcher", 
mockOwnerDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "internalOwnerDispatcher", 
mockInternalOwnerDispatcher, true);
 
     // Create mock IcebergRESTServerContext
     IcebergConfigProvider mockConfigProvider = 
mock(IcebergConfigProvider.class);
@@ -131,8 +147,18 @@ public class TestIcebergTableHookDispatcher {
     FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", 
previousEntityStore, true);
     FieldUtils.writeField(
         GravitinoEnv.getInstance(), "tableDispatcher", 
previousTableDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "internalTableDispatcher",
+        previousInternalTableDispatcher,
+        true);
     FieldUtils.writeField(
         GravitinoEnv.getInstance(), "ownerDispatcher", 
previousOwnerDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "internalOwnerDispatcher",
+        previousInternalOwnerDispatcher,
+        true);
 
     // Reset IcebergRESTServerContext singleton
     Class<?> holderClass =
@@ -161,12 +187,14 @@ public class TestIcebergTableHookDispatcher {
     NameIdentifier expectedIdentifier =
         IcebergIdentifierUtils.toGravitinoTableIdentifier(
             TEST_METALAKE, TEST_CATALOG, TableIdentifier.of(namespace, 
"test_table"), ":");
-    verify(mockTableDispatcher).loadTable(expectedIdentifier);
+    verify(mockInternalTableDispatcher).loadTable(expectedIdentifier);
+    verify(mockTableDispatcher, never()).loadTable(any());
 
     // Verify ownership was set
     ArgumentCaptor<String> userCaptor = ArgumentCaptor.forClass(String.class);
-    verify(mockOwnerDispatcher)
+    verify(mockInternalOwnerDispatcher)
         .setOwner(eq(TEST_METALAKE), any(), userCaptor.capture(), 
eq(Owner.Type.USER));
+    verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
     Assertions.assertEquals(TEST_USER, userCaptor.getValue());
   }
 
@@ -196,7 +224,8 @@ public class TestIcebergTableHookDispatcher {
         IcebergIdentifierUtils.toGravitinoTableIdentifier(
             TEST_METALAKE, TEST_CATALOG, tableId, ":");
     verify(mockEntityStore, never()).delete(expectedIdentifier, 
Entity.EntityType.TABLE);
-    verify(mockTableDispatcher).loadTable(expectedIdentifier);
+    verify(mockInternalTableDispatcher).loadTable(expectedIdentifier);
+    verify(mockTableDispatcher, never()).loadTable(any());
   }
 
   @Test
@@ -211,7 +240,8 @@ public class TestIcebergTableHookDispatcher {
         IcebergIdentifierUtils.toGravitinoTableIdentifier(
             TEST_METALAKE, TEST_CATALOG, tableId, ":");
     verify(mockEntityStore).delete(expectedIdentifier, 
Entity.EntityType.TABLE);
-    verify(mockTableDispatcher).loadTable(expectedIdentifier);
+    verify(mockInternalTableDispatcher).loadTable(expectedIdentifier);
+    verify(mockTableDispatcher, never()).loadTable(any());
   }
 
   @Test
@@ -293,7 +323,8 @@ public class TestIcebergTableHookDispatcher {
     NameIdentifier destIdentifier =
         IcebergIdentifierUtils.toGravitinoTableIdentifier(TEST_METALAKE, 
TEST_CATALOG, dest, ":");
     verify(mockEntityStore).delete(sourceIdentifier, Entity.EntityType.TABLE);
-    verify(mockTableDispatcher).loadTable(destIdentifier);
+    verify(mockInternalTableDispatcher).loadTable(destIdentifier);
+    verify(mockTableDispatcher, never()).loadTable(any());
   }
 
   @Test
@@ -312,12 +343,15 @@ public class TestIcebergTableHookDispatcher {
     when(mockEntityStore.update(any(), eq(TableEntity.class), 
eq(Entity.EntityType.TABLE), any()))
         .thenReturn(mockTableEntity);
     when(mockDispatcher.tableExists(mockContext, dest)).thenReturn(true);
-    doThrow(new RuntimeException("import 
failed")).when(mockTableDispatcher).loadTable(any());
+    doThrow(new RuntimeException("import failed"))
+        .when(mockInternalTableDispatcher)
+        .loadTable(any());
 
     Assertions.assertDoesNotThrow(() -> 
hookDispatcher.renameTable(mockContext, request));
 
     verify(mockDispatcher).renameTable(mockContext, request);
-    verify(mockTableDispatcher).loadTable(any());
+    verify(mockInternalTableDispatcher).loadTable(any());
+    verify(mockTableDispatcher, never()).loadTable(any());
   }
 
   @Test
@@ -364,7 +398,7 @@ public class TestIcebergTableHookDispatcher {
     when(mockDispatcher.createTable(mockContext, namespace, 
request)).thenReturn(mockResponse);
 
     doThrow(new RuntimeException("Set owner failed"))
-        .when(mockOwnerDispatcher)
+        .when(mockInternalOwnerDispatcher)
         .setOwner(any(), any(), any(), any());
 
     RuntimeException thrown =
@@ -373,6 +407,7 @@ public class TestIcebergTableHookDispatcher {
             () -> hookDispatcher.createTable(mockContext, namespace, request));
     Assertions.assertEquals("Set owner failed", thrown.getMessage());
     verify(mockDispatcher).createTable(mockContext, namespace, request);
+    verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
   }
 
   @Test
@@ -394,9 +429,11 @@ public class TestIcebergTableHookDispatcher {
     verify(mockDispatcher).createTable(mockContext, namespace, request);
 
     // Verify table import was NOT called for staged create
+    verify(mockInternalTableDispatcher, never()).loadTable(any());
     verify(mockTableDispatcher, never()).loadTable(any());
 
     // Verify ownership was NOT set for staged create
+    verify(mockInternalOwnerDispatcher, never()).setOwner(any(), any(), any(), 
any());
     verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
   }
 
@@ -419,11 +456,13 @@ public class TestIcebergTableHookDispatcher {
     NameIdentifier gravitinoTableId =
         IcebergIdentifierUtils.toGravitinoTableIdentifier(
             TEST_METALAKE, TEST_CATALOG, tableId, ":");
-    verify(mockTableDispatcher).loadTable(gravitinoTableId);
+    verify(mockInternalTableDispatcher).loadTable(gravitinoTableId);
+    verify(mockTableDispatcher, never()).loadTable(any());
 
     ArgumentCaptor<String> userCaptor = ArgumentCaptor.forClass(String.class);
-    verify(mockOwnerDispatcher)
+    verify(mockInternalOwnerDispatcher)
         .setOwner(eq(TEST_METALAKE), any(), userCaptor.capture(), 
eq(Owner.Type.USER));
+    verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
     Assertions.assertEquals(TEST_USER, userCaptor.getValue());
   }
 
@@ -444,9 +483,11 @@ public class TestIcebergTableHookDispatcher {
     verify(mockDispatcher).updateTable(mockContext, tableId, request);
 
     // Verify table import was NOT called for a regular update
+    verify(mockInternalTableDispatcher, never()).loadTable(any());
     verify(mockTableDispatcher, never()).loadTable(any());
 
     // Verify ownership was NOT set
+    verify(mockInternalOwnerDispatcher, never()).setOwner(any(), any(), any(), 
any());
     verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
   }
 
@@ -474,7 +515,9 @@ public class TestIcebergTableHookDispatcher {
 
     // Import failure (the loadTable call) must propagate so the caller learns 
the table exists in
     // Iceberg but is not registered in Gravitino. setOwner is therefore 
unreachable.
-    doThrow(new RuntimeException("Import 
failed")).when(mockTableDispatcher).loadTable(any());
+    doThrow(new RuntimeException("Import failed"))
+        .when(mockInternalTableDispatcher)
+        .loadTable(any());
 
     RuntimeException thrown =
         Assertions.assertThrows(
@@ -483,7 +526,9 @@ public class TestIcebergTableHookDispatcher {
 
     Assertions.assertEquals("Import failed", thrown.getMessage());
     verify(mockDispatcher).createTable(mockContext, namespace, request);
-    verify(mockTableDispatcher).loadTable(any());
+    verify(mockInternalTableDispatcher).loadTable(any());
+    verify(mockTableDispatcher, never()).loadTable(any());
+    verify(mockInternalOwnerDispatcher, never()).setOwner(any(), any(), any(), 
any());
     verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
   }
 }
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewHookDispatcher.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewHookDispatcher.java
index 4fdf1426b9..929aeac842 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewHookDispatcher.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewHookDispatcher.java
@@ -74,21 +74,27 @@ public class TestIcebergViewHookDispatcher {
   private IcebergViewOperationDispatcher mockExecutor;
   private EntityStore mockEntityStore;
   private ViewDispatcher mockViewDispatcher;
+  private ViewDispatcher mockInternalViewDispatcher;
   private OwnerDispatcher mockOwnerDispatcher;
+  private OwnerDispatcher mockInternalOwnerDispatcher;
   private IcebergRequestContext mockContext;
   private GravitinoEnv gravitinoEnv;
 
   private Config previousConfig;
   private EntityStore previousEntityStore;
   private ViewDispatcher previousViewDispatcher;
+  private ViewDispatcher previousInternalViewDispatcher;
   private OwnerDispatcher previousOwnerDispatcher;
+  private OwnerDispatcher previousInternalOwnerDispatcher;
 
   @BeforeEach
   public void setUp() {
     mockExecutor = mock(IcebergViewOperationDispatcher.class);
     mockEntityStore = mock(EntityStore.class);
     mockViewDispatcher = mock(ViewDispatcher.class);
+    mockInternalViewDispatcher = mock(ViewDispatcher.class);
     mockOwnerDispatcher = mock(OwnerDispatcher.class);
+    mockInternalOwnerDispatcher = mock(OwnerDispatcher.class);
     mockContext = mock(IcebergRequestContext.class);
 
     when(mockContext.catalogName()).thenReturn(CATALOG);
@@ -103,12 +109,20 @@ public class TestIcebergViewHookDispatcher {
       previousEntityStore = (EntityStore) FieldUtils.readField(gravitinoEnv, 
"entityStore", true);
       previousViewDispatcher =
           (ViewDispatcher) FieldUtils.readField(gravitinoEnv, 
"viewDispatcher", true);
+      previousInternalViewDispatcher =
+          (ViewDispatcher) FieldUtils.readField(gravitinoEnv, 
"internalViewDispatcher", true);
       previousOwnerDispatcher =
           (OwnerDispatcher) FieldUtils.readField(gravitinoEnv, 
"ownerDispatcher", true);
+      previousInternalOwnerDispatcher =
+          (OwnerDispatcher) FieldUtils.readField(gravitinoEnv, 
"internalOwnerDispatcher", true);
       FieldUtils.writeField(gravitinoEnv, "config", mockConfig, true);
       FieldUtils.writeField(gravitinoEnv, "entityStore", mockEntityStore, 
true);
       FieldUtils.writeField(gravitinoEnv, "viewDispatcher", 
mockViewDispatcher, true);
+      FieldUtils.writeField(
+          gravitinoEnv, "internalViewDispatcher", mockInternalViewDispatcher, 
true);
       FieldUtils.writeField(gravitinoEnv, "ownerDispatcher", 
mockOwnerDispatcher, true);
+      FieldUtils.writeField(
+          gravitinoEnv, "internalOwnerDispatcher", 
mockInternalOwnerDispatcher, true);
     } catch (Exception e) {
       throw new RuntimeException("Failed to setup test", e);
     }
@@ -124,7 +138,11 @@ public class TestIcebergViewHookDispatcher {
       FieldUtils.writeField(gravitinoEnv, "config", previousConfig, true);
       FieldUtils.writeField(gravitinoEnv, "entityStore", previousEntityStore, 
true);
       FieldUtils.writeField(gravitinoEnv, "viewDispatcher", 
previousViewDispatcher, true);
+      FieldUtils.writeField(
+          gravitinoEnv, "internalViewDispatcher", 
previousInternalViewDispatcher, true);
       FieldUtils.writeField(gravitinoEnv, "ownerDispatcher", 
previousOwnerDispatcher, true);
+      FieldUtils.writeField(
+          gravitinoEnv, "internalOwnerDispatcher", 
previousInternalOwnerDispatcher, true);
     } catch (Exception e) {
       // Ignore cleanup errors
     }
@@ -161,10 +179,12 @@ public class TestIcebergViewHookDispatcher {
 
     // Verify view was imported into Gravitino
     NameIdentifier expectedIdent = NameIdentifier.of(METALAKE, CATALOG, 
SCHEMA_NAME, VIEW_NAME);
-    verify(mockViewDispatcher, times(1)).loadView(eq(expectedIdent));
+    verify(mockInternalViewDispatcher, times(1)).loadView(eq(expectedIdent));
+    verify(mockViewDispatcher, never()).loadView(any());
 
     // Verify ownership was set
-    verify(mockOwnerDispatcher, times(1)).setOwner(any(), any(), eq(USER), 
any());
+    verify(mockInternalOwnerDispatcher, times(1)).setOwner(any(), any(), 
eq(USER), any());
+    verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
 
     assertEquals(mockResponse, response);
   }
@@ -195,7 +215,7 @@ public class TestIcebergViewHookDispatcher {
 
     // Simulate import failure
     doThrow(new RuntimeException("Import failed"))
-        .when(mockViewDispatcher)
+        .when(mockInternalViewDispatcher)
         .loadView(any(NameIdentifier.class));
 
     // Should not throw - import is best-effort
@@ -230,7 +250,7 @@ public class TestIcebergViewHookDispatcher {
     when(mockExecutor.createView(mockContext, namespace, 
createRequest)).thenReturn(mockResponse);
 
     doThrow(new RuntimeException("Set owner failed"))
-        .when(mockOwnerDispatcher)
+        .when(mockInternalOwnerDispatcher)
         .setOwner(any(), any(), any(), any());
 
     RuntimeException thrown =
@@ -239,7 +259,9 @@ public class TestIcebergViewHookDispatcher {
             () -> hookDispatcher.createView(mockContext, namespace, 
createRequest));
     assertEquals("Set owner failed", thrown.getMessage());
     verify(mockExecutor, times(1)).createView(mockContext, namespace, 
createRequest);
-    verify(mockViewDispatcher, times(1)).loadView(any(NameIdentifier.class));
+    verify(mockInternalViewDispatcher, 
times(1)).loadView(any(NameIdentifier.class));
+    verify(mockViewDispatcher, never()).loadView(any());
+    verify(mockOwnerDispatcher, never()).setOwner(any(), any(), any(), any());
   }
 
   @Test
@@ -268,7 +290,8 @@ public class TestIcebergViewHookDispatcher {
     NameIdentifier expectedIdent =
         IcebergIdentifierUtils.toGravitinoTableIdentifier(METALAKE, CATALOG, 
viewIdent, ":");
     verify(mockEntityStore, never()).delete(eq(expectedIdent), 
eq(Entity.EntityType.VIEW));
-    verify(mockViewDispatcher, times(1)).loadView(eq(expectedIdent));
+    verify(mockInternalViewDispatcher, times(1)).loadView(eq(expectedIdent));
+    verify(mockViewDispatcher, never()).loadView(any());
   }
 
   @Test
@@ -282,7 +305,8 @@ public class TestIcebergViewHookDispatcher {
     NameIdentifier expectedIdent =
         IcebergIdentifierUtils.toGravitinoTableIdentifier(METALAKE, CATALOG, 
viewIdent, ":");
     verify(mockEntityStore, times(1)).delete(eq(expectedIdent), 
eq(Entity.EntityType.VIEW));
-    verify(mockViewDispatcher, times(1)).loadView(eq(expectedIdent));
+    verify(mockInternalViewDispatcher, times(1)).loadView(eq(expectedIdent));
+    verify(mockViewDispatcher, never()).loadView(any());
   }
 
   @Test
@@ -354,7 +378,8 @@ public class TestIcebergViewHookDispatcher {
     NameIdentifier destGravitinoIdent =
         IcebergIdentifierUtils.toGravitinoTableIdentifier(METALAKE, CATALOG, 
destIdent, ":");
     verify(mockEntityStore, times(1)).delete(eq(sourceGravitinoIdent), 
eq(Entity.EntityType.VIEW));
-    verify(mockViewDispatcher, times(1)).loadView(eq(destGravitinoIdent));
+    verify(mockInternalViewDispatcher, 
times(1)).loadView(eq(destGravitinoIdent));
+    verify(mockViewDispatcher, never()).loadView(any());
   }
 
   @Test
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/provider/TestDynamicIcebergConfigProvider.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/provider/TestDynamicIcebergConfigProvider.java
index 669e41a24d..2177778734 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/provider/TestDynamicIcebergConfigProvider.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/provider/TestDynamicIcebergConfigProvider.java
@@ -63,6 +63,7 @@ public class TestDynamicIcebergConfigProvider {
   public void tearDown() throws IllegalAccessException {
     // Clean up GravitinoEnv and IcebergRESTServerContext state after each test
     FieldUtils.writeField(GravitinoEnv.getInstance(), "catalogDispatcher", 
null, true);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), 
"internalCatalogDispatcher", null, true);
     resetServerContext();
   }
 
@@ -265,12 +266,13 @@ public class TestDynamicIcebergConfigProvider {
     // Enable authorization to use internal fetcher
     createMockServerContext(true);
 
-    // Mock CatalogDispatcher
+    // Mock CatalogDispatchers
     CatalogDispatcher mockCatalogDispatcher = 
Mockito.mock(CatalogDispatcher.class);
+    CatalogDispatcher mockInternalCatalogDispatcher = 
Mockito.mock(CatalogDispatcher.class);
     Catalog mockCatalog = Mockito.mock(Catalog.class);
 
     NameIdentifier catalogIdent = NameIdentifierUtil.ofCatalog(metalakeName, 
catalogName);
-    
Mockito.when(mockCatalogDispatcher.loadCatalog(catalogIdent)).thenReturn(mockCatalog);
+    
Mockito.when(mockInternalCatalogDispatcher.loadCatalog(catalogIdent)).thenReturn(mockCatalog);
     Mockito.when(mockCatalog.provider()).thenReturn("lakehouse-iceberg");
     Mockito.when(mockCatalog.properties())
         .thenReturn(
@@ -281,9 +283,14 @@ public class TestDynamicIcebergConfigProvider {
               }
             });
 
-    // Set the mock CatalogDispatcher to GravitinoEnv
+    // Set the mock CatalogDispatchers to GravitinoEnv
     FieldUtils.writeField(
         GravitinoEnv.getInstance(), "catalogDispatcher", 
mockCatalogDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "internalCatalogDispatcher",
+        mockInternalCatalogDispatcher,
+        true);
 
     // Initialize provider with required properties
     Map<String, String> properties = new HashMap<>();
@@ -292,11 +299,12 @@ public class TestDynamicIcebergConfigProvider {
     DynamicIcebergConfigProvider provider = new DynamicIcebergConfigProvider();
     provider.initialize(properties);
 
-    // Test that internal interface is used (CatalogDispatcher should be 
called)
+    // Test that internal interface is used (internal CatalogDispatcher should 
be called)
     Optional<IcebergConfig> icebergConfig = 
provider.getIcebergCatalogConfig(catalogName);
 
     Assertions.assertTrue(icebergConfig.isPresent());
-    Mockito.verify(mockCatalogDispatcher).loadCatalog(catalogIdent);
+    Mockito.verify(mockInternalCatalogDispatcher).loadCatalog(catalogIdent);
+    Mockito.verify(mockCatalogDispatcher, 
Mockito.never()).loadCatalog(catalogIdent);
   }
 
   @Test
@@ -345,8 +353,9 @@ public class TestDynamicIcebergConfigProvider {
     // Enable authorization to use internal fetcher
     createMockServerContext(true);
 
-    // Ensure CatalogDispatcher is null (simulating GravitinoEnv not 
initialized)
+    // Ensure internal CatalogDispatcher is null (simulating GravitinoEnv not 
initialized)
     FieldUtils.writeField(GravitinoEnv.getInstance(), "catalogDispatcher", 
null, true);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), 
"internalCatalogDispatcher", null, true);
 
     // Initialize provider with required properties
     Map<String, String> properties = new HashMap<>();
@@ -355,9 +364,13 @@ public class TestDynamicIcebergConfigProvider {
     DynamicIcebergConfigProvider provider = new DynamicIcebergConfigProvider();
     provider.initialize(properties);
 
-    // Test that IllegalStateException is thrown when CatalogDispatcher is null
-    Assertions.assertThrows(
-        IllegalStateException.class, () -> 
provider.getIcebergCatalogConfig(catalogName));
+    IllegalStateException exception =
+        Assertions.assertThrows(
+            IllegalStateException.class, () -> 
provider.getIcebergCatalogConfig(catalogName));
+    Assertions.assertEquals(
+        "Internal CatalogDispatcher is not available. "
+            + "Internal catalog fetcher requires running within Gravitino 
server.",
+        exception.getMessage());
   }
 
   @Test
@@ -368,16 +381,22 @@ public class TestDynamicIcebergConfigProvider {
     // Enable authorization to use internal fetcher
     createMockServerContext(true);
 
-    // Mock CatalogDispatcher to throw NoSuchCatalogException
+    // Mock internal CatalogDispatcher to throw NoSuchCatalogException
     CatalogDispatcher mockCatalogDispatcher = 
Mockito.mock(CatalogDispatcher.class);
+    CatalogDispatcher mockInternalCatalogDispatcher = 
Mockito.mock(CatalogDispatcher.class);
     NameIdentifier catalogIdent =
         NameIdentifierUtil.ofCatalog(metalakeName, nonExistentCatalogName);
-    Mockito.when(mockCatalogDispatcher.loadCatalog(catalogIdent))
+    Mockito.when(mockInternalCatalogDispatcher.loadCatalog(catalogIdent))
         .thenThrow(new NoSuchCatalogException("Catalog not found: %s", 
nonExistentCatalogName));
 
-    // Set the mock CatalogDispatcher to GravitinoEnv
+    // Set the mock CatalogDispatchers to GravitinoEnv
     FieldUtils.writeField(
         GravitinoEnv.getInstance(), "catalogDispatcher", 
mockCatalogDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "internalCatalogDispatcher",
+        mockInternalCatalogDispatcher,
+        true);
 
     // Initialize provider with required properties
     Map<String, String> properties = new HashMap<>();
@@ -390,7 +409,8 @@ public class TestDynamicIcebergConfigProvider {
     Optional<IcebergConfig> result = 
provider.getIcebergCatalogConfig(nonExistentCatalogName);
 
     Assertions.assertFalse(result.isPresent());
-    Mockito.verify(mockCatalogDispatcher).loadCatalog(catalogIdent);
+    Mockito.verify(mockInternalCatalogDispatcher).loadCatalog(catalogIdent);
+    Mockito.verify(mockCatalogDispatcher, 
Mockito.never()).loadCatalog(catalogIdent);
   }
 
   @Test
@@ -474,12 +494,13 @@ public class TestDynamicIcebergConfigProvider {
     // Enable authorization to use internal fetcher
     createMockServerContext(true);
 
-    // Mock CatalogDispatcher
+    // Mock CatalogDispatchers
     CatalogDispatcher mockCatalogDispatcher = 
Mockito.mock(CatalogDispatcher.class);
+    CatalogDispatcher mockInternalCatalogDispatcher = 
Mockito.mock(CatalogDispatcher.class);
     Catalog mockCatalog = Mockito.mock(Catalog.class);
 
     NameIdentifier catalogIdent = NameIdentifierUtil.ofCatalog(metalakeName, 
catalogName);
-    
Mockito.when(mockCatalogDispatcher.loadCatalog(catalogIdent)).thenReturn(mockCatalog);
+    
Mockito.when(mockInternalCatalogDispatcher.loadCatalog(catalogIdent)).thenReturn(mockCatalog);
     Mockito.when(mockCatalog.provider()).thenReturn("lakehouse-iceberg");
     Mockito.when(mockCatalog.properties())
         .thenReturn(
@@ -490,9 +511,14 @@ public class TestDynamicIcebergConfigProvider {
               }
             });
 
-    // Set the mock CatalogDispatcher to GravitinoEnv
+    // Set the mock CatalogDispatchers to GravitinoEnv
     FieldUtils.writeField(
         GravitinoEnv.getInstance(), "catalogDispatcher", 
mockCatalogDispatcher, true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "internalCatalogDispatcher",
+        mockInternalCatalogDispatcher,
+        true);
 
     // Initialize provider with required properties
     Map<String, String> properties = new HashMap<>();
@@ -535,8 +561,10 @@ public class TestDynamicIcebergConfigProvider {
       Assertions.assertTrue(result.isPresent(), "Each thread should get a 
valid config");
     }
 
-    // Verify CatalogDispatcher was called (at least once, possibly more due 
to concurrency)
-    Mockito.verify(mockCatalogDispatcher, 
Mockito.atLeastOnce()).loadCatalog(catalogIdent);
+    // Verify internal CatalogDispatcher was called (at least once, possibly 
more due to
+    // concurrency)
+    Mockito.verify(mockInternalCatalogDispatcher, 
Mockito.atLeastOnce()).loadCatalog(catalogIdent);
+    Mockito.verify(mockCatalogDispatcher, 
Mockito.never()).loadCatalog(catalogIdent);
 
     executor.shutdown();
     executor.awaitTermination(5, TimeUnit.SECONDS);
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
index c58ffea12e..deab84b743 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
@@ -83,7 +83,8 @@ public class PassThroughAuthorizer implements 
GravitinoAuthorizer {
 
   @Override
   public boolean isMetalakeUser(String metalake, AuthorizationRequestContext 
requestContext) {
-    AccessControlDispatcher dispatcher = 
GravitinoEnv.getInstance().accessControlDispatcher();
+    AccessControlDispatcher dispatcher =
+        GravitinoEnv.getInstance().internalAccessControlDispatcher();
     if (dispatcher != null) {
       try {
         dispatcher.getUser(metalake, PrincipalUtils.getCurrentUserName());
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java
index e6099a6c47..4f7efb875e 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java
@@ -18,15 +18,22 @@
 package org.apache.gravitino.server.authorization;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.io.IOException;
 import java.security.Principal;
+import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Entity;
+import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.authorization.AccessControlDispatcher;
 import org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.authorization.User;
+import org.apache.gravitino.utils.PrincipalUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -73,4 +80,32 @@ public class TestPassThroughAuthorizer {
               "metalake", "type", "fullName", new 
AuthorizationRequestContext()));
     }
   }
+
+  @Test
+  public void testIsMetalakeUserUsesInternalAccessControlDispatcher() throws 
Exception {
+    AccessControlDispatcher dispatcher = mock(AccessControlDispatcher.class);
+    User user = mock(User.class);
+    when(dispatcher.getUser("metalake", "testUser")).thenReturn(user);
+
+    AccessControlDispatcher previousDispatcher =
+        (AccessControlDispatcher)
+            FieldUtils.readField(
+                GravitinoEnv.getInstance(), "internalAccessControlDispatcher", 
true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "internalAccessControlDispatcher", 
dispatcher, true);
+    try (PassThroughAuthorizer passThroughAuthorizer = new 
PassThroughAuthorizer()) {
+      PrincipalUtils.doAs(
+          new UserPrincipal("testUser"),
+          () -> {
+            Assertions.assertTrue(
+                passThroughAuthorizer.isMetalakeUser(
+                    "metalake", new AuthorizationRequestContext()));
+            return null;
+          });
+      verify(dispatcher).getUser("metalake", "testUser");
+    } finally {
+      FieldUtils.writeField(
+          GravitinoEnv.getInstance(), "internalAccessControlDispatcher", 
previousDispatcher, true);
+    }
+  }
 }
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
index 67f84a6f99..5f28bfc7dd 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
@@ -372,6 +372,12 @@ public class TestJcasbinAuthorizer {
     }
   }
 
+  @Test
+  public void testIsMetalakeUserUsesUserInfoCache() {
+    assertTrue(jcasbinAuthorizer.isMetalakeUser(METALAKE, new 
AuthorizationRequestContext()));
+    verify(userMetaMapper).getUserUpdatedAt(METALAKE, USERNAME);
+  }
+
   @Test
   public void testAuthorize() throws Exception {
     makeCompletableFutureUseCurrentThread(jcasbinAuthorizer);

Reply via email to