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 f27e0a1855 [#12457] feat(secret): Add getSecrets API for 
catalog/schema/fileset (#12458)
f27e0a1855 is described below

commit f27e0a185508aab78739829a2be5018ed8b02bde
Author: MaSai <[email protected]>
AuthorDate: Mon Aug 24 16:27:27 2026 +0800

    [#12457] feat(secret): Add getSecrets API for catalog/schema/fileset 
(#12458)
    
    ### What changes were proposed in this pull request?
    
    Add a dedicated **`getSecrets`** API so callers can retrieve
    secret-manager plaintext for catalog/schema/fileset **without** changing
    default `load*` omit behavior and **without** folding secrets into
    credential vending.
    
    - Core: `SecretPropertyUtils.buildSecrets` +
    `SecretPropertyOperationDispatcher`
    - REST: `GET .../objects/{catalog|schema|fileset}/{fullName}/secrets`
    - OpenAPI + Java/Python clients: `SupportsSecrets#getSecrets()`
    - `getSecrets()` returns **every** secret-URN property (including keys
    that may also appear in credential vending, e.g. `jdbc-password`)
    
    Example:
    
    ```java
    Catalog catalog = metalake.loadCatalog(catalogName);
    Map<String, String> conf = new HashMap<>(catalog.properties());
    conf.putAll(catalog.supportsSecrets().getSecrets());
    ```
    
    ### Scope / follow-ups
    
    - **This PR**: platform API + clients + docs/tests only.
    - **Connector adoption** (Spark / Flink / Trino / IRC / Lance / GVFS):
    separate PR.
    - **Drop/cascade write-through secret cleanup**: separate PR (not
    included here).
    
    ### Why are the changes needed?
    
    Fix: #12457
    
    Entity secrets persist URNs in properties; default HTTP load omits those
    keys. Credential vending covers typed providers, not arbitrary
    secretBindings. Callers need a narrow secrets map to merge with default
    load properties.
    
    ### Does this PR introduce _any_ user-facing change?
    
    - Yes: new `getSecrets()` / `SupportsSecrets` on catalog, schema,
    fileset
    - Yes: new REST endpoint `GET .../objects/{type}/{fullName}/secrets`
    - Default `loadCatalog` / `loadSchema` / `loadFileset` property omit
    semantics unchanged
    - Credential vending API unchanged
    
    ### How was this patch tested?
    
    - `./gradlew :core:test --tests
    'org.apache.gravitino.secret.TestSecretPropertyUtils'`
    - `./gradlew :server:test --tests
    'org.apache.gravitino.server.web.rest.TestMetadataObjectSecretOperations'`
    - Compile checks for api/common/core/server/client-java
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 .../main/java/org/apache/gravitino/Catalog.java    |  10 ++
 api/src/main/java/org/apache/gravitino/Schema.java |  10 ++
 .../java/org/apache/gravitino/file/Fileset.java    |  10 ++
 .../apache/gravitino/secret/SupportsSecrets.java   |  39 ++++++
 .../fileset/integration/test/FilesetSecretsIT.java | 115 +++++++++++++++++
 .../apache/gravitino/client/BaseSchemaCatalog.java |   3 +
 .../org/apache/gravitino/client/ErrorHandlers.java |  42 +++++++
 .../apache/gravitino/client/FilesetCatalog.java    |  13 +-
 .../apache/gravitino/client/GenericFileset.java    |  21 +++-
 .../org/apache/gravitino/client/GenericSchema.java |  17 ++-
 .../client/MetadataObjectSecretOperations.java     |  61 +++++++++
 .../apache/gravitino/client/RelationalCatalog.java |  13 +-
 .../api/{secret.py => secret/__init__.py}          |  30 +----
 .../api/{secret.py => secret/secret_types.py}      |   0
 .../api/{secret.py => secret/supports_secrets.py}  |  31 ++---
 .../gravitino/client/base_schema_catalog.py        |  12 ++
 .../gravitino/client/fileset_catalog.py            |   9 +-
 .../gravitino/client/generic_fileset.py            |  17 +++
 .../gravitino/client/generic_schema.py             |  16 +++
 .../client/metadata_object_secret_operations.py    |  53 ++++++++
 .../gravitino/client/relational_catalog.py         |   6 +-
 .../responses/secrets_response.py}                 |  37 +++---
 .../handlers/secret_error_handler.py}              |  35 +++---
 .../gravitino/dto/responses/SecretsResponse.java   |  62 +++++++++
 .../java/org/apache/gravitino/GravitinoEnv.java    |  16 +++
 .../secret/SecretPropertyOperationDispatcher.java  | 136 ++++++++++++++++++++
 .../gravitino/secret/SecretPropertyUtils.java      |  58 +++++++++
 .../gravitino/secret/TestSecretPropertyUtils.java  |  57 +++++++++
 docs/apache-hive-catalog.md                        |   2 +-
 docs/aws-glue-catalog.md                           |   2 +-
 docs/jdbc-doris-catalog.md                         |   2 +-
 docs/jdbc-hologres-catalog.md                      |   2 +-
 docs/jdbc-mysql-catalog.md                         |   2 +-
 docs/jdbc-oceanbase-catalog.md                     |   2 +-
 docs/jdbc-postgresql-catalog.md                    |   2 +-
 docs/jdbc-starrocks-catalog.md                     |   2 +-
 docs/lakehouse-iceberg-catalog.md                  |   2 +-
 docs/lakehouse-paimon-catalog.md                   |   2 +-
 docs/open-api/catalogs.yaml                        |   4 +-
 docs/open-api/filesets.yaml                        |   2 +-
 docs/open-api/openapi.yaml                         |   3 +
 docs/open-api/schemas.yaml                         |   2 +-
 docs/open-api/secrets.yaml                         |  68 ++++++++++
 docs/security/credential-vending.md                |   2 +-
 .../apache/gravitino/server/GravitinoServer.java   |   4 +
 .../web/filter/GravitinoInterceptionService.java   |   4 +-
 .../server/web/rest/ExceptionHandlers.java         |  33 +++++
 .../web/rest/MetadataObjectSecretOperations.java   | 116 +++++++++++++++++
 .../server/web/rest/TestCatalogOperations.java     |  19 +++
 .../rest/TestMetadataObjectSecretOperations.java   | 140 +++++++++++++++++++++
 50 files changed, 1232 insertions(+), 114 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/Catalog.java 
b/api/src/main/java/org/apache/gravitino/Catalog.java
index 9880f8d8d0..3fb8b81693 100644
--- a/api/src/main/java/org/apache/gravitino/Catalog.java
+++ b/api/src/main/java/org/apache/gravitino/Catalog.java
@@ -30,6 +30,7 @@ import org.apache.gravitino.model.ModelCatalog;
 import org.apache.gravitino.policy.SupportsPolicies;
 import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.ViewCatalog;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.tag.SupportsTags;
 
 /**
@@ -286,4 +287,13 @@ public interface Catalog extends Auditable {
   default SupportsCredentials supportsCredentials() throws 
UnsupportedOperationException {
     throw new UnsupportedOperationException("Catalog does not support 
credential operations");
   }
+
+  /**
+   * @return the {@link SupportsSecrets} if the catalog supports secret 
property operations.
+   * @throws UnsupportedOperationException if the catalog does not support 
secret property
+   *     operations.
+   */
+  default SupportsSecrets supportsSecrets() throws 
UnsupportedOperationException {
+    throw new UnsupportedOperationException("Catalog does not support secret 
property operations");
+  }
 }
diff --git a/api/src/main/java/org/apache/gravitino/Schema.java 
b/api/src/main/java/org/apache/gravitino/Schema.java
index c42d760011..19d84c5163 100644
--- a/api/src/main/java/org/apache/gravitino/Schema.java
+++ b/api/src/main/java/org/apache/gravitino/Schema.java
@@ -24,6 +24,7 @@ import javax.annotation.Nullable;
 import org.apache.gravitino.annotation.Evolving;
 import org.apache.gravitino.authorization.SupportsRoles;
 import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.tag.SupportsTags;
 
 /**
@@ -93,4 +94,13 @@ public interface Schema extends Auditable {
   default SupportsRoles supportsRoles() {
     throw new UnsupportedOperationException("Schema does not support role 
operations.");
   }
+
+  /**
+   * @return the {@link SupportsSecrets} if the schema supports secret 
property operations.
+   * @throws UnsupportedOperationException if the schema does not support 
secret property
+   *     operations.
+   */
+  default SupportsSecrets supportsSecrets() {
+    throw new UnsupportedOperationException("Schema does not support secret 
property operations.");
+  }
 }
diff --git a/api/src/main/java/org/apache/gravitino/file/Fileset.java 
b/api/src/main/java/org/apache/gravitino/file/Fileset.java
index 7089213984..342161293d 100644
--- a/api/src/main/java/org/apache/gravitino/file/Fileset.java
+++ b/api/src/main/java/org/apache/gravitino/file/Fileset.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.annotation.Evolving;
 import org.apache.gravitino.authorization.SupportsRoles;
 import org.apache.gravitino.credential.SupportsCredentials;
 import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.tag.SupportsTags;
 
 /**
@@ -272,4 +273,13 @@ public interface Fileset extends Auditable {
   default SupportsCredentials supportsCredentials() {
     throw new UnsupportedOperationException("Fileset does not support 
credential operations.");
   }
+
+  /**
+   * @return The {@link SupportsSecrets} if the fileset supports secret 
property operations.
+   * @throws UnsupportedOperationException If the fileset does not support 
secret property
+   *     operations.
+   */
+  default SupportsSecrets supportsSecrets() {
+    throw new UnsupportedOperationException("Fileset does not support secret 
property operations.");
+  }
 }
diff --git a/api/src/main/java/org/apache/gravitino/secret/SupportsSecrets.java 
b/api/src/main/java/org/apache/gravitino/secret/SupportsSecrets.java
new file mode 100644
index 0000000000..cf04a38537
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/secret/SupportsSecrets.java
@@ -0,0 +1,39 @@
+/*
+ * 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.secret;
+
+import java.util.Map;
+
+/**
+ * Interface to retrieve secret-manager plaintext properties for a metadata 
object.
+ *
+ * <p>Every secret-URN property value is resolved and returned, including keys 
that may also be
+ * delivered via {@link org.apache.gravitino.credential.SupportsCredentials} 
(for example {@code
+ * jdbc-password}). Normal non-secret properties are not included; combine 
with {@code
+ * load*().properties()} on the client.
+ */
+public interface SupportsSecrets {
+
+  /**
+   * Returns secret-manager plaintext properties for this metadata object.
+   *
+   * @return a map of property key to resolved plaintext value; never null, 
may be empty
+   */
+  Map<String, String> getSecrets();
+}
diff --git 
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetSecretsIT.java
 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetSecretsIT.java
new file mode 100644
index 0000000000..6bc0774c71
--- /dev/null
+++ 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetSecretsIT.java
@@ -0,0 +1,115 @@
+/*
+ * 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.catalog.fileset.integration.test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretProviderRegistry;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * IT covering fileset create-time secretBindings with the in-memory secret 
provider and getSecrets.
+ */
+public class FilesetSecretsIT extends BaseIT {
+
+  private String metalakeName = 
GravitinoITUtils.genRandomName("fileset_secrets_ml");
+  private String catalogName = 
GravitinoITUtils.genRandomName("fileset_secrets_cat");
+  private String schemaName = 
GravitinoITUtils.genRandomName("fileset_secrets_schema");
+  private GravitinoMetalake metalake;
+  private Catalog catalog;
+  private String baseLocation;
+
+  @BeforeAll
+  @Override
+  public void startIntegrationTest() throws Exception {
+    Map<String, String> configs = Maps.newHashMap();
+    configs.put(SecretProviderRegistry.GRAVITINO_SECRET_PROVIDERS, "memory");
+    configs.put(
+        SecretProviderRegistry.GRAVITINO_SECRET_PROVIDER_PREFIX
+            + "memory."
+            + SecretProviderRegistry.CLASS_NAME,
+        InMemorySecretsProvider.class.getName());
+    registerCustomConfigs(configs);
+    super.startIntegrationTest();
+
+    baseLocation =
+        
Files.createTempDirectory("fileset-secrets-it").toAbsolutePath().toUri().toString();
+    client.createMetalake(metalakeName, "comment", Collections.emptyMap());
+    metalake = client.loadMetalake(metalakeName);
+    metalake.createCatalog(
+        catalogName, Catalog.Type.FILESET, "hadoop", "comment", 
ImmutableMap.of());
+    catalog = metalake.loadCatalog(catalogName);
+    catalog
+        .asSchemas()
+        .createSchema(schemaName, "comment", ImmutableMap.of("location", 
baseLocation));
+  }
+
+  @AfterAll
+  @Override
+  public void stopIntegrationTest() throws IOException, InterruptedException {
+    if (metalake != null) {
+      catalog.asSchemas().dropSchema(schemaName, true);
+      metalake.dropCatalog(catalogName, true);
+      client.dropMetalake(metalakeName, true);
+    }
+    super.stopIntegrationTest();
+  }
+
+  @Test
+  public void testCreateFilesetWithMemorySecretBindingsAndGetSecrets() {
+    NameIdentifier filesetIdent = NameIdentifier.of(schemaName, 
"secret_fileset");
+    String location = baseLocation + "/secret_fileset";
+    Fileset fileset =
+        catalog
+            .asFilesetCatalog()
+            .createFileset(
+                filesetIdent,
+                "comment",
+                Fileset.Type.MANAGED,
+                location,
+                ImmutableMap.of("visible-key", "visible-value"),
+                ImmutableMap.of("custom-secret", new SecretBinding("memory", 
"mem-plaintext")),
+                ImmutableMap.of());
+
+    Assertions.assertEquals("visible-value", 
fileset.properties().get("visible-key"));
+    // Default load properties omit secret-manager values; plaintext comes 
from getSecrets().
+    Assertions.assertFalse(
+        fileset.properties().containsKey("custom-secret"),
+        "secret bindings must not appear in default load properties");
+
+    Map<String, String> secrets = fileset.supportsSecrets().getSecrets();
+    Assertions.assertEquals("mem-plaintext", secrets.get("custom-secret"));
+    Assertions.assertFalse(secrets.containsKey("visible-key"));
+  }
+}
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
index 636090dba0..cba10fa820 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
@@ -83,6 +83,7 @@ abstract class BaseSchemaCatalog extends CatalogDTO
   private final MetadataObjectPolicyOperations objectPolicyOperations;
   private final MetadataObjectRoleOperations objectRoleOperations;
   protected final MetadataObjectCredentialOperations 
objectCredentialOperations;
+  protected final MetadataObjectSecretOperations objectSecretOperations;
   private final FunctionCatalogOperations functionOperations;
 
   BaseSchemaCatalog(
@@ -114,6 +115,8 @@ abstract class BaseSchemaCatalog extends CatalogDTO
     this.objectCredentialOperations =
         new MetadataObjectCredentialOperations(
             catalogNamespace.level(0), metadataObject, restClient);
+    this.objectSecretOperations =
+        new MetadataObjectSecretOperations(catalogNamespace.level(0), 
metadataObject, restClient);
     this.functionOperations =
         new FunctionCatalogOperations(restClient, catalogNamespace, 
this.name());
   }
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
index 79d4ca6903..99547936c8 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
@@ -247,6 +247,15 @@ public class ErrorHandlers {
     return CredentialErrorHandler.INSTANCE;
   }
 
+  /**
+   * Creates an error handler specific to secret property operations.
+   *
+   * @return A Consumer representing the secret error handler.
+   */
+  public static Consumer<ErrorResponse> secretErrorHandler() {
+    return SecretErrorHandler.INSTANCE;
+  }
+
   /**
    * Creates an error handler specific to Owner operations.
    *
@@ -1036,6 +1045,39 @@ public class ErrorHandlers {
     }
   }
 
+  /** Error handler specific to secret property operations. */
+  @SuppressWarnings("FormatStringAnnotation")
+  private static class SecretErrorHandler extends RestErrorHandler {
+
+    private static final SecretErrorHandler INSTANCE = new 
SecretErrorHandler();
+
+    @Override
+    public void accept(ErrorResponse errorResponse) {
+      String errorMessage = formatErrorMessage(errorResponse);
+
+      switch (errorResponse.getCode()) {
+        case ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
+          throw new IllegalArgumentException(errorMessage);
+
+        case ErrorConstants.NOT_FOUND_CODE:
+          if 
(errorResponse.getType().equals(NoSuchMetalakeException.class.getSimpleName())) 
{
+            throw new NoSuchMetalakeException(errorMessage);
+          } else {
+            throw new NotFoundException(errorMessage);
+          }
+
+        case ErrorConstants.NOT_IN_USE_CODE:
+          throw new MetalakeNotInUseException(errorMessage);
+
+        case ErrorConstants.INTERNAL_ERROR_CODE:
+          throw new RuntimeException(errorMessage);
+
+        default:
+          super.accept(errorResponse);
+      }
+    }
+  }
+
   /** Error handler specific to Tag operations. */
   @SuppressWarnings("FormatStringAnnotation")
   private static class TagErrorHandler extends RestErrorHandler {
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
index 40dc2d7286..6c144548b1 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
@@ -53,6 +53,7 @@ import org.apache.gravitino.file.FilesetChange;
 import org.apache.gravitino.rest.RESTUtils;
 import org.apache.gravitino.secret.SecretBinding;
 import org.apache.gravitino.secret.SecretReference;
+import org.apache.gravitino.secret.SupportsSecrets;
 
 /**
  * Fileset catalog is a catalog implementation that supports fileset like 
metadata operations, for
@@ -60,7 +61,7 @@ import org.apache.gravitino.secret.SecretReference;
  * metalake.
  */
 class FilesetCatalog extends BaseSchemaCatalog
-    implements org.apache.gravitino.file.FilesetCatalog, SupportsCredentials {
+    implements org.apache.gravitino.file.FilesetCatalog, SupportsCredentials, 
SupportsSecrets {
 
   FilesetCatalog(
       Namespace namespace,
@@ -291,6 +292,16 @@ class FilesetCatalog extends BaseSchemaCatalog
     return objectCredentialOperations.getCredentials();
   }
 
+  @Override
+  public SupportsSecrets supportsSecrets() throws 
UnsupportedOperationException {
+    return this;
+  }
+
+  @Override
+  public Map<String, String> getSecrets() {
+    return objectSecretOperations.getSecrets();
+  }
+
   @VisibleForTesting
   static String formatFilesetRequestPath(Namespace ns) {
     Namespace schemaNs = Namespace.of(ns.level(0), ns.level(1));
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
index d51e4fea37..065445ada3 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
@@ -36,19 +36,26 @@ import 
org.apache.gravitino.exceptions.PolicyAlreadyAssociatedException;
 import org.apache.gravitino.file.Fileset;
 import org.apache.gravitino.policy.Policy;
 import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.tag.SupportsTags;
 import org.apache.gravitino.tag.Tag;
 import org.apache.gravitino.tag.TagValue;
 
 /** Represents a generic fileset. */
 class GenericFileset
-    implements Fileset, SupportsTags, SupportsRoles, SupportsCredentials, 
SupportsPolicies {
+    implements Fileset,
+        SupportsTags,
+        SupportsRoles,
+        SupportsCredentials,
+        SupportsSecrets,
+        SupportsPolicies {
 
   private final FilesetDTO filesetDTO;
 
   private final MetadataObjectTagOperations objectTagOperations;
   private final MetadataObjectRoleOperations objectRoleOperations;
   private final MetadataObjectCredentialOperations objectCredentialOperations;
+  private final MetadataObjectSecretOperations objectSecretOperations;
   private final MetadataObjectPolicyOperations objectPolicyOperations;
 
   GenericFileset(FilesetDTO filesetDTO, RESTClient restClient, Namespace 
filesetNs) {
@@ -62,6 +69,8 @@ class GenericFileset
         new MetadataObjectRoleOperations(filesetNs.level(0), filesetObject, 
restClient);
     this.objectCredentialOperations =
         new MetadataObjectCredentialOperations(filesetNs.level(0), 
filesetObject, restClient);
+    this.objectSecretOperations =
+        new MetadataObjectSecretOperations(filesetNs.level(0), filesetObject, 
restClient);
     this.objectPolicyOperations =
         new MetadataObjectPolicyOperations(filesetNs.level(0), filesetObject, 
restClient);
   }
@@ -117,6 +126,11 @@ class GenericFileset
     return this;
   }
 
+  @Override
+  public SupportsSecrets supportsSecrets() {
+    return this;
+  }
+
   @Override
   public String[] listTags() {
     return objectTagOperations.listTags();
@@ -173,6 +187,11 @@ class GenericFileset
     return objectCredentialOperations.getCredentials();
   }
 
+  @Override
+  public Map<String, String> getSecrets() {
+    return objectSecretOperations.getSecrets();
+  }
+
   @Override
   public boolean equals(Object obj) {
     if (this == obj) {
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
index 09ecbc8916..6c4d437643 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
@@ -31,18 +31,21 @@ import org.apache.gravitino.exceptions.NoSuchTagException;
 import org.apache.gravitino.exceptions.PolicyAlreadyAssociatedException;
 import org.apache.gravitino.policy.Policy;
 import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.secret.SupportsSecrets;
 import org.apache.gravitino.tag.SupportsTags;
 import org.apache.gravitino.tag.Tag;
 import org.apache.gravitino.tag.TagValue;
 
 /** Represents a generic schema. */
-class GenericSchema implements Schema, SupportsTags, SupportsRoles, 
SupportsPolicies {
+class GenericSchema
+    implements Schema, SupportsTags, SupportsRoles, SupportsPolicies, 
SupportsSecrets {
 
   private final SchemaDTO schemaDTO;
 
   private final MetadataObjectTagOperations objectTagOperations;
   private final MetadataObjectRoleOperations objectRoleOperations;
   private final MetadataObjectPolicyOperations objectPolicyOperations;
+  private final MetadataObjectSecretOperations objectSecretOperations;
 
   GenericSchema(SchemaDTO schemaDTO, RESTClient restClient, String metalake, 
String catalog) {
     this.schemaDTO = schemaDTO;
@@ -53,6 +56,8 @@ class GenericSchema implements Schema, SupportsTags, 
SupportsRoles, SupportsPoli
         new MetadataObjectRoleOperations(metalake, schemaObject, restClient);
     this.objectPolicyOperations =
         new MetadataObjectPolicyOperations(metalake, schemaObject, restClient);
+    this.objectSecretOperations =
+        new MetadataObjectSecretOperations(metalake, schemaObject, restClient);
   }
 
   @Override
@@ -70,6 +75,16 @@ class GenericSchema implements Schema, SupportsTags, 
SupportsRoles, SupportsPoli
     return this;
   }
 
+  @Override
+  public SupportsSecrets supportsSecrets() {
+    return this;
+  }
+
+  @Override
+  public Map<String, String> getSecrets() {
+    return objectSecretOperations.getSecrets();
+  }
+
   @Override
   public String name() {
     return schemaDTO.name();
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectSecretOperations.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectSecretOperations.java
new file mode 100644
index 0000000000..a723df3538
--- /dev/null
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectSecretOperations.java
@@ -0,0 +1,61 @@
+/*
+ * 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.client;
+
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.dto.responses.SecretsResponse;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.secret.SupportsSecrets;
+
+/**
+ * The implementation of {@link SupportsSecrets}. This interface will be 
composited into catalog,
+ * schema, and fileset to provide secret property operations for these 
metadata objects.
+ */
+class MetadataObjectSecretOperations implements SupportsSecrets {
+
+  private final RESTClient restClient;
+
+  private final String secretsRequestPath;
+
+  MetadataObjectSecretOperations(
+      String metalakeName, MetadataObject metadataObject, RESTClient 
restClient) {
+    this.restClient = restClient;
+    this.secretsRequestPath =
+        String.format(
+            "api/metalakes/%s/objects/%s/%s/secrets",
+            RESTUtils.encodeString(metalakeName),
+            metadataObject.type().name().toLowerCase(Locale.ROOT),
+            RESTUtils.encodeString(metadataObject.fullName()));
+  }
+
+  @Override
+  public Map<String, String> getSecrets() {
+    SecretsResponse resp =
+        restClient.get(
+            secretsRequestPath,
+            SecretsResponse.class,
+            Collections.emptyMap(),
+            ErrorHandlers.secretErrorHandler());
+    resp.validate();
+    return resp.getSecrets();
+  }
+}
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
index 1ae0df08b5..22fa794f69 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
@@ -71,6 +71,7 @@ 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.rest.RESTUtils;
+import org.apache.gravitino.secret.SupportsSecrets;
 
 /**
  * Relational catalog is a catalog implementation that supports relational 
database like metadata
@@ -78,7 +79,7 @@ import org.apache.gravitino.rest.RESTUtils;
  * catalog is under the metalake.
  */
 class RelationalCatalog extends BaseSchemaCatalog
-    implements TableCatalog, ViewCatalog, SupportsCredentials {
+    implements TableCatalog, ViewCatalog, SupportsCredentials, SupportsSecrets 
{
 
   public static final String PRIVILEGES = "privileges";
 
@@ -313,6 +314,16 @@ class RelationalCatalog extends BaseSchemaCatalog
     return objectCredentialOperations.getCredentials();
   }
 
+  @Override
+  public SupportsSecrets supportsSecrets() throws 
UnsupportedOperationException {
+    return this;
+  }
+
+  @Override
+  public Map<String, String> getSecrets() {
+    return objectSecretOperations.getSecrets();
+  }
+
   /**
    * List all the views under the given Schema namespace.
    *
diff --git a/clients/client-python/gravitino/api/secret.py 
b/clients/client-python/gravitino/api/secret/__init__.py
similarity index 50%
copy from clients/client-python/gravitino/api/secret.py
copy to clients/client-python/gravitino/api/secret/__init__.py
index fb1526afae..8b1c26b337 100644
--- a/clients/client-python/gravitino/api/secret.py
+++ b/clients/client-python/gravitino/api/secret/__init__.py
@@ -15,31 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from dataclasses import dataclass
-from typing import Dict
+from gravitino.api.secret.secret_types import SecretBinding, SecretReference
+from gravitino.api.secret.supports_secrets import SupportsSecrets
 
-
-@dataclass
-class SecretBinding:
-    """Write-through secret binding: provider instance name plus plaintext."""
-
-    provider: str
-    plaintext: str
-
-    def __repr__(self) -> str:
-        return f"SecretBinding(provider={self.provider!r}, plaintext=***)"
-
-
-@dataclass
-class SecretReference:
-    """External secret locator: provider instance name plus provider-specific 
attributes."""
-
-    provider: str
-    attributes: Dict[str, str]
-
-    def __post_init__(self):
-        if self.attributes is None or len(self.attributes) == 0:
-            raise ValueError("attributes must not be null or empty")
-
-    def __repr__(self) -> str:
-        return f"SecretReference(provider={self.provider!r}, 
attributes={self.attributes!r})"
+__all__ = ["SecretBinding", "SecretReference", "SupportsSecrets"]
diff --git a/clients/client-python/gravitino/api/secret.py 
b/clients/client-python/gravitino/api/secret/secret_types.py
similarity index 100%
copy from clients/client-python/gravitino/api/secret.py
copy to clients/client-python/gravitino/api/secret/secret_types.py
diff --git a/clients/client-python/gravitino/api/secret.py 
b/clients/client-python/gravitino/api/secret/supports_secrets.py
similarity index 51%
copy from clients/client-python/gravitino/api/secret.py
copy to clients/client-python/gravitino/api/secret/supports_secrets.py
index fb1526afae..92bcc7a155 100644
--- a/clients/client-python/gravitino/api/secret.py
+++ b/clients/client-python/gravitino/api/secret/supports_secrets.py
@@ -15,31 +15,14 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from dataclasses import dataclass
+from abc import ABC, abstractmethod
 from typing import Dict
 
 
-@dataclass
-class SecretBinding:
-    """Write-through secret binding: provider instance name plus plaintext."""
+class SupportsSecrets(ABC):
+    """Interface to retrieve secret-manager plaintext properties."""
 
-    provider: str
-    plaintext: str
-
-    def __repr__(self) -> str:
-        return f"SecretBinding(provider={self.provider!r}, plaintext=***)"
-
-
-@dataclass
-class SecretReference:
-    """External secret locator: provider instance name plus provider-specific 
attributes."""
-
-    provider: str
-    attributes: Dict[str, str]
-
-    def __post_init__(self):
-        if self.attributes is None or len(self.attributes) == 0:
-            raise ValueError("attributes must not be null or empty")
-
-    def __repr__(self) -> str:
-        return f"SecretReference(provider={self.provider!r}, 
attributes={self.attributes!r})"
+    @abstractmethod
+    def get_secrets(self) -> Dict[str, str]:
+        """Return secret-manager plaintext properties for this metadata 
object."""
+        pass
diff --git a/clients/client-python/gravitino/client/base_schema_catalog.py 
b/clients/client-python/gravitino/client/base_schema_catalog.py
index cda7b933b1..e0fb8c20ce 100644
--- a/clients/client-python/gravitino/client/base_schema_catalog.py
+++ b/clients/client-python/gravitino/client/base_schema_catalog.py
@@ -36,6 +36,9 @@ from gravitino.client.generic_schema import GenericSchema
 from gravitino.client.metadata_object_credential_operations import (
     MetadataObjectCredentialOperations,
 )
+from gravitino.client.metadata_object_secret_operations import (
+    MetadataObjectSecretOperations,
+)
 from gravitino.client.metadata_object_tag_operations import 
MetadataObjectTagOperations
 from gravitino.dto.audit_dto import AuditDTO
 from gravitino.dto.catalog_dto import CatalogDTO
@@ -76,6 +79,9 @@ class BaseSchemaCatalog(
     # The metadata object credential operations
     _object_credential_operations: MetadataObjectCredentialOperations
 
+    # The metadata object secret property operations
+    _object_secret_operations: MetadataObjectSecretOperations
+
     _function_operations: FunctionCatalogOperations
 
     def __init__(
@@ -104,6 +110,9 @@ class BaseSchemaCatalog(
         self._object_credential_operations = 
MetadataObjectCredentialOperations(
             catalog_namespace.level(0), metadata_object, rest_client
         )
+        self._object_secret_operations = MetadataObjectSecretOperations(
+            catalog_namespace.level(0), metadata_object, rest_client
+        )
         self._function_operations = FunctionCatalogOperations(
             rest_client, catalog_namespace, self.name()
         )
@@ -113,6 +122,9 @@ class BaseSchemaCatalog(
 
         self.validate()
 
+    def get_secrets(self) -> Dict[str, str]:
+        return self._object_secret_operations.get_secrets()
+
     def as_schemas(self):
         return self
 
diff --git a/clients/client-python/gravitino/client/fileset_catalog.py 
b/clients/client-python/gravitino/client/fileset_catalog.py
index 50b458b379..9f4a0edafc 100644
--- a/clients/client-python/gravitino/client/fileset_catalog.py
+++ b/clients/client-python/gravitino/client/fileset_catalog.py
@@ -24,6 +24,7 @@ from gravitino.dto.requests.fileset_create_request import 
FilesetCreateRequest
 from gravitino.api.catalog import Catalog
 from gravitino.api.credential.credential import Credential
 from gravitino.api.credential.supports_credentials import SupportsCredentials
+from gravitino.api.secret.supports_secrets import SupportsSecrets
 from gravitino.api.file.fileset import Fileset
 from gravitino.api.file.fileset_change import FilesetChange
 from gravitino.api.secret import SecretBinding, SecretReference
@@ -50,7 +51,7 @@ _EMPTY_SECRET_REFERENCES: Mapping[str, SecretReference] = 
MappingProxyType({})
 
 
 class FilesetCatalog(
-    BaseSchemaCatalog, SupportsCredentials
+    BaseSchemaCatalog, SupportsCredentials, SupportsSecrets
 ):  # pylint: disable=too-many-ancestors
     """
     Fileset catalog is a catalog implementation that supports fileset like 
metadata operations, for
@@ -402,3 +403,9 @@ class FilesetCatalog(
 
     def get_credentials(self) -> List[Credential]:
         return self._object_credential_operations.get_credentials()
+
+    def support_secrets(self) -> SupportsSecrets:
+        return self
+
+    def get_secrets(self) -> Dict[str, str]:
+        return self._object_secret_operations.get_secrets()
diff --git a/clients/client-python/gravitino/client/generic_fileset.py 
b/clients/client-python/gravitino/client/generic_fileset.py
index f7c90e63c4..9516cea807 100644
--- a/clients/client-python/gravitino/client/generic_fileset.py
+++ b/clients/client-python/gravitino/client/generic_fileset.py
@@ -18,6 +18,7 @@ from typing import Dict, List, Optional
 
 from gravitino.api.credential.credential import Credential
 from gravitino.api.credential.supports_credentials import SupportsCredentials
+from gravitino.api.secret.supports_secrets import SupportsSecrets
 from gravitino.api.file.fileset import Fileset
 from gravitino.api.metadata_object import MetadataObject
 from gravitino.api.metadata_objects import MetadataObjects
@@ -26,6 +27,9 @@ from gravitino.api.tag.tag import Tag
 from gravitino.client.metadata_object_credential_operations import (
     MetadataObjectCredentialOperations,
 )
+from gravitino.client.metadata_object_secret_operations import (
+    MetadataObjectSecretOperations,
+)
 from gravitino.client.metadata_object_tag_operations import 
MetadataObjectTagOperations
 from gravitino.dto.audit_dto import AuditDTO
 from gravitino.dto.fileset_dto import FilesetDTO
@@ -36,6 +40,7 @@ from gravitino.utils import HTTPClient
 class GenericFileset(
     Fileset,
     SupportsCredentials,
+    SupportsSecrets,
     SupportsTags,
 ):
     _fileset: FilesetDTO
@@ -44,6 +49,9 @@ class GenericFileset(
     _object_credential_operations: MetadataObjectCredentialOperations
     """The metadata object credential operations"""
 
+    _object_secret_operations: MetadataObjectSecretOperations
+    """The metadata object secret property operations"""
+
     def __init__(
         self, fileset: FilesetDTO, rest_client: HTTPClient, full_namespace: 
Namespace
     ):
@@ -55,6 +63,9 @@ class GenericFileset(
         self._object_credential_operations = 
MetadataObjectCredentialOperations(
             full_namespace.level(0), metadata_object, rest_client
         )
+        self._object_secret_operations = MetadataObjectSecretOperations(
+            full_namespace.level(0), metadata_object, rest_client
+        )
         self._object_tag_operations = MetadataObjectTagOperations(
             full_namespace.level(0), metadata_object, rest_client
         )
@@ -86,6 +97,12 @@ class GenericFileset(
     def get_credentials(self) -> List[Credential]:
         return self._object_credential_operations.get_credentials()
 
+    def support_secrets(self) -> SupportsSecrets:
+        return self
+
+    def get_secrets(self) -> Dict[str, str]:
+        return self._object_secret_operations.get_secrets()
+
     def list_tags(self) -> List[str]:
         return self._object_tag_operations.list_tags()
 
diff --git a/clients/client-python/gravitino/client/generic_schema.py 
b/clients/client-python/gravitino/client/generic_schema.py
index 09c895dcf0..52ade4c630 100644
--- a/clients/client-python/gravitino/client/generic_schema.py
+++ b/clients/client-python/gravitino/client/generic_schema.py
@@ -23,8 +23,12 @@ from gravitino.api.audit import Audit
 from gravitino.api.metadata_object import MetadataObject
 from gravitino.api.metadata_objects import MetadataObjects
 from gravitino.api.schema import Schema
+from gravitino.api.secret.supports_secrets import SupportsSecrets
 from gravitino.api.tag.supports_tags import SupportsTags
 from gravitino.api.tag.tag import Tag
+from gravitino.client.metadata_object_secret_operations import (
+    MetadataObjectSecretOperations,
+)
 from gravitino.client.metadata_object_tag_operations import 
MetadataObjectTagOperations
 from gravitino.dto.schema_dto import SchemaDTO
 from gravitino.utils.http_client import HTTPClient
@@ -33,6 +37,7 @@ from gravitino.utils.http_client import HTTPClient
 class GenericSchema(
     Schema,
     SupportsTags,
+    SupportsSecrets,
 ):
     def __init__(
         self,
@@ -54,6 +59,11 @@ class GenericSchema(
             metadata_object,
             rest_client,
         )
+        self._object_secret_operations = MetadataObjectSecretOperations(
+            metalake,
+            metadata_object,
+            rest_client,
+        )
 
     def __eq__(self, value: object) -> bool:
         if not isinstance(value, GenericSchema):
@@ -103,3 +113,9 @@ class GenericSchema(
 
     def supports_tags(self) -> SupportsTags:
         return self
+
+    def support_secrets(self) -> SupportsSecrets:
+        return self
+
+    def get_secrets(self) -> Dict[str, str]:
+        return self._object_secret_operations.get_secrets()
diff --git 
a/clients/client-python/gravitino/client/metadata_object_secret_operations.py 
b/clients/client-python/gravitino/client/metadata_object_secret_operations.py
new file mode 100644
index 0000000000..63996f3a6d
--- /dev/null
+++ 
b/clients/client-python/gravitino/client/metadata_object_secret_operations.py
@@ -0,0 +1,53 @@
+# 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.
+
+from typing import Dict
+
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.secret.supports_secrets import SupportsSecrets
+from gravitino.dto.responses.secrets_response import SecretsResponse
+from gravitino.exceptions.handlers.secret_error_handler import 
SECRET_ERROR_HANDLER
+from gravitino.rest.rest_utils import encode_string
+from gravitino.utils import HTTPClient
+
+
+class MetadataObjectSecretOperations(SupportsSecrets):
+    _rest_client: HTTPClient
+    _request_path: str
+
+    def __init__(
+        self,
+        metalake_name: str,
+        metadata_object: MetadataObject,
+        rest_client: HTTPClient,
+    ):
+        self._rest_client = rest_client
+        metadata_object_type = metadata_object.type().value
+        metadata_object_fullname = metadata_object.full_name()
+        self._request_path = (
+            
f"api/metalakes/{encode_string(metalake_name)}/objects/{metadata_object_type}/"
+            f"{encode_string(metadata_object_fullname)}/secrets"
+        )
+
+    def get_secrets(self) -> Dict[str, str]:
+        resp = self._rest_client.get(
+            self._request_path,
+            error_handler=SECRET_ERROR_HANDLER,
+        )
+        secret_resp = SecretsResponse.from_json(resp.body, infer_missing=True)
+        secret_resp.validate()
+        return secret_resp.secrets()
diff --git a/clients/client-python/gravitino/client/relational_catalog.py 
b/clients/client-python/gravitino/client/relational_catalog.py
index c8cffbe662..c4024e26f3 100644
--- a/clients/client-python/gravitino/client/relational_catalog.py
+++ b/clients/client-python/gravitino/client/relational_catalog.py
@@ -19,6 +19,7 @@ from typing import Final, Optional, overload
 
 from gravitino.api.authorization.privileges import Privilege
 from gravitino.api.catalog import Catalog
+from gravitino.api.secret.supports_secrets import SupportsSecrets
 from gravitino.api.rel.column import Column
 from gravitino.api.rel.expressions.distributions.distribution import 
Distribution
 from gravitino.api.rel.expressions.sorts.sort_order import SortOrder
@@ -64,7 +65,7 @@ from gravitino.utils import HTTPClient
 
 
 class RelationalCatalog(
-    BaseSchemaCatalog, TableCatalog, ViewCatalog
+    BaseSchemaCatalog, TableCatalog, ViewCatalog, SupportsSecrets
 ):  # pylint: disable=too-many-ancestors
     """Relational catalog is a catalog implementation
 
@@ -97,6 +98,9 @@ class RelationalCatalog(
             rest_client,
         )
 
+    def support_secrets(self) -> SupportsSecrets:
+        return self
+
     def as_table_catalog(self) -> TableCatalog:
         """Return this relational catalog as a :class:`TableCatalog`.
 
diff --git a/clients/client-python/gravitino/api/secret.py 
b/clients/client-python/gravitino/dto/responses/secrets_response.py
similarity index 52%
copy from clients/client-python/gravitino/api/secret.py
copy to clients/client-python/gravitino/dto/responses/secrets_response.py
index fb1526afae..ab7654d4e5 100644
--- a/clients/client-python/gravitino/api/secret.py
+++ b/clients/client-python/gravitino/dto/responses/secrets_response.py
@@ -15,31 +15,30 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from dataclasses import dataclass
 from typing import Dict
+from dataclasses import dataclass, field
+from dataclasses_json import config
 
+from gravitino.dto.responses.base_response import BaseResponse
+from gravitino.exceptions.base import IllegalArgumentException
 
-@dataclass
-class SecretBinding:
-    """Write-through secret binding: provider instance name plus plaintext."""
-
-    provider: str
-    plaintext: str
 
-    def __repr__(self) -> str:
-        return f"SecretBinding(provider={self.provider!r}, plaintext=***)"
+@dataclass
+class SecretsResponse(BaseResponse):
+    """Response for secret properties."""
 
+    _secrets: Dict[str, str] = field(metadata=config(field_name="secrets"))
 
-@dataclass
-class SecretReference:
-    """External secret locator: provider instance name plus provider-specific 
attributes."""
+    def secrets(self) -> Dict[str, str]:
+        return self._secrets
 
-    provider: str
-    attributes: Dict[str, str]
+    def validate(self):
+        """Validates the response data.
 
-    def __post_init__(self):
-        if self.attributes is None or len(self.attributes) == 0:
-            raise ValueError("attributes must not be null or empty")
+        Raises:
+            IllegalArgumentException if secrets are None.
+        """
+        super().validate()
 
-    def __repr__(self) -> str:
-        return f"SecretReference(provider={self.provider!r}, 
attributes={self.attributes!r})"
+        if self._secrets is None:
+            raise IllegalArgumentException('"secrets" must not be null')
diff --git a/clients/client-python/gravitino/api/secret.py 
b/clients/client-python/gravitino/exceptions/handlers/secret_error_handler.py
similarity index 50%
rename from clients/client-python/gravitino/api/secret.py
rename to 
clients/client-python/gravitino/exceptions/handlers/secret_error_handler.py
index fb1526afae..4eff8c9544 100644
--- a/clients/client-python/gravitino/api/secret.py
+++ 
b/clients/client-python/gravitino/exceptions/handlers/secret_error_handler.py
@@ -15,31 +15,24 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from dataclasses import dataclass
-from typing import Dict
+from gravitino.constants.error import ErrorConstants
+from gravitino.dto.responses.error_response import ErrorResponse
+from gravitino.exceptions.base import CatalogNotInUseException, 
NotFoundException
+from gravitino.exceptions.handlers.rest_error_handler import RestErrorHandler
 
 
-@dataclass
-class SecretBinding:
-    """Write-through secret binding: provider instance name plus plaintext."""
+class SecretErrorHandler(RestErrorHandler):
+    def handle(self, error_response: ErrorResponse):
+        error_message = error_response.format_error_message()
+        code = error_response.code()
 
-    provider: str
-    plaintext: str
+        if code == ErrorConstants.NOT_FOUND_CODE:
+            raise NotFoundException(error_message)
 
-    def __repr__(self) -> str:
-        return f"SecretBinding(provider={self.provider!r}, plaintext=***)"
+        if code == ErrorConstants.NOT_IN_USE_CODE:
+            raise CatalogNotInUseException(error_message)
 
+        super().handle(error_response)
 
-@dataclass
-class SecretReference:
-    """External secret locator: provider instance name plus provider-specific 
attributes."""
 
-    provider: str
-    attributes: Dict[str, str]
-
-    def __post_init__(self):
-        if self.attributes is None or len(self.attributes) == 0:
-            raise ValueError("attributes must not be null or empty")
-
-    def __repr__(self) -> str:
-        return f"SecretReference(provider={self.provider!r}, 
attributes={self.attributes!r})"
+SECRET_ERROR_HANDLER = SecretErrorHandler()
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/SecretsResponse.java 
b/common/src/main/java/org/apache/gravitino/dto/responses/SecretsResponse.java
new file mode 100644
index 0000000000..ebef221da0
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/responses/SecretsResponse.java
@@ -0,0 +1,62 @@
+/*
+ * 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.dto.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+/** Represents a response for secret properties. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class SecretsResponse extends BaseResponse {
+
+  @JsonProperty("secrets")
+  private final Map<String, String> secrets;
+
+  /**
+   * Creates a new SecretsResponse.
+   *
+   * @param secrets The secret properties map.
+   */
+  public SecretsResponse(Map<String, String> secrets) {
+    super(0);
+    this.secrets = secrets;
+  }
+
+  /**
+   * This is the constructor that is used by Jackson deserializer to create an 
instance of
+   * SecretsResponse.
+   */
+  public SecretsResponse() {
+    super();
+    this.secrets = null;
+  }
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+
+    Preconditions.checkArgument(secrets != null, "\"secrets\" must not be 
null");
+  }
+}
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java 
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index e0a671b844..6c9e9f52ea 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -100,6 +100,7 @@ import org.apache.gravitino.metrics.source.JVMMetricsSource;
 import org.apache.gravitino.policy.PolicyDispatcher;
 import org.apache.gravitino.policy.PolicyManager;
 import org.apache.gravitino.secret.SecretManager;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
 import org.apache.gravitino.secret.SecretProviderRegistry;
 import org.apache.gravitino.stats.StatisticDispatcher;
 import org.apache.gravitino.stats.StatisticManager;
@@ -158,6 +159,8 @@ public class GravitinoEnv {
 
   private CredentialOperationDispatcher credentialOperationDispatcher;
 
+  private SecretPropertyOperationDispatcher secretPropertyOperationDispatcher;
+
   private KmsClientRegistry kmsClientRegistry;
 
   private SecretManager secretManager;
@@ -427,6 +430,15 @@ public class GravitinoEnv {
     return credentialOperationDispatcher;
   }
 
+  /**
+   * Get the {@link SecretPropertyOperationDispatcher} associated with the 
Gravitino environment.
+   *
+   * @return The {@link SecretPropertyOperationDispatcher} instance.
+   */
+  public SecretPropertyOperationDispatcher secretPropertyOperationDispatcher() 
{
+    return secretPropertyOperationDispatcher;
+  }
+
   /**
    * Get the metadata-only KMS client registry associated with the Gravitino 
environment.
    *
@@ -756,6 +768,10 @@ public class GravitinoEnv {
     this.credentialOperationDispatcher =
         new CredentialOperationDispatcher(catalogManager, entityStore, 
idGenerator, secretManager);
 
+    this.secretPropertyOperationDispatcher =
+        new SecretPropertyOperationDispatcher(
+            catalogManager, entityStore, idGenerator, secretManager);
+
     SchemaOperationDispatcher schemaOperationDispatcher =
         new SchemaOperationDispatcher(catalogManager, entityStore, 
idGenerator, secretManager);
     this.internalSchemaDispatcher = schemaOperationDispatcher;
diff --git 
a/core/src/main/java/org/apache/gravitino/secret/SecretPropertyOperationDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyOperationDispatcher.java
new file mode 100644
index 0000000000..0c82fa3e8a
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyOperationDispatcher.java
@@ -0,0 +1,136 @@
+/*
+ * 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.secret;
+
+import java.io.IOException;
+import java.util.Map;
+import javax.ws.rs.NotSupportedException;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.catalog.CatalogManager;
+import org.apache.gravitino.catalog.OperationDispatcher;
+import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.NoSuchFilesetException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.meta.FilesetEntity;
+import org.apache.gravitino.meta.SchemaEntity;
+import org.apache.gravitino.storage.IdGenerator;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/**
+ * Dispatches secrets requests for catalog, schema, and fileset metadata 
objects.
+ *
+ * <p>Loads raw entity properties (including secret URNs) from the entity 
store / catalog entity,
+ * then resolves only secret-manager URNs via {@link 
SecretPropertyUtils#buildSecrets}.
+ */
+public class SecretPropertyOperationDispatcher extends OperationDispatcher {
+
+  /**
+   * Creates a new SecretPropertyOperationDispatcher.
+   *
+   * @param catalogManager The CatalogManager instance.
+   * @param store The EntityStore instance.
+   * @param idGenerator The IdGenerator instance.
+   * @param secretManager The SecretManager instance.
+   */
+  public SecretPropertyOperationDispatcher(
+      CatalogManager catalogManager,
+      EntityStore store,
+      IdGenerator idGenerator,
+      SecretManager secretManager) {
+    super(catalogManager, store, idGenerator, secretManager);
+  }
+
+  /**
+   * Returns resolved secret-manager plaintext properties for the given 
metadata object.
+   *
+   * @param identifier The entity name identifier.
+   * @param entityType The entity type (CATALOG, SCHEMA, or FILESET).
+   * @return secret plaintext properties; never null
+   */
+  public Map<String, String> getSecrets(NameIdentifier identifier, 
Entity.EntityType entityType) {
+    Map<String, String> rawProperties = loadRawProperties(identifier, 
entityType);
+    return SecretPropertyUtils.buildSecrets(secretManager, rawProperties);
+  }
+
+  private Map<String, String> loadRawProperties(
+      NameIdentifier identifier, Entity.EntityType entityType) {
+    switch (entityType) {
+      case CATALOG:
+        return loadCatalogRawProperties(identifier);
+      case SCHEMA:
+        return loadSchemaRawProperties(identifier);
+      case FILESET:
+        return loadFilesetRawProperties(identifier);
+      default:
+        throw new NotSupportedException(
+            "Doesn't support secret property operations for entity type: " + 
entityType);
+    }
+  }
+
+  private Map<String, String> loadCatalogRawProperties(NameIdentifier 
identifier) {
+    return doWithCatalog(
+        identifier,
+        wrapper -> {
+          wrapper.catalog().checkMetalakeInUse();
+          return wrapper.catalog().entity().getProperties();
+        },
+        NoSuchCatalogException.class);
+  }
+
+  private Map<String, String> loadSchemaRawProperties(NameIdentifier 
identifier) {
+    NameIdentifier catalogIdent = 
NameIdentifierUtil.getCatalogIdentifier(identifier);
+    doWithCatalog(
+        catalogIdent,
+        wrapper -> {
+          wrapper.catalog().checkMetalakeInUse();
+          return null;
+        },
+        NoSuchCatalogException.class);
+    try {
+      SchemaEntity entity = store.get(identifier, Entity.EntityType.SCHEMA, 
SchemaEntity.class);
+      return entity.properties() == null ? Map.of() : entity.properties();
+    } catch (NoSuchEntityException e) {
+      throw new NoSuchSchemaException(e, "Schema %s does not exist", 
identifier);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to load schema entity " + identifier, 
e);
+    }
+  }
+
+  private Map<String, String> loadFilesetRawProperties(NameIdentifier 
identifier) {
+    NameIdentifier catalogIdent = 
NameIdentifierUtil.getCatalogIdentifier(identifier);
+    doWithCatalog(
+        catalogIdent,
+        wrapper -> {
+          wrapper.catalog().checkMetalakeInUse();
+          return null;
+        },
+        NoSuchCatalogException.class);
+    try {
+      FilesetEntity entity = store.get(identifier, Entity.EntityType.FILESET, 
FilesetEntity.class);
+      return entity.properties() == null ? Map.of() : entity.properties();
+    } catch (NoSuchEntityException e) {
+      throw new NoSuchFilesetException(e, "Fileset %s does not exist", 
identifier);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to load fileset entity " + 
identifier, e);
+    }
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java 
b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
index b7988cd6dc..f7ae60b570 100644
--- a/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
+++ b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
@@ -48,6 +48,64 @@ public final class SecretPropertyUtils {
     return key != null && value != null && value.startsWith(URN_PREFIX) && 
value.endsWith(key);
   }
 
+  /**
+   * Builds a map of secret-manager plaintext properties only.
+   *
+   * <p>Starting from raw entity properties:
+   *
+   * <ol>
+   *   <li>Include every entry where {@link #isSecretProperty} is true, 
including keys that may also
+   *       appear in credential vending (for example {@code jdbc-password} or 
{@code
+   *       s3-secret-access-key}).
+   *   <li>Resolve secret URN values to plaintext via {@link 
SecretManager#readSecret}.
+   * </ol>
+   *
+   * <p>Normal non-secret properties are not included. Plaintext values that 
are not secret URNs are
+   * not included even when the key is sensitive.
+   *
+   * @param secretManager secret manager used to resolve URNs
+   * @param rawProperties raw entity properties (may be null)
+   * @return a new secret plaintext property map; never null
+   */
+  public static Map<String, String> buildSecrets(
+      SecretManager secretManager, @Nullable Map<String, String> 
rawProperties) {
+    Preconditions.checkArgument(secretManager != null, "secretManager must not 
be null");
+    if (rawProperties == null || rawProperties.isEmpty()) {
+      return Map.of();
+    }
+    Map<String, String> secrets = new HashMap<>();
+    for (Map.Entry<String, String> entry : rawProperties.entrySet()) {
+      String key = entry.getKey();
+      String value = entry.getValue();
+      if (key == null || value == null) {
+        continue;
+      }
+      if (isSecretProperty(key, value)) {
+        secrets.put(key, secretManager.readSecret(SecretUrn.parse(value)));
+      }
+    }
+    return secrets;
+  }
+
+  /**
+   * Merges base properties with secret plaintext properties.
+   *
+   * <p>Returns a new mutable map containing all entries from {@code base}, 
then overlays {@code
+   * secrets}. Null maps are treated as empty.
+   *
+   * @param base non-secret / default-load properties (may be null)
+   * @param secrets secret plaintext properties from {@link #buildSecrets} 
(may be null)
+   * @return a new mutable merged property map; never null
+   */
+  public static Map<String, String> mergeProperties(
+      @Nullable Map<String, String> base, @Nullable Map<String, String> 
secrets) {
+    Map<String, String> merged = copyEntityProperties(base);
+    if (secrets != null && !secrets.isEmpty()) {
+      merged.putAll(secrets);
+    }
+    return merged;
+  }
+
   /**
    * Returns a mutable copy of a property map for create-time assembly.
    *
diff --git 
a/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java 
b/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
index 89fd5d065b..d51e0a8859 100644
--- 
a/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
+++ 
b/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
@@ -70,6 +70,63 @@ public class TestSecretPropertyUtils {
     }
   }
 
+  @Test
+  void testBuildSecrets() {
+    try (SecretManager sm = memorySecretManager()) {
+      Map<String, String> entityProps = new HashMap<>();
+      entityProps.put("jdbc-url", "jdbc:mysql://localhost/db");
+      entityProps.put("jdbc-user", "root");
+      Map<String, SecretBinding> bindings =
+          Map.of(
+              "jdbc-password",
+              new SecretBinding("memory", "s3cr3t"),
+              "custom-secret",
+              new SecretBinding("memory", "custom-value"),
+              "s3-secret-access-key",
+              new SecretBinding("memory", "s3-secret-value"));
+      List<SecretMaterial> writes =
+          sm.assembleSecretMaterials(
+              Map.of("jdbc-url", "jdbc:mysql://localhost/db", "jdbc-user", 
"root"),
+              entityProps,
+              "catalog",
+              42L,
+              bindings,
+              Map.of());
+      sm.writeSecrets(writes);
+
+      entityProps.put("s3-access-key-id", "AKIA");
+      entityProps.put("visible", "ok");
+
+      Map<String, String> secrets = SecretPropertyUtils.buildSecrets(sm, 
entityProps);
+
+      // All secret-URN entries, including keys also used by credential vending
+      Assertions.assertEquals("custom-value", secrets.get("custom-secret"));
+      Assertions.assertEquals("s3cr3t", secrets.get("jdbc-password"));
+      Assertions.assertEquals("s3-secret-value", 
secrets.get("s3-secret-access-key"));
+      Assertions.assertFalse(secrets.containsKey("jdbc-user"));
+      Assertions.assertFalse(secrets.containsKey("jdbc-url"));
+      Assertions.assertFalse(secrets.containsKey("visible"));
+      Assertions.assertFalse(secrets.containsKey("s3-access-key-id"));
+    }
+  }
+
+  @Test
+  void testBuildSecretsNullAndEmpty() {
+    try (SecretManager sm = memorySecretManager()) {
+      Assertions.assertTrue(SecretPropertyUtils.buildSecrets(sm, 
null).isEmpty());
+      Assertions.assertTrue(SecretPropertyUtils.buildSecrets(sm, 
Map.of()).isEmpty());
+    }
+  }
+
+  @Test
+  void testMergeProperties() {
+    Map<String, String> merged =
+        SecretPropertyUtils.mergeProperties(Map.of("a", "1"), Map.of("b", "2", 
"a", "override"));
+    Assertions.assertEquals("override", merged.get("a"));
+    Assertions.assertEquals("2", merged.get("b"));
+    Assertions.assertTrue(SecretPropertyUtils.mergeProperties(null, 
null).isEmpty());
+  }
+
   private static SecretManager memorySecretManager() {
     Config config = new Config(false) {};
     Properties properties = new Properties();
diff --git a/docs/apache-hive-catalog.md b/docs/apache-hive-catalog.md
index 67f6fcc584..3d6770ecdf 100644
--- a/docs/apache-hive-catalog.md
+++ b/docs/apache-hive-catalog.md
@@ -83,7 +83,7 @@ When using Gravitino authorization for Hive with Apache 
Ranger, see the [Authori
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `s3-access-key-id`, 
`s3-secret-access-key`, `oss-access-key-id`, and `oss-secret-access-key` are 
hidden from the load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as credential-vending keys are hidden from 
the default load catalog response. Retrieve secret-manager-backed properties 
(including keys that overlap with credential vending) via `getSecrets` / `GET 
.../objects/{type}/{fullName}/secrets`. The [credential vending 
API](security/credential-vending.md) remains available for typed credential 
delivery.
 :::
 
 ## Schema
diff --git a/docs/aws-glue-catalog.md b/docs/aws-glue-catalog.md
index 590f5bae2e..658e733f51 100644
--- a/docs/aws-glue-catalog.md
+++ b/docs/aws-glue-catalog.md
@@ -55,7 +55,7 @@ Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-pro
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `aws-access-key-id` and 
`aws-secret-access-key` are hidden from the load catalog response. Use the 
[credential vending API](security/credential-vending.md) to retrieve them at 
runtime.
+Sensitive catalog properties such as credential-vending keys are hidden from 
the default load catalog response. Retrieve secret-manager-backed properties 
(including keys that overlap with credential vending) via `getSecrets` / `GET 
.../objects/{type}/{fullName}/secrets`. The [credential vending 
API](security/credential-vending.md) remains available for typed credential 
delivery.
 :::
 
 ## Schema
diff --git a/docs/jdbc-doris-catalog.md b/docs/jdbc-doris-catalog.md
index 9b9647d87e..998d57b2b2 100644
--- a/docs/jdbc-doris-catalog.md
+++ b/docs/jdbc-doris-catalog.md
@@ -84,7 +84,7 @@ Returning null for DATETIME type precision. Driver version: 
mysql-connector-java
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the default load catalog response. Retrieve secret-manager-backed 
properties (including `jdbc-password` when stored as a secret URN) via 
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential 
vending API](security/credential-vending.md) (`getCredentials` / 
`JdbcCredential`) remains available for typed credential delivery.
 :::
 
 ## Schema
diff --git a/docs/jdbc-hologres-catalog.md b/docs/jdbc-hologres-catalog.md
index a4412dbd47..332ce4ccb4 100644
--- a/docs/jdbc-hologres-catalog.md
+++ b/docs/jdbc-hologres-catalog.md
@@ -62,7 +62,7 @@ Hologres uses the PostgreSQL JDBC Driver (version 42.3.2 or 
later recommended).
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the default load catalog response. Retrieve secret-manager-backed 
properties (including `jdbc-password` when stored as a secret URN) via 
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential 
vending API](security/credential-vending.md) (`getCredentials` / 
`JdbcCredential`) remains available for typed credential delivery.
 :::
 
 ## Schema
diff --git a/docs/jdbc-mysql-catalog.md b/docs/jdbc-mysql-catalog.md
index 6e7c4172ce..58429cf994 100644
--- a/docs/jdbc-mysql-catalog.md
+++ b/docs/jdbc-mysql-catalog.md
@@ -87,7 +87,7 @@ Returning null for TIMESTAMP type precision. Driver version: 
mysql-connector-jav
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the default load catalog response. Retrieve secret-manager-backed 
properties (including `jdbc-password` when stored as a secret URN) via 
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential 
vending API](security/credential-vending.md) (`getCredentials` / 
`JdbcCredential`) remains available for typed credential delivery.
 :::
 
 ## Schema
diff --git a/docs/jdbc-oceanbase-catalog.md b/docs/jdbc-oceanbase-catalog.md
index 319a2ad302..815d631937 100644
--- a/docs/jdbc-oceanbase-catalog.md
+++ b/docs/jdbc-oceanbase-catalog.md
@@ -87,7 +87,7 @@ Returning null for TIMESTAMP type precision. Driver version: 
mysql-connector-jav
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the default load catalog response. Retrieve secret-manager-backed 
properties (including `jdbc-password` when stored as a secret URN) via 
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential 
vending API](security/credential-vending.md) (`getCredentials` / 
`JdbcCredential`) remains available for typed credential delivery.
 :::
 
 ## Schema
diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md
index 28fa2b2c8b..674cc26c24 100644
--- a/docs/jdbc-postgresql-catalog.md
+++ b/docs/jdbc-postgresql-catalog.md
@@ -63,7 +63,7 @@ In PostgreSQL, the database corresponds to the Gravitino 
catalog, and the schema
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the default load catalog response. Retrieve secret-manager-backed 
properties (including `jdbc-password` when stored as a secret URN) via 
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential 
vending API](security/credential-vending.md) (`getCredentials` / 
`JdbcCredential`) remains available for typed credential delivery.
 :::
 
 ## Schema
diff --git a/docs/jdbc-starrocks-catalog.md b/docs/jdbc-starrocks-catalog.md
index 178f8bf4d1..f16d47988f 100644
--- a/docs/jdbc-starrocks-catalog.md
+++ b/docs/jdbc-starrocks-catalog.md
@@ -83,7 +83,7 @@ Returning null for DATETIME type precision. Driver version: 
mysql-connector-java
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the default load catalog response. Retrieve secret-manager-backed 
properties (including `jdbc-password` when stored as a secret URN) via 
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential 
vending API](security/credential-vending.md) (`getCredentials` / 
`JdbcCredential`) remains available for typed credential delivery.
 :::
 
 ## Schema
diff --git a/docs/lakehouse-iceberg-catalog.md 
b/docs/lakehouse-iceberg-catalog.md
index 281c8d86ac..996c0427e9 100644
--- a/docs/lakehouse-iceberg-catalog.md
+++ b/docs/lakehouse-iceberg-catalog.md
@@ -257,7 +257,7 @@ Gravitino provides the build-in 
`org.apache.gravitino.iceberg.common.cache.Local
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `s3-access-key-id`, 
`s3-secret-access-key`, `oss-access-key-id`, and `oss-secret-access-key` are 
hidden from the load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as credential-vending keys are hidden from 
the default load catalog response. Retrieve secret-manager-backed properties 
(including keys that overlap with credential vending) via `getSecrets` / `GET 
.../objects/{type}/{fullName}/secrets`. The [credential vending 
API](security/credential-vending.md) remains available for typed credential 
delivery.
 :::
 
 ## Schema
diff --git a/docs/lakehouse-paimon-catalog.md b/docs/lakehouse-paimon-catalog.md
index e398c5e5b4..0ee3cc9e31 100644
--- a/docs/lakehouse-paimon-catalog.md
+++ b/docs/lakehouse-paimon-catalog.md
@@ -99,7 +99,7 @@ Download the corresponding JDBC driver and place it to the 
`catalogs/lakehouse-p
 Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
 
 :::note
-Sensitive catalog properties such as `s3-access-key-id`, 
`s3-secret-access-key`, `jdbc-user`, and `jdbc-password` are hidden from the 
load catalog response. Use the [credential vending 
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are 
hidden from the default load catalog response. Retrieve secret-manager-backed 
properties (including `jdbc-password` when stored as a secret URN) via 
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential 
vending API](security/credential-vending.md) (`getCredentials` / 
`JdbcCredential`) remains available for typed credential delivery.
 :::
 
 ## Schema
diff --git a/docs/open-api/catalogs.yaml b/docs/open-api/catalogs.yaml
index f568b04c52..00be079033 100644
--- a/docs/open-api/catalogs.yaml
+++ b/docs/open-api/catalogs.yaml
@@ -154,7 +154,7 @@ paths:
         - catalog
       summary: Get catalog
       operationId: loadCatalog
-      description: Returns the specified catalog information in the specified 
metalake
+      description: Returns the specified catalog information in the specified 
metalake.
       responses:
         "200":
           $ref: "#/components/responses/CatalogResponse"
@@ -258,8 +258,6 @@ paths:
         "5xx":
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
-
-
 components:
   parameters:
     details:
diff --git a/docs/open-api/filesets.yaml b/docs/open-api/filesets.yaml
index 6ef16f304a..6ed8fe32bc 100644
--- a/docs/open-api/filesets.yaml
+++ b/docs/open-api/filesets.yaml
@@ -79,7 +79,7 @@ paths:
         - fileset
       summary: Get fileset
       operationId: loadFileset
-      description: Returns the specified fileset object
+      description: Returns the specified fileset object.
       responses:
         "200":
           $ref: "#/components/responses/FilesetResponse"
diff --git a/docs/open-api/openapi.yaml b/docs/open-api/openapi.yaml
index 76d32c69fb..c8007aa52e 100644
--- a/docs/open-api/openapi.yaml
+++ b/docs/open-api/openapi.yaml
@@ -99,6 +99,9 @@ paths:
   
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/credentials:
     $ref: 
"./credentials.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1objects~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1credentials"
 
+  
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/secrets:
+    $ref: 
"./secrets.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1objects~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1secrets"
+
   
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/tags/{tag}:
     $ref: 
"./tags.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1objects~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1tags~1%7Btag%7D"
 
diff --git a/docs/open-api/schemas.yaml b/docs/open-api/schemas.yaml
index 06f90f5056..634519cbbb 100644
--- a/docs/open-api/schemas.yaml
+++ b/docs/open-api/schemas.yaml
@@ -79,7 +79,7 @@ paths:
         - schema
       summary: Get schema
       operationId: loadSchema
-      description: Returns the specified schema in the specified catalog and 
metalake
+      description: Returns the specified schema in the specified catalog and 
metalake.
       responses:
         "200":
           $ref: "#/components/responses/SchemaResponse"
diff --git a/docs/open-api/secrets.yaml b/docs/open-api/secrets.yaml
index cfcf9db149..dfea6a9041 100644
--- a/docs/open-api/secrets.yaml
+++ b/docs/open-api/secrets.yaml
@@ -16,6 +16,49 @@
 # under the License.
 
 ---
+
+paths:
+
+  
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/secrets:
+    parameters:
+      - $ref: "./openapi.yaml#/components/parameters/metalake"
+      - $ref: "./openapi.yaml#/components/parameters/metadataObjectType"
+      - $ref: "./openapi.yaml#/components/parameters/metadataObjectFullName"
+    get:
+      tags:
+        - secrets
+      summary: Get secrets
+      operationId: getSecrets
+      description: |
+        Returns secret-manager plaintext properties for the metadata object.
+        Every secret-URN value is resolved and returned, including keys that 
may
+        also appear in the credentials API (for example jdbc-password).
+        Supported types: catalog, schema, fileset.
+      responses:
+        "200":
+          description: Returns the secrets map for the specified metadata 
object.
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "#/components/responses/SecretsResponse"
+              examples:
+                SecretsResponse:
+                  $ref: "#/components/examples/SecretsResponse"
+        "400":
+          $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+        "404":
+          description: Not Found - The specified metalake or object does not 
exist
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                NoSuchMetalakeException:
+                  $ref: 
"./metalakes.yaml#/components/examples/NoSuchMetalakeException"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+
 components:
   schemas:
     SecretBinding:
@@ -55,3 +98,28 @@ components:
           additionalProperties:
             type: string
           minProperties: 1
+
+  responses:
+    SecretsResponse:
+      type: object
+      properties:
+        code:
+          type: integer
+          format: int32
+          description: Status code of the response
+          enum:
+            - 0
+        secrets:
+          type: object
+          description: Map of secret property key to plaintext value
+          additionalProperties:
+            type: string
+
+  examples:
+    SecretsResponse:
+      value: {
+        "code": 0,
+        "secrets": {
+          "authentication.kerberos.keytab": "plaintext-keytab-content"
+        }
+      }
diff --git a/docs/security/credential-vending.md 
b/docs/security/credential-vending.md
index a8277cfa95..ea8f11306b 100755
--- a/docs/security/credential-vending.md
+++ b/docs/security/credential-vending.md
@@ -399,7 +399,7 @@ Bundle jars on Maven Central:
 
 ## Upgrading From a Release Earlier Than 1.3.0
 
-Sensitive catalog properties such as `s3-access-key-id`, 
`s3-secret-access-key`, `jdbc-user`, and `jdbc-password` are excluded from `GET 
/api/metalakes/{metalake}/catalogs/{catalog}`. Clients written against earlier 
releases that read those properties directly lose access to them.
+Sensitive catalog properties such as `s3-access-key-id`, 
`s3-secret-access-key`, `jdbc-user`, and `jdbc-password` are excluded from the 
default `GET /api/metalakes/{metalake}/catalogs/{catalog}` response. Retrieve 
secret-manager-backed properties (including those keys when stored as secret 
URNs) via `getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The 
credentials API (`getCredentials` / `JdbcCredential`) remains available for 
typed credential delivery. Clients written against  [...]
 
 For a zero-downtime migration, set the following in `gravitino.conf`:
 
diff --git 
a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java 
b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
index a9391470af..b12308c228 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -46,6 +46,7 @@ import org.apache.gravitino.metalake.MetalakeDispatcher;
 import org.apache.gravitino.metrics.MetricsSystem;
 import org.apache.gravitino.metrics.source.MetricsSource;
 import org.apache.gravitino.policy.PolicyDispatcher;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
 import org.apache.gravitino.server.authentication.ServerAuthenticator;
 import org.apache.gravitino.server.authorization.GravitinoAuthorizerProvider;
 import org.apache.gravitino.server.web.ConfigServlet;
@@ -156,6 +157,9 @@ public class GravitinoServer extends ResourceConfig {
             bind(gravitinoEnv.credentialOperationDispatcher())
                 .to(CredentialOperationDispatcher.class)
                 .ranked(1);
+            bind(gravitinoEnv.secretPropertyOperationDispatcher())
+                .to(SecretPropertyOperationDispatcher.class)
+                .ranked(1);
             
bind(gravitinoEnv.modelDispatcher()).to(ModelDispatcher.class).ranked(1);
             
bind(gravitinoEnv.functionDispatcher()).to(FunctionDispatcher.class).ranked(1);
             bind(lineageService).to(LineageDispatcher.class).ranked(1);
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
index ae67980587..11926facc3 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
@@ -62,6 +62,7 @@ import org.apache.gravitino.server.web.rest.GroupOperations;
 import org.apache.gravitino.server.web.rest.JobOperations;
 import org.apache.gravitino.server.web.rest.MetadataObjectCredentialOperations;
 import org.apache.gravitino.server.web.rest.MetadataObjectPolicyOperations;
+import org.apache.gravitino.server.web.rest.MetadataObjectSecretOperations;
 import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
 import org.apache.gravitino.server.web.rest.MetalakeOperations;
 import org.apache.gravitino.server.web.rest.ModelOperations;
@@ -115,7 +116,8 @@ public class GravitinoInterceptionService implements 
InterceptionService {
             PolicyOperations.class.getName(),
             MetadataObjectPolicyOperations.class.getName(),
             JobOperations.class.getName(),
-            MetadataObjectCredentialOperations.class.getName()));
+            MetadataObjectCredentialOperations.class.getName(),
+            MetadataObjectSecretOperations.class.getName()));
   }
 
   @Override
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index a3fb6e0a99..2570eac02a 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -145,6 +145,11 @@ public class ExceptionHandlers {
     return CredentialExceptionHandler.INSTANCE.handle(op, metadataObjectName, 
"", e);
   }
 
+  public static Response handleSecretException(
+      OperationType op, String metadataObjectName, Exception e) {
+    return SecretExceptionHandler.INSTANCE.handle(op, metadataObjectName, "", 
e);
+  }
+
   public static Response handleModelException(
       OperationType op, String model, String schema, Exception e) {
     return ModelExceptionHandler.INSTANCE.handle(op, model, schema, e);
@@ -749,6 +754,34 @@ public class ExceptionHandlers {
     }
   }
 
+  private static class SecretExceptionHandler extends BaseExceptionHandler {
+
+    private static final ExceptionHandler INSTANCE = new 
SecretExceptionHandler();
+
+    private static String getSecretErrorMsg(String parent, String reason) {
+      return String.format("Failed to get secrets under object [%s], reason 
[%s]", parent, reason);
+    }
+
+    @Override
+    public Response handle(OperationType op, String secret, String parent, 
Exception e) {
+      String errorMsg = getSecretErrorMsg(parent, getErrorMsg(e));
+      LOG.warn(errorMsg, e);
+
+      if (e instanceof IllegalArgumentException) {
+        return Utils.illegalArguments(errorMsg, e);
+
+      } else if (e instanceof NotFoundException) {
+        return Utils.notFound(errorMsg, e);
+
+      } else if (e instanceof NotInUseException) {
+        return Utils.notInUse(errorMsg, e);
+
+      } else {
+        return super.handle(op, secret, parent, e);
+      }
+    }
+  }
+
   private static class TagExceptionHandler extends BaseExceptionHandler {
 
     private static final ExceptionHandler INSTANCE = new TagExceptionHandler();
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectSecretOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectSecretOperations.java
new file mode 100644
index 0000000000..a55a6e6767
--- /dev/null
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectSecretOperations.java
@@ -0,0 +1,116 @@
+/*
+ *  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.server.web.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import com.google.common.collect.ImmutableSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotSupportedException;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.dto.responses.SecretsResponse;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationFullName;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationObjectType;
+import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
+import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.utils.MetadataObjectUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("/metalakes/{metalake}/objects/{type}/{fullName}/secrets")
+public class MetadataObjectSecretOperations {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(MetadataObjectSecretOperations.class);
+
+  private static final Set<MetadataObject.Type> supportsSecretMetadataTypes =
+      ImmutableSet.of(
+          MetadataObject.Type.CATALOG, MetadataObject.Type.SCHEMA, 
MetadataObject.Type.FILESET);
+
+  private final SecretPropertyOperationDispatcher 
secretPropertyOperationDispatcher;
+
+  @SuppressWarnings("unused")
+  @Context
+  private HttpServletRequest httpRequest;
+
+  @Inject
+  public MetadataObjectSecretOperations(SecretPropertyOperationDispatcher 
dispatcher) {
+    this.secretPropertyOperationDispatcher = dispatcher;
+  }
+
+  @GET
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "get-secrets." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "get-secrets", absolute = true)
+  @AuthorizationExpression(expression = 
AuthorizationExpressionConstants.CAN_ACCESS_METADATA)
+  public Response getSecrets(
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      @PathParam("type") @AuthorizationObjectType String type,
+      @PathParam("fullName") @AuthorizationFullName String fullName) {
+    LOG.info(
+        "Received get secrets request for object type: {}, full name: {} under 
metalake: {}",
+        type,
+        fullName,
+        metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            MetadataObject object =
+                MetadataObjects.parse(
+                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+            if (!supportsSecretOperations(object)) {
+              throw new NotSupportedException(
+                  "Doesn't support secret property operations for metadata 
object type");
+            }
+
+            NameIdentifier identifier = 
MetadataObjectUtil.toEntityIdent(metalake, object);
+            Entity.EntityType entityType = 
MetadataObjectUtil.toEntityType(object);
+            Map<String, String> secrets =
+                secretPropertyOperationDispatcher.getSecrets(identifier, 
entityType);
+            return Utils.ok(new SecretsResponse(secrets));
+          });
+    } catch (Exception e) {
+      return ExceptionHandlers.handleSecretException(OperationType.GET, 
fullName, e);
+    }
+  }
+
+  private static boolean supportsSecretOperations(MetadataObject 
metadataObject) {
+    return supportsSecretMetadataTypes.contains(metadataObject.type());
+  }
+}
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
index 4298c5d2c7..36fda659ea 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
@@ -37,6 +37,7 @@ import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.time.Instant;
 import java.util.Collections;
+import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.Application;
@@ -588,6 +589,24 @@ public class TestCatalogOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse1.getType());
   }
 
+  private static TestCatalog buildCatalogWithProperties(
+      String metalake, String catalogName, Map<String, String> properties) {
+    CatalogEntity entity =
+        CatalogEntity.builder()
+            .withId(1L)
+            .withName(catalogName)
+            .withComment("comment")
+            .withNamespace(Namespace.of(metalake))
+            .withProperties(properties)
+            .withType(Catalog.Type.RELATIONAL)
+            .withProvider("test")
+            .withAuditInfo(
+                
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+            .build();
+
+    return new 
TestCatalog().withCatalogConf(Collections.emptyMap()).withCatalogEntity(entity);
+  }
+
   private static TestCatalog buildCatalog(String metalake, String catalogName) 
{
     CatalogEntity entity =
         CatalogEntity.builder()
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectSecretOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectSecretOperations.java
new file mode 100644
index 0000000000..0d2e3703f6
--- /dev/null
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectSecretOperations.java
@@ -0,0 +1,140 @@
+/*
+ * 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.server.web.rest;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.responses.SecretsResponse;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestMetadataObjectSecretOperations extends JerseyTest {
+
+  private static class MockServletRequestFactory extends 
ServletRequestFactoryBase {
+
+    @Override
+    public HttpServletRequest get() {
+      HttpServletRequest request = mock(HttpServletRequest.class);
+      when(request.getRemoteUser()).thenReturn(null);
+      return request;
+    }
+  }
+
+  private SecretPropertyOperationDispatcher secretPropertyOperationDispatcher =
+      mock(SecretPropertyOperationDispatcher.class);
+
+  private String metalake = "test_metalake";
+
+  @Override
+  protected Application configure() {
+    try {
+      forceSet(
+          TestProperties.CONTAINER_PORT, 
String.valueOf(RESTUtils.findAvailablePort(2000, 3000)));
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    ResourceConfig resourceConfig = new ResourceConfig();
+    resourceConfig.register(MetadataObjectSecretOperations.class);
+    resourceConfig.register(
+        new AbstractBinder() {
+          @Override
+          protected void configure() {
+            bind(secretPropertyOperationDispatcher)
+                .to(SecretPropertyOperationDispatcher.class)
+                .ranked(2);
+            
bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class);
+          }
+        });
+
+    return resourceConfig;
+  }
+
+  @Test
+  public void testGetSecretsForCatalog() {
+    testGetSecretsForObject(MetadataObjects.parse("catalog", 
MetadataObject.Type.CATALOG));
+  }
+
+  @Test
+  public void testGetSecretsForSchema() {
+    testGetSecretsForObject(MetadataObjects.parse("catalog.schema", 
MetadataObject.Type.SCHEMA));
+  }
+
+  @Test
+  public void testGetSecretsForFileset() {
+    testGetSecretsForObject(
+        MetadataObjects.parse("catalog.schema.fileset", 
MetadataObject.Type.FILESET));
+  }
+
+  private void testGetSecretsForObject(MetadataObject metadataObject) {
+    when(secretPropertyOperationDispatcher.getSecrets(any(), 
any(Entity.EntityType.class)))
+        .thenReturn(Map.of("custom-secret", "plaintext"));
+
+    Response response =
+        target(basePath(metalake))
+            .path(metadataObject.type().toString())
+            .path(metadataObject.fullName())
+            .path("/secrets")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    SecretsResponse secretResponse = 
response.readEntity(SecretsResponse.class);
+    Assertions.assertEquals(0, secretResponse.getCode());
+    Assertions.assertEquals("plaintext", 
secretResponse.getSecrets().get("custom-secret"));
+
+    when(secretPropertyOperationDispatcher.getSecrets(any(), 
any(Entity.EntityType.class)))
+        .thenReturn(Map.of());
+    response =
+        target(basePath(metalake))
+            .path(metadataObject.type().toString())
+            .path(metadataObject.fullName())
+            .path("/secrets")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    secretResponse = response.readEntity(SecretsResponse.class);
+    Assertions.assertEquals(0, secretResponse.getCode());
+    Assertions.assertTrue(secretResponse.getSecrets().isEmpty());
+  }
+
+  private String basePath(String metalake) {
+    return "/metalakes/" + metalake + "/objects";
+  }
+}

Reply via email to