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 1661e8a5b6 [#12724] fix(core): resolve secret URNs in catalog 
credential vending (#12725)
1661e8a5b6 is described below

commit 1661e8a5b620c7e5a9e5a89ce7f6b81602f70189
Author: MaSai <[email protected]>
AuthorDate: Sun Aug 30 10:55:46 2026 +0800

    [#12724] fix(core): resolve secret URNs in catalog credential vending 
(#12725)
    
    ### What changes were proposed in this pull request?
    
    Resolve secret URNs to plaintext when initializing
    `CatalogCredentialManager` in
    `BaseCatalog.catalogCredentialManager()`, so `getCredentials` /
    `JdbcCredential`
    (and other static-key providers) return usable plaintext instead of
    storage URNs.
    Add a unit test covering JDBC password URN resolution.
    
    ### Why are the changes needed?
    
    Entity storage keeps secret-backed properties as URNs. `getSecrets`
    already
    resolves them, but credential vending initialized providers from raw
    entity
    props, so clients received URNs and standalone Iceberg REST JDBC auth
    failed.
    
    Fix: #12724
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. `getCredentials` for secret-backed static credentials (e.g.
    `jdbc-password`,
    S3 secret keys) now returns plaintext instead of secret URNs. Storage /
    getCatalog
    masking (`******`) is unchanged.
    
    ### How was this patch tested?
    
    - Unit: `TestBaseCatalogCredentialSecrets`
    - Manual E2E: Vault KV entity secrets with Iceberg REST (aux +
    standalone) and
    Lance REST (aux + standalone); credentials API returns plaintext;
    IRC/Lance OK
    
    Made with [Cursor](https://cursor.com)
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 .../tests/integration/integration_test_env.py      |  20 ++-
 .../apache/gravitino/catalog/CatalogManager.java   |   3 +-
 .../apache/gravitino/connector/BaseCatalog.java    |  26 +++-
 .../TestBaseCatalogCredentialSecrets.java          | 148 +++++++++++++++++++++
 4 files changed, 188 insertions(+), 9 deletions(-)

diff --git a/clients/client-python/tests/integration/integration_test_env.py 
b/clients/client-python/tests/integration/integration_test_env.py
index caa16ad5d8..06fa23588d 100644
--- a/clients/client-python/tests/integration/integration_test_env.py
+++ b/clients/client-python/tests/integration/integration_test_env.py
@@ -47,9 +47,18 @@ def get_gravitino_server_version(**kwargs):
         return False
 
 
-def check_gravitino_server_status(**kwargs) -> bool:
+def check_gravitino_server_status(
+    max_attempts: int = 30, interval_secs: float = 1.0, **kwargs
+) -> bool:
+    """Poll until the Gravitino server answers /api/version, or give up.
+
+    ``gravitino.sh restart`` returns as soon as the JVM process is up; Jetty 
can
+    still take several seconds before port 8090 accepts connections. Five
+    one-second polls are not enough under CI load (PythonIT has timed out when
+    Jetty became ready milliseconds after the last attempt).
+    """
     gravitino_server_running = False
-    for i in range(5):
+    for i in range(max_attempts):
         logger.info("Monitoring Gravitino server status. Attempt %s", i + 1)
         if get_gravitino_server_version(**kwargs):
             logger.debug("Gravitino Server is running")
@@ -57,7 +66,7 @@ def check_gravitino_server_status(**kwargs) -> bool:
             break
         else:
             logger.debug("Gravitino Server is not running")
-            time.sleep(1)
+            time.sleep(interval_secs)
     return gravitino_server_running
 
 
@@ -153,9 +162,10 @@ class IntegrationTestEnv(unittest.TestCase):
             logger.info("stderr: %s", result.stderr)
 
         gravitino_server_running = True
-        for i in range(5):
+        for i in range(30):
             logger.debug("Monitoring Gravitino server status. Attempt %s", i + 
1)
-            if check_gravitino_server_status():
+            # Single probe: the nested startup poll must not run here.
+            if get_gravitino_server_version():
                 logger.debug("Gravitino server still running")
                 time.sleep(1)
             else:
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java 
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index 8cc0625f6a..ec3439c4f4 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -1437,7 +1437,8 @@ public class CatalogManager implements CatalogDispatcher, 
Closeable {
     // Resolve secret URNs to plaintext for connector init only; entity 
storage keeps URNs.
     catalog
         
.withCatalogConf(secretManager.toPlaintextProperties(entity.getProperties()))
-        .withCatalogEntity(entity);
+        .withCatalogEntity(entity)
+        .withSecretManager(secretManager);
     catalog.initAuthorizationPluginInstance(classLoader, metalakeEntity.id());
     return catalog;
   }
diff --git a/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java 
b/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
index 0b7e581c55..43d1bdfb69 100644
--- a/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
+++ b/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
@@ -48,6 +48,7 @@ import org.apache.gravitino.credential.S3SecretKeyCredential;
 import org.apache.gravitino.exceptions.CatalogNotInUseException;
 import org.apache.gravitino.exceptions.MetalakeNotInUseException;
 import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.secret.SecretManager;
 import org.apache.gravitino.storage.AzureProperties;
 import org.apache.gravitino.storage.GCSProperties;
 import org.apache.gravitino.storage.OSSProperties;
@@ -87,6 +88,8 @@ public abstract class BaseCatalog<T extends BaseCatalog>
 
   private Map<String, String> conf;
 
+  private SecretManager secretManager;
+
   private volatile CatalogOperations ops;
 
   private volatile Capability capability;
@@ -426,6 +429,17 @@ public abstract class BaseCatalog<T extends BaseCatalog>
     return (T) this;
   }
 
+  /**
+   * Sets the {@link SecretManager} used to resolve secret URNs for this 
catalog.
+   *
+   * @param secretManager The SecretManager instance; may be null when secrets 
are not configured.
+   * @return The instance of the concrete subclass of BaseCatalog.
+   */
+  public T withSecretManager(SecretManager secretManager) {
+    this.secretManager = secretManager;
+    return (T) this;
+  }
+
   /**
    * Retrieves the CatalogEntity associated with this catalog.
    *
@@ -488,13 +502,19 @@ public abstract class BaseCatalog<T extends BaseCatalog>
   /**
    * Retrieves the properties of the catalog including credential providers. 
Detects storage and
    * catalog-specific credential providers from the raw entity properties 
(including hidden ones)
-   * and injects them before {@link CatalogCredentialManager} is initialized. 
Subclasses may
-   * override {@link #addCatalogSpecificCredentialProviders} to add additional 
providers.
+   * and injects them before {@link CatalogCredentialManager} is initialized. 
When a {@link
+   * SecretManager} is set, secret URN values are resolved to plaintext so 
credential vending can
+   * use them. Subclasses may override {@link 
#addCatalogSpecificCredentialProviders} to add
+   * additional providers.
    *
-   * @return A map of raw properties with credential providers set.
+   * @return A map of properties with credential providers set.
    */
   public Map<String, String> propertiesWithCredentialProviders() {
+    // Entity storage keeps secret URNs; resolve to plaintext when 
SecretManager is available.
     Map<String, String> props = Maps.newHashMap(entity().getProperties());
+    if (secretManager != null) {
+      props = Maps.newHashMap(secretManager.toPlaintextProperties(props));
+    }
     if 
(StringUtils.isNotBlank(props.get(CredentialConstants.CREDENTIAL_PROVIDERS))) {
       return props;
     }
diff --git 
a/core/src/test/java/org/apache/gravitino/credential/TestBaseCatalogCredentialSecrets.java
 
b/core/src/test/java/org/apache/gravitino/credential/TestBaseCatalogCredentialSecrets.java
new file mode 100644
index 0000000000..379e48dcbd
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/credential/TestBaseCatalogCredentialSecrets.java
@@ -0,0 +1,148 @@
+/*
+ * 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.credential;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.TestCatalog;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.secret.SecretConstants;
+import org.apache.gravitino.secret.SecretManager;
+import org.apache.gravitino.secret.SecretMaterial;
+import org.apache.gravitino.secret.SecretProviderRegistry;
+import org.apache.gravitino.secret.SecretUrn;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that {@code propertiesWithCredentialProviders} resolves entity 
secret URNs to plaintext
+ * via the catalog's {@link SecretManager}.
+ */
+public class TestBaseCatalogCredentialSecrets {
+
+  @Test
+  void testPropertiesWithCredentialProvidersResolvesEntityUrn() throws 
Exception {
+    try (SecretManager secretManager = memorySecretManager()) {
+      SecretUrn urn =
+          SecretUrn.buildWriteThrough(
+              "memory",
+              Map.of(
+                  SecretConstants.ATTR_ENTITY_TYPE,
+                  "catalog",
+                  SecretConstants.ATTR_ENTITY_ID,
+                  "1",
+                  SecretConstants.ATTR_PROPERTY_KEY,
+                  "jdbc-password"));
+      secretManager.writeSecrets(java.util.List.of(new SecretMaterial(urn, 
"plain-jdbc-pass")));
+
+      Map<String, String> entityProps =
+          Map.of(
+              CredentialConstants.CREDENTIAL_PROVIDERS,
+              JdbcCredential.JDBC_CREDENTIAL_TYPE,
+              JdbcCredential.GRAVITINO_JDBC_USER,
+              "iceberg",
+              JdbcCredential.GRAVITINO_JDBC_PASSWORD,
+              urn.toString());
+
+      CatalogEntity entity =
+          CatalogEntity.builder()
+              .withId(1L)
+              .withName("jdbc-secret-catalog")
+              .withNamespace(Namespace.of("metalake"))
+              .withType(Catalog.Type.RELATIONAL)
+              .withProvider("test")
+              .withProperties(entityProps)
+              .withAuditInfo(
+                  
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+              .build();
+
+      TestCatalog catalog =
+          new TestCatalog()
+              .withCatalogEntity(entity)
+              .withCatalogConf(entityProps)
+              .withSecretManager(secretManager);
+
+      Assertions.assertEquals(
+          "plain-jdbc-pass",
+          
catalog.propertiesWithCredentialProviders().get(JdbcCredential.GRAVITINO_JDBC_PASSWORD));
+
+      Optional<Credential> credential =
+          catalog
+              .catalogCredentialManager()
+              .getCredential(
+                  JdbcCredential.JDBC_CREDENTIAL_TYPE, new 
CatalogCredentialContext("user"));
+
+      Assertions.assertTrue(credential.isPresent());
+      JdbcCredential jdbc = (JdbcCredential) credential.get();
+      Assertions.assertEquals("iceberg", jdbc.jdbcUser());
+      Assertions.assertEquals("plain-jdbc-pass", jdbc.jdbcPassword());
+    }
+  }
+
+  @Test
+  void testPropertiesWithCredentialProvidersWithoutSecretManagerKeepsUrn() {
+    String urn = "urn:gravitino-secret:memory:catalog:1:jdbc-password";
+    Map<String, String> entityProps =
+        Map.of(
+            CredentialConstants.CREDENTIAL_PROVIDERS,
+            JdbcCredential.JDBC_CREDENTIAL_TYPE,
+            JdbcCredential.GRAVITINO_JDBC_USER,
+            "iceberg",
+            JdbcCredential.GRAVITINO_JDBC_PASSWORD,
+            urn);
+
+    CatalogEntity entity =
+        CatalogEntity.builder()
+            .withId(1L)
+            .withName("jdbc-secret-catalog")
+            .withNamespace(Namespace.of("metalake"))
+            .withType(Catalog.Type.RELATIONAL)
+            .withProvider("test")
+            .withProperties(entityProps)
+            .withAuditInfo(
+                
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+            .build();
+
+    TestCatalog catalog = new 
TestCatalog().withCatalogEntity(entity).withCatalogConf(entityProps);
+
+    Assertions.assertEquals(
+        urn,
+        
catalog.propertiesWithCredentialProviders().get(JdbcCredential.GRAVITINO_JDBC_PASSWORD));
+  }
+
+  private static SecretManager memorySecretManager() {
+    Config config = new Config(false) {};
+    Properties properties = new Properties();
+    properties.setProperty(SecretProviderRegistry.GRAVITINO_SECRET_PROVIDERS, 
"memory");
+    properties.setProperty(
+        SecretProviderRegistry.GRAVITINO_SECRET_PROVIDER_PREFIX
+            + "memory."
+            + SecretProviderRegistry.CLASS_NAME,
+        InMemorySecretsProvider.class.getName());
+    config.loadFromProperties(properties);
+    return new SecretManager(config);
+  }
+}

Reply via email to