This is an automated email from the ASF dual-hosted git repository.
FANNG1 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 40dfd5358d [#10080] improvement(authz): `IcebergCatalog`,
`PaimonCatalog` and `JdbcCatalog` support credential vending (#10081)
40dfd5358d is described below
commit 40dfd5358d0aa9ed289b17a0d1a75ba56b8936ad
Author: roryqi <[email protected]>
AuthorDate: Fri May 15 12:55:24 2026 +0800
[#10080] improvement(authz): `IcebergCatalog`, `PaimonCatalog` and
`JdbcCatalog` support credential vending (#10081)
### What changes were proposed in this pull request?
`IcebergCatalog`, `PaimonCatalog` and `JdbcCatalog` support credential
vending
### Why are the changes needed?
Fix #10080
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Added IT.
---------
Co-authored-by: diqiu50 <[email protected]>
---
.../workflows/backend-integration-test-action.yml | 1 +
.../gravitino/credential/JdbcCredential.java | 116 +++++++++++
.../org.apache.gravitino.credential.Credential | 1 +
.../apache/gravitino/catalog/jdbc/JdbcCatalog.java | 27 +++
.../catalog/jdbc/TestJdbcCatalogCredential.java | 195 ++++++++++++++++++
.../integration/test/CatalogMysqlCredentialIT.java | 163 +++++++++++++++
.../catalog/lakehouse/iceberg/IcebergCatalog.java | 43 ++++
.../lakehouse/iceberg/TestIcebergCatalog.java | 215 ++++++++++++++++++++
.../integration/test/TestMultipleJDBCLoad.java | 12 ++
.../catalog/lakehouse/paimon/PaimonCatalog.java | 43 ++++
.../lakehouse/paimon/TestPaimonCatalog.java | 220 +++++++++++++++++++++
.../test/CatalogPaimonJdbcCredentialIT.java | 155 +++++++++++++++
.../apache/gravitino/client/RelationalCatalog.java | 15 +-
.../credential/TestCredentialFactory.java | 22 +++
.../apache/gravitino/connector/BaseCatalog.java | 54 ++++-
.../credential/CredentialOperationDispatcher.java | 2 +-
.../credential/JdbcCredentialProvider.java | 62 ++++++
....apache.gravitino.credential.CredentialProvider | 3 +-
.../credential/TestJdbcCredentialProvider.java | 193 ++++++++++++++++++
....apache.gravitino.credential.CredentialProvider | 1 +
.../gravitino/integration/test/util/BaseIT.java | 20 +-
.../integration/test/util/TestDatabaseName.java | 3 +
22 files changed, 1556 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/backend-integration-test-action.yml
b/.github/workflows/backend-integration-test-action.yml
index fe1169001d..9e2239db13 100644
--- a/.github/workflows/backend-integration-test-action.yml
+++ b/.github/workflows/backend-integration-test-action.yml
@@ -104,3 +104,4 @@ jobs:
catalogs-contrib/**/*.log
catalogs-contrib/**/*.tar
distribution/**/*.log
+ distribution/**/*.out
diff --git
a/api/src/main/java/org/apache/gravitino/credential/JdbcCredential.java
b/api/src/main/java/org/apache/gravitino/credential/JdbcCredential.java
new file mode 100644
index 0000000000..eeaccc2620
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/credential/JdbcCredential.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.credential;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+
+/** JDBC credential for accessing JDBC backend services. */
+public class JdbcCredential implements Credential {
+
+ /** JDBC credential type. */
+ public static final String JDBC_CREDENTIAL_TYPE = "jdbc-user-password";
+ /** The JDBC user name. */
+ public static final String GRAVITINO_JDBC_USER = "jdbc-user";
+ /** The JDBC password. */
+ public static final String GRAVITINO_JDBC_PASSWORD = "jdbc-password";
+
+ private String jdbcUser;
+ private String jdbcPassword;
+
+ /**
+ * Constructs an instance of {@link JdbcCredential} with the JDBC user and
password.
+ *
+ * @param jdbcUser The JDBC user name.
+ * @param jdbcPassword The JDBC password.
+ */
+ public JdbcCredential(String jdbcUser, String jdbcPassword) {
+ validate(jdbcUser, jdbcPassword, 0);
+ this.jdbcUser = jdbcUser;
+ this.jdbcPassword = jdbcPassword;
+ }
+
+ /**
+ * This is the constructor that is used by credential factory to create an
instance of credential
+ * according to the credential information.
+ */
+ public JdbcCredential() {}
+
+ @Override
+ public String credentialType() {
+ return JDBC_CREDENTIAL_TYPE;
+ }
+
+ @Override
+ public long expireTimeInMs() {
+ return 0;
+ }
+
+ @Override
+ public Map<String, String> credentialInfo() {
+ return (new ImmutableMap.Builder<String, String>())
+ .put(GRAVITINO_JDBC_USER, jdbcUser)
+ .put(GRAVITINO_JDBC_PASSWORD, jdbcPassword)
+ .build();
+ }
+
+ @Override
+ public void initialize(Map<String, String> credentialInfo, long
expireTimeInMs) {
+ String jdbcUser = credentialInfo.get(GRAVITINO_JDBC_USER);
+ String jdbcPassword = credentialInfo.get(GRAVITINO_JDBC_PASSWORD);
+ validate(jdbcUser, jdbcPassword, expireTimeInMs);
+ this.jdbcUser = jdbcUser;
+ this.jdbcPassword = jdbcPassword;
+ }
+
+ /**
+ * Get JDBC user name.
+ *
+ * @return The JDBC user name.
+ */
+ public String jdbcUser() {
+ return jdbcUser;
+ }
+
+ /**
+ * Get JDBC password.
+ *
+ * @return The JDBC password.
+ */
+ public String jdbcPassword() {
+ return jdbcPassword;
+ }
+
+ @Override
+ public String toString() {
+ return "JdbcCredential{jdbcUser='" + jdbcUser + "'}";
+ }
+
+ private void validate(String jdbcUser, String jdbcPassword, long
expireTimeInMs) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(jdbcUser), "JDBC user
should not be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(jdbcPassword), "JDBC password should not be
empty");
+ // JDBC credentials are static (no server-issued expiry). expireTimeInMs
must always be 0.
+ Preconditions.checkArgument(
+ expireTimeInMs == 0, "The expire time of JdbcCredential should be 0");
+ }
+}
diff --git
a/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
b/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
index 2cd80032a7..b940abd0b5 100644
---
a/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
+++
b/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
@@ -25,3 +25,4 @@ org.apache.gravitino.credential.OSSSecretKeyCredential
org.apache.gravitino.credential.ADLSTokenCredential
org.apache.gravitino.credential.AzureAccountKeyCredential
org.apache.gravitino.credential.AwsIrsaCredential
+org.apache.gravitino.credential.JdbcCredential
diff --git
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalog.java
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalog.java
index 34b6f6927a..2faf1f9693 100644
---
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalog.java
+++
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalog.java
@@ -18,8 +18,12 @@
*/
package org.apache.gravitino.catalog.jdbc;
+import com.google.common.collect.Maps;
import java.util.Collections;
import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
import
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
import org.apache.gravitino.catalog.jdbc.converter.JdbcExceptionConverter;
import org.apache.gravitino.catalog.jdbc.converter.JdbcTypeConverter;
@@ -30,6 +34,8 @@ import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.PropertiesMetadata;
import org.apache.gravitino.connector.PropertyEntry;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.JdbcCredential;
/** Implementation of an Jdbc catalog in Gravitino. */
public abstract class JdbcCatalog extends BaseCatalog<JdbcCatalog> {
@@ -113,4 +119,25 @@ public abstract class JdbcCatalog extends
BaseCatalog<JdbcCatalog> {
public PropertiesMetadata tablePropertiesMetadata() throws
UnsupportedOperationException {
return TABLE_PROPERTIES_META;
}
+
+ @Override
+ @Evolving
+ public Map<String, String> propertiesWithCredentialProviders() {
+ Map<String, String> properties =
Maps.newHashMap(super.propertiesWithCredentialProviders());
+ return applyDefaultCredentialProviders(properties);
+ }
+
+ private Map<String, String> applyDefaultCredentialProviders(Map<String,
String> properties) {
+ if
(StringUtils.isNotBlank(properties.get(CredentialConstants.CREDENTIAL_PROVIDERS)))
{
+ return properties;
+ }
+
+ String jdbcUser = properties.get(JdbcConfig.USERNAME.getKey());
+ String jdbcPassword = properties.get(JdbcConfig.PASSWORD.getKey());
+ if (StringUtils.isNotBlank(jdbcUser) &&
StringUtils.isNotBlank(jdbcPassword)) {
+ properties.put(CredentialConstants.CREDENTIAL_PROVIDERS,
JdbcCredential.JDBC_CREDENTIAL_TYPE);
+ }
+
+ return properties;
+ }
}
diff --git
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogCredential.java
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogCredential.java
new file mode 100644
index 0000000000..5c9b4a463b
--- /dev/null
+++
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogCredential.java
@@ -0,0 +1,195 @@
+/*
+ * 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.jdbc;
+
+import com.google.common.collect.Maps;
+import java.time.Instant;
+import java.util.Map;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
+import
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
+import org.apache.gravitino.catalog.jdbc.converter.JdbcTypeConverter;
+import org.apache.gravitino.catalog.jdbc.operation.JdbcDatabaseOperations;
+import org.apache.gravitino.catalog.jdbc.operation.JdbcTableOperations;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.JdbcCredential;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.CatalogEntity;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Tests for JdbcCatalog credential provider functionality. */
+public class TestJdbcCatalogCredential {
+
+ /** A concrete implementation of JdbcCatalog for testing purposes. */
+ private static class TestableJdbcCatalog extends JdbcCatalog {
+
+ @Override
+ public String shortName() {
+ return "jdbc-test";
+ }
+
+ @Override
+ protected JdbcTypeConverter createJdbcTypeConverter() {
+ throw new UnsupportedOperationException("Not needed for credential
test");
+ }
+
+ @Override
+ protected JdbcDatabaseOperations createJdbcDatabaseOperations() {
+ throw new UnsupportedOperationException("Not needed for credential
test");
+ }
+
+ @Override
+ protected JdbcTableOperations createJdbcTableOperations() {
+ throw new UnsupportedOperationException("Not needed for credential
test");
+ }
+
+ @Override
+ protected JdbcColumnDefaultValueConverter
createJdbcColumnDefaultValueConverter() {
+ throw new UnsupportedOperationException("Not needed for credential
test");
+ }
+ }
+
+ @Test
+ void testJdbcCatalogDefaultCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC catalog with jdbc-user and jdbc-password
+ Map<String, String> jdbcProps = Maps.newHashMap();
+ jdbcProps.put(JdbcConfig.JDBC_URL.getKey(),
"jdbc:mysql://localhost:3306/test");
+ jdbcProps.put(JdbcConfig.JDBC_DRIVER.getKey(), "com.mysql.cj.jdbc.Driver");
+ jdbcProps.put(JdbcConfig.USERNAME.getKey(), "test-user");
+ jdbcProps.put(JdbcConfig.PASSWORD.getKey(), "test-password");
+
+ CatalogEntity jdbcEntity =
+ CatalogEntity.builder()
+ .withId(1L)
+ .withName("jdbc-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(TestableJdbcCatalog.Type.RELATIONAL)
+ .withProvider("jdbc-mysql")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcProps)
+ .build();
+
+ TestableJdbcCatalog jdbcCatalog = new TestableJdbcCatalog();
+ jdbcCatalog.withCatalogConf(jdbcProps).withCatalogEntity(jdbcEntity);
+ Map<String, String> properties =
jdbcCatalog.propertiesWithCredentialProviders();
+
+ // Should have jdbc credential provider
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+ Assertions.assertEquals(JdbcCredential.JDBC_CREDENTIAL_TYPE,
credentialProviders);
+ }
+
+ @Test
+ void testJdbcCatalogNoCredentialProvidersWithoutPassword() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC catalog without password - should not add credential provider
+ Map<String, String> jdbcProps = Maps.newHashMap();
+ jdbcProps.put(JdbcConfig.JDBC_URL.getKey(),
"jdbc:mysql://localhost:3306/test");
+ jdbcProps.put(JdbcConfig.JDBC_DRIVER.getKey(), "com.mysql.cj.jdbc.Driver");
+ jdbcProps.put(JdbcConfig.USERNAME.getKey(), "test-user");
+
+ CatalogEntity jdbcEntity =
+ CatalogEntity.builder()
+ .withId(2L)
+ .withName("jdbc-catalog-no-password")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(TestableJdbcCatalog.Type.RELATIONAL)
+ .withProvider("jdbc-mysql")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcProps)
+ .build();
+
+ TestableJdbcCatalog jdbcCatalog = new TestableJdbcCatalog();
+ jdbcCatalog.withCatalogConf(jdbcProps).withCatalogEntity(jdbcEntity);
+ Map<String, String> properties =
jdbcCatalog.propertiesWithCredentialProviders();
+
+ // Should not have credential providers
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNull(credentialProviders);
+ }
+
+ @Test
+ void testJdbcCatalogNoCredentialProvidersWithNeitherUserNorPassword() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC catalog with neither user nor password
+ Map<String, String> jdbcProps = Maps.newHashMap();
+ jdbcProps.put(JdbcConfig.JDBC_URL.getKey(),
"jdbc:mysql://localhost:3306/test");
+ jdbcProps.put(JdbcConfig.JDBC_DRIVER.getKey(), "com.mysql.cj.jdbc.Driver");
+
+ CatalogEntity jdbcEntity =
+ CatalogEntity.builder()
+ .withId(4L)
+ .withName("jdbc-catalog-no-creds")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(TestableJdbcCatalog.Type.RELATIONAL)
+ .withProvider("jdbc-mysql")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcProps)
+ .build();
+
+ TestableJdbcCatalog jdbcCatalog = new TestableJdbcCatalog();
+ jdbcCatalog.withCatalogConf(jdbcProps).withCatalogEntity(jdbcEntity);
+ Map<String, String> properties =
jdbcCatalog.propertiesWithCredentialProviders();
+
+ // Should not have credential providers
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNull(credentialProviders);
+ }
+
+ @Test
+ void testJdbcCatalogExplicitCredentialProvidersNotOverridden() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test that explicit credential-providers setting is not overridden
+ Map<String, String> explicitProps = Maps.newHashMap();
+ explicitProps.put(JdbcConfig.JDBC_URL.getKey(),
"jdbc:mysql://localhost:3306/test");
+ explicitProps.put(JdbcConfig.JDBC_DRIVER.getKey(),
"com.mysql.cj.jdbc.Driver");
+ explicitProps.put(JdbcConfig.USERNAME.getKey(), "test-user");
+ explicitProps.put(JdbcConfig.PASSWORD.getKey(), "test-password");
+ explicitProps.put(CredentialConstants.CREDENTIAL_PROVIDERS,
"custom-provider");
+
+ CatalogEntity explicitEntity =
+ CatalogEntity.builder()
+ .withId(3L)
+ .withName("explicit-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(TestableJdbcCatalog.Type.RELATIONAL)
+ .withProvider("jdbc-mysql")
+ .withAuditInfo(auditInfo)
+ .withProperties(explicitProps)
+ .build();
+
+ TestableJdbcCatalog explicitCatalog = new TestableJdbcCatalog();
+
explicitCatalog.withCatalogConf(explicitProps).withCatalogEntity(explicitEntity);
+ Map<String, String> properties =
explicitCatalog.propertiesWithCredentialProviders();
+
+ // Should keep explicit credential providers, not override
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertEquals("custom-provider", credentialProviders);
+ }
+}
diff --git
a/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/integration/test/CatalogMysqlCredentialIT.java
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/integration/test/CatalogMysqlCredentialIT.java
new file mode 100644
index 0000000000..4d909e98f8
--- /dev/null
+++
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/integration/test/CatalogMysqlCredentialIT.java
@@ -0,0 +1,163 @@
+/*
+ * 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.mysql.integration.test;
+
+import com.google.common.collect.Maps;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.JdbcCredential;
+import org.apache.gravitino.integration.test.container.ContainerSuite;
+import org.apache.gravitino.integration.test.container.MySQLContainer;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.apache.gravitino.integration.test.util.TestDatabaseName;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Integration test for MySQL catalog credential vending. Tests that JDBC
credentials can be
+ * retrieved through the credential API.
+ */
+@Tag("gravitino-docker-test")
+public class CatalogMysqlCredentialIT extends BaseIT {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(CatalogMysqlCredentialIT.class);
+ private static final ContainerSuite containerSuite =
ContainerSuite.getInstance();
+ private static final String provider = "jdbc-mysql";
+ private static final TestDatabaseName TEST_DB_NAME =
TestDatabaseName.MYSQL_CATALOG_CREDENTIAL_IT;
+
+ private String metalakeName =
GravitinoITUtils.genRandomName("mysql_credential_metalake");
+ private String catalogName =
GravitinoITUtils.genRandomName("mysql_credential_catalog");
+ private GravitinoMetalake metalake;
+ private MySQLContainer mysqlContainer;
+
+ @BeforeAll
+ public void startIntegrationTest() {
+ // Do nothing - override to prevent auto start
+ }
+
+ @BeforeAll
+ public void startUp() throws Exception {
+ containerSuite.startMySQLContainer(TEST_DB_NAME);
+ mysqlContainer = containerSuite.getMySQLContainer();
+ super.startIntegrationTest();
+
+ Assertions.assertFalse(client.metalakeExists(metalakeName));
+ metalake = client.createMetalake(metalakeName, "metalake comment",
Collections.emptyMap());
+ Assertions.assertTrue(client.metalakeExists(metalakeName));
+
+ createCatalog();
+ }
+
+ @AfterAll
+ public void tearDown() {
+ try {
+ if (metalake != null && metalake.catalogExists(catalogName)) {
+ metalake.disableCatalog(catalogName);
+ metalake.dropCatalog(catalogName, true);
+ }
+ if (client != null && client.metalakeExists(metalakeName)) {
+ client.disableMetalake(metalakeName);
+ client.dropMetalake(metalakeName, true);
+ }
+ } finally {
+ if (client != null) {
+ try {
+ client.close();
+ } catch (Exception e) {
+ LOG.error("Exception in closing client", e);
+ }
+ client = null;
+ }
+ try {
+ super.stopIntegrationTest();
+ } catch (Exception e) {
+ LOG.error("Exception in closing BaseIT", e);
+ }
+ }
+ }
+
+ @AfterAll
+ public void stopIntegrationTest() {
+ // Do nothing - override to prevent auto stop
+ }
+
+ private void createCatalog() throws SQLException {
+ Map<String, String> catalogProperties = Maps.newHashMap();
+ catalogProperties.put(
+ JdbcConfig.JDBC_URL.getKey(),
+ String.format(
+ "jdbc:mysql://%s:%d/",
+ mysqlContainer.getContainerIpAddress(),
MySQLContainer.MYSQL_PORT));
+ catalogProperties.put(
+ JdbcConfig.JDBC_DRIVER.getKey(),
mysqlContainer.getDriverClassName(TEST_DB_NAME));
+ catalogProperties.put(JdbcConfig.USERNAME.getKey(),
mysqlContainer.getUsername());
+ catalogProperties.put(JdbcConfig.PASSWORD.getKey(),
mysqlContainer.getPassword());
+
+ Catalog createdCatalog =
+ metalake.createCatalog(
+ catalogName,
+ Catalog.Type.RELATIONAL,
+ provider,
+ "MySQL catalog for credential testing",
+ catalogProperties);
+ Assertions.assertNotNull(createdCatalog);
+ Assertions.assertTrue(metalake.catalogExists(catalogName));
+ }
+
+ @Test
+ void testGetJdbcCredentialFromCatalog() {
+ Catalog catalog = metalake.loadCatalog(catalogName);
+ Credential[] credentials = catalog.supportsCredentials().getCredentials();
+
+ // Should have JDBC credential automatically configured
+ Assertions.assertEquals(1, credentials.length);
+ Assertions.assertInstanceOf(JdbcCredential.class, credentials[0]);
+
+ JdbcCredential jdbcCredential = (JdbcCredential) credentials[0];
+ Assertions.assertEquals(mysqlContainer.getUsername(),
jdbcCredential.jdbcUser());
+ Assertions.assertEquals(mysqlContainer.getPassword(),
jdbcCredential.jdbcPassword());
+ Assertions.assertEquals(0, jdbcCredential.expireTimeInMs());
+ Assertions.assertEquals(JdbcCredential.JDBC_CREDENTIAL_TYPE,
jdbcCredential.credentialType());
+ }
+
+ @Test
+ void testGetJdbcCredentialByType() {
+ Catalog catalog = metalake.loadCatalog(catalogName);
+ Credential credential =
+
catalog.supportsCredentials().getCredential(JdbcCredential.JDBC_CREDENTIAL_TYPE);
+
+ Assertions.assertNotNull(credential);
+ Assertions.assertInstanceOf(JdbcCredential.class, credential);
+
+ JdbcCredential jdbcCredential = (JdbcCredential) credential;
+ Assertions.assertEquals(mysqlContainer.getUsername(),
jdbcCredential.jdbcUser());
+ Assertions.assertEquals(mysqlContainer.getPassword(),
jdbcCredential.jdbcPassword());
+ }
+}
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
index 2e838e2be5..9dc1d53414 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
@@ -18,11 +18,18 @@
*/
package org.apache.gravitino.catalog.lakehouse.iceberg;
+import com.google.common.collect.Maps;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
import org.apache.gravitino.connector.BaseCatalog;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.PropertiesMetadata;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.JdbcCredential;
import org.apache.gravitino.rel.ViewCatalog;
/** Implementation of an Apache Iceberg catalog in Apache Gravitino. */
@@ -81,4 +88,40 @@ public class IcebergCatalog extends
BaseCatalog<IcebergCatalog> {
public PropertiesMetadata schemaPropertiesMetadata() throws
UnsupportedOperationException {
return SCHEMA_PROPERTIES_META;
}
+
+ @Override
+ @Evolving
+ public Map<String, String> propertiesWithCredentialProviders() {
+ Map<String, String> properties =
Maps.newHashMap(super.propertiesWithCredentialProviders());
+ return applyDefaultCredentialProviders(properties);
+ }
+
+ private Map<String, String> applyDefaultCredentialProviders(Map<String,
String> properties) {
+ // If credential providers already set, return as is
+ if
(StringUtils.isNotBlank(properties.get(CredentialConstants.CREDENTIAL_PROVIDERS)))
{
+ return properties;
+ }
+
+ List<String> credentialProviders = new ArrayList<>();
+
+ // Add JDBC credential provider if backend is JDBC and
jdbc-user/jdbc-password are set
+ String catalogBackend = properties.get(IcebergConstants.CATALOG_BACKEND);
+ if (catalogBackend != null
+ && IcebergCatalogBackend.JDBC.name().equalsIgnoreCase(catalogBackend))
{
+ String jdbcUser = properties.get(IcebergConstants.GRAVITINO_JDBC_USER);
+ String jdbcPassword =
properties.get(IcebergConstants.GRAVITINO_JDBC_PASSWORD);
+ if (StringUtils.isNotBlank(jdbcUser) &&
StringUtils.isNotBlank(jdbcPassword)) {
+ credentialProviders.add(JdbcCredential.JDBC_CREDENTIAL_TYPE);
+ }
+ }
+
+ addStorageCredentialProviders(properties, credentialProviders);
+
+ if (!credentialProviders.isEmpty()) {
+ properties.put(
+ CredentialConstants.CREDENTIAL_PROVIDERS, String.join(",",
credentialProviders));
+ }
+
+ return properties;
+ }
}
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
index e6e2f20741..ca8fb57c34 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
@@ -30,10 +30,18 @@ import
org.apache.gravitino.catalog.PropertiesMetadataHelpers;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.PropertiesMetadata;
+import org.apache.gravitino.credential.AzureAccountKeyCredential;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.JdbcCredential;
+import org.apache.gravitino.credential.OSSSecretKeyCredential;
+import org.apache.gravitino.credential.S3SecretKeyCredential;
import org.apache.gravitino.iceberg.common.ops.IcebergCatalogWrapper;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.CatalogEntity;
import org.apache.gravitino.rel.ViewCatalog;
+import org.apache.gravitino.storage.AzureProperties;
+import org.apache.gravitino.storage.OSSProperties;
+import org.apache.gravitino.storage.S3Properties;
import org.apache.iceberg.rest.responses.ListNamespacesResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -224,4 +232,211 @@ public class TestIcebergCatalog {
Assertions.assertNotNull(viewCatalog);
Assertions.assertTrue(viewCatalog instanceof IcebergCatalogOperations);
}
+
+ @Test
+ void testJdbcBackendDefaultCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC backend with jdbc-user and jdbc-password
+ Map<String, String> jdbcProps = Maps.newHashMap();
+ jdbcProps.put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+ jdbcProps.put(IcebergConstants.URI, "jdbc:sqlite::memory:");
+ jdbcProps.put(IcebergConstants.GRAVITINO_JDBC_USER, "test-user");
+ jdbcProps.put(IcebergConstants.GRAVITINO_JDBC_PASSWORD, "test-password");
+
+ CatalogEntity jdbcEntity =
+ CatalogEntity.builder()
+ .withId(1L)
+ .withName("jdbc-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(IcebergCatalog.Type.RELATIONAL)
+ .withProvider("iceberg")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcProps)
+ .build();
+
+ IcebergCatalog jdbcCatalog =
+ new
IcebergCatalog().withCatalogConf(jdbcProps).withCatalogEntity(jdbcEntity);
+ Map<String, String> properties =
jdbcCatalog.propertiesWithCredentialProviders();
+
+ // Should have jdbc credential provider
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+
Assertions.assertTrue(credentialProviders.contains(JdbcCredential.JDBC_CREDENTIAL_TYPE));
+ }
+
+ @Test
+ void testJdbcBackendWithS3CredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC backend with jdbc-user, jdbc-password, and S3 credentials
+ Map<String, String> jdbcS3Props = Maps.newHashMap();
+ jdbcS3Props.put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+ jdbcS3Props.put(IcebergConstants.URI, "jdbc:sqlite::memory:");
+ jdbcS3Props.put(IcebergConstants.GRAVITINO_JDBC_USER, "test-user");
+ jdbcS3Props.put(IcebergConstants.GRAVITINO_JDBC_PASSWORD, "test-password");
+ jdbcS3Props.put(S3Properties.GRAVITINO_S3_ACCESS_KEY_ID, "access-key");
+ jdbcS3Props.put(S3Properties.GRAVITINO_S3_SECRET_ACCESS_KEY, "secret-key");
+
+ CatalogEntity jdbcS3Entity =
+ CatalogEntity.builder()
+ .withId(2L)
+ .withName("jdbc-s3-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(IcebergCatalog.Type.RELATIONAL)
+ .withProvider("iceberg")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcS3Props)
+ .build();
+
+ IcebergCatalog jdbcS3Catalog =
+ new
IcebergCatalog().withCatalogConf(jdbcS3Props).withCatalogEntity(jdbcS3Entity);
+ Map<String, String> properties =
jdbcS3Catalog.propertiesWithCredentialProviders();
+
+ // Should have both jdbc and s3-secret-key credential providers
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+
Assertions.assertTrue(credentialProviders.contains(JdbcCredential.JDBC_CREDENTIAL_TYPE));
+ Assertions.assertTrue(
+
credentialProviders.contains(S3SecretKeyCredential.S3_SECRET_KEY_CREDENTIAL_TYPE));
+ }
+
+ @Test
+ void testNonJdbcBackendNoDefaultCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test non-JDBC backend (hive) - should not add default credential
providers
+ Map<String, String> hiveProps = Maps.newHashMap();
+ hiveProps.put(IcebergConstants.CATALOG_BACKEND, "hive");
+ hiveProps.put(IcebergConstants.URI, "thrift://localhost:9083");
+
+ CatalogEntity hiveEntity =
+ CatalogEntity.builder()
+ .withId(3L)
+ .withName("hive-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(IcebergCatalog.Type.RELATIONAL)
+ .withProvider("iceberg")
+ .withAuditInfo(auditInfo)
+ .withProperties(hiveProps)
+ .build();
+
+ IcebergCatalog hiveCatalog =
+ new
IcebergCatalog().withCatalogConf(hiveProps).withCatalogEntity(hiveEntity);
+ Map<String, String> properties =
hiveCatalog.propertiesWithCredentialProviders();
+
+ // Should not have credential providers for non-JDBC backend
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNull(credentialProviders);
+ }
+
+ @Test
+ void testExplicitCredentialProvidersNotOverridden() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test that explicit credential-providers setting is not overridden
+ Map<String, String> explicitProps = Maps.newHashMap();
+ explicitProps.put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+ explicitProps.put(IcebergConstants.URI, "jdbc:sqlite::memory:");
+ explicitProps.put(IcebergConstants.GRAVITINO_JDBC_USER, "test-user");
+ explicitProps.put(IcebergConstants.GRAVITINO_JDBC_PASSWORD,
"test-password");
+ explicitProps.put(CredentialConstants.CREDENTIAL_PROVIDERS,
"custom-provider");
+
+ CatalogEntity explicitEntity =
+ CatalogEntity.builder()
+ .withId(4L)
+ .withName("explicit-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(IcebergCatalog.Type.RELATIONAL)
+ .withProvider("iceberg")
+ .withAuditInfo(auditInfo)
+ .withProperties(explicitProps)
+ .build();
+
+ IcebergCatalog explicitCatalog =
+ new
IcebergCatalog().withCatalogConf(explicitProps).withCatalogEntity(explicitEntity);
+ Map<String, String> properties =
explicitCatalog.propertiesWithCredentialProviders();
+
+ // Should keep explicit credential providers, not override
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertEquals("custom-provider", credentialProviders);
+ }
+
+ @Test
+ void testJdbcBackendWithOSSCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC backend with jdbc-user, jdbc-password, and OSS credentials
+ Map<String, String> jdbcOssProps = Maps.newHashMap();
+ jdbcOssProps.put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+ jdbcOssProps.put(IcebergConstants.URI, "jdbc:sqlite::memory:");
+ jdbcOssProps.put(IcebergConstants.GRAVITINO_JDBC_USER, "test-user");
+ jdbcOssProps.put(IcebergConstants.GRAVITINO_JDBC_PASSWORD,
"test-password");
+ jdbcOssProps.put(OSSProperties.GRAVITINO_OSS_ACCESS_KEY_ID,
"oss-access-key");
+ jdbcOssProps.put(OSSProperties.GRAVITINO_OSS_ACCESS_KEY_SECRET,
"oss-secret-key");
+
+ CatalogEntity jdbcOssEntity =
+ CatalogEntity.builder()
+ .withId(5L)
+ .withName("jdbc-oss-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(IcebergCatalog.Type.RELATIONAL)
+ .withProvider("iceberg")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcOssProps)
+ .build();
+
+ IcebergCatalog jdbcOssCatalog =
+ new
IcebergCatalog().withCatalogConf(jdbcOssProps).withCatalogEntity(jdbcOssEntity);
+ Map<String, String> properties =
jdbcOssCatalog.propertiesWithCredentialProviders();
+
+ // Should have both jdbc and oss-secret-key credential providers
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+
Assertions.assertTrue(credentialProviders.contains(JdbcCredential.JDBC_CREDENTIAL_TYPE));
+ Assertions.assertTrue(
+
credentialProviders.contains(OSSSecretKeyCredential.OSS_SECRET_KEY_CREDENTIAL_TYPE));
+ }
+
+ @Test
+ void testJdbcBackendWithAzureCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC backend with jdbc-user, jdbc-password, and Azure credentials
+ Map<String, String> jdbcAzureProps = Maps.newHashMap();
+ jdbcAzureProps.put(IcebergConstants.CATALOG_BACKEND, "jdbc");
+ jdbcAzureProps.put(IcebergConstants.URI, "jdbc:sqlite::memory:");
+ jdbcAzureProps.put(IcebergConstants.GRAVITINO_JDBC_USER, "test-user");
+ jdbcAzureProps.put(IcebergConstants.GRAVITINO_JDBC_PASSWORD,
"test-password");
+ jdbcAzureProps.put(AzureProperties.GRAVITINO_AZURE_STORAGE_ACCOUNT_NAME,
"azure-account-name");
+ jdbcAzureProps.put(AzureProperties.GRAVITINO_AZURE_STORAGE_ACCOUNT_KEY,
"azure-account-key");
+
+ CatalogEntity jdbcAzureEntity =
+ CatalogEntity.builder()
+ .withId(6L)
+ .withName("jdbc-azure-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(IcebergCatalog.Type.RELATIONAL)
+ .withProvider("iceberg")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcAzureProps)
+ .build();
+
+ IcebergCatalog jdbcAzureCatalog =
+ new
IcebergCatalog().withCatalogConf(jdbcAzureProps).withCatalogEntity(jdbcAzureEntity);
+ Map<String, String> properties =
jdbcAzureCatalog.propertiesWithCredentialProviders();
+
+ // Should have both jdbc and azure-account-key credential providers
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+
Assertions.assertTrue(credentialProviders.contains(JdbcCredential.JDBC_CREDENTIAL_TYPE));
+ Assertions.assertTrue(
+
credentialProviders.contains(AzureAccountKeyCredential.AZURE_ACCOUNT_KEY_CREDENTIAL_TYPE));
+ }
}
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/TestMultipleJDBCLoad.java
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/TestMultipleJDBCLoad.java
index 75a7a8be4a..c2e5077c7b 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/TestMultipleJDBCLoad.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/TestMultipleJDBCLoad.java
@@ -31,6 +31,8 @@ import java.util.Map;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.JdbcCredential;
import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.iceberg.common.IcebergConfig;
import org.apache.gravitino.integration.test.container.MySQLContainer;
@@ -181,6 +183,16 @@ public class TestMultipleJDBCLoad extends BaseIT {
comment,
Collections.emptyMap());
+ Credential credential =
+
postgreSqlCatalog.supportsCredentials().getCredential(JdbcCredential.JDBC_CREDENTIAL_TYPE);
+
+ Assertions.assertNotNull(credential);
+ Assertions.assertInstanceOf(JdbcCredential.class, credential);
+
+ JdbcCredential jdbcCredential = (JdbcCredential) credential;
+ Assertions.assertEquals(postgreSQLContainer.getUsername(),
jdbcCredential.jdbcUser());
+ Assertions.assertEquals(postgreSQLContainer.getPassword(),
jdbcCredential.jdbcPassword());
+
Assertions.assertTrue(
mysqlCatalog.asTableCatalog().tableExists(NameIdentifier.of(schemaName,
tableName)));
Assertions.assertTrue(
diff --git
a/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/PaimonCatalog.java
b/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/PaimonCatalog.java
index 7fa8499b77..ba69cbfc0b 100644
---
a/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/PaimonCatalog.java
+++
b/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/PaimonCatalog.java
@@ -18,12 +18,19 @@
*/
package org.apache.gravitino.catalog.lakehouse.paimon;
+import com.google.common.collect.Maps;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Catalog;
+import org.apache.gravitino.annotation.Evolving;
import org.apache.gravitino.connector.BaseCatalog;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.PropertiesMetadata;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.JdbcCredential;
/**
* Implementation of {@link Catalog} that represents an Apache Paimon catalog
in Apache Gravitino.
@@ -77,4 +84,40 @@ public class PaimonCatalog extends
BaseCatalog<PaimonCatalog> {
public PropertiesMetadata schemaPropertiesMetadata() throws
UnsupportedOperationException {
return SCHEMA_PROPERTIES_META;
}
+
+ @Override
+ @Evolving
+ public Map<String, String> propertiesWithCredentialProviders() {
+ Map<String, String> properties =
Maps.newHashMap(super.propertiesWithCredentialProviders());
+ return applyDefaultCredentialProviders(properties);
+ }
+
+ private Map<String, String> applyDefaultCredentialProviders(Map<String,
String> properties) {
+ // If credential providers already set, return as is
+ if
(StringUtils.isNotBlank(properties.get(CredentialConstants.CREDENTIAL_PROVIDERS)))
{
+ return properties;
+ }
+
+ List<String> credentialProviders = new ArrayList<>();
+
+ // Add JDBC credential provider if backend is JDBC and
jdbc-user/jdbc-password are set
+ String catalogBackend = properties.get(PaimonConstants.CATALOG_BACKEND);
+ if (catalogBackend != null
+ && PaimonCatalogBackend.JDBC.name().equalsIgnoreCase(catalogBackend)) {
+ String jdbcUser = properties.get(PaimonConstants.GRAVITINO_JDBC_USER);
+ String jdbcPassword =
properties.get(PaimonConstants.GRAVITINO_JDBC_PASSWORD);
+ if (StringUtils.isNotBlank(jdbcUser) &&
StringUtils.isNotBlank(jdbcPassword)) {
+ credentialProviders.add(JdbcCredential.JDBC_CREDENTIAL_TYPE);
+ }
+ }
+
+ addStorageCredentialProviders(properties, credentialProviders);
+
+ if (!credentialProviders.isEmpty()) {
+ properties.put(
+ CredentialConstants.CREDENTIAL_PROVIDERS, String.join(",",
credentialProviders));
+ }
+
+ return properties;
+ }
}
diff --git
a/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
index f7187258f4..e83f8c7d78 100644
---
a/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
+++
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
@@ -37,8 +37,16 @@ import
org.apache.gravitino.catalog.lakehouse.paimon.ops.PaimonCatalogOps;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.PropertiesMetadata;
+import org.apache.gravitino.credential.AzureAccountKeyCredential;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.JdbcCredential;
+import org.apache.gravitino.credential.OSSSecretKeyCredential;
+import org.apache.gravitino.credential.S3SecretKeyCredential;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.storage.AzureProperties;
+import org.apache.gravitino.storage.OSSProperties;
+import org.apache.gravitino.storage.S3Properties;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -183,4 +191,216 @@ public class TestPaimonCatalog {
.contains(PaimonCatalogPropertiesMetadata.GRAVITINO_CATALOG_BACKEND));
}
}
+
+ @Test
+ void testJdbcBackendDefaultCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC backend with jdbc-user and jdbc-password
+ Map<String, String> jdbcProps = Maps.newHashMap();
+ jdbcProps.put(PaimonConstants.CATALOG_BACKEND, "jdbc");
+ jdbcProps.put(PaimonConstants.URI, "jdbc:sqlite::memory:");
+ jdbcProps.put(PaimonConstants.WAREHOUSE, tempDir);
+ jdbcProps.put(PaimonConstants.GRAVITINO_JDBC_USER, "test-user");
+ jdbcProps.put(PaimonConstants.GRAVITINO_JDBC_PASSWORD, "test-password");
+
+ CatalogEntity jdbcEntity =
+ CatalogEntity.builder()
+ .withId(1L)
+ .withName("jdbc-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(PaimonCatalog.Type.RELATIONAL)
+ .withProvider("lakehouse-paimon")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcProps)
+ .build();
+
+ PaimonCatalog jdbcCatalog =
+ new
PaimonCatalog().withCatalogConf(jdbcProps).withCatalogEntity(jdbcEntity);
+ Map<String, String> properties =
jdbcCatalog.propertiesWithCredentialProviders();
+
+ // Should have jdbc credential provider
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+
Assertions.assertTrue(credentialProviders.contains(JdbcCredential.JDBC_CREDENTIAL_TYPE));
+ }
+
+ @Test
+ void testJdbcBackendWithS3CredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC backend with jdbc-user, jdbc-password, and S3 credentials
+ Map<String, String> jdbcS3Props = Maps.newHashMap();
+ jdbcS3Props.put(PaimonConstants.CATALOG_BACKEND, "jdbc");
+ jdbcS3Props.put(PaimonConstants.URI, "jdbc:sqlite::memory:");
+ jdbcS3Props.put(PaimonConstants.WAREHOUSE, tempDir);
+ jdbcS3Props.put(PaimonConstants.GRAVITINO_JDBC_USER, "test-user");
+ jdbcS3Props.put(PaimonConstants.GRAVITINO_JDBC_PASSWORD, "test-password");
+ jdbcS3Props.put(S3Properties.GRAVITINO_S3_ACCESS_KEY_ID, "access-key");
+ jdbcS3Props.put(S3Properties.GRAVITINO_S3_SECRET_ACCESS_KEY, "secret-key");
+
+ CatalogEntity jdbcS3Entity =
+ CatalogEntity.builder()
+ .withId(2L)
+ .withName("jdbc-s3-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(PaimonCatalog.Type.RELATIONAL)
+ .withProvider("lakehouse-paimon")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcS3Props)
+ .build();
+
+ PaimonCatalog jdbcS3Catalog =
+ new
PaimonCatalog().withCatalogConf(jdbcS3Props).withCatalogEntity(jdbcS3Entity);
+ Map<String, String> properties =
jdbcS3Catalog.propertiesWithCredentialProviders();
+
+ // Should have both jdbc and s3-secret-key credential providers
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+
Assertions.assertTrue(credentialProviders.contains(JdbcCredential.JDBC_CREDENTIAL_TYPE));
+ Assertions.assertTrue(
+
credentialProviders.contains(S3SecretKeyCredential.S3_SECRET_KEY_CREDENTIAL_TYPE));
+ }
+
+ @Test
+ void testNonJdbcBackendNoDefaultCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test non-JDBC backend (filesystem) - should not add default credential
providers
+ Map<String, String> fsProps = Maps.newHashMap();
+ fsProps.put(PaimonConstants.CATALOG_BACKEND, "filesystem");
+ fsProps.put(PaimonConstants.WAREHOUSE, tempDir);
+
+ CatalogEntity fsEntity =
+ CatalogEntity.builder()
+ .withId(3L)
+ .withName("fs-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(PaimonCatalog.Type.RELATIONAL)
+ .withProvider("lakehouse-paimon")
+ .withAuditInfo(auditInfo)
+ .withProperties(fsProps)
+ .build();
+
+ PaimonCatalog fsCatalog =
+ new
PaimonCatalog().withCatalogConf(fsProps).withCatalogEntity(fsEntity);
+ Map<String, String> properties =
fsCatalog.propertiesWithCredentialProviders();
+
+ // Should not have credential providers for non-JDBC backend
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNull(credentialProviders);
+ }
+
+ @Test
+ void testJdbcBackendWithOSSCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC backend with jdbc-user, jdbc-password, and OSS credentials
+ Map<String, String> jdbcOssProps = Maps.newHashMap();
+ jdbcOssProps.put(PaimonConstants.CATALOG_BACKEND, "jdbc");
+ jdbcOssProps.put(PaimonConstants.URI, "jdbc:sqlite::memory:");
+ jdbcOssProps.put(PaimonConstants.WAREHOUSE, tempDir);
+ jdbcOssProps.put(PaimonConstants.GRAVITINO_JDBC_USER, "test-user");
+ jdbcOssProps.put(PaimonConstants.GRAVITINO_JDBC_PASSWORD, "test-password");
+ jdbcOssProps.put(OSSProperties.GRAVITINO_OSS_ACCESS_KEY_ID,
"oss-access-key");
+ jdbcOssProps.put(OSSProperties.GRAVITINO_OSS_ACCESS_KEY_SECRET,
"oss-secret-key");
+
+ CatalogEntity jdbcOssEntity =
+ CatalogEntity.builder()
+ .withId(5L)
+ .withName("jdbc-oss-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(PaimonCatalog.Type.RELATIONAL)
+ .withProvider("lakehouse-paimon")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcOssProps)
+ .build();
+
+ PaimonCatalog jdbcOssCatalog =
+ new
PaimonCatalog().withCatalogConf(jdbcOssProps).withCatalogEntity(jdbcOssEntity);
+ Map<String, String> properties =
jdbcOssCatalog.propertiesWithCredentialProviders();
+
+ // Should have both jdbc and oss-secret-key credential providers
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+
Assertions.assertTrue(credentialProviders.contains(JdbcCredential.JDBC_CREDENTIAL_TYPE));
+ Assertions.assertTrue(
+
credentialProviders.contains(OSSSecretKeyCredential.OSS_SECRET_KEY_CREDENTIAL_TYPE));
+ }
+
+ @Test
+ void testJdbcBackendWithAzureCredentialProviders() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test JDBC backend with jdbc-user, jdbc-password, and Azure credentials
+ Map<String, String> jdbcAzureProps = Maps.newHashMap();
+ jdbcAzureProps.put(PaimonConstants.CATALOG_BACKEND, "jdbc");
+ jdbcAzureProps.put(PaimonConstants.URI, "jdbc:sqlite::memory:");
+ jdbcAzureProps.put(PaimonConstants.WAREHOUSE, tempDir);
+ jdbcAzureProps.put(PaimonConstants.GRAVITINO_JDBC_USER, "test-user");
+ jdbcAzureProps.put(PaimonConstants.GRAVITINO_JDBC_PASSWORD,
"test-password");
+ jdbcAzureProps.put(AzureProperties.GRAVITINO_AZURE_STORAGE_ACCOUNT_NAME,
"azure-account-name");
+ jdbcAzureProps.put(AzureProperties.GRAVITINO_AZURE_STORAGE_ACCOUNT_KEY,
"azure-account-key");
+
+ CatalogEntity jdbcAzureEntity =
+ CatalogEntity.builder()
+ .withId(6L)
+ .withName("jdbc-azure-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(PaimonCatalog.Type.RELATIONAL)
+ .withProvider("lakehouse-paimon")
+ .withAuditInfo(auditInfo)
+ .withProperties(jdbcAzureProps)
+ .build();
+
+ PaimonCatalog jdbcAzureCatalog =
+ new
PaimonCatalog().withCatalogConf(jdbcAzureProps).withCatalogEntity(jdbcAzureEntity);
+ Map<String, String> properties =
jdbcAzureCatalog.propertiesWithCredentialProviders();
+
+ // Should have both jdbc and azure-account-key credential providers
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertNotNull(credentialProviders);
+
Assertions.assertTrue(credentialProviders.contains(JdbcCredential.JDBC_CREDENTIAL_TYPE));
+ Assertions.assertTrue(
+
credentialProviders.contains(AzureAccountKeyCredential.AZURE_ACCOUNT_KEY_CREDENTIAL_TYPE));
+ }
+
+ @Test
+ void testExplicitCredentialProvidersNotOverridden() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ // Test that explicit credential-providers setting is not overridden
+ Map<String, String> explicitProps = Maps.newHashMap();
+ explicitProps.put(PaimonConstants.CATALOG_BACKEND, "jdbc");
+ explicitProps.put(PaimonConstants.URI, "jdbc:sqlite::memory:");
+ explicitProps.put(PaimonConstants.WAREHOUSE, tempDir);
+ explicitProps.put(PaimonConstants.GRAVITINO_JDBC_USER, "test-user");
+ explicitProps.put(PaimonConstants.GRAVITINO_JDBC_PASSWORD,
"test-password");
+ explicitProps.put(CredentialConstants.CREDENTIAL_PROVIDERS,
"custom-provider");
+
+ CatalogEntity explicitEntity =
+ CatalogEntity.builder()
+ .withId(4L)
+ .withName("explicit-catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(PaimonCatalog.Type.RELATIONAL)
+ .withProvider("lakehouse-paimon")
+ .withAuditInfo(auditInfo)
+ .withProperties(explicitProps)
+ .build();
+
+ PaimonCatalog explicitCatalog =
+ new
PaimonCatalog().withCatalogConf(explicitProps).withCatalogEntity(explicitEntity);
+ Map<String, String> properties =
explicitCatalog.propertiesWithCredentialProviders();
+
+ // Should keep explicit credential providers, not override
+ String credentialProviders =
properties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ Assertions.assertEquals("custom-provider", credentialProviders);
+ }
}
diff --git
a/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/integration/test/CatalogPaimonJdbcCredentialIT.java
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/integration/test/CatalogPaimonJdbcCredentialIT.java
new file mode 100644
index 0000000000..b143105e01
--- /dev/null
+++
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/integration/test/CatalogPaimonJdbcCredentialIT.java
@@ -0,0 +1,155 @@
+/*
+ * 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.lakehouse.paimon.integration.test;
+
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import
org.apache.gravitino.catalog.lakehouse.paimon.PaimonCatalogPropertiesMetadata;
+import org.apache.gravitino.catalog.lakehouse.paimon.PaimonConstants;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.JdbcCredential;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Integration test for Paimon catalog with JDBC backend credential vending.
Tests that JDBC
+ * credentials can be retrieved through the credential API.
+ */
+@Tag("gravitino-docker-test")
+public class CatalogPaimonJdbcCredentialIT extends BaseIT {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(CatalogPaimonJdbcCredentialIT.class);
+
+ private static final String JDBC_USER = "paimon_user";
+ private static final String JDBC_PASSWORD = "paimon_password";
+
+ private String metalakeName =
GravitinoITUtils.genRandomName("paimon_jdbc_credential_metalake");
+ private String catalogName =
GravitinoITUtils.genRandomName("paimon_jdbc_credential_catalog");
+ private GravitinoMetalake metalake;
+
+ @BeforeAll
+ public void startIntegrationTest() {
+ // Do nothing - override to prevent auto start
+ }
+
+ @BeforeAll
+ public void startUp() throws Exception {
+ super.startIntegrationTest();
+
+ Assertions.assertFalse(client.metalakeExists(metalakeName));
+ metalake = client.createMetalake(metalakeName, "metalake comment",
Collections.emptyMap());
+ Assertions.assertTrue(client.metalakeExists(metalakeName));
+
+ createCatalog();
+ }
+
+ @AfterAll
+ public void tearDown() throws IOException {
+ try {
+ if (metalake != null && metalake.catalogExists(catalogName)) {
+ metalake.disableCatalog(catalogName);
+ metalake.dropCatalog(catalogName, true);
+ }
+ if (client != null && client.metalakeExists(metalakeName)) {
+ client.disableMetalake(metalakeName);
+ client.dropMetalake(metalakeName, true);
+ }
+ } finally {
+ if (client != null) {
+ try {
+ client.close();
+ } catch (Exception e) {
+ LOG.error("Exception in closing client", e);
+ }
+ client = null;
+ }
+ try {
+ super.stopIntegrationTest();
+ } catch (Exception e) {
+ LOG.error("Exception in closing BaseIT", e);
+ }
+ }
+ }
+
+ @AfterAll
+ public void stopIntegrationTest() {
+ // Do nothing - override to prevent auto stop
+ }
+
+ private void createCatalog() {
+ Map<String, String> catalogProperties = Maps.newHashMap();
+
catalogProperties.put(PaimonCatalogPropertiesMetadata.GRAVITINO_CATALOG_BACKEND,
"jdbc");
+ catalogProperties.put(PaimonCatalogPropertiesMetadata.URI,
"jdbc:sqlite::memory:");
+ catalogProperties.put(PaimonCatalogPropertiesMetadata.WAREHOUSE,
"/tmp/paimon-jdbc-test");
+ catalogProperties.put(PaimonConstants.GRAVITINO_JDBC_DRIVER,
"org.sqlite.JDBC");
+ catalogProperties.put(PaimonConstants.GRAVITINO_JDBC_USER, JDBC_USER);
+ catalogProperties.put(PaimonConstants.GRAVITINO_JDBC_PASSWORD,
JDBC_PASSWORD);
+
+ Catalog createdCatalog =
+ metalake.createCatalog(
+ catalogName,
+ Catalog.Type.RELATIONAL,
+ "lakehouse-paimon",
+ "JDBC backend Paimon catalog for credential testing",
+ catalogProperties);
+ Assertions.assertNotNull(createdCatalog);
+ Assertions.assertTrue(metalake.catalogExists(catalogName));
+ }
+
+ @Test
+ void testGetJdbcCredentialFromCatalog() {
+ Catalog catalog = metalake.loadCatalog(catalogName);
+ Credential[] credentials = catalog.supportsCredentials().getCredentials();
+
+ // Should have JDBC credential automatically configured for JDBC backend
+ Assertions.assertEquals(1, credentials.length);
+ Assertions.assertInstanceOf(JdbcCredential.class, credentials[0]);
+
+ JdbcCredential jdbcCredential = (JdbcCredential) credentials[0];
+ Assertions.assertEquals(JDBC_USER, jdbcCredential.jdbcUser());
+ Assertions.assertEquals(JDBC_PASSWORD, jdbcCredential.jdbcPassword());
+ Assertions.assertEquals(0, jdbcCredential.expireTimeInMs());
+ Assertions.assertEquals(JdbcCredential.JDBC_CREDENTIAL_TYPE,
jdbcCredential.credentialType());
+ }
+
+ @Test
+ void testGetJdbcCredentialByType() {
+ Catalog catalog = metalake.loadCatalog(catalogName);
+ Credential credential =
+
catalog.supportsCredentials().getCredential(JdbcCredential.JDBC_CREDENTIAL_TYPE);
+
+ Assertions.assertNotNull(credential);
+ Assertions.assertInstanceOf(JdbcCredential.class, credential);
+
+ JdbcCredential jdbcCredential = (JdbcCredential) credential;
+ Assertions.assertEquals(JDBC_USER, jdbcCredential.jdbcUser());
+ Assertions.assertEquals(JDBC_PASSWORD, jdbcCredential.jdbcPassword());
+ }
+}
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 2df786d2d9..c94d3bba2c 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
@@ -37,6 +37,8 @@ import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.SupportsCredentials;
import org.apache.gravitino.dto.AuditDTO;
import org.apache.gravitino.dto.CatalogDTO;
import org.apache.gravitino.dto.rel.ColumnDTO;
@@ -75,7 +77,8 @@ import org.apache.gravitino.rest.RESTUtils;
* operations, for example, schemas and tables list, creation, update and
deletion. A Relational
* catalog is under the metalake.
*/
-class RelationalCatalog extends BaseSchemaCatalog implements TableCatalog,
ViewCatalog {
+class RelationalCatalog extends BaseSchemaCatalog
+ implements TableCatalog, ViewCatalog, SupportsCredentials {
public static final String PRIVILEGES = "privileges";
@@ -300,6 +303,16 @@ class RelationalCatalog extends BaseSchemaCatalog
implements TableCatalog, ViewC
return resp.dropped();
}
+ @Override
+ public SupportsCredentials supportsCredentials() throws
UnsupportedOperationException {
+ return this;
+ }
+
+ @Override
+ public Credential[] getCredentials() {
+ return objectCredentialOperations.getCredentials();
+ }
+
/**
* List all the views under the given Schema namespace.
*
diff --git
a/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
b/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
index 7bb766a462..443399daf6 100644
---
a/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
+++
b/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
@@ -190,4 +190,26 @@ public class TestCredentialFactory {
Assertions.assertEquals(storageAccountKey,
azureAccountKeyCredential.accountKey());
Assertions.assertEquals(expireTime,
azureAccountKeyCredential.expireTimeInMs());
}
+
+ @Test
+ void testJdbcCredential() {
+ String jdbcUser = "test-user";
+ String jdbcPassword = "test-password";
+ Map<String, String> jdbcCredentialInfo =
+ ImmutableMap.of(
+ JdbcCredential.GRAVITINO_JDBC_USER,
+ jdbcUser,
+ JdbcCredential.GRAVITINO_JDBC_PASSWORD,
+ jdbcPassword);
+ long expireTime = 0;
+ Credential credential =
+ CredentialFactory.create(
+ JdbcCredential.JDBC_CREDENTIAL_TYPE, jdbcCredentialInfo,
expireTime);
+ Assertions.assertEquals(JdbcCredential.JDBC_CREDENTIAL_TYPE,
credential.credentialType());
+ Assertions.assertInstanceOf(JdbcCredential.class, credential);
+ JdbcCredential jdbcCredential = (JdbcCredential) credential;
+ Assertions.assertEquals(jdbcUser, jdbcCredential.jdbcUser());
+ Assertions.assertEquals(jdbcPassword, jdbcCredential.jdbcPassword());
+ Assertions.assertEquals(expireTime, jdbcCredential.expireTimeInMs());
+ }
}
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 2cfed0f645..947474008f 100644
--- a/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
+++ b/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
@@ -24,8 +24,10 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import java.io.Closeable;
import java.io.IOException;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Audit;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.CatalogProvider;
@@ -33,10 +35,16 @@ import org.apache.gravitino.annotation.Evolving;
import org.apache.gravitino.connector.authorization.AuthorizationPlugin;
import org.apache.gravitino.connector.authorization.BaseAuthorization;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.credential.AzureAccountKeyCredential;
import org.apache.gravitino.credential.CatalogCredentialManager;
+import org.apache.gravitino.credential.OSSSecretKeyCredential;
+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.storage.AzureProperties;
+import org.apache.gravitino.storage.OSSProperties;
+import org.apache.gravitino.storage.S3Properties;
import org.apache.gravitino.utils.IsolatedClassLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -341,7 +349,8 @@ public abstract class BaseCatalog<T extends BaseCatalog>
if (catalogCredentialManager == null) {
synchronized (this) {
if (catalogCredentialManager == null) {
- this.catalogCredentialManager = new CatalogCredentialManager(name(),
properties());
+ this.catalogCredentialManager =
+ new CatalogCredentialManager(name(),
propertiesWithCredentialProviders());
}
}
}
@@ -445,6 +454,49 @@ public abstract class BaseCatalog<T extends BaseCatalog>
return properties;
}
+ /**
+ * Retrieves the properties of the catalog including credential providers.
Subclasses should
+ * override this method to inject auto-detected credential provider names
into the properties map
+ * before the {@link CatalogCredentialManager} is initialized. The default
implementation returns
+ * {@link #properties()} unchanged.
+ *
+ * @return A map of properties including credential providers.
+ */
+ public Map<String, String> propertiesWithCredentialProviders() {
+ return properties();
+ }
+
+ /**
+ * Detects storage credential providers (S3, OSS, Azure) from catalog
properties and appends them
+ * to the provided list. Subclasses can call this method in their {@link
+ * #propertiesWithCredentialProviders()} implementation to avoid duplicating
storage credential
+ * detection logic.
+ *
+ * @param properties The catalog properties map to scan for storage
credentials.
+ * @param credentialProviders The list to append detected storage credential
providers to.
+ */
+ @Evolving
+ protected void addStorageCredentialProviders(
+ Map<String, String> properties, List<String> credentialProviders) {
+ String s3AccessKeyId =
properties.get(S3Properties.GRAVITINO_S3_ACCESS_KEY_ID);
+ String s3SecretAccessKey =
properties.get(S3Properties.GRAVITINO_S3_SECRET_ACCESS_KEY);
+ if (StringUtils.isNotBlank(s3AccessKeyId) &&
StringUtils.isNotBlank(s3SecretAccessKey)) {
+
credentialProviders.add(S3SecretKeyCredential.S3_SECRET_KEY_CREDENTIAL_TYPE);
+ }
+
+ String ossAccessKeyId =
properties.get(OSSProperties.GRAVITINO_OSS_ACCESS_KEY_ID);
+ String ossSecretAccessKey =
properties.get(OSSProperties.GRAVITINO_OSS_ACCESS_KEY_SECRET);
+ if (StringUtils.isNotBlank(ossAccessKeyId) &&
StringUtils.isNotBlank(ossSecretAccessKey)) {
+
credentialProviders.add(OSSSecretKeyCredential.OSS_SECRET_KEY_CREDENTIAL_TYPE);
+ }
+
+ String azureAccountName =
properties.get(AzureProperties.GRAVITINO_AZURE_STORAGE_ACCOUNT_NAME);
+ String azureAccountKey =
properties.get(AzureProperties.GRAVITINO_AZURE_STORAGE_ACCOUNT_KEY);
+ if (StringUtils.isNotBlank(azureAccountName) &&
StringUtils.isNotBlank(azureAccountKey)) {
+
credentialProviders.add(AzureAccountKeyCredential.AZURE_ACCOUNT_KEY_CREDENTIAL_TYPE);
+ }
+ }
+
@Override
public Audit auditInfo() {
Preconditions.checkArgument(entity != null, ENTITY_IS_NOT_SET);
diff --git
a/core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java
index ba25fe3e5a..88430cb693 100644
---
a/core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java
@@ -88,7 +88,7 @@ public class CredentialOperationDispatcher extends
OperationDispatcher {
private Map<String, CredentialContext> getCredentialContexts(
BaseCatalog baseCatalog, NameIdentifier nameIdentifier,
CredentialPrivilege privilege) {
if
(nameIdentifier.equals(NameIdentifierUtil.getCatalogIdentifier(nameIdentifier)))
{
- return getCatalogCredentialContexts(baseCatalog.properties());
+ return
getCatalogCredentialContexts(baseCatalog.propertiesWithCredentialProviders());
}
if (baseCatalog.ops() instanceof SupportsPathBasedCredentials) {
diff --git
a/core/src/main/java/org/apache/gravitino/credential/JdbcCredentialProvider.java
b/core/src/main/java/org/apache/gravitino/credential/JdbcCredentialProvider.java
new file mode 100644
index 0000000000..9971cd78ca
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/credential/JdbcCredentialProvider.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.credential;
+
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+
+/** Generate JDBC user and password credentials to access JDBC backend
services. */
+public class JdbcCredentialProvider implements CredentialProvider {
+
+ private String jdbcUser;
+ private String jdbcPassword;
+
+ @Override
+ public void initialize(Map<String, String> properties) {
+ if (properties == null) {
+ return;
+ }
+ // jdbcUser / jdbcPassword will be null when the catalog was not
configured with JDBC
+ // credentials.
+ // getCredential() handles this by returning null (no credential
available).
+ // Note: The property keys "jdbc-user" and "jdbc-password" match the
configuration keys
+ // used by JDBC-based catalogs (e.g., Iceberg JDBC backend, Paimon JDBC
backend).
+ this.jdbcUser = properties.get(JdbcCredential.GRAVITINO_JDBC_USER);
+ this.jdbcPassword = properties.get(JdbcCredential.GRAVITINO_JDBC_PASSWORD);
+ }
+
+ @Override
+ public void close() {}
+
+ @Override
+ public String credentialType() {
+ return JdbcCredential.JDBC_CREDENTIAL_TYPE;
+ }
+
+ @Nullable
+ @Override
+ public Credential getCredential(CredentialContext context) {
+ if (StringUtils.isBlank(jdbcUser) || StringUtils.isBlank(jdbcPassword)) {
+ return null;
+ }
+ return new JdbcCredential(jdbcUser, jdbcPassword);
+ }
+}
diff --git
a/core/src/test/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
b/core/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
similarity index 87%
copy from
core/src/test/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
copy to
core/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
index 6e1fdde4bd..6b992cebfb 100644
---
a/core/src/test/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
+++
b/core/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
@@ -16,5 +16,4 @@
# specific language governing permissions and limitations
# under the License.
#
-org.apache.gravitino.credential.DummyCredentialProvider
-org.apache.gravitino.credential.Dummy2CredentialProvider
+org.apache.gravitino.credential.JdbcCredentialProvider
diff --git
a/core/src/test/java/org/apache/gravitino/credential/TestJdbcCredentialProvider.java
b/core/src/test/java/org/apache/gravitino/credential/TestJdbcCredentialProvider.java
new file mode 100644
index 0000000000..41343d651c
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/credential/TestJdbcCredentialProvider.java
@@ -0,0 +1,193 @@
+/*
+ * 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 com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestJdbcCredentialProvider {
+
+ @Test
+ void testJdbcCredentialProvider() {
+ String jdbcUser = "test-user";
+ String jdbcPassword = "test-password";
+ Map<String, String> catalogProperties =
+ ImmutableMap.of(
+ JdbcCredential.GRAVITINO_JDBC_USER,
+ jdbcUser,
+ JdbcCredential.GRAVITINO_JDBC_PASSWORD,
+ jdbcPassword);
+
+ CredentialProvider credentialProvider =
+ CredentialProviderFactory.create(JdbcCredential.JDBC_CREDENTIAL_TYPE,
catalogProperties);
+
+ Assertions.assertEquals(
+ JdbcCredential.JDBC_CREDENTIAL_TYPE,
credentialProvider.credentialType());
+ Assertions.assertInstanceOf(JdbcCredentialProvider.class,
credentialProvider);
+
+ CatalogCredentialContext context = new
CatalogCredentialContext("test-user");
+ Credential credential = credentialProvider.getCredential(context);
+
+ Assertions.assertNotNull(credential);
+ Assertions.assertInstanceOf(JdbcCredential.class, credential);
+ JdbcCredential jdbcCredential = (JdbcCredential) credential;
+
+ Assertions.assertEquals(jdbcUser, jdbcCredential.jdbcUser());
+ Assertions.assertEquals(jdbcPassword, jdbcCredential.jdbcPassword());
+ Assertions.assertEquals(0, jdbcCredential.expireTimeInMs());
+ }
+
+ @Test
+ void testJdbcCredentialProviderWithMissingProperties() {
+ Map<String, String> catalogProperties = ImmutableMap.of();
+
+ CredentialProvider credentialProvider =
+ CredentialProviderFactory.create(JdbcCredential.JDBC_CREDENTIAL_TYPE,
catalogProperties);
+
+ CatalogCredentialContext context = new
CatalogCredentialContext("test-user");
+ Credential credential = credentialProvider.getCredential(context);
+
+ Assertions.assertNull(credential);
+ }
+
+ @Test
+ void testJdbcCredentialInfo() {
+ String jdbcUser = "test-user";
+ String jdbcPassword = "test-password";
+ JdbcCredential jdbcCredential = new JdbcCredential(jdbcUser, jdbcPassword);
+
+ Map<String, String> credentialInfo = jdbcCredential.credentialInfo();
+ Assertions.assertEquals(2, credentialInfo.size());
+ Assertions.assertEquals(jdbcUser,
credentialInfo.get(JdbcCredential.GRAVITINO_JDBC_USER));
+ Assertions.assertEquals(
+ jdbcPassword,
credentialInfo.get(JdbcCredential.GRAVITINO_JDBC_PASSWORD));
+ }
+
+ @Test
+ void testJdbcCredentialInitialize() {
+ String jdbcUser = "test-user";
+ String jdbcPassword = "test-password";
+ Map<String, String> credentialInfo =
+ ImmutableMap.of(
+ JdbcCredential.GRAVITINO_JDBC_USER,
+ jdbcUser,
+ JdbcCredential.GRAVITINO_JDBC_PASSWORD,
+ jdbcPassword);
+
+ JdbcCredential jdbcCredential = new JdbcCredential();
+ jdbcCredential.initialize(credentialInfo, 0);
+
+ Assertions.assertEquals(jdbcUser, jdbcCredential.jdbcUser());
+ Assertions.assertEquals(jdbcPassword, jdbcCredential.jdbcPassword());
+ }
+
+ @Test
+ void testPartialCredentials() {
+ Map<String, String> catalogProperties =
+ ImmutableMap.of(JdbcCredential.GRAVITINO_JDBC_USER, "test-user");
+
+ CredentialProvider credentialProvider =
+ CredentialProviderFactory.create(JdbcCredential.JDBC_CREDENTIAL_TYPE,
catalogProperties);
+
+ CatalogCredentialContext context = new
CatalogCredentialContext("test-user");
+ Credential credential = credentialProvider.getCredential(context);
+
+ Assertions.assertNull(credential);
+ }
+
+ @Test
+ void testEmptyStringCredentials() {
+ Map<String, String> catalogProperties =
+ ImmutableMap.of(
+ JdbcCredential.GRAVITINO_JDBC_USER, "",
+ JdbcCredential.GRAVITINO_JDBC_PASSWORD, "test-password");
+
+ CredentialProvider credentialProvider =
+ CredentialProviderFactory.create(JdbcCredential.JDBC_CREDENTIAL_TYPE,
catalogProperties);
+
+ CatalogCredentialContext context = new
CatalogCredentialContext("test-user");
+ Credential credential = credentialProvider.getCredential(context);
+
+ Assertions.assertNull(credential);
+ }
+
+ @Test
+ void testNullProperties() {
+ CredentialProvider credentialProvider =
+ CredentialProviderFactory.create(JdbcCredential.JDBC_CREDENTIAL_TYPE,
null);
+
+ CatalogCredentialContext context = new
CatalogCredentialContext("test-user");
+ Credential credential = credentialProvider.getCredential(context);
+
+ Assertions.assertNull(credential);
+ }
+
+ @Test
+ void testEmptyPasswordReturnsNull() {
+ Map<String, String> catalogProperties =
+ ImmutableMap.of(
+ JdbcCredential.GRAVITINO_JDBC_USER,
+ "test-user",
+ JdbcCredential.GRAVITINO_JDBC_PASSWORD,
+ "");
+
+ CredentialProvider credentialProvider =
+ CredentialProviderFactory.create(JdbcCredential.JDBC_CREDENTIAL_TYPE,
catalogProperties);
+
+ CatalogCredentialContext context = new
CatalogCredentialContext("test-user");
+ Credential credential = credentialProvider.getCredential(context);
+
+ Assertions.assertNull(credential);
+ }
+
+ @Test
+ void testJdbcCredentialConstructorBlankUserThrows() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> new JdbcCredential("",
"password"));
+ }
+
+ @Test
+ void testJdbcCredentialConstructorBlankPasswordThrows() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> new
JdbcCredential("user", ""));
+ }
+
+ @Test
+ void testJdbcCredentialInitializeNonZeroExpireTimeThrows() {
+ Map<String, String> credentialInfo =
+ ImmutableMap.of(
+ JdbcCredential.GRAVITINO_JDBC_USER,
+ "user",
+ JdbcCredential.GRAVITINO_JDBC_PASSWORD,
+ "pass");
+ JdbcCredential jdbcCredential = new JdbcCredential();
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
jdbcCredential.initialize(credentialInfo, 1000L));
+ }
+
+ @Test
+ void testToStringDoesNotContainPassword() {
+ JdbcCredential jdbcCredential = new JdbcCredential("user", "secret");
+ String str = jdbcCredential.toString();
+ Assertions.assertTrue(str.contains("user"));
+ Assertions.assertFalse(str.contains("secret"));
+ }
+}
diff --git
a/core/src/test/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
b/core/src/test/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
index 6e1fdde4bd..e672bd50eb 100644
---
a/core/src/test/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
+++
b/core/src/test/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
@@ -18,3 +18,4 @@
#
org.apache.gravitino.credential.DummyCredentialProvider
org.apache.gravitino.credential.Dummy2CredentialProvider
+org.apache.gravitino.credential.JdbcCredentialProvider
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
index 73ec2fa3ce..b003b6e5f8 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
@@ -368,12 +368,20 @@ public class BaseIT {
serverConfig = new ServerConfig();
customConfigs.put(ENTITY_RELATIONAL_JDBC_BACKEND_PATH.getKey(),
file.getAbsolutePath());
- if (ignoreLanceAuxRestService && ignoreIcebergAuxRestService) {
- customConfigs.put(
- AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX
- + AuxiliaryServiceManager.AUX_SERVICE_NAMES,
- "");
+
+ List<String> auxServicesList = new ArrayList<>();
+ if (!ignoreIcebergAuxRestService) {
+ auxServicesList.add("iceberg-rest");
+ }
+ if (!ignoreLanceAuxRestService) {
+ auxServicesList.add("lance-rest");
}
+ String auxServices = String.join(",", auxServicesList);
+
+ customConfigs.put(
+ AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX
+ + AuxiliaryServiceManager.AUX_SERVICE_NAMES,
+ auxServices);
if (!ignoreLanceAuxRestService) {
customConfigs.put(
LANCE_CONFIG_PREFIX + METALAKE_NAME.getKey(),
@@ -415,6 +423,8 @@ public class BaseIT {
List<String> authenticators = new ArrayList<>();
String authenticatorStr =
customConfigs.get(Configs.AUTHENTICATORS.getKey());
+ LOG.info("Creating authenticator {}", authenticatorStr);
+
if (authenticatorStr != null) {
authenticators = COMMA.splitToList(authenticatorStr);
}
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestDatabaseName.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestDatabaseName.java
index 610bb6730c..1c178036e2 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestDatabaseName.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestDatabaseName.java
@@ -60,6 +60,9 @@ public enum TestDatabaseName {
/** Represents the MySQL database used for testing the catalog integration
with MySQL. */
MYSQL_CATALOG_MYSQL_IT,
+ /** Represents the MySQL database used for testing catalog credential
integration. */
+ MYSQL_CATALOG_CREDENTIAL_IT,
+
PG_JDBC_BACKEND,
/** Represents the PostgreSQL database for CatalogPostgreSqlIT. */