This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new e4da71a289 [Cherry-pick to branch-1.3] [#14069] fix(core): Reject
token credential providers on catalogs without path-based credentials (#14080)
(#14201)
e4da71a289 is described below
commit e4da71a289a0f286a38efde90fcc990689da3e2f
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Oct 26 17:09:20 2026 +0800
[Cherry-pick to branch-1.3] [#14069] fix(core): Reject token credential
providers on catalogs without path-based credentials (#14080) (#14201)
**Cherry-pick Information:**
- Original commit: 10ba57615a95be334640dd8c8c07c245bd291695
- Target branch: `branch-1.3`
- Status: ⚠️ **Has conflicts - manual resolution required**
Co-authored-by: Yuhui <[email protected]>
Co-authored-by: Jerry Shao <[email protected]>
---
.../fileset/TestFilesetCatalogCredential.java | 11 ++
.../apache/gravitino/catalog/glue/GlueCatalog.java | 5 +
.../apache/gravitino/catalog/hive/HiveCatalog.java | 5 +
.../gravitino/catalog/hive/TestHiveCatalog.java | 25 ++++
.../apache/gravitino/catalog/jdbc/JdbcCatalog.java | 5 +
.../gravitino/catalog/kafka/KafkaCatalog.java | 5 +
.../catalog/lakehouse/generic/GenericCatalog.java | 5 +
.../catalog/lakehouse/hudi/HudiCatalog.java | 5 +
.../lakehouse/iceberg/TestIcebergCatalog.java | 11 ++
.../catalog/lakehouse/paimon/PaimonCatalog.java | 5 +
.../gravitino/catalog/model/ModelCatalogImpl.java | 5 +
.../apache/gravitino/catalog/CatalogManager.java | 15 +++
.../apache/gravitino/connector/BaseCatalog.java | 51 +++++++-
.../gravitino/credential/CredentialUtils.java | 13 +++
.../java/org/apache/gravitino/TestCatalog.java | 5 +
.../gravitino/catalog/TestCatalogManager.java | 130 +++++++++++++++++++++
.../TestBaseCatalogCredentialProviders.java | 92 ++++++++++++++-
docs/security/credential-vending.md | 7 +-
18 files changed, 395 insertions(+), 5 deletions(-)
diff --git
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogCredential.java
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogCredential.java
index 11d8978947..3ea59baba6 100644
---
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogCredential.java
+++
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogCredential.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.catalog.fileset;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.time.Instant;
import java.util.Map;
@@ -74,4 +75,14 @@ public class TestFilesetCatalogCredential {
.propertiesWithCredentialProviders()
.get(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX));
}
+
+ @Test
+ void testPathBasedCredentialProvidersAllowed() {
+ FilesetCatalogImpl catalog = newCatalog(Maps.newHashMap());
+ Assertions.assertTrue(catalog.supportsPathBasedCredentials());
+ Assertions.assertDoesNotThrow(
+ () ->
+ catalog.validateCredentialProviders(
+ ImmutableMap.of(CredentialConstants.CREDENTIAL_PROVIDERS,
"s3-token,gcs-token")));
+ }
}
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalog.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalog.java
index 8222c0d71b..70b25be499 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalog.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalog.java
@@ -54,6 +54,11 @@ public class GlueCatalog extends BaseCatalog<GlueCatalog> {
return "glue";
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
/**
* Creates a new instance of {@link GlueCatalogOperations} with the provided
configuration.
*
diff --git
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalog.java
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalog.java
index 6fcccdf210..5df89aae53 100644
---
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalog.java
+++
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalog.java
@@ -51,6 +51,11 @@ public class HiveCatalog extends BaseCatalog<HiveCatalog> {
return "hive";
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
/**
* Creates a new instance of {@link HiveCatalogOperations} with the provided
configuration.
*
diff --git
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalog.java
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalog.java
index 8fdb8dabae..45ddc5e433 100644
---
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalog.java
+++
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalog.java
@@ -38,6 +38,7 @@ import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.PropertiesMetadata;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.apache.gravitino.credential.CredentialConstants;
import org.apache.gravitino.hive.client.HiveClientClassLoader.HiveVersion;
import org.apache.gravitino.hive.hms.MiniHiveMetastoreService;
import org.apache.gravitino.meta.AuditInfo;
@@ -228,4 +229,28 @@ public class TestHiveCatalog extends
MiniHiveMetastoreService {
"Properties or property prefixes are required and must be
set: [%s]",
METASTORE_URIS)));
}
+
+ @Test
+ void testPathBasedCredentialProvidersRejected() {
+ CatalogEntity entity =
+ CatalogEntity.builder()
+ .withId(1L)
+ .withName("catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(HiveCatalog.Type.RELATIONAL)
+ .withProvider("hive")
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ HiveCatalog catalog = new HiveCatalog().withCatalogEntity(entity);
+
+ Assertions.assertFalse(catalog.supportsPathBasedCredentials());
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalog.validateCredentialProviders(
+ ImmutableMap.of(CredentialConstants.CREDENTIAL_PROVIDERS,
"s3-token")));
+ Assertions.assertTrue(exception.getMessage().contains("s3-token"));
+ }
}
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 2d8877f8c8..7f5c2f831c 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
@@ -81,6 +81,11 @@ public abstract class JdbcCatalog extends
BaseCatalog<JdbcCatalog> {
return new JdbcCatalogCapability();
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
/**
* Returns the effective {@link JdbcConfig#VERIFY_ON_CREATE
verify-on-create} setting for this
* catalog. A relational JDBC catalog verifies its backend at create time by
default; an
diff --git
a/catalogs/catalog-kafka/src/main/java/org/apache/gravitino/catalog/kafka/KafkaCatalog.java
b/catalogs/catalog-kafka/src/main/java/org/apache/gravitino/catalog/kafka/KafkaCatalog.java
index a0de7228d6..aa667c7ce3 100644
---
a/catalogs/catalog-kafka/src/main/java/org/apache/gravitino/catalog/kafka/KafkaCatalog.java
+++
b/catalogs/catalog-kafka/src/main/java/org/apache/gravitino/catalog/kafka/KafkaCatalog.java
@@ -42,6 +42,11 @@ public class KafkaCatalog extends BaseCatalog<KafkaCatalog> {
return "kafka";
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
@Override
protected CatalogOperations newOps(Map<String, String> config) {
KafkaCatalogOperations ops = new KafkaCatalogOperations();
diff --git
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalog.java
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalog.java
index c8000989a4..690392d1a9 100644
---
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalog.java
+++
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalog.java
@@ -46,6 +46,11 @@ public class GenericCatalog extends
BaseCatalog<GenericCatalog> {
return "lakehouse-generic";
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
/**
* Creates a new instance of {@link GenericCatalogOperations} with the
provided configuration.
*
diff --git
a/catalogs/catalog-lakehouse-hudi/src/main/java/org/apache/gravitino/catalog/lakehouse/hudi/HudiCatalog.java
b/catalogs/catalog-lakehouse-hudi/src/main/java/org/apache/gravitino/catalog/lakehouse/hudi/HudiCatalog.java
index d5356cd56d..8dcf787ae2 100644
---
a/catalogs/catalog-lakehouse-hudi/src/main/java/org/apache/gravitino/catalog/lakehouse/hudi/HudiCatalog.java
+++
b/catalogs/catalog-lakehouse-hudi/src/main/java/org/apache/gravitino/catalog/lakehouse/hudi/HudiCatalog.java
@@ -43,6 +43,11 @@ public class HudiCatalog extends BaseCatalog<HudiCatalog> {
return "lakehouse-hudi";
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
/**
* Creates a new instance of {@link HudiCatalogOperations} with the provided
configuration.
*
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 51e184526f..c99bfbdb6b 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
@@ -22,6 +22,7 @@ import static
org.apache.gravitino.catalog.lakehouse.iceberg.IcebergCatalog.CATA
import static
org.apache.gravitino.catalog.lakehouse.iceberg.IcebergCatalog.SCHEMA_PROPERTIES_META;
import static
org.apache.gravitino.catalog.lakehouse.iceberg.IcebergCatalog.TABLE_PROPERTIES_META;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.time.Instant;
import java.util.Map;
@@ -490,4 +491,14 @@ public class TestIcebergCatalog {
Assertions.assertTrue(
credentialProviders.contains(AzureAccountKeyCredential.AZURE_ACCOUNT_KEY_CREDENTIAL_TYPE));
}
+
+ @Test
+ void testPathBasedCredentialProvidersAllowed() {
+ IcebergCatalog catalog = new IcebergCatalog();
+ Assertions.assertTrue(catalog.supportsPathBasedCredentials());
+ Assertions.assertDoesNotThrow(
+ () ->
+ catalog.validateCredentialProviders(
+ ImmutableMap.of(CredentialConstants.CREDENTIAL_PROVIDERS,
"s3-token,gcs-token")));
+ }
}
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 5c7afa7a63..9950514472 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
@@ -51,6 +51,11 @@ public class PaimonCatalog extends
BaseCatalog<PaimonCatalog> {
return "lakehouse-paimon";
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
/**
* Creates a new instance of {@link PaimonCatalogOperations} with the
provided configuration.
*
diff --git
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
index ba3bfa4984..27b8724c84 100644
---
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
+++
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
@@ -45,6 +45,11 @@ public class ModelCatalogImpl extends
BaseCatalog<ModelCatalogImpl> {
return "model";
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
@Override
protected CatalogOperations newOps(Map<String, String> config) {
EntityStore store = GravitinoEnv.getInstance().entityStore();
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 b840c8d25f..724a391288 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -1023,6 +1023,7 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
metadata.catalogPropertiesMetadata(),
alterProperty.getLeft(),
alterProperty.getRight());
+
storedCatalog.validateCredentialProviders(alterProperty.getLeft());
return null;
});
@@ -1182,6 +1183,7 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
f.catalogPropertiesMetadata(),
alterProperty.getLeft(),
alterProperty.getRight());
+
catalog.validateCredentialProviders(alterProperty.getLeft());
return null;
});
} catch (IllegalArgumentException e1) {
@@ -1705,6 +1707,19 @@ public class CatalogManager implements
CatalogDispatcher, Closeable {
classLoader.withClassLoader(
cl -> {
validatePropertyForCreate(catalog.catalogPropertiesMetadata(),
propsToValidate);
+ catalog.validateCredentialProviders(propsToValidate);
+ if (propsToValidate == null) {
+ // Loading stays lenient for catalogs persisted with unsupported
providers, so only
+ // warn about them.
+ try {
+ catalog.validateCredentialProviders(conf);
+ } catch (IllegalArgumentException e) {
+ LOG.warn(
+ "Catalog {} has credential providers that cannot vend
credentials: {}",
+ entity.nameIdentifier(),
+ e.getMessage());
+ }
+ }
// Call wrapper.catalog.properties() to make BaseCatalog#properties
in IsolatedClassLoader
// not null. Why do we do this? Because wrapper.catalog.properties()
needs to be called in
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 777a2cce54..6d23332d80 100644
--- a/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
+++ b/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
@@ -26,8 +26,10 @@ import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Audit;
@@ -40,9 +42,11 @@ 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.connector.credential.SupportsPathBasedCredentials;
import org.apache.gravitino.credential.AzureAccountKeyCredential;
import org.apache.gravitino.credential.CatalogCredentialManager;
import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.CredentialUtils;
import org.apache.gravitino.credential.GCSTokenCredential;
import org.apache.gravitino.credential.OSSSecretKeyCredential;
import org.apache.gravitino.credential.S3SecretKeyCredential;
@@ -237,6 +241,50 @@ public abstract class BaseCatalog<T extends BaseCatalog>
ops();
}
+ /**
+ * Whether this catalog provides path-based credential contexts, which token
credential providers
+ * such as {@code s3-token} require to vend credentials.
+ *
+ * <p>A catalog that cannot provide path-based contexts, neither through
operations implementing
+ * {@link SupportsPathBasedCredentials} nor through another server such as
the Iceberg REST
+ * server, should override this method to return {@code false}, so that
creating or altering it
+ * with a token provider fails.
+ *
+ * @return {@code true} (default) if path-based credentials are supported;
{@code false} otherwise
+ */
+ public boolean supportsPathBasedCredentials() {
+ return true;
+ }
+
+ /**
+ * Validates that the credential providers set in {@code properties} can
vend credentials for this
+ * catalog. Token providers listed in {@link
CredentialUtils#PATH_BASED_CREDENTIAL_TYPES} are
+ * rejected when {@link #supportsPathBasedCredentials()} returns {@code
false}.
+ *
+ * @param properties the catalog properties to validate, may be null
+ * @throws IllegalArgumentException if a credential provider cannot be
vended by this catalog
+ */
+ public void validateCredentialProviders(@Nullable Map<String, String>
properties) {
+ if (properties == null || supportsPathBasedCredentials()) {
+ return;
+ }
+ List<String> unsupportedProviders =
+ CredentialUtils.getCredentialProvidersByOrder(() ->
properties).stream()
+ // Provider lookup ignores case, so match the type names
case-insensitively too.
+ .filter(
+ p ->
+ CredentialUtils.PATH_BASED_CREDENTIAL_TYPES.contains(
+ p.toLowerCase(Locale.ROOT)))
+ .sorted()
+ .collect(Collectors.toList());
+ Preconditions.checkArgument(
+ unsupportedProviders.isEmpty(),
+ "Credential providers %s are not supported by catalog provider %s, it
cannot vend"
+ + " path-based credentials",
+ unsupportedProviders,
+ provider());
+ }
+
public void checkMetalakeAndCatalogInUse() {
Map<String, String> catalogProperties = entity.getProperties();
if (catalogProperties == null) {
@@ -587,7 +635,8 @@ public abstract class BaseCatalog<T extends BaseCatalog>
}
String gcsServiceAccountFile =
properties.get(GCSProperties.GRAVITINO_GCS_SERVICE_ACCOUNT_FILE);
- if (StringUtils.isNotBlank(gcsServiceAccountFile)) {
+ // gcs-token only vends credentials for path-based contexts.
+ if (StringUtils.isNotBlank(gcsServiceAccountFile) &&
supportsPathBasedCredentials()) {
credentialProviders.add(GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE);
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/credential/CredentialUtils.java
b/core/src/main/java/org/apache/gravitino/credential/CredentialUtils.java
index 9d2ea43e66..b61e5ddad9 100644
--- a/core/src/main/java/org/apache/gravitino/credential/CredentialUtils.java
+++ b/core/src/main/java/org/apache/gravitino/credential/CredentialUtils.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.credential;
+import com.google.common.collect.ImmutableSet;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -29,6 +30,18 @@ import
org.apache.gravitino.credential.config.CredentialConfig;
public class CredentialUtils {
+ /**
+ * Credential types that only vend credentials for a path-based credential
context. They return
+ * nothing for a catalog-level context, so they only work on catalogs that
provide path contexts.
+ */
+ public static final Set<String> PATH_BASED_CREDENTIAL_TYPES =
+ ImmutableSet.of(
+ S3TokenCredential.S3_TOKEN_CREDENTIAL_TYPE,
+ AwsIrsaCredential.AWS_IRSA_CREDENTIAL_TYPE,
+ OSSTokenCredential.OSS_TOKEN_CREDENTIAL_TYPE,
+ GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE,
+ ADLSTokenCredential.ADLS_TOKEN_CREDENTIAL_TYPE);
+
public static Map<String, CredentialProvider> loadCredentialProviders(
Map<String, String> catalogProperties) {
CredentialConfig credentialConfig = new
CredentialConfig(catalogProperties);
diff --git a/core/src/test/java/org/apache/gravitino/TestCatalog.java
b/core/src/test/java/org/apache/gravitino/TestCatalog.java
index d3fb322182..6d5a27bee0 100644
--- a/core/src/test/java/org/apache/gravitino/TestCatalog.java
+++ b/core/src/test/java/org/apache/gravitino/TestCatalog.java
@@ -55,6 +55,11 @@ public class TestCatalog extends BaseCatalog<TestCatalog> {
return "test";
}
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+
@Override
protected CatalogOperations newOps(Map<String, String> config) {
return new TestCatalogOperations(config);
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
index 05ca5228b3..4633fd8e23 100644
--- a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
+++ b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
@@ -39,6 +39,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -68,6 +69,7 @@ import org.apache.gravitino.connector.SupportsSchemas;
import org.apache.gravitino.connector.TestCatalogOperations;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.apache.gravitino.credential.CredentialConstants;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
import org.apache.gravitino.exceptions.CatalogNotInUseException;
import org.apache.gravitino.exceptions.ConnectionFailedException;
@@ -93,6 +95,13 @@ import
org.apache.gravitino.storage.relational.po.cache.EntityChangeRecord;
import org.apache.gravitino.storage.relational.po.cache.OperateType;
import org.apache.gravitino.utils.PrincipalUtils;
import org.apache.gravitino.utils.ThrowableFunction;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
@@ -277,6 +286,68 @@ public class TestCatalogManager {
reset();
}
+ @Test
+ void testPathBasedCredentialProviderValidation() throws IOException {
+ NameIdentifier ident = NameIdentifier.of("metalake", "token_provider");
+ Map<String, String> props =
+ ImmutableMap.<String, String>builder()
+ .put(PROPERTY_KEY1, "value1")
+ .put(PROPERTY_KEY2, "value2")
+ .put(PROPERTY_KEY3, "3")
+ .put(PROPERTY_KEY4, "value4")
+ .put(PROPERTY_KEY5_PREFIX + "1", "value1")
+ .put(PROPERTY_KEY6_PREFIX + "1", "value1")
+ .put("mock", "mock")
+ .build();
+ Map<String, String> tokenProps =
+ ImmutableMap.<String, String>builder()
+ .putAll(props)
+ .put(CredentialConstants.CREDENTIAL_PROVIDERS,
"s3-secret-key,gcs-token")
+ .build();
+
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalogManager.createCatalog(
+ ident, Catalog.Type.RELATIONAL, provider, "comment",
tokenProps));
+ Assertions.assertTrue(exception.getMessage().contains("gcs-token"));
+ Assertions.assertTrue(exception.getMessage().contains(provider));
+ Assertions.assertFalse(catalogManager.catalogExists(ident));
+
+ catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider,
"comment", props);
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalogManager.alterCatalog(
+ ident,
+ CatalogChange.setProperty(
+ CredentialConstants.CREDENTIAL_PROVIDERS,
"adls-token")));
+ Assertions.assertTrue(exception.getMessage().contains("adls-token"));
+
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalogManager.testConnection(
+ ident,
+ CatalogChange.setProperty(
+ CredentialConstants.CREDENTIAL_PROVIDERS,
"s3-token")));
+ Assertions.assertTrue(exception.getMessage().contains("s3-token"));
+
+ catalogManager.alterCatalog(
+ ident,
+ CatalogChange.setProperty(CredentialConstants.CREDENTIAL_PROVIDERS,
"s3-secret-key"));
+ Assertions.assertEquals(
+ "s3-secret-key",
+ catalogManager
+ .loadCatalog(ident)
+ .properties()
+ .get(CredentialConstants.CREDENTIAL_PROVIDERS));
+ reset();
+ }
+
@Test
void testPropertyValidationInCreate() throws IOException {
// key1 is required and immutable and do not have default value, is not
hidden and not reserved
@@ -1691,6 +1762,65 @@ public class TestCatalogManager {
}
}
+ @Test
+ void testLoadWarnsForUnsupportedCredentialProviders() throws Exception {
+ LoggerContext context =
+ (LoggerContext)
LogManager.getContext(CatalogManager.class.getClassLoader(), false);
+ Configuration logConfig = context.getConfiguration();
+ List<LogEvent> events = new ArrayList<>();
+ AbstractAppender appender =
+ new AbstractAppender("credentialProvidersCapture", null, null, true,
null) {
+ @Override
+ public void append(LogEvent event) {
+ events.add(event.toImmutable());
+ }
+ };
+ appender.start();
+ LoggerConfig logger = new LoggerConfig(CatalogManager.class.getName(),
Level.WARN, false);
+ logger.addAppender(appender, Level.WARN, null);
+ logConfig.addLogger(CatalogManager.class.getName(), logger);
+ context.updateLoggers();
+
+ try (CatalogManager manager =
+ new CatalogManager(config, entityStore, new RandomIdGenerator())) {
+ // A catalog persisted before unsupported credential providers were
rejected.
+ NameIdentifier ident = NameIdentifier.of("metalake",
"legacy_token_providers");
+ Map<String, String> props =
+ ImmutableMap.<String, String>builder()
+ .put(PROPERTY_KEY1, "value1")
+ .put(PROPERTY_KEY2, "value2")
+ .put(PROPERTY_KEY3, "3")
+ .put(PROPERTY_KEY4, "value4")
+ .put(PROPERTY_KEY5_PREFIX + "1", "value1")
+ .put(PROPERTY_KEY6_PREFIX + "1", "value1")
+ .put("mock", "mock")
+ .put(CredentialConstants.CREDENTIAL_PROVIDERS, "gcs-token")
+ .build();
+ entityStore.put(
+ CatalogEntity.builder()
+ .withId(new RandomIdGenerator().nextId())
+ .withName(ident.name())
+ .withNamespace(ident.namespace())
+ .withType(Catalog.Type.RELATIONAL)
+ .withProvider(provider)
+ .withComment("comment")
+ .withProperties(props)
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+ .build());
+
+ Assertions.assertDoesNotThrow(() -> manager.loadCatalog(ident));
+ Assertions.assertTrue(
+ events.stream()
+ .anyMatch(e ->
e.getMessage().getFormattedMessage().contains("gcs-token")));
+ Assertions.assertTrue(manager.dropCatalog(ident, true));
+ } finally {
+ logConfig.removeLogger(CatalogManager.class.getName());
+ context.updateLoggers();
+ appender.stop();
+ }
+ }
+
@Test
public void testCatalogCacheRemoveListener() throws IOException {
NameIdentifier ident = NameIdentifier.of(metalake, "catalog");
diff --git
a/core/src/test/java/org/apache/gravitino/connector/TestBaseCatalogCredentialProviders.java
b/core/src/test/java/org/apache/gravitino/connector/TestBaseCatalogCredentialProviders.java
index 8b9ff46acd..976100f37b 100644
---
a/core/src/test/java/org/apache/gravitino/connector/TestBaseCatalogCredentialProviders.java
+++
b/core/src/test/java/org/apache/gravitino/connector/TestBaseCatalogCredentialProviders.java
@@ -18,6 +18,8 @@
*/
package org.apache.gravitino.connector;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@@ -25,10 +27,17 @@ import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.credential.ADLSTokenCredential;
+import org.apache.gravitino.credential.AwsIrsaCredential;
import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.CredentialUtils;
+import org.apache.gravitino.credential.GCSTokenCredential;
+import org.apache.gravitino.credential.OSSTokenCredential;
import org.apache.gravitino.credential.S3SecretKeyCredential;
+import org.apache.gravitino.credential.S3TokenCredential;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.storage.GCSProperties;
import org.apache.gravitino.storage.S3Properties;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -68,7 +77,81 @@ public class TestBaseCatalogCredentialProviders {
Assertions.assertFalse(result.containsKey(CredentialConstants.CREDENTIAL_PROVIDERS));
}
+ @Test
+ void testPathBasedCredentialTypes() {
+ Assertions.assertEquals(
+ ImmutableSet.of(
+ S3TokenCredential.S3_TOKEN_CREDENTIAL_TYPE,
+ AwsIrsaCredential.AWS_IRSA_CREDENTIAL_TYPE,
+ OSSTokenCredential.OSS_TOKEN_CREDENTIAL_TYPE,
+ GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE,
+ ADLSTokenCredential.ADLS_TOKEN_CREDENTIAL_TYPE),
+ CredentialUtils.PATH_BASED_CREDENTIAL_TYPES);
+ }
+
+ @Test
+ void testValidateCredentialProviders() throws IllegalAccessException {
+ TestCatalogImpl catalog = newCatalog(new
NoPathBasedCredentialsCatalogImpl(), new HashMap<>());
+ Assertions.assertDoesNotThrow(() ->
catalog.validateCredentialProviders(null));
+ Assertions.assertDoesNotThrow(
+ () ->
+ catalog.validateCredentialProviders(
+ ImmutableMap.of(
+ CredentialConstants.CREDENTIAL_PROVIDERS,
+ "s3-secret-key,oss-secret-key,azure-account-key")));
+
+ for (String type : CredentialUtils.PATH_BASED_CREDENTIAL_TYPES) {
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalog.validateCredentialProviders(
+ ImmutableMap.of(
+ CredentialConstants.CREDENTIAL_PROVIDERS,
"s3-secret-key," + type)));
+ Assertions.assertTrue(exception.getMessage().contains(type));
+ Assertions.assertTrue(exception.getMessage().contains("test"));
+ }
+
+ // Provider lookup ignores case, so validation does too.
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalog.validateCredentialProviders(
+ ImmutableMap.of(CredentialConstants.CREDENTIAL_PROVIDERS,
"GCS-Token")));
+ }
+
+ @Test
+ void testGcsTokenInferredOnlyWithPathBasedCredentials() throws
IllegalAccessException {
+ Map<String, String> props = new HashMap<>();
+ props.put(GCSProperties.GRAVITINO_GCS_SERVICE_ACCOUNT_FILE,
"/tmp/gcs.json");
+
+ Map<String, String> result =
+ newCatalog(new NoPathBasedCredentialsCatalogImpl(),
props).properties();
+
Assertions.assertFalse(result.containsKey(CredentialConstants.CREDENTIAL_PROVIDERS));
+
+ Assertions.assertEquals(
+ GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE,
+
newCatalog(props).properties().get(CredentialConstants.CREDENTIAL_PROVIDERS));
+ }
+
+ @Test
+ void testPathBasedCredentialsSupportedByDefault() throws
IllegalAccessException {
+ // A catalog that does not override supportsPathBasedCredentials() keeps
accepting token
+ // providers.
+ TestCatalogImpl catalog = newCatalog(new HashMap<>());
+ Assertions.assertTrue(catalog.supportsPathBasedCredentials());
+ Assertions.assertDoesNotThrow(
+ () ->
+ catalog.validateCredentialProviders(
+ ImmutableMap.of(CredentialConstants.CREDENTIAL_PROVIDERS,
"s3-token,gcs-token")));
+ }
+
private TestCatalogImpl newCatalog(Map<String, String> props) throws
IllegalAccessException {
+ return newCatalog(new TestCatalogImpl(), props);
+ }
+
+ private TestCatalogImpl newCatalog(TestCatalogImpl catalog, Map<String,
String> props)
+ throws IllegalAccessException {
CatalogEntity entity =
CatalogEntity.builder()
.withId(1L)
@@ -81,7 +164,6 @@ public class TestBaseCatalogCredentialProviders {
.withAuditInfo(
AuditInfo.builder().withCreator("tester").withCreateTime(Instant.now()).build())
.build();
- TestCatalogImpl catalog = new TestCatalogImpl();
FieldUtils.writeField(catalog, "entity", entity, true);
return catalog;
}
@@ -108,4 +190,12 @@ public class TestBaseCatalogCredentialProviders {
return BaseCatalogPropertiesMetadata.BASIC_CATALOG_PROPERTIES_METADATA;
}
}
+
+ private static class NoPathBasedCredentialsCatalogImpl extends
TestCatalogImpl {
+
+ @Override
+ public boolean supportsPathBasedCredentials() {
+ return false;
+ }
+ }
}
diff --git a/docs/security/credential-vending.md
b/docs/security/credential-vending.md
index 5567634ccd..ddb4e88d35 100644
--- a/docs/security/credential-vending.md
+++ b/docs/security/credential-vending.md
@@ -17,7 +17,7 @@ Gravitino credential vending is used to generate temporary or
static credentials
| Catalog type | Supported credential types |
|------------------|----------------------------------------------------|
| Hadoop (Fileset) | S3, OSS, GCS, ADLS |
-| Hive | S3, OSS, GCS, ADLS |
+| Hive | S3, OSS, ADLS |
| Iceberg | S3, OSS, GCS, ADLS |
| Glue | S3 |
| JDBC | JDBC user/password (`jdbc-user-password`) |
@@ -27,7 +27,8 @@ Gravitino credential vending is used to generate temporary or
static credentials
- GCS: `GCSTokenCredential`
- ADLS: `ADLSTokenCredential`, `AzureAccountKeyCredential`
- OSS: `OSSTokenCredential`, `OSSSecretKeyCredential`
-- Spark, Flink, and Trino connectors automatically consume vended credentials
for Hive, Iceberg, Glue, JDBC, and Paimon catalogs. For Iceberg catalogs
without a `REST` backend that vends credentials, the Flink connector receives
only static credentials, see [Flink Iceberg credential
vending](../flink-connector/flink-catalog-iceberg.md#credential-vending).
+- Spark, Flink, and Trino connectors automatically consume vended credentials
for Hive, Iceberg, Glue, JDBC, and Paimon catalogs, except that the Trino Hive
connector does not consume OSS credentials. For Iceberg catalogs without a
`REST` backend that vends credentials, the Flink connector receives only static
credentials, see [Flink Iceberg credential
vending](../flink-connector/flink-catalog-iceberg.md#credential-vending).
+- Token providers (`s3-token`, `oss-token`, `gcs-token`, and `adls-token`) and
`aws-irsa` scope credentials to a path, so only Fileset and Iceberg catalogs
support them. Other catalogs vend static keys only, such as `s3-secret-key`,
`oss-secret-key`, and `azure-account-key`. Creating or altering such a catalog
with one of these providers fails.
## General Configurations
@@ -224,7 +225,7 @@ Clients retrieve vended credentials from:
GET /api/metalakes/{metalake}/objects/catalog/{catalog}/credentials
```
-The server returns short-lived or static credentials that the client can use
to access the underlying storage directly. These credentials are catalog-level
and are not refreshed: token providers such as `s3-token` vend nothing at
catalog level, and `aws-irsa` credentials expire after
`s3-token-expire-in-secs`.
+The server returns short-lived or static credentials that the client can use
to access the underlying storage directly. These credentials are catalog-level
and are not refreshed. Path-based providers such as `s3-token` and `aws-irsa`
vend nothing at catalog level.
### Spark and Flink Connector Integration