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);
+ }
+}