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 3f277bee15 [#12457] feat(secret): Wire connectors and aux services to 
getSecrets (#12572)
3f277bee15 is described below

commit 3f277bee159543785810b05f015ce96096277a8a
Author: MaSai <[email protected]>
AuthorDate: Tue Aug 25 15:58:17 2026 +0800

    [#12457] feat(secret): Wire connectors and aux services to getSecrets 
(#12572)
    
    ### What changes were proposed in this pull request?
    
    Wire engines and aux services to the `getSecrets` API from #12458 so
    runtime
    conf merges secret-manager plaintext without changing default `load*`
    omit
    behavior:
    
    - Spark / Flink / Trino: `properties() + getSecrets()`
    - IRC (`DynamicIcebergConfigProvider`): merge `getSecrets()`, then
    overlay
    `JdbcCredential` so **credentials win** (aligned with JDBC connectors);
      auxiliary mode uses `SecretManager.toPlaintextProperties`
    - Lance: catalog/schema properties with secrets (in-process dispatcher
    when
      available, otherwise client `supportsSecrets()`)
    - GVFS (Java + Python): merge catalog/schema/fileset secrets into FS
    conf
      (fileset > schema > catalog precedence)
    
    ### Why are the changes needed?
    
    Fix: #12457
    
    Connectors need secret plaintext at runtime. #12458 added the platform
    `getSecrets` API; this PR wires the call sites.
    
    ### Does this PR introduce _any_ user-facing change?
    
    - Yes: connector/aux runtime behavior now merges secret-manager
    plaintext via
      `getSecrets`
    - No new public REST/API surface beyond #12458
    
    ### How was this patch tested?
    
    ```bash
    ./gradlew spotlessApply
    
    ./gradlew \
      :spark-connector:spark-common:test \
        --tests 
'org.apache.gravitino.spark.connector.catalog.TestBaseCatalogSecrets' \
      :flink-connector:flink-common:test \
        --tests 
'org.apache.gravitino.flink.connector.store.TestGravitinoCatalogStore' \
      :trino-connector:trino-connector:test \
        --tests 
'org.apache.gravitino.trino.connector.catalog.TestCatalogConnectorManager' \
      :iceberg:iceberg-rest-server:test \
        --tests 
'org.apache.gravitino.iceberg.service.provider.TestDynamicIcebergConfigProvider'
 \
      :lance:lance-common:test \
        --tests 
'org.apache.gravitino.lance.common.ops.gravitino.TestGravitinoLanceNamespaceWrapper'
 \
      :clients:filesystem-hadoop3:test \
        --tests 
'org.apache.gravitino.filesystem.hadoop.TestBaseGVFSOperationsSecrets' \
      -PskipITs
    
    # Python
    python -m unittest \
      clients/client-python/tests/unittests/test_gvfs_merge_secrets.py
    ```
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 .../gravitino/filesystem/gvfs_base_operations.py   |  36 +--
 clients/client-python/tests/unittests/mock_base.py |  14 +-
 .../tests/unittests/test_gvfs_merge_secrets.py     | 125 ++++++++++
 .../filesystem/hadoop/BaseGVFSOperations.java      |  41 ++--
 .../filesystem/hadoop/GravitinoMockServerBase.java |  19 ++
 .../hadoop/TestBaseGVFSOperationsSecrets.java      | 260 +++++++++++++++++++++
 .../connector/store/GravitinoCatalogStore.java     |  22 +-
 .../connector/store/TestGravitinoCatalogStore.java | 150 ++++++++++++
 .../provider/DynamicIcebergConfigProvider.java     |  65 +++---
 .../provider/TestDynamicIcebergConfigProvider.java | 254 ++++++++++++++++++--
 .../GravitinoLanceNameSpaceOperations.java         |   7 +-
 .../gravitino/GravitinoLanceNamespaceWrapper.java  |  44 ++++
 .../gravitino/GravitinoLanceTableOperations.java   |   6 +-
 .../TestGravitinoLanceNamespaceWrapper.java        | 233 +++++++++++++++++-
 .../gravitino/TestGravitinoLanceModeParsing.java   |   3 +
 .../spark/connector/catalog/BaseCatalog.java       |  12 +-
 .../connector/catalog/TestBaseCatalogSecrets.java  | 180 ++++++++++++++
 .../connector/catalog/CatalogConnectorManager.java |  13 +-
 .../trino/connector/metadata/GravitinoCatalog.java |  13 +-
 .../trino/connector/GravitinoMockServer.java       |  40 ++--
 .../catalog/TestCatalogConnectorManager.java       |  33 +++
 .../connector/metadata/TestGravitinoCatalog.java   |  17 ++
 22 files changed, 1472 insertions(+), 115 deletions(-)

diff --git a/clients/client-python/gravitino/filesystem/gvfs_base_operations.py 
b/clients/client-python/gravitino/filesystem/gvfs_base_operations.py
index 37c091caf3..71cac7801d 100644
--- a/clients/client-python/gravitino/filesystem/gvfs_base_operations.py
+++ b/clients/client-python/gravitino/filesystem/gvfs_base_operations.py
@@ -495,21 +495,32 @@ class BaseGVFSOperations(ABC):
 
     def _merge_fileset_properties(
         self,
-        catalog: FilesetCatalog,
-        schema: Schema,
-        fileset: Fileset,
+        fileset_ident: NameIdentifier,
         actual_location: str,
     ) -> Dict[str, str]:
-        """Merge properties from catalog, schema, fileset, options, and 
user-defined configs.
-        :param catalog: The fileset catalog
-        :param schema: The schema
-        :param fileset: The fileset
+        """Merge properties from catalog, schema, fileset, options, and 
configs.
+
+        Combines default load*.properties() with get_secrets() so every secret 
URN
+        (including keys that may also appear in credential vending) becomes 
plaintext
+        for FS access. Typed credentials remain available via get_credentials.
+
+        :param fileset_ident: The fileset identifier
         :param actual_location: The actual storage location
         :return: Merged properties dictionary
         """
+        catalog_name = fileset_ident.namespace().level(1)
+        schema_name = fileset_ident.namespace().level(2)
+        catalog = self._get_gravitino_client().load_catalog(catalog_name)
+        schema = catalog.as_schemas().load_schema(schema_name)
+        fileset = catalog.as_fileset_catalog().load_fileset(
+            NameIdentifier.of(schema_name, fileset_ident.name())
+        )
         fileset_props = dict(catalog.properties() or {})
+        fileset_props.update(catalog.get_secrets())
         fileset_props.update(schema.properties() or {})
+        fileset_props.update(schema.get_secrets())
         fileset_props.update(fileset.properties() or {})
+        fileset_props.update(fileset.get_secrets())
         if self._options:
             fileset_props.update(self._options)
         # Get user-defined configurations for the actual location
@@ -530,13 +541,6 @@ class BaseGVFSOperations(ABC):
         :param location_name: The location name, None means the default 
location
         :return: The actual filesystem
         """
-        catalog_ident: NameIdentifier = NameIdentifier.of(
-            self._metalake, fileset_ident.namespace().level(1)
-        )
-        catalog = self._get_fileset_catalog(catalog_ident)
-        schema = self._get_fileset_schema(
-            NameIdentifier.parse(str(fileset_ident.namespace()))
-        )
         fileset = self._get_fileset(fileset_ident)
 
         # Determine target location name
@@ -553,9 +557,7 @@ class BaseGVFSOperations(ABC):
                 f"Cannot find the location: {target_location_name} in fileset: 
{fileset_ident}"
             )
 
-        fileset_props = self._merge_fileset_properties(
-            catalog, schema, fileset, actual_location
-        )
+        fileset_props = self._merge_fileset_properties(fileset_ident, 
actual_location)
 
         # Set caller context for credential vending
         if location_name:
diff --git a/clients/client-python/tests/unittests/mock_base.py 
b/clients/client-python/tests/unittests/mock_base.py
index 684dc77992..ca247d3170 100644
--- a/clients/client-python/tests/unittests/mock_base.py
+++ b/clients/client-python/tests/unittests/mock_base.py
@@ -224,15 +224,27 @@ def mock_load_schema(name: str):
         _last_modifier="test",
         _last_modified_time="2024-04-05T10:10:35.218Z",
     )
-    return SchemaDTO(
+    schema = SchemaDTO(
         _name=name,
         _comment="this is schema test",
         _properties={"schema-prop": "schema-val"},
         _audit=audit_dto,
     )
+    mock_schema = MagicMock()
+    mock_schema.properties.return_value = schema.properties()
+    mock_schema.get_secrets.return_value = {}
+    return mock_schema
 
 
 def mock_data(cls):
+    @patch(
+        "gravitino.client.generic_fileset.GenericFileset.get_secrets",
+        return_value={},
+    )
+    @patch(
+        "gravitino.client.fileset_catalog.FilesetCatalog.get_secrets",
+        return_value={},
+    )
     @patch(
         
"gravitino.client.gravitino_client_base.GravitinoClientBase.load_metalake",
         return_value=mock_load_metalake(),
diff --git a/clients/client-python/tests/unittests/test_gvfs_merge_secrets.py 
b/clients/client-python/tests/unittests/test_gvfs_merge_secrets.py
new file mode 100644
index 0000000000..e13319dd9d
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_gvfs_merge_secrets.py
@@ -0,0 +1,125 @@
+# 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.
+import unittest
+from unittest.mock import MagicMock, patch
+
+from gravitino.filesystem.gvfs_default_operations import DefaultGVFSOperations
+from gravitino.name_identifier import NameIdentifier
+
+
+# pylint: disable=protected-access
+class TestGVFSMergeSecrets(unittest.TestCase):
+    """Unit tests for _merge_fileset_properties including get_secrets()."""
+
+    def test_merge_secrets(self):
+        operations = DefaultGVFSOperations(
+            server_uri="http://localhost:8090";, metalake_name="ml", options={}
+        )
+
+        catalog = MagicMock()
+        schema = MagicMock()
+        fileset = MagicMock()
+        catalog.properties.return_value = {"c-vis": "1"}
+        catalog.get_secrets.return_value = {"c-secret": "cs"}
+        catalog.as_schemas.return_value.load_schema.return_value = schema
+        schema.properties.return_value = {"s-vis": "2"}
+        schema.get_secrets.return_value = {"s-secret": "ss"}
+        catalog.as_fileset_catalog.return_value.load_fileset.return_value = 
fileset
+        fileset.properties.return_value = {"f-vis": "3"}
+        fileset.get_secrets.return_value = {"f-secret": "fs"}
+
+        client = MagicMock()
+        client.load_catalog.return_value = catalog
+
+        with patch.object(operations, "_get_gravitino_client", 
return_value=client):
+            with patch.object(operations, "_get_user_defined_configs", 
return_value={}):
+                merged = operations._merge_fileset_properties(
+                    NameIdentifier.of("ml", "catalog", "schema", "fs"),
+                    "file:///tmp/data",
+                )
+
+        self.assertEqual(merged["c-vis"], "1")
+        self.assertEqual(merged["c-secret"], "cs")
+        self.assertEqual(merged["s-vis"], "2")
+        self.assertEqual(merged["s-secret"], "ss")
+        self.assertEqual(merged["f-vis"], "3")
+        self.assertEqual(merged["f-secret"], "fs")
+
+    def test_secret_override(self):
+        operations = DefaultGVFSOperations(
+            server_uri="http://localhost:8090";, metalake_name="ml", options={}
+        )
+
+        catalog = MagicMock()
+        schema = MagicMock()
+        fileset = MagicMock()
+        catalog.properties.return_value = {"shared": "from-catalog-props"}
+        catalog.get_secrets.return_value = {"shared": "from-catalog-secret"}
+        catalog.as_schemas.return_value.load_schema.return_value = schema
+        schema.properties.return_value = {"shared": "from-schema-props"}
+        schema.get_secrets.return_value = {"shared": "from-schema-secret"}
+        catalog.as_fileset_catalog.return_value.load_fileset.return_value = 
fileset
+        fileset.properties.return_value = {"shared": "from-fileset-props"}
+        fileset.get_secrets.return_value = {"shared": "from-fileset-secret"}
+
+        client = MagicMock()
+        client.load_catalog.return_value = catalog
+
+        with patch.object(operations, "_get_gravitino_client", 
return_value=client):
+            with patch.object(operations, "_get_user_defined_configs", 
return_value={}):
+                merged = operations._merge_fileset_properties(
+                    NameIdentifier.of("ml", "catalog", "schema", "fs"),
+                    "file:///tmp/data",
+                )
+
+        self.assertEqual(merged["shared"], "from-fileset-secret")
+
+    def test_null_props(self):
+        operations = DefaultGVFSOperations(
+            server_uri="http://localhost:8090";, metalake_name="ml", options={}
+        )
+
+        catalog = MagicMock()
+        schema = MagicMock()
+        fileset = MagicMock()
+        catalog.properties.return_value = None
+        catalog.get_secrets.return_value = {"c-secret": "cs"}
+        catalog.as_schemas.return_value.load_schema.return_value = schema
+        schema.properties.return_value = None
+        schema.get_secrets.return_value = {"s-secret": "ss"}
+        catalog.as_fileset_catalog.return_value.load_fileset.return_value = 
fileset
+        fileset.properties.return_value = None
+        fileset.get_secrets.return_value = {"f-secret": "fs"}
+
+        client = MagicMock()
+        client.load_catalog.return_value = catalog
+
+        with patch.object(operations, "_get_gravitino_client", 
return_value=client):
+            with patch.object(operations, "_get_user_defined_configs", 
return_value={}):
+                merged = operations._merge_fileset_properties(
+                    NameIdentifier.of("ml", "catalog", "schema", "fs"),
+                    "file:///tmp/data",
+                )
+
+        self.assertEqual(merged["c-secret"], "cs")
+        self.assertEqual(merged["s-secret"], "ss")
+        self.assertEqual(merged["f-secret"], "fs")
+        self.assertEqual(len(merged), 3)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git 
a/clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java
 
b/clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java
index bece7047f5..19fb0f72ee 100644
--- 
a/clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java
+++ 
b/clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java
@@ -80,6 +80,7 @@ import org.apache.gravitino.exceptions.NoSuchFilesetException;
 import org.apache.gravitino.exceptions.NoSuchLocationNameException;
 import org.apache.gravitino.file.Fileset;
 import org.apache.gravitino.file.FilesetCatalog;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.storage.AzureProperties;
 import org.apache.gravitino.storage.OSSProperties;
 import org.apache.gravitino.storage.S3Properties;
@@ -726,7 +727,7 @@ public abstract class BaseGVFSOperations implements 
Closeable {
           filesetIdent);
 
       Path targetLocation = new 
Path(fileset.storageLocations().get(targetLocationName));
-      Map<String, String> allProperties = getAllProperties(filesetIdent, 
fileset.properties());
+      Map<String, String> allProperties = getAllProperties(filesetIdent);
       allProperties.putAll(
           FilesetUtil.getUserDefinedFileSystemConfigs(
               targetLocation.toUri(), allProperties, 
FS_GRAVITINO_PATH_CONFIG_PREFIX));
@@ -961,21 +962,29 @@ public abstract class BaseGVFSOperations implements 
Closeable {
     return cacheBuilder.build();
   }
 
-  private Map<String, String> getAllProperties(
-      NameIdentifier filesetIdent, Map<String, String> filesetProperties) {
-    Map<String, String> allProperties = new HashMap<>();
-    Catalog catalog =
-        (Catalog)
-            getFilesetCatalog(
-                NameIdentifier.of(
-                    filesetIdent.namespace().level(0), 
filesetIdent.namespace().level(1)));
-    allProperties.putAll(catalog.properties());
-
-    Schema schema = 
getSchema(NameIdentifier.parse(filesetIdent.namespace().toString()));
-    allProperties.putAll(schema.properties());
-    allProperties.putAll(filesetProperties);
-    allProperties.putAll(extractNonDefaultConfig(conf));
-    return allProperties;
+  @VisibleForTesting
+  Map<String, String> getAllProperties(NameIdentifier filesetIdent) {
+    String catalogName = filesetIdent.namespace().level(1);
+    String schemaName = filesetIdent.namespace().level(2);
+    Catalog catalog = getGravitinoClient().loadCatalog(catalogName);
+    Schema schema = catalog.asSchemas().loadSchema(schemaName);
+    Fileset fileset =
+        catalog.asFilesetCatalog().loadFileset(NameIdentifier.of(schemaName, 
filesetIdent.name()));
+
+    Map<String, String> all = new HashMap<>();
+    putPropsAndSecrets(all, catalog.properties(), catalog.supportsSecrets());
+    putPropsAndSecrets(all, schema.properties(), schema.supportsSecrets());
+    putPropsAndSecrets(all, fileset.properties(), fileset.supportsSecrets());
+    all.putAll(extractNonDefaultConfig(conf));
+    return all;
+  }
+
+  private static void putPropsAndSecrets(
+      Map<String, String> target, Map<String, String> props, SupportsSecrets 
secrets) {
+    if (props != null) {
+      target.putAll(props);
+    }
+    target.putAll(secrets.getSecrets());
   }
 
   private Map<String, String> getNecessaryProperties(Map<String, String> 
properties) {
diff --git 
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/GravitinoMockServerBase.java
 
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/GravitinoMockServerBase.java
index b950536d12..aa8b1b6784 100644
--- 
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/GravitinoMockServerBase.java
+++ 
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/GravitinoMockServerBase.java
@@ -37,6 +37,7 @@ import org.apache.gravitino.dto.file.FilesetDTO;
 import org.apache.gravitino.dto.responses.CatalogResponse;
 import org.apache.gravitino.dto.responses.FilesetResponse;
 import org.apache.gravitino.dto.responses.MetalakeResponse;
+import org.apache.gravitino.dto.responses.SecretsResponse;
 import org.apache.gravitino.dto.responses.VersionResponse;
 import org.apache.gravitino.file.Fileset;
 import org.apache.gravitino.json.JsonUtils;
@@ -67,12 +68,14 @@ public abstract class GravitinoMockServerBase {
     mockServer = ClientAndServer.startClientAndServer(0);
     port = mockServer.getLocalPort();
     mockAPIVersion();
+    mockEmptySecretsAPI();
   }
 
   @AfterEach
   public void reset() {
     mockServer.reset();
     mockAPIVersion();
+    mockEmptySecretsAPI();
   }
 
   @AfterAll
@@ -132,6 +135,22 @@ public abstract class GravitinoMockServerBase {
     }
   }
 
+  /** Mocks metadata-object secrets endpoints used by {@code 
getAllProperties}. */
+  protected static void mockEmptySecretsAPI() {
+    try {
+      String emptySecretsJson = MAPPER.writeValueAsString(new 
SecretsResponse(ImmutableMap.of()));
+      mockServer
+          .when(
+              HttpRequest.request()
+                  .withMethod("GET")
+                  .withPath("/api/metalakes/.*/objects/.*/.*/secrets"),
+              Times.unlimited())
+          
.respond(HttpResponse.response().withStatusCode(SC_OK).withBody(emptySecretsJson));
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   protected static void mockMetalakeDTO(String name, String comment) {
     MetalakeDTO mockMetalake =
         MetalakeDTO.builder()
diff --git 
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestBaseGVFSOperationsSecrets.java
 
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestBaseGVFSOperationsSecrets.java
new file mode 100644
index 0000000000..2aa4c129c6
--- /dev/null
+++ 
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestBaseGVFSOperationsSecrets.java
@@ -0,0 +1,260 @@
+/*
+ * 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.filesystem.hadoop;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.SupportsSchemas;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.file.FilesetCatalog;
+import org.apache.gravitino.secret.SupportsSecrets;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FSDataInputStream;
+import org.apache.hadoop.fs.FSDataOutputStream;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.permission.FsPermission;
+import org.apache.hadoop.security.Credentials;
+import org.apache.hadoop.security.token.Token;
+import org.apache.hadoop.util.Progressable;
+import org.junit.jupiter.api.Test;
+
+public class TestBaseGVFSOperationsSecrets {
+
+  @Test
+  public void testMergeSecrets() throws Exception {
+    Configuration conf = new Configuration();
+    
conf.set(GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_METALAKE_KEY,
 "ml");
+    conf.set(
+        GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_SERVER_URI_KEY,
+        "http://localhost:8090";);
+
+    Catalog catalog = mock(Catalog.class);
+    Schema schema = mock(Schema.class);
+    Fileset fileset = mock(Fileset.class);
+    SupportsSchemas schemas = mock(SupportsSchemas.class);
+    FilesetCatalog filesetCatalog = mock(FilesetCatalog.class);
+    SupportsSecrets catalogSecrets = mock(SupportsSecrets.class);
+    SupportsSecrets schemaSecrets = mock(SupportsSecrets.class);
+    SupportsSecrets filesetSecrets = mock(SupportsSecrets.class);
+
+    when(catalog.properties()).thenReturn(Map.of("c-vis", "1"));
+    when(catalog.supportsSecrets()).thenReturn(catalogSecrets);
+    when(catalogSecrets.getSecrets()).thenReturn(Map.of("c-secret", "cs"));
+    when(catalog.asSchemas()).thenReturn(schemas);
+    when(schemas.loadSchema("schema")).thenReturn(schema);
+    when(schema.properties()).thenReturn(Map.of("s-vis", "2"));
+    when(schema.supportsSecrets()).thenReturn(schemaSecrets);
+    when(schemaSecrets.getSecrets()).thenReturn(Map.of("s-secret", "ss"));
+    when(catalog.asFilesetCatalog()).thenReturn(filesetCatalog);
+    when(filesetCatalog.loadFileset(NameIdentifier.of("schema", 
"fs"))).thenReturn(fileset);
+    when(fileset.properties()).thenReturn(Map.of("f-vis", "3"));
+    when(fileset.supportsSecrets()).thenReturn(filesetSecrets);
+    when(filesetSecrets.getSecrets()).thenReturn(Map.of("f-secret", "fs"));
+
+    GravitinoClient client = mock(GravitinoClient.class);
+    when(client.loadCatalog("catalog")).thenReturn(catalog);
+
+    TestOps ops = new TestOps(conf, client);
+    Map<String, String> all =
+        ops.getAllProperties(NameIdentifier.of("ml", "catalog", "schema", 
"fs"));
+
+    assertEquals("1", all.get("c-vis"));
+    assertEquals("cs", all.get("c-secret"));
+    assertEquals("2", all.get("s-vis"));
+    assertEquals("ss", all.get("s-secret"));
+    assertEquals("3", all.get("f-vis"));
+    assertEquals("fs", all.get("f-secret"));
+  }
+
+  @Test
+  public void testSecretOverride() throws Exception {
+    Configuration conf = new Configuration();
+    
conf.set(GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_METALAKE_KEY,
 "ml");
+    conf.set(
+        GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_SERVER_URI_KEY,
+        "http://localhost:8090";);
+
+    Catalog catalog = mock(Catalog.class);
+    Schema schema = mock(Schema.class);
+    Fileset fileset = mock(Fileset.class);
+    SupportsSchemas schemas = mock(SupportsSchemas.class);
+    FilesetCatalog filesetCatalog = mock(FilesetCatalog.class);
+    SupportsSecrets catalogSecrets = mock(SupportsSecrets.class);
+    SupportsSecrets schemaSecrets = mock(SupportsSecrets.class);
+    SupportsSecrets filesetSecrets = mock(SupportsSecrets.class);
+
+    when(catalog.properties()).thenReturn(Map.of("shared", 
"from-catalog-props"));
+    when(catalog.supportsSecrets()).thenReturn(catalogSecrets);
+    when(catalogSecrets.getSecrets()).thenReturn(Map.of("shared", 
"from-catalog-secret"));
+    when(catalog.asSchemas()).thenReturn(schemas);
+    when(schemas.loadSchema("schema")).thenReturn(schema);
+    when(schema.properties()).thenReturn(Map.of("shared", 
"from-schema-props"));
+    when(schema.supportsSecrets()).thenReturn(schemaSecrets);
+    when(schemaSecrets.getSecrets()).thenReturn(Map.of("shared", 
"from-schema-secret"));
+    when(catalog.asFilesetCatalog()).thenReturn(filesetCatalog);
+    when(filesetCatalog.loadFileset(NameIdentifier.of("schema", 
"fs"))).thenReturn(fileset);
+    when(fileset.properties()).thenReturn(Map.of("shared", 
"from-fileset-props"));
+    when(fileset.supportsSecrets()).thenReturn(filesetSecrets);
+    when(filesetSecrets.getSecrets()).thenReturn(Map.of("shared", 
"from-fileset-secret"));
+
+    GravitinoClient client = mock(GravitinoClient.class);
+    when(client.loadCatalog("catalog")).thenReturn(catalog);
+
+    TestOps ops = new TestOps(conf, client);
+    Map<String, String> all =
+        ops.getAllProperties(NameIdentifier.of("ml", "catalog", "schema", 
"fs"));
+
+    assertEquals("from-fileset-secret", all.get("shared"));
+  }
+
+  @Test
+  public void testNullProps() throws Exception {
+    Configuration conf = new Configuration();
+    
conf.set(GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_METALAKE_KEY,
 "ml");
+    conf.set(
+        GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_SERVER_URI_KEY,
+        "http://localhost:8090";);
+
+    Catalog catalog = mock(Catalog.class);
+    Schema schema = mock(Schema.class);
+    Fileset fileset = mock(Fileset.class);
+    SupportsSchemas schemas = mock(SupportsSchemas.class);
+    FilesetCatalog filesetCatalog = mock(FilesetCatalog.class);
+    SupportsSecrets catalogSecrets = mock(SupportsSecrets.class);
+    SupportsSecrets schemaSecrets = mock(SupportsSecrets.class);
+    SupportsSecrets filesetSecrets = mock(SupportsSecrets.class);
+
+    when(catalog.properties()).thenReturn(null);
+    when(catalog.supportsSecrets()).thenReturn(catalogSecrets);
+    when(catalogSecrets.getSecrets()).thenReturn(Map.of("c-secret", "cs"));
+    when(catalog.asSchemas()).thenReturn(schemas);
+    when(schemas.loadSchema("schema")).thenReturn(schema);
+    when(schema.properties()).thenReturn(null);
+    when(schema.supportsSecrets()).thenReturn(schemaSecrets);
+    when(schemaSecrets.getSecrets()).thenReturn(Map.of("s-secret", "ss"));
+    when(catalog.asFilesetCatalog()).thenReturn(filesetCatalog);
+    when(filesetCatalog.loadFileset(NameIdentifier.of("schema", 
"fs"))).thenReturn(fileset);
+    when(fileset.properties()).thenReturn(null);
+    when(fileset.supportsSecrets()).thenReturn(filesetSecrets);
+    when(filesetSecrets.getSecrets()).thenReturn(Map.of("f-secret", "fs"));
+
+    GravitinoClient client = mock(GravitinoClient.class);
+    when(client.loadCatalog("catalog")).thenReturn(catalog);
+
+    TestOps ops = new TestOps(conf, client);
+    Map<String, String> all =
+        ops.getAllProperties(NameIdentifier.of("ml", "catalog", "schema", 
"fs"));
+
+    assertEquals("cs", all.get("c-secret"));
+    assertEquals("ss", all.get("s-secret"));
+    assertEquals("fs", all.get("f-secret"));
+  }
+
+  private static final class TestOps extends BaseGVFSOperations {
+    private final GravitinoClient client;
+
+    private TestOps(Configuration configuration, GravitinoClient client) {
+      super(configuration);
+      this.client = client;
+    }
+
+    @Override
+    GravitinoClient getGravitinoClient() {
+      return client;
+    }
+
+    @Override
+    public FSDataInputStream open(Path gvfsPath, int bufferSize) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setWorkingDirectory(Path gvfsDir) {}
+
+    @Override
+    public FSDataOutputStream create(
+        Path gvfsPath,
+        FsPermission permission,
+        boolean overwrite,
+        int bufferSize,
+        short replication,
+        long blockSize,
+        Progressable progress)
+        throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public FSDataOutputStream append(Path gvfsPath, int bufferSize, 
Progressable progress) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean rename(Path srcGvfsPath, Path dstGvfsPath) {
+      return false;
+    }
+
+    @Override
+    public boolean delete(Path gvfsPath, boolean recursive) {
+      return false;
+    }
+
+    @Override
+    public FileStatus getFileStatus(Path gvfsPath) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public FileStatus[] listStatus(Path gvfsPath) {
+      return new FileStatus[0];
+    }
+
+    @Override
+    public boolean mkdirs(Path gvfsPath, FsPermission permission) {
+      return false;
+    }
+
+    @Override
+    public short getDefaultReplication(Path gvfsPath) {
+      return 1;
+    }
+
+    @Override
+    public long getDefaultBlockSize(Path gvfsPath) {
+      return 1L;
+    }
+
+    @Override
+    public Token<?>[] addDelegationTokens(String renewer, Credentials 
credentials) {
+      return new Token<?>[0];
+    }
+
+    @Override
+    public void close() {}
+  }
+}
diff --git 
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/store/GravitinoCatalogStore.java
 
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/store/GravitinoCatalogStore.java
index fb7c15d29f..ac45fa74e0 100644
--- 
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/store/GravitinoCatalogStore.java
+++ 
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/store/GravitinoCatalogStore.java
@@ -20,6 +20,8 @@
 package org.apache.gravitino.flink.connector.store;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -97,11 +99,12 @@ public class GravitinoCatalogStore extends 
AbstractCatalogStore {
   public Optional<CatalogDescriptor> getCatalog(String catalogName) throws 
CatalogException {
     try {
       Catalog catalog = 
gravitinoCatalogManager.getGravitinoCatalogInfo(catalogName);
-      BaseCatalogFactory catalogFactory = 
getCatalogFactory(catalog.provider());
+      BaseCatalogFactory catalogFactory = 
catalogFactoryForProvider(catalog.provider());
       CatalogPropertiesConverter catalogPropertiesConverter =
           catalogFactory.catalogPropertiesConverter();
+      Map<String, String> catalogProperties = propsWithSecrets(catalog);
       Map<String, String> flinkCatalogProperties =
-          
catalogPropertiesConverter.toFlinkCatalogProperties(catalog.properties());
+          
catalogPropertiesConverter.toFlinkCatalogProperties(catalogProperties);
       CatalogDescriptor descriptor =
           newCatalogDescriptor(catalogName, 
Configuration.fromMap(flinkCatalogProperties));
       return Optional.of(descriptor);
@@ -150,7 +153,13 @@ public class GravitinoCatalogStore extends 
AbstractCatalogStore {
             catalogType));
   }
 
-  private BaseCatalogFactory getCatalogFactory(String provider) {
+  /**
+   * Resolve the Flink catalog factory for a Gravitino provider. 
Package-visible for unit tests.
+   *
+   * @param provider Gravitino catalog provider name
+   * @return matching {@link BaseCatalogFactory}
+   */
+  BaseCatalogFactory catalogFactoryForProvider(String provider) {
     return discoverFactories(
         catalogFactory ->
             ((BaseCatalogFactory) catalogFactory)
@@ -210,4 +219,11 @@ public class GravitinoCatalogStore extends 
AbstractCatalogStore {
     }
     return (BaseCatalogFactory) factories.get(0);
   }
+
+  private static Map<String, String> propsWithSecrets(Catalog catalog) {
+    Map<String, String> props =
+        new HashMap<>(catalog.properties() == null ? Collections.emptyMap() : 
catalog.properties());
+    props.putAll(catalog.supportsSecrets().getSecrets());
+    return props;
+  }
 }
diff --git 
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/store/TestGravitinoCatalogStore.java
 
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/store/TestGravitinoCatalogStore.java
index ac3a74a42e..fc83d072cd 100644
--- 
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/store/TestGravitinoCatalogStore.java
+++ 
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/store/TestGravitinoCatalogStore.java
@@ -18,6 +18,7 @@
  */
 package org.apache.gravitino.flink.connector.store;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -25,13 +26,28 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
 import java.util.ServiceConfigurationError;
 import java.util.function.Predicate;
+import org.apache.flink.table.catalog.CatalogDescriptor;
 import org.apache.flink.table.catalog.exceptions.CatalogException;
 import org.apache.flink.table.factories.Factory;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.flink.connector.CatalogPropertiesConverter;
 import org.apache.gravitino.flink.connector.catalog.BaseCatalogFactory;
 import org.apache.gravitino.flink.connector.catalog.GravitinoCatalogManager;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretManager;
+import org.apache.gravitino.secret.SecretMaterial;
+import org.apache.gravitino.secret.SecretPropertyUtils;
+import org.apache.gravitino.secret.SecretProviderRegistry;
+import org.apache.gravitino.secret.SupportsSecrets;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -131,4 +147,138 @@ public class TestGravitinoCatalogStore {
 
     assertSame(expectedFactory, actualFactory);
   }
+
+  @Test
+  public void testMergeSecrets() {
+    Catalog catalog = mock(Catalog.class);
+    SupportsSecrets supportsSecrets = mock(SupportsSecrets.class);
+    
when(gravitinoCatalogMockManager.getGravitinoCatalogInfo("sec")).thenReturn(catalog);
+    when(catalog.provider()).thenReturn("test-provider");
+    when(catalog.properties()).thenReturn(Map.of("visible", "v1"));
+    when(catalog.supportsSecrets()).thenReturn(supportsSecrets);
+    when(supportsSecrets.getSecrets()).thenReturn(Map.of("jdbc-password", 
"secret"));
+
+    BaseCatalogFactory factory = mock(BaseCatalogFactory.class);
+    CatalogPropertiesConverter converter = 
mock(CatalogPropertiesConverter.class);
+    when(factory.catalogPropertiesConverter()).thenReturn(converter);
+    
when(converter.toFlinkCatalogProperties(org.mockito.ArgumentMatchers.anyMap()))
+        .thenAnswer(
+            invocation -> {
+              Map<String, String> in = invocation.getArgument(0);
+              Map<String, String> out = new HashMap<>(in);
+              out.put("type", "generic_in_memory");
+              return out;
+            });
+
+    GravitinoCatalogStore store =
+        new GravitinoCatalogStore(gravitinoCatalogMockManager) {
+          @Override
+          BaseCatalogFactory catalogFactoryForProvider(String provider) {
+            return factory;
+          }
+        };
+
+    Optional<CatalogDescriptor> descriptor = store.getCatalog("sec");
+    assertTrue(descriptor.isPresent());
+    assertEquals("secret", 
descriptor.get().getConfiguration().toMap().get("jdbc-password"));
+    assertEquals("v1", 
descriptor.get().getConfiguration().toMap().get("visible"));
+  }
+
+  @Test
+  public void testMergeSecretsNullProps() {
+    Catalog catalog = mock(Catalog.class);
+    SupportsSecrets supportsSecrets = mock(SupportsSecrets.class);
+    
when(gravitinoCatalogMockManager.getGravitinoCatalogInfo("sec-null")).thenReturn(catalog);
+    when(catalog.provider()).thenReturn("test-provider");
+    when(catalog.properties()).thenReturn(null);
+    when(catalog.supportsSecrets()).thenReturn(supportsSecrets);
+    when(supportsSecrets.getSecrets()).thenReturn(Map.of("jdbc-password", 
"secret"));
+
+    BaseCatalogFactory factory = mock(BaseCatalogFactory.class);
+    CatalogPropertiesConverter converter = 
mock(CatalogPropertiesConverter.class);
+    when(factory.catalogPropertiesConverter()).thenReturn(converter);
+    
when(converter.toFlinkCatalogProperties(org.mockito.ArgumentMatchers.anyMap()))
+        .thenAnswer(
+            invocation -> {
+              Map<String, String> in = invocation.getArgument(0);
+              Map<String, String> out = new HashMap<>(in);
+              out.put("type", "generic_in_memory");
+              return out;
+            });
+
+    GravitinoCatalogStore store =
+        new GravitinoCatalogStore(gravitinoCatalogMockManager) {
+          @Override
+          BaseCatalogFactory catalogFactoryForProvider(String provider) {
+            return factory;
+          }
+        };
+
+    Optional<CatalogDescriptor> descriptor = store.getCatalog("sec-null");
+    assertTrue(descriptor.isPresent());
+    assertEquals("secret", 
descriptor.get().getConfiguration().toMap().get("jdbc-password"));
+  }
+
+  @Test
+  public void testMergeMemorySecrets() {
+    try (SecretManager sm = memorySecretManager()) {
+      Map<String, String> entityProps = new HashMap<>();
+      entityProps.put("jdbc-user", "root");
+      java.util.List<SecretMaterial> writes =
+          sm.assembleSecretMaterials(
+              Map.of("jdbc-user", "root"),
+              entityProps,
+              "catalog",
+              2L,
+              Map.of("jdbc-password", new SecretBinding("memory", "mem-pwd")),
+              Map.of());
+      sm.writeSecrets(writes);
+      Map<String, String> secrets = SecretPropertyUtils.buildSecrets(sm, 
entityProps);
+
+      Catalog catalog = mock(Catalog.class);
+      SupportsSecrets supportsSecrets = mock(SupportsSecrets.class);
+      
when(gravitinoCatalogMockManager.getGravitinoCatalogInfo("mem")).thenReturn(catalog);
+      when(catalog.provider()).thenReturn("test-provider");
+      when(catalog.properties()).thenReturn(Map.of("jdbc-url", 
"jdbc:mysql://localhost/db"));
+      when(catalog.supportsSecrets()).thenReturn(supportsSecrets);
+      when(supportsSecrets.getSecrets()).thenReturn(secrets);
+
+      BaseCatalogFactory factory = mock(BaseCatalogFactory.class);
+      CatalogPropertiesConverter converter = 
mock(CatalogPropertiesConverter.class);
+      when(factory.catalogPropertiesConverter()).thenReturn(converter);
+      
when(converter.toFlinkCatalogProperties(org.mockito.ArgumentMatchers.anyMap()))
+          .thenAnswer(
+              invocation -> {
+                Map<String, String> in = invocation.getArgument(0);
+                Map<String, String> out = new HashMap<>(in);
+                out.put("type", "generic_in_memory");
+                return out;
+              });
+
+      GravitinoCatalogStore store =
+          new GravitinoCatalogStore(gravitinoCatalogMockManager) {
+            @Override
+            BaseCatalogFactory catalogFactoryForProvider(String provider) {
+              return factory;
+            }
+          };
+
+      Optional<CatalogDescriptor> descriptor = store.getCatalog("mem");
+      assertTrue(descriptor.isPresent());
+      assertEquals("mem-pwd", 
descriptor.get().getConfiguration().toMap().get("jdbc-password"));
+    }
+  }
+
+  private static SecretManager memorySecretManager() {
+    Config config = new Config(false) {};
+    Properties properties = new Properties();
+    properties.setProperty(SecretProviderRegistry.GRAVITINO_SECRET_PROVIDERS, 
"memory");
+    properties.setProperty(
+        SecretProviderRegistry.GRAVITINO_SECRET_PROVIDER_PREFIX
+            + "memory."
+            + SecretProviderRegistry.CLASS_NAME,
+        InMemorySecretsProvider.class.getName());
+    config.loadFromProperties(properties);
+    return new SecretManager(config);
+  }
 }
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 43ec66812e..b4f724626f 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
@@ -43,6 +43,7 @@ import org.apache.gravitino.credential.SupportsCredentials;
 import org.apache.gravitino.exceptions.NoSuchCatalogException;
 import org.apache.gravitino.iceberg.common.IcebergConfig;
 import 
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.server.web.JettyServerConfig;
 import org.apache.gravitino.utils.MapUtils;
 import org.apache.gravitino.utils.NameIdentifierUtil;
@@ -104,36 +105,44 @@ public class DynamicIcebergConfigProvider implements 
IcebergConfigProvider {
         "lakehouse-iceberg".equals(catalog.provider()),
         String.format("%s.%s is not iceberg catalog", gravitinoMetalake, 
catalogName));
 
-    // Sensitive credentials (e.g. jdbc-password) are marked hidden in 
PropertiesMetadata and
-    // filtered out of catalog.properties(). We need two different strategies 
to recover them:
-    //
-    // Auxiliary mode: the catalog is a BaseCatalog running in the same JVM as 
the Gravitino
-    // server. Call propertiesWithCredentialProviders() which returns the raw 
entity properties
-    // including all hidden fields.
-    //
-    // Standalone mode: the catalog is a client-side object obtained via the 
Gravitino REST API.
-    // Call getCredentials() to retrieve vended credentials, then inject any 
JdbcCredential
-    // fields into the properties map so the JDBC backend can connect.
-    Map<String, String> catalogProperties;
+    // Auxiliary: BaseCatalog + SecretManager plaintext. Standalone: 
properties + getSecrets,
+    // then JdbcCredential overlays so credentials win.
+    return 
Optional.of(getIcebergConfigFromCatalogProperties(resolveProps(catalog)));
+  }
+
+  private static Map<String, String> resolveProps(Catalog catalog) {
     if (catalog instanceof BaseCatalog) {
-      catalogProperties = ((BaseCatalog<?>) 
catalog).propertiesWithCredentialProviders();
-    } else {
-      catalogProperties = new HashMap<>(catalog.properties());
-      if (catalog instanceof SupportsCredentials) {
-        Arrays.stream(((SupportsCredentials) catalog).getCredentials())
-            .filter(c -> c instanceof JdbcCredential)
-            .map(c -> (JdbcCredential) c)
-            .findFirst()
-            .ifPresent(
-                jdbc -> {
-                  catalogProperties.putIfAbsent(
-                      IcebergConstants.GRAVITINO_JDBC_USER, jdbc.jdbcUser());
-                  catalogProperties.putIfAbsent(
-                      IcebergConstants.GRAVITINO_JDBC_PASSWORD, 
jdbc.jdbcPassword());
-                });
+      return new HashMap<>(
+          GravitinoEnv.getInstance()
+              .secretManager()
+              .toPlaintextProperties(
+                  ((BaseCatalog<?>) 
catalog).propertiesWithCredentialProviders()));
+    }
+    Map<String, String> props =
+        new HashMap<>(catalog.properties() == null ? Map.of() : 
catalog.properties());
+    try {
+      SupportsSecrets supportsSecrets = catalog.supportsSecrets();
+      if (supportsSecrets != null) {
+        Map<String, String> secrets = supportsSecrets.getSecrets();
+        if (secrets != null) {
+          props.putAll(secrets);
+        }
       }
+    } catch (UnsupportedOperationException ignored) {
+      // Catalog does not support secret property operations.
+    }
+    if (catalog instanceof SupportsCredentials) {
+      Arrays.stream(((SupportsCredentials) catalog).getCredentials())
+          .filter(c -> c instanceof JdbcCredential)
+          .map(c -> (JdbcCredential) c)
+          .findFirst()
+          .ifPresent(
+              jdbc -> {
+                props.put(IcebergConstants.GRAVITINO_JDBC_USER, 
jdbc.jdbcUser());
+                props.put(IcebergConstants.GRAVITINO_JDBC_PASSWORD, 
jdbc.jdbcPassword());
+              });
     }
-    return 
Optional.of(getIcebergConfigFromCatalogProperties(catalogProperties));
+    return props;
   }
 
   /**
@@ -317,7 +326,7 @@ public class DynamicIcebergConfigProvider implements 
IcebergConfigProvider {
 
     @Override
     public Catalog loadCatalog(String catalogName) throws 
NoSuchCatalogException {
-      return 
getGravitinoClient().loadMetalake(metalake).loadCatalog(catalogName);
+      return getGravitinoClient().loadCatalog(catalogName);
     }
 
     @Override
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 d5b2024152..42807c1816 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
@@ -24,6 +24,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Properties;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -31,14 +32,26 @@ import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Config;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.catalog.CatalogDispatcher;
 import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
+import org.apache.gravitino.connector.BaseCatalog;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.JdbcCredential;
+import org.apache.gravitino.credential.SupportsCredentials;
 import org.apache.gravitino.exceptions.NoSuchCatalogException;
 import org.apache.gravitino.iceberg.common.IcebergConfig;
 import org.apache.gravitino.iceberg.common.ops.IcebergCatalogWrapper;
 import 
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretManager;
+import org.apache.gravitino.secret.SecretMaterial;
+import org.apache.gravitino.secret.SecretPropertyUtils;
+import org.apache.gravitino.secret.SecretProviderRegistry;
+import org.apache.gravitino.secret.SupportsSecrets;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.apache.iceberg.hive.HiveCatalog;
 import org.apache.iceberg.jdbc.JdbcCatalog;
@@ -63,6 +76,7 @@ public class TestDynamicIcebergConfigProvider {
   public void tearDown() throws IllegalAccessException {
     // Clean up GravitinoEnv and IcebergRESTServerContext state after each test
     FieldUtils.writeField(GravitinoEnv.getInstance(), 
"internalCatalogDispatcher", null, true);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "secretManager", null, 
true);
     resetServerContext();
   }
 
@@ -94,12 +108,15 @@ public class TestDynamicIcebergConfigProvider {
   private void setMockCatalogFetcher(
       DynamicIcebergConfigProvider provider, Map<String, Catalog> catalogMap) {
     DynamicIcebergConfigProvider.CatalogFetcher mockFetcher =
-        catalogName -> {
-          Catalog catalog = catalogMap.get(catalogName);
-          if (catalog == null) {
-            throw new NoSuchCatalogException("Catalog not found: %s", 
catalogName);
+        new DynamicIcebergConfigProvider.CatalogFetcher() {
+          @Override
+          public Catalog loadCatalog(String catalogName) throws 
NoSuchCatalogException {
+            Catalog catalog = catalogMap.get(catalogName);
+            if (catalog == null) {
+              throw new NoSuchCatalogException("Catalog not found: %s", 
catalogName);
+            }
+            return catalog;
           }
-          return catalog;
         };
     provider.setCatalogFetcher(mockFetcher);
   }
@@ -289,15 +306,15 @@ public class TestDynamicIcebergConfigProvider {
 
     NameIdentifier catalogIdent = NameIdentifierUtil.ofCatalog(metalakeName, 
catalogName);
     
Mockito.when(mockInternalCatalogDispatcher.loadCatalog(catalogIdent)).thenReturn(mockCatalog);
+    Map<String, String> catalogProperties =
+        new HashMap<String, String>() {
+          {
+            put(IcebergConstants.CATALOG_BACKEND, "custom");
+            put(IcebergConstants.CATALOG_BACKEND_NAME, catalogName);
+          }
+        };
     Mockito.when(mockCatalog.provider()).thenReturn("lakehouse-iceberg");
-    Mockito.when(mockCatalog.properties())
-        .thenReturn(
-            new HashMap<String, String>() {
-              {
-                put(IcebergConstants.CATALOG_BACKEND, "custom");
-                put(IcebergConstants.CATALOG_BACKEND_NAME, catalogName);
-              }
-            });
+    Mockito.when(mockCatalog.properties()).thenReturn(catalogProperties);
 
     // Set the mock CatalogDispatchers to GravitinoEnv
     FieldUtils.writeField(
@@ -516,15 +533,15 @@ public class TestDynamicIcebergConfigProvider {
 
     NameIdentifier catalogIdent = NameIdentifierUtil.ofCatalog(metalakeName, 
catalogName);
     
Mockito.when(mockInternalCatalogDispatcher.loadCatalog(catalogIdent)).thenReturn(mockCatalog);
+    Map<String, String> catalogProperties =
+        new HashMap<String, String>() {
+          {
+            put(IcebergConstants.CATALOG_BACKEND, "custom");
+            put(IcebergConstants.CATALOG_BACKEND_NAME, catalogName);
+          }
+        };
     Mockito.when(mockCatalog.provider()).thenReturn("lakehouse-iceberg");
-    Mockito.when(mockCatalog.properties())
-        .thenReturn(
-            new HashMap<String, String>() {
-              {
-                put(IcebergConstants.CATALOG_BACKEND, "custom");
-                put(IcebergConstants.CATALOG_BACKEND_NAME, catalogName);
-              }
-            });
+    Mockito.when(mockCatalog.properties()).thenReturn(catalogProperties);
 
     // Set the mock CatalogDispatchers to GravitinoEnv
     FieldUtils.writeField(
@@ -584,4 +601,199 @@ public class TestDynamicIcebergConfigProvider {
     executor.shutdown();
     executor.awaitTermination(5, TimeUnit.SECONDS);
   }
+
+  @Test
+  public void testMergeSecrets() {
+    String metalakeName = "test_metalake";
+    String catalogName = "jdbc_catalog";
+
+    Catalog mockCatalog = Mockito.mock(Catalog.class);
+    SupportsSecrets supportsSecrets = Mockito.mock(SupportsSecrets.class);
+    Mockito.when(mockCatalog.provider()).thenReturn("lakehouse-iceberg");
+    Mockito.when(mockCatalog.properties())
+        .thenReturn(
+            new HashMap<String, String>() {
+              {
+                put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+                put(IcebergConstants.CATALOG_BACKEND_NAME, catalogName);
+                put(IcebergConstants.WAREHOUSE, "s3://bucket/wh");
+              }
+            });
+    Mockito.when(mockCatalog.supportsSecrets()).thenReturn(supportsSecrets);
+    Mockito.when(supportsSecrets.getSecrets())
+        .thenReturn(Map.of(IcebergConstants.GRAVITINO_JDBC_PASSWORD, 
"secret-pwd"));
+
+    Map<String, String> properties = new HashMap<>();
+    properties.put(IcebergConstants.GRAVITINO_URI, "http://localhost:8090";);
+    properties.put(IcebergConstants.GRAVITINO_METALAKE, metalakeName);
+
+    DynamicIcebergConfigProvider provider = new DynamicIcebergConfigProvider();
+    provider.initialize(properties);
+    setMockCatalogFetcher(provider, Map.of(catalogName, mockCatalog));
+
+    Optional<IcebergConfig> config = 
provider.getIcebergCatalogConfig(catalogName);
+    Assertions.assertTrue(config.isPresent());
+    Assertions.assertEquals(
+        "secret-pwd",
+        
config.get().getIcebergCatalogProperties().get(IcebergConstants.GRAVITINO_JDBC_PASSWORD));
+  }
+
+  @Test
+  public void testCredsOverrideSecrets() {
+    String metalakeName = "test_metalake";
+    String catalogName = "jdbc_catalog";
+
+    Catalog mockCatalog =
+        Mockito.mock(
+            Catalog.class,
+            Mockito.withSettings()
+                .extraInterfaces(SupportsCredentials.class, 
SupportsSecrets.class));
+    SupportsSecrets supportsSecrets = (SupportsSecrets) mockCatalog;
+    SupportsCredentials supportsCredentials = (SupportsCredentials) 
mockCatalog;
+
+    Mockito.when(mockCatalog.provider()).thenReturn("lakehouse-iceberg");
+    Mockito.when(mockCatalog.properties())
+        .thenReturn(
+            new HashMap<String, String>() {
+              {
+                put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+                put(IcebergConstants.CATALOG_BACKEND_NAME, catalogName);
+              }
+            });
+    Mockito.when(mockCatalog.supportsSecrets()).thenReturn(supportsSecrets);
+    Mockito.when(supportsSecrets.getSecrets())
+        .thenReturn(
+            Map.of(
+                IcebergConstants.GRAVITINO_JDBC_USER,
+                "from-secret",
+                IcebergConstants.GRAVITINO_JDBC_PASSWORD,
+                "secret-pwd"));
+    Mockito.when(supportsCredentials.getCredentials())
+        .thenReturn(new Credential[] {new JdbcCredential("cred-user", 
"cred-pwd")});
+
+    Map<String, String> properties = new HashMap<>();
+    properties.put(IcebergConstants.GRAVITINO_URI, "http://localhost:8090";);
+    properties.put(IcebergConstants.GRAVITINO_METALAKE, metalakeName);
+
+    DynamicIcebergConfigProvider provider = new DynamicIcebergConfigProvider();
+    provider.initialize(properties);
+    setMockCatalogFetcher(provider, Map.of(catalogName, mockCatalog));
+
+    Optional<IcebergConfig> config = 
provider.getIcebergCatalogConfig(catalogName);
+    Assertions.assertTrue(config.isPresent());
+    Map<String, String> icebergProps = 
config.get().getIcebergCatalogProperties();
+    Assertions.assertEquals("cred-user", 
icebergProps.get(IcebergConstants.GRAVITINO_JDBC_USER));
+    Assertions.assertEquals("cred-pwd", 
icebergProps.get(IcebergConstants.GRAVITINO_JDBC_PASSWORD));
+  }
+
+  @Test
+  public void testMergeMemorySecrets() {
+    try (SecretManager sm = memorySecretManager()) {
+      Map<String, String> entityProps = new HashMap<>();
+      entityProps.put(IcebergConstants.GRAVITINO_JDBC_USER, "root");
+      List<SecretMaterial> writes =
+          sm.assembleSecretMaterials(
+              Map.of(IcebergConstants.GRAVITINO_JDBC_USER, "root"),
+              entityProps,
+              "catalog",
+              3L,
+              Map.of(
+                  IcebergConstants.GRAVITINO_JDBC_PASSWORD,
+                  new SecretBinding("memory", "mem-jdbc-pwd")),
+              Map.of());
+      sm.writeSecrets(writes);
+      Map<String, String> secrets = SecretPropertyUtils.buildSecrets(sm, 
entityProps);
+
+      String metalakeName = "test_metalake";
+      String catalogName = "jdbc_catalog";
+      Catalog mockCatalog = Mockito.mock(Catalog.class);
+      SupportsSecrets supportsSecrets = Mockito.mock(SupportsSecrets.class);
+      Mockito.when(mockCatalog.provider()).thenReturn("lakehouse-iceberg");
+      Mockito.when(mockCatalog.properties())
+          .thenReturn(
+              new HashMap<String, String>() {
+                {
+                  put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+                  put(IcebergConstants.CATALOG_BACKEND_NAME, catalogName);
+                }
+              });
+      Mockito.when(mockCatalog.supportsSecrets()).thenReturn(supportsSecrets);
+      Mockito.when(supportsSecrets.getSecrets()).thenReturn(secrets);
+
+      Map<String, String> properties = new HashMap<>();
+      properties.put(IcebergConstants.GRAVITINO_URI, "http://localhost:8090";);
+      properties.put(IcebergConstants.GRAVITINO_METALAKE, metalakeName);
+
+      DynamicIcebergConfigProvider provider = new 
DynamicIcebergConfigProvider();
+      provider.initialize(properties);
+      setMockCatalogFetcher(provider, Map.of(catalogName, mockCatalog));
+
+      Optional<IcebergConfig> config = 
provider.getIcebergCatalogConfig(catalogName);
+      Assertions.assertTrue(config.isPresent());
+      Assertions.assertEquals(
+          "mem-jdbc-pwd",
+          
config.get().getIcebergCatalogProperties().get(IcebergConstants.GRAVITINO_JDBC_PASSWORD));
+    }
+  }
+
+  @Test
+  public void testAuxPlaintext() throws Exception {
+    try (SecretManager sm = memorySecretManager()) {
+      String metalakeName = "test_metalake";
+      String catalogName = "jdbc_catalog";
+
+      Map<String, String> entityProps = new HashMap<>();
+      entityProps.put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+      entityProps.put(IcebergConstants.CATALOG_BACKEND_NAME, catalogName);
+      entityProps.put(IcebergConstants.GRAVITINO_JDBC_USER, "root");
+      List<SecretMaterial> writes =
+          sm.assembleSecretMaterials(
+              Map.of(IcebergConstants.GRAVITINO_JDBC_USER, "root"),
+              entityProps,
+              "catalog",
+              9L,
+              Map.of(
+                  IcebergConstants.GRAVITINO_JDBC_PASSWORD,
+                  new SecretBinding("memory", "aux-mem-pwd")),
+              Map.of());
+      sm.writeSecrets(writes);
+
+      @SuppressWarnings("unchecked")
+      BaseCatalog<?> baseCatalog = Mockito.mock(BaseCatalog.class);
+      Mockito.when(baseCatalog.provider()).thenReturn("lakehouse-iceberg");
+      
Mockito.when(baseCatalog.propertiesWithCredentialProviders()).thenReturn(entityProps);
+
+      FieldUtils.writeField(GravitinoEnv.getInstance(), "secretManager", sm, 
true);
+
+      Map<String, String> properties = new HashMap<>();
+      properties.put(IcebergConstants.GRAVITINO_URI, "http://localhost:8090";);
+      properties.put(IcebergConstants.GRAVITINO_METALAKE, metalakeName);
+
+      DynamicIcebergConfigProvider provider = new 
DynamicIcebergConfigProvider();
+      provider.initialize(properties);
+      setMockCatalogFetcher(provider, Map.of(catalogName, baseCatalog));
+
+      Optional<IcebergConfig> config = 
provider.getIcebergCatalogConfig(catalogName);
+      Assertions.assertTrue(config.isPresent());
+      Assertions.assertEquals(
+          "aux-mem-pwd",
+          
config.get().getIcebergCatalogProperties().get(IcebergConstants.GRAVITINO_JDBC_PASSWORD));
+      Assertions.assertEquals(
+          "root",
+          
config.get().getIcebergCatalogProperties().get(IcebergConstants.GRAVITINO_JDBC_USER));
+    }
+  }
+
+  private static SecretManager memorySecretManager() {
+    Config config = new Config(false) {};
+    Properties properties = new Properties();
+    properties.setProperty(SecretProviderRegistry.GRAVITINO_SECRET_PROVIDERS, 
"memory");
+    properties.setProperty(
+        SecretProviderRegistry.GRAVITINO_SECRET_PROVIDER_PREFIX
+            + "memory."
+            + SecretProviderRegistry.CLASS_NAME,
+        InMemorySecretsProvider.class.getName());
+    config.loadFromProperties(properties);
+    return new SecretManager(config);
+  }
 }
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNameSpaceOperations.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNameSpaceOperations.java
index 025cac747e..73afbd411a 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNameSpaceOperations.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNameSpaceOperations.java
@@ -142,12 +142,13 @@ public class GravitinoLanceNameSpaceOperations implements 
LanceNamespaceOperatio
 
     switch (nsId.levels()) {
       case 1:
-        
Optional.ofNullable(catalog.properties()).ifPresent(properties::putAll);
+        Optional.ofNullable(namespaceWrapper.propsWithSecrets(catalog))
+            .ifPresent(properties::putAll);
         break;
       case 2:
         String schemaName = nsId.levelAtListPos(1);
-        Schema schema = namespaceWrapper.loadSchema(catalog, schemaName);
-        Optional.ofNullable(schema.properties()).ifPresent(properties::putAll);
+        Optional.ofNullable(namespaceWrapper.schemaPropsWithSecrets(catalog, 
schemaName))
+            .ifPresent(properties::putAll);
         break;
       default:
         throw new IllegalArgumentException(
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java
index 001f993f7c..d6880ce320 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java
@@ -31,6 +31,7 @@ import java.util.Optional;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.Entity;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
@@ -64,6 +65,7 @@ import 
org.apache.gravitino.rel.expressions.distributions.Distribution;
 import org.apache.gravitino.rel.expressions.sorts.SortOrder;
 import org.apache.gravitino.rel.expressions.transforms.Transform;
 import org.apache.gravitino.rel.indexes.Index;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.lance.namespace.errors.NamespaceNotFoundException;
 import org.slf4j.Logger;
@@ -129,6 +131,48 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper {
     return catalogOperator.loadCatalog(catalogName);
   }
 
+  Map<String, String> propsWithSecrets(Catalog catalog) {
+    Map<String, String> props = copyProps(catalog.properties());
+    props.putAll(catalogSecrets(catalog.name()));
+    return props;
+  }
+
+  Map<String, String> schemaPropsWithSecrets(Catalog catalog, String 
schemaName) {
+    Schema schema = loadSchema(catalog, schemaName);
+    Map<String, String> props = copyProps(schema.properties());
+    props.putAll(schemaSecrets(catalog.name(), schemaName));
+    return props;
+  }
+
+  private Map<String, String> catalogSecrets(String catalogName) {
+    SecretPropertyOperationDispatcher dispatcher = secretDispatcher();
+    if (dispatcher != null) {
+      return dispatcher.getSecrets(
+          NameIdentifierUtil.ofCatalog(metalakeName, catalogName), 
Entity.EntityType.CATALOG);
+    }
+    return loadCatalog(catalogName).supportsSecrets().getSecrets();
+  }
+
+  private Map<String, String> schemaSecrets(String catalogName, String 
schemaName) {
+    SecretPropertyOperationDispatcher dispatcher = secretDispatcher();
+    if (dispatcher != null) {
+      return dispatcher.getSecrets(schemaIdent(catalogName, schemaName), 
Entity.EntityType.SCHEMA);
+    }
+    return loadSchema(loadCatalog(catalogName), 
schemaName).supportsSecrets().getSecrets();
+  }
+
+  private SecretPropertyOperationDispatcher secretDispatcher() {
+    try {
+      return GravitinoEnv.getInstance().secretPropertyOperationDispatcher();
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  private static Map<String, String> copyProps(Map<String, String> props) {
+    return new HashMap<>(props == null ? Map.of() : props);
+  }
+
   Catalog createCatalog(
       String catalogName,
       Catalog.Type type,
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
index 6a1fcd6e11..08bd9bae3a 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
@@ -146,7 +146,8 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
             .map(Long::valueOf)
             .orElse(null));
     response.setStorageOptions(
-        LancePropertiesUtils.resolveLanceStorageOptions(catalog.properties(), 
table.properties()));
+        LancePropertiesUtils.resolveLanceStorageOptions(
+            namespaceWrapper.propsWithSecrets(catalog), table.properties()));
     response.setManagedVersioning(false);
     if (checkDeclared) {
       response.setIsOnlyDeclared(
@@ -200,7 +201,8 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
                 tableIdentifier, columns.toArray(new Column[0]), null, 
createTableProperties);
     Map<String, String> properties = t.properties();
     Map<String, String> effectiveStorageOptions =
-        LancePropertiesUtils.resolveLanceStorageOptions(catalog.properties(), 
properties);
+        LancePropertiesUtils.resolveLanceStorageOptions(
+            namespaceWrapper.propsWithSecrets(catalog), properties);
 
     CreateTableResponse response = new CreateTableResponse();
     response.setStorageOptions(effectiveStorageOptions);
diff --git 
a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNamespaceWrapper.java
 
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNamespaceWrapper.java
index ee8fd98c93..0b1eaec04c 100644
--- 
a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNamespaceWrapper.java
+++ 
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNamespaceWrapper.java
@@ -26,16 +26,20 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.Entity;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.Schema;
+import org.apache.gravitino.SupportsSchemas;
 import org.apache.gravitino.catalog.CatalogDispatcher;
 import org.apache.gravitino.catalog.SchemaDispatcher;
 import org.apache.gravitino.catalog.TableDispatcher;
 import org.apache.gravitino.lance.common.config.LanceConfig;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
@@ -49,6 +53,8 @@ public class TestGravitinoLanceNamespaceWrapper {
     FieldUtils.writeField(GravitinoEnv.getInstance(), "catalogManager", null, 
true);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "schemaDispatcher", 
null, true);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "tableDispatcher", null, 
true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "secretPropertyOperationDispatcher", null, 
true);
   }
 
   @Test
@@ -310,6 +316,140 @@ public class TestGravitinoLanceNamespaceWrapper {
     Assertions.assertSame(expectedCatalog, 
wrapper.loadAndValidateLakehouseCatalog("test_catalog"));
   }
 
+  @Test
+  public void testPropsWithSecrets() {
+    GravitinoLanceNamespaceWrapper wrapper = new 
GravitinoLanceNamespaceWrapper();
+    Catalog catalog =
+        createCatalogProxy(
+            Catalog.Type.RELATIONAL,
+            "lakehouse-generic",
+            Map.of("visible", "v1"),
+            Map.of("jdbc-password", "secret"));
+    wrapper.setCatalogOperator(
+        new GravitinoLanceNamespaceWrapper.CatalogOperator() {
+          @Override
+          public Catalog[] listCatalogsInfo() {
+            return new Catalog[0];
+          }
+
+          @Override
+          public Catalog loadCatalog(String catalogName) {
+            return catalog;
+          }
+
+          @Override
+          public Catalog createCatalog(
+              String catalogName,
+              Catalog.Type type,
+              String provider,
+              String comment,
+              Map<String, String> properties) {
+            throw new UnsupportedOperationException();
+          }
+
+          @Override
+          public Catalog alterCatalog(String catalogName, CatalogChange... 
changes) {
+            throw new UnsupportedOperationException();
+          }
+
+          @Override
+          public boolean dropCatalog(String catalogName, boolean force) {
+            throw new UnsupportedOperationException();
+          }
+        });
+
+    Map<String, String> merged = wrapper.propsWithSecrets(catalog);
+    Assertions.assertEquals("v1", merged.get("visible"));
+    Assertions.assertEquals("secret", merged.get("jdbc-password"));
+  }
+
+  @Test
+  public void testSchemaPropsWithSecrets() {
+    Schema schema = createSchemaProxy(Map.of("visible", "s1"), 
Map.of("schema-pwd", "s-secret"));
+    Catalog catalog =
+        createCatalogProxy(
+            Catalog.Type.RELATIONAL, "lakehouse-generic", Map.of(), Map.of(), 
schema);
+    GravitinoLanceNamespaceWrapper wrapper =
+        new GravitinoLanceNamespaceWrapper(
+            new 
LanceConfig(ImmutableMap.of(LanceConfig.METALAKE_NAME.getKey(), 
"test_metalake")),
+            false);
+    wrapper.setCatalogOperator(catalogOperatorReturning(catalog));
+
+    Map<String, String> merged = wrapper.schemaPropsWithSecrets(catalog, 
"test_schema");
+    Assertions.assertEquals("s1", merged.get("visible"));
+    Assertions.assertEquals("s-secret", merged.get("schema-pwd"));
+  }
+
+  @Test
+  public void testPropsViaDispatcher() throws Exception {
+    Catalog catalog =
+        createCatalogProxy(
+            Catalog.Type.RELATIONAL, "lakehouse-generic", Map.of("visible", 
"c1"), Map.of());
+    Schema schema = createSchemaProxy(Map.of("visible", "s1"), Map.of());
+
+    NameIdentifier catalogIdent = 
NameIdentifierUtil.ofCatalog("test_metalake", "test_catalog");
+    NameIdentifier schemaIdent =
+        NameIdentifierUtil.ofSchema("test_metalake", "test_catalog", 
"test_schema");
+    SecretPropertyOperationDispatcher dispatcher =
+        new SecretPropertyOperationDispatcher(null, null, null, null) {
+          @Override
+          public Map<String, String> getSecrets(
+              NameIdentifier identifier, Entity.EntityType entityType) {
+            if (catalogIdent.equals(identifier) && entityType == 
Entity.EntityType.CATALOG) {
+              return Map.of("jdbc-password", "from-dispatcher");
+            }
+            if (schemaIdent.equals(identifier) && entityType == 
Entity.EntityType.SCHEMA) {
+              return Map.of("schema-pwd", "from-dispatcher-schema");
+            }
+            return Map.of();
+          }
+        };
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "secretPropertyOperationDispatcher", 
dispatcher, true);
+
+    SchemaDispatcher schemaDispatcher =
+        (SchemaDispatcher)
+            Proxy.newProxyInstance(
+                SchemaDispatcher.class.getClassLoader(),
+                new Class<?>[] {SchemaDispatcher.class},
+                (proxy, method, args) -> {
+                  if ("loadSchema".equals(method.getName())) {
+                    return schema;
+                  }
+                  Class<?> returnType = method.getReturnType();
+                  if (returnType.equals(boolean.class)) {
+                    return false;
+                  }
+                  if (returnType.equals(int.class)) {
+                    return 0;
+                  }
+                  return null;
+                });
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(),
+        "catalogDispatcher",
+        Proxy.newProxyInstance(
+            CatalogDispatcher.class.getClassLoader(),
+            new Class<?>[] {CatalogDispatcher.class},
+            (proxy, method, args) -> null),
+        true);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "schemaDispatcher", 
schemaDispatcher, true);
+
+    LanceConfig lanceConfig =
+        new LanceConfig(ImmutableMap.of(LanceConfig.METALAKE_NAME.getKey(), 
"test_metalake"));
+    GravitinoLanceNamespaceWrapper wrapper = new 
GravitinoLanceNamespaceWrapper(lanceConfig, true);
+    wrapper.asNamespaceOps();
+    wrapper.setCatalogOperator(catalogOperatorReturning(catalog));
+
+    Map<String, String> catalogMerged = wrapper.propsWithSecrets(catalog);
+    Assertions.assertEquals("c1", catalogMerged.get("visible"));
+    Assertions.assertEquals("from-dispatcher", 
catalogMerged.get("jdbc-password"));
+
+    Map<String, String> schemaMerged = wrapper.schemaPropsWithSecrets(catalog, 
"test_schema");
+    Assertions.assertEquals("s1", schemaMerged.get("visible"));
+    Assertions.assertEquals("from-dispatcher-schema", 
schemaMerged.get("schema-pwd"));
+  }
+
   @Test
   public void testLoadSchemaUsesSchemaDispatcherInAuxMode() throws Exception {
     Schema expectedSchema = createSchemaProxy();
@@ -419,11 +559,35 @@ public class TestGravitinoLanceNamespaceWrapper {
   }
 
   private Schema createSchemaProxy() {
+    return createSchemaProxy(null, Map.of());
+  }
+
+  private Schema createSchemaProxy(Map<String, String> properties, Map<String, 
String> secrets) {
     return (Schema)
         Proxy.newProxyInstance(
             Schema.class.getClassLoader(),
-            new Class<?>[] {Schema.class},
-            (proxy, method, args) -> null);
+            new Class<?>[] {Schema.class, SupportsSecrets.class},
+            (proxy, method, args) -> {
+              switch (method.getName()) {
+                case "name":
+                  return "test_schema";
+                case "properties":
+                  return properties;
+                case "supportsSecrets":
+                  return proxy;
+                case "getSecrets":
+                  return secrets;
+                default:
+                  Class<?> returnType = method.getReturnType();
+                  if (returnType.equals(boolean.class)) {
+                    return false;
+                  }
+                  if (returnType.equals(int.class)) {
+                    return 0;
+                  }
+                  return null;
+              }
+            });
   }
 
   private Table createTableProxy() {
@@ -435,10 +599,27 @@ public class TestGravitinoLanceNamespaceWrapper {
   }
 
   private Catalog createCatalogProxy(Catalog.Type type, String provider) {
+    return createCatalogProxy(type, provider, null, Map.of());
+  }
+
+  private Catalog createCatalogProxy(
+      Catalog.Type type,
+      String provider,
+      Map<String, String> properties,
+      Map<String, String> secrets) {
+    return createCatalogProxy(type, provider, properties, secrets, null);
+  }
+
+  private Catalog createCatalogProxy(
+      Catalog.Type type,
+      String provider,
+      Map<String, String> properties,
+      Map<String, String> secrets,
+      Schema schema) {
     return (Catalog)
         Proxy.newProxyInstance(
             Catalog.class.getClassLoader(),
-            new Class<?>[] {Catalog.class},
+            new Class<?>[] {Catalog.class, SupportsSecrets.class, 
SupportsSchemas.class},
             (proxy, method, args) -> {
               switch (method.getName()) {
                 case "type":
@@ -448,7 +629,17 @@ public class TestGravitinoLanceNamespaceWrapper {
                 case "name":
                   return "test_catalog";
                 case "properties":
-                  return null;
+                  return properties;
+                case "supportsSecrets":
+                  return proxy;
+                case "getSecrets":
+                  return secrets;
+                case "asSchemas":
+                  return proxy;
+                case "loadSchema":
+                  return schema;
+                case "schemaExists":
+                  return schema != null;
                 default:
                   Class<?> returnType = method.getReturnType();
                   if (returnType.equals(boolean.class)) {
@@ -464,4 +655,38 @@ public class TestGravitinoLanceNamespaceWrapper {
               }
             });
   }
+
+  private GravitinoLanceNamespaceWrapper.CatalogOperator 
catalogOperatorReturning(Catalog catalog) {
+    return new GravitinoLanceNamespaceWrapper.CatalogOperator() {
+      @Override
+      public Catalog[] listCatalogsInfo() {
+        return new Catalog[0];
+      }
+
+      @Override
+      public Catalog loadCatalog(String catalogName) {
+        return catalog;
+      }
+
+      @Override
+      public Catalog createCatalog(
+          String catalogName,
+          Catalog.Type type,
+          String provider,
+          String comment,
+          Map<String, String> properties) {
+        throw new UnsupportedOperationException();
+      }
+
+      @Override
+      public Catalog alterCatalog(String catalogName, CatalogChange... 
changes) {
+        throw new UnsupportedOperationException();
+      }
+
+      @Override
+      public boolean dropCatalog(String catalogName, boolean force) {
+        throw new UnsupportedOperationException();
+      }
+    };
+  }
 }
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceModeParsing.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceModeParsing.java
index 2c142e55b3..e18820bb9e 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceModeParsing.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceModeParsing.java
@@ -227,7 +227,10 @@ class TestGravitinoLanceModeParsing {
     GravitinoLanceNamespaceWrapper namespaceWrapper =
         Mockito.mock(GravitinoLanceNamespaceWrapper.class);
     Catalog catalog = Mockito.mock(Catalog.class);
+    when(catalog.name()).thenReturn("catalog");
+    when(catalog.properties()).thenReturn(Map.of());
     
when(namespaceWrapper.loadAndValidateLakehouseCatalog("catalog")).thenReturn(catalog);
+    when(namespaceWrapper.propsWithSecrets(catalog)).thenReturn(Map.of());
     when(namespaceWrapper.asTableCatalog(catalog)).thenReturn(tableCatalog);
     return new GravitinoLanceTableOperations(namespaceWrapper);
   }
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
index 7ebdf160a3..b59fea1c7a 100644
--- 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
@@ -22,6 +22,7 @@ package org.apache.gravitino.spark.connector.catalog;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Sets;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
@@ -165,8 +166,8 @@ public abstract class BaseCatalog implements TableCatalog, 
SupportsNamespaces, F
     String provider = gravitinoCatalogClient.provider();
     Preconditions.checkArgument(
         StringUtils.isNotBlank(provider), name + " catalog provider is empty");
-    this.sparkCatalog =
-        createAndInitSparkCatalog(name, options, 
gravitinoCatalogClient.properties());
+    Map<String, String> catalogProperties = 
propsWithSecrets(gravitinoCatalogClient);
+    this.sparkCatalog = createAndInitSparkCatalog(name, options, 
catalogProperties);
     this.propertiesConverter = getPropertiesConverter();
     this.sparkTransformConverter = getSparkTransformConverter();
     this.sparkTypeConverter = getSparkTypeConverter();
@@ -709,6 +710,13 @@ public abstract class BaseCatalog implements TableCatalog, 
SupportsNamespaces, F
     }
   }
 
+  private static Map<String, String> propsWithSecrets(Catalog catalog) {
+    Map<String, String> props =
+        new HashMap<>(catalog.properties() == null ? Collections.emptyMap() : 
catalog.properties());
+    props.putAll(catalog.supportsSecrets().getSecrets());
+    return props;
+  }
+
   @Override
   public void invalidateTable(Identifier ident) {
     sparkCatalog.invalidateTable(ident);
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogSecrets.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogSecrets.java
new file mode 100644
index 0000000000..593ba6c529
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogSecrets.java
@@ -0,0 +1,180 @@
+/*
+ * 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.spark.connector.catalog;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretManager;
+import org.apache.gravitino.secret.SecretMaterial;
+import org.apache.gravitino.secret.SecretPropertyUtils;
+import org.apache.gravitino.secret.SecretProviderRegistry;
+import org.apache.gravitino.secret.SupportsSecrets;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
+import org.apache.gravitino.spark.connector.PropertiesConverter;
+import org.apache.gravitino.spark.connector.SparkTransformConverter;
+import org.apache.gravitino.spark.connector.SparkTypeConverter;
+import org.apache.spark.SparkConf;
+import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.Table;
+import org.apache.spark.sql.connector.catalog.TableCatalog;
+import org.apache.spark.sql.util.CaseInsensitiveStringMap;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestBaseCatalogSecrets {
+
+  private GravitinoClient gravitinoClient;
+  private CapturingCatalog catalog;
+
+  @BeforeAll
+  void initCatalogManager() {
+    gravitinoClient = mock(GravitinoClient.class);
+    GravitinoCatalogManager.create(new SparkConf(false), "user", identity -> 
gravitinoClient);
+  }
+
+  @AfterAll
+  void cleanupCatalogManager() {
+    GravitinoCatalogManager.get().close();
+  }
+
+  @Test
+  void testMergeSecrets() {
+    setUpCatalog(Map.of("metastore.uris", "thrift://localhost:9083"), 
Map.of("s3-sk", "secret-sk"));
+
+    catalog.initialize("hive", new CaseInsensitiveStringMap(Map.of()));
+
+    assertEquals("thrift://localhost:9083", 
catalog.lastProperties.get("metastore.uris"));
+    assertEquals("secret-sk", catalog.lastProperties.get("s3-sk"));
+  }
+
+  @Test
+  void testMergeSecretsNullProps() {
+    setUpCatalog(null, Map.of("s3-sk", "secret-sk"));
+
+    catalog.initialize("hive", new CaseInsensitiveStringMap(Map.of()));
+
+    assertEquals("secret-sk", catalog.lastProperties.get("s3-sk"));
+    assertEquals(1, catalog.lastProperties.size());
+  }
+
+  @Test
+  void testMergeMemorySecrets() {
+    try (SecretManager sm = memorySecretManager()) {
+      Map<String, String> entityProps = new HashMap<>();
+      entityProps.put("jdbc-user", "root");
+      List<SecretMaterial> writes =
+          sm.assembleSecretMaterials(
+              Map.of("jdbc-user", "root"),
+              entityProps,
+              "catalog",
+              1L,
+              Map.of("jdbc-password", new SecretBinding("memory", 
"from-memory")),
+              Map.of());
+      sm.writeSecrets(writes);
+      Map<String, String> secrets = SecretPropertyUtils.buildSecrets(sm, 
entityProps);
+
+      setUpCatalog(Map.of("jdbc-url", "jdbc:mysql://localhost/db"), secrets);
+      catalog.initialize("jdbc", new CaseInsensitiveStringMap(Map.of()));
+
+      assertEquals("jdbc:mysql://localhost/db", 
catalog.lastProperties.get("jdbc-url"));
+      assertEquals("from-memory", catalog.lastProperties.get("jdbc-password"));
+    }
+  }
+
+  private void setUpCatalog(Map<String, String> properties, Map<String, 
String> secrets) {
+    Catalog gravitinoCatalog = mock(Catalog.class);
+    SupportsSecrets supportsSecrets = mock(SupportsSecrets.class);
+    TableCatalog sparkCatalog = mock(TableCatalog.class);
+    when(gravitinoCatalog.type()).thenReturn(Catalog.Type.RELATIONAL);
+    when(gravitinoCatalog.provider()).thenReturn("hive");
+    when(gravitinoCatalog.properties()).thenReturn(properties);
+    when(gravitinoCatalog.supportsSecrets()).thenReturn(supportsSecrets);
+    when(supportsSecrets.getSecrets()).thenReturn(secrets);
+    when(gravitinoClient.loadCatalog(any())).thenReturn(gravitinoCatalog);
+    // Catalog info is cached; recreate the manager so each test loads the new 
mock.
+    GravitinoCatalogManager.get().close();
+    GravitinoCatalogManager.create(new SparkConf(false), "user", identity -> 
gravitinoClient);
+    catalog = new CapturingCatalog(sparkCatalog);
+  }
+
+  private static SecretManager memorySecretManager() {
+    Config config = new Config(false) {};
+    Properties properties = new Properties();
+    properties.setProperty(SecretProviderRegistry.GRAVITINO_SECRET_PROVIDERS, 
"memory");
+    properties.setProperty(
+        SecretProviderRegistry.GRAVITINO_SECRET_PROVIDER_PREFIX
+            + "memory."
+            + SecretProviderRegistry.CLASS_NAME,
+        InMemorySecretsProvider.class.getName());
+    config.loadFromProperties(properties);
+    return new SecretManager(config);
+  }
+
+  private static class CapturingCatalog extends BaseCatalog {
+    private final TableCatalog backing;
+    private Map<String, String> lastProperties = Map.of();
+
+    private CapturingCatalog(TableCatalog backing) {
+      this.backing = backing;
+    }
+
+    @Override
+    protected TableCatalog createAndInitSparkCatalog(
+        String name, CaseInsensitiveStringMap options, Map<String, String> 
properties) {
+      this.lastProperties = Map.copyOf(properties);
+      return backing;
+    }
+
+    @Override
+    protected Table createSparkTable(
+        Identifier identifier,
+        org.apache.gravitino.rel.Table gravitinoTable,
+        Table sparkTable,
+        TableCatalog sparkCatalog,
+        PropertiesConverter propertiesConverter,
+        SparkTransformConverter sparkTransformConverter,
+        SparkTypeConverter sparkTypeConverter) {
+      return sparkTable;
+    }
+
+    @Override
+    protected PropertiesConverter getPropertiesConverter() {
+      return mock(PropertiesConverter.class);
+    }
+
+    @Override
+    protected SparkTransformConverter getSparkTransformConverter() {
+      return mock(SparkTransformConverter.class);
+    }
+  }
+}
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
index 34872d2e2f..9d477676af 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
@@ -23,6 +23,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import io.trino.spi.TrinoException;
 import io.trino.spi.connector.ConnectorContext;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -263,7 +264,9 @@ public class CatalogConnectorManager {
             (String catalogName) -> {
               try {
                 Catalog catalog = metalake.loadCatalog(catalogName);
-                GravitinoCatalog gravitinoCatalog = new 
GravitinoCatalog(metalake.name(), catalog);
+                Map<String, String> properties = propsWithSecrets(catalog);
+                GravitinoCatalog gravitinoCatalog =
+                    new GravitinoCatalog(metalake.name(), catalog, properties);
                 if 
(catalogConnectors.containsKey(getTrinoCatalogName(gravitinoCatalog))) {
                   // Reload catalogs that have been updated in Gravitino 
server.
                   reloadCatalog(gravitinoCatalog);
@@ -484,6 +487,14 @@ public class CatalogConnectorManager {
     return false;
   }
 
+  /** Visible catalog properties overlaid with {@code getSecrets()}. */
+  static Map<String, String> propsWithSecrets(Catalog catalog) {
+    Map<String, String> props =
+        new HashMap<>(catalog.properties() == null ? Map.of() : 
catalog.properties());
+    props.putAll(catalog.supportsSecrets().getSecrets());
+    return props;
+  }
+
   public interface TrinoCatalogNameHandler {
     String getCatalogName(String metalake, String catalog);
   }
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/metadata/GravitinoCatalog.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/metadata/GravitinoCatalog.java
index e595d763f8..08b6831c8a 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/metadata/GravitinoCatalog.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/metadata/GravitinoCatalog.java
@@ -62,10 +62,21 @@ public class GravitinoCatalog {
    * @param catalog the catalog
    */
   public GravitinoCatalog(String metalake, Catalog catalog) {
+    this(metalake, catalog, catalog.properties());
+  }
+
+  /**
+   * Constructs a new GravitinoCatalog with resolved plaintext properties.
+   *
+   * @param metalake the name of the metalake
+   * @param catalog the catalog
+   * @param properties resolved catalog properties (secret URNs replaced with 
plaintext)
+   */
+  public GravitinoCatalog(String metalake, Catalog catalog, Map<String, 
String> properties) {
     this.metalake = metalake;
     this.provider = catalog.provider();
     this.name = catalog.name();
-    this.properties = catalog.properties();
+    this.properties = properties;
     Instant time =
         catalog.auditInfo().lastModifiedTime() == null
             ? catalog.auditInfo().createTime()
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/GravitinoMockServer.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/GravitinoMockServer.java
index e578f7e23d..648de60c07 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/GravitinoMockServer.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/GravitinoMockServer.java
@@ -59,6 +59,7 @@ import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
 import 
org.apache.gravitino.trino.connector.catalog.CatalogConnectorMetadataAdapter;
 import 
org.apache.gravitino.trino.connector.catalog.hive.HiveDataTypeTransformer;
@@ -208,19 +209,20 @@ public class GravitinoMockServer implements AutoCloseable 
{
               }
             });
 
-    when(metaLake.loadCatalog(anyString()))
-        .thenAnswer(
-            new Answer<Catalog>() {
-              @Override
-              public Catalog answer(InvocationOnMock invocation) throws 
Throwable {
-                String catalogName = invocation.getArgument(0);
-                if 
(!metalakes.get(metalakeName).catalogs.containsKey(catalogName)) {
-                  throw new NoSuchCatalogException("catalog does not be 
found");
-                }
-
-                return metalakes.get(metalakeName).catalogs.get(catalogName);
-              }
-            });
+    Answer<Catalog> loadCatalogAnswer =
+        new Answer<Catalog>() {
+          @Override
+          public Catalog answer(InvocationOnMock invocation) throws Throwable {
+            String catalogName = invocation.getArgument(0);
+            if 
(!metalakes.get(metalakeName).catalogs.containsKey(catalogName)) {
+              throw new NoSuchCatalogException("catalog does not be found");
+            }
+
+            return metalakes.get(metalakeName).catalogs.get(catalogName);
+          }
+        };
+    when(metaLake.loadCatalog(anyString())).thenAnswer(loadCatalogAnswer);
+    // Connectors load catalogs with secrets resolved for backend 
configuration.
     when(metaLake.listCatalogsInfo())
         .thenAnswer(
             new Answer<Catalog[]>() {
@@ -240,15 +242,21 @@ public class GravitinoMockServer implements AutoCloseable 
{
     when(catalog.name()).thenReturn(catalogName);
     when(catalog.provider()).thenReturn(testCatalogProvider);
     when(catalog.type()).thenReturn(Catalog.Type.RELATIONAL);
-    when(catalog.properties())
-        .thenReturn(properties.isEmpty() ? Map.of("max_ttl", "10") : 
properties);
+    Map<String, String> baseProperties =
+        properties.isEmpty() ? Map.of("max_ttl", "10") : properties;
+    when(catalog.properties()).thenReturn(baseProperties);
+    SupportsSecrets supportsSecrets = mock(SupportsSecrets.class);
+    when(supportsSecrets.getSecrets()).thenReturn(Map.of());
+    when(catalog.supportsSecrets()).thenReturn(supportsSecrets);
 
     Audit mockAudit = mock(Audit.class);
     when(mockAudit.creator()).thenReturn("gravitino");
     when(mockAudit.createTime()).thenReturn(Instant.now());
     when(catalog.auditInfo()).thenReturn(mockAudit);
 
-    GravitinoCatalog gravitinoCatalog = new GravitinoCatalog(testMetalake, 
catalog);
+    Map<String, String> resolved = new HashMap<>(baseProperties);
+    resolved.putAll(supportsSecrets.getSecrets());
+    GravitinoCatalog gravitinoCatalog = new GravitinoCatalog(testMetalake, 
catalog, resolved);
     when(catalog.asTableCatalog()).thenAnswer(answer -> 
createTableCatalog(gravitinoCatalog));
 
     when(catalog.asSchemas()).thenAnswer(answer -> 
createSchemas(gravitinoCatalog));
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
index f03cf0a474..781cf6755c 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
@@ -30,7 +30,10 @@ import static org.mockito.Mockito.when;
 import com.google.common.collect.ImmutableMap;
 import io.trino.spi.TrinoException;
 import io.trino.spi.connector.ConnectorContext;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
 import org.apache.gravitino.client.GravitinoAdminClient;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.trino.connector.GravitinoConfig;
 import org.apache.gravitino.trino.connector.GravitinoErrorCode;
 import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
@@ -140,6 +143,36 @@ public class TestCatalogConnectorManager {
     assertFalse(manager.skipCatalog("b2"));
   }
 
+  @Test
+  public void testPropsWithSecrets() {
+    Catalog catalog = mock(Catalog.class);
+    SupportsSecrets supportsSecrets = mock(SupportsSecrets.class);
+    when(catalog.properties()).thenReturn(Map.of("visible", "v1", "shared", 
"from-props"));
+    when(catalog.supportsSecrets()).thenReturn(supportsSecrets);
+    when(supportsSecrets.getSecrets())
+        .thenReturn(Map.of("jdbc-password", "secret", "shared", 
"from-secret"));
+
+    Map<String, String> merged = 
CatalogConnectorManager.propsWithSecrets(catalog);
+
+    assertEquals("v1", merged.get("visible"));
+    assertEquals("secret", merged.get("jdbc-password"));
+    assertEquals("from-secret", merged.get("shared"));
+  }
+
+  @Test
+  public void testPropsWithSecretsNullProps() {
+    Catalog catalog = mock(Catalog.class);
+    SupportsSecrets supportsSecrets = mock(SupportsSecrets.class);
+    when(catalog.properties()).thenReturn(null);
+    when(catalog.supportsSecrets()).thenReturn(supportsSecrets);
+    when(supportsSecrets.getSecrets()).thenReturn(Map.of("jdbc-password", 
"secret"));
+
+    Map<String, String> merged = 
CatalogConnectorManager.propsWithSecrets(catalog);
+
+    assertEquals("secret", merged.get("jdbc-password"));
+    assertEquals(1, merged.size());
+  }
+
   private CatalogConnectorManager createManager(ImmutableMap<String, String> 
configMap)
       throws Exception {
     return createManager(createCatalogConnectorFactory(), configMap);
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/metadata/TestGravitinoCatalog.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/metadata/TestGravitinoCatalog.java
index d5a42e3f8b..3fa867c5c0 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/metadata/TestGravitinoCatalog.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/metadata/TestGravitinoCatalog.java
@@ -106,6 +106,23 @@ public class TestGravitinoCatalog {
     assertTrue(catalog.isSameRegion("c2"));
   }
 
+  @Test
+  public void testCatalogWithResolvedSecretsProperties() {
+    String catalogName = "mock";
+    String provider = "hive";
+    HashMap<String, String> properties = new HashMap<>();
+    properties.put("metastore.uris", "thrift://localhost:9083");
+    Catalog mockCatalog =
+        mockCatalog(catalogName, provider, "test catalog", 
Catalog.Type.RELATIONAL, properties);
+
+    HashMap<String, String> resolved = new HashMap<>(properties);
+    resolved.put("jdbc-password", "from-secrets");
+    GravitinoCatalog catalog = new GravitinoCatalog("test", mockCatalog, 
resolved);
+
+    assertEquals("from-secrets", catalog.getProperty("jdbc-password", ""));
+    assertEquals("thrift://localhost:9083", 
catalog.getProperty("metastore.uris", ""));
+  }
+
   public static Catalog mockCatalog(
       String name,
       String provider,

Reply via email to