This is an automated email from the ASF dual-hosted git repository.
diqiu50 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 f559909447 [#14031] fix(flink-connector): Validate credential
providers for Iceberg non-REST backends (#14032)
f559909447 is described below
commit f559909447f33aca3abbfd15547dfad25e22ef36
Author: Yuhui <[email protected]>
AuthorDate: Thu Oct 22 09:49:33 2026 +0800
[#14031] fix(flink-connector): Validate credential providers for Iceberg
non-REST backends (#14032)
### What changes were proposed in this pull request?
For Iceberg catalogs with a non-REST backend, the Flink connector now
checks `credential-providers` when it opens the catalog:
- `aws-irsa` is rejected, because its catalog-level credential is
injected once and expires after `s3-token-expire-in-secs`.
- `s3-token`, `gcs-token`, `adls-token`, `oss-token` and `cos-token` log
a warning, because they vend nothing at catalog level and Iceberg falls
back to the credentials on the Flink nodes.
- REST backends are not affected.
The docs now state that Flink only receives static credentials for
non-REST backends, and that per-table refreshable credentials require a
REST backend with `data-access=vended-credentials`.
### Why are the changes needed?
The Flink connector fetches catalog-level credentials once and never
refreshes them. With `aws-irsa`, jobs fail with `ExpiredToken` after
about an hour; with the other token providers, Flink silently gets no
credential, while the docs claim credentials are consumed automatically.
Fix: #14031
### Does this PR introduce _any_ user-facing change?
Yes. Loading an Iceberg catalog with a Hive or JDBC backend and
`aws-irsa` now fails in Flink. Token providers only log a warning.
### How was this patch tested?
Added unit tests in `TestGravitinoIcebergCatalog`.
---
docs/flink-connector/flink-catalog-iceberg.md | 9 ++
docs/security/credential-vending.md | 4 +-
.../connector/iceberg/GravitinoIcebergCatalog.java | 93 +++++++++++-
.../iceberg/IcebergPropertiesConstants.java | 9 ++
.../iceberg/TestGravitinoIcebergCatalog.java | 159 +++++++++++++++++++++
.../iceberg/TestGravitinoIcebergCatalogOpen.java | 133 +++++++++++++++++
6 files changed, 404 insertions(+), 3 deletions(-)
diff --git a/docs/flink-connector/flink-catalog-iceberg.md
b/docs/flink-connector/flink-catalog-iceberg.md
index db88e7a2b4..f11491abbb 100644
--- a/docs/flink-connector/flink-catalog-iceberg.md
+++ b/docs/flink-connector/flink-catalog-iceberg.md
@@ -120,6 +120,15 @@ Gravitino catalog property names with the prefix
`flink.bypass.` are passed to F
## Storage
+### Credential Vending
+
+Unless the catalog uses a `REST` backend with `data-access` set to
`vended-credentials`, the Flink connector fetches catalog-level credentials
from Gravitino once, when the catalog is opened, and never refreshes them. Only
static credential providers, such as `s3-secret-key`, `oss-secret-key` and
`azure-account-key`, are vended to Flink:
+
+- `s3-token`, `gcs-token`, `adls-token`, `oss-token` and `cos-token` vend no
credential at catalog level. Flink logs a warning and Iceberg uses the
credentials available on the Flink nodes, such as an instance profile.
+- `aws-irsa` is rejected when the catalog is loaded, because its credential
expires and is never refreshed.
+
+For temporary credentials vended per table and refreshed automatically, use a
`REST` backend with `data-access` set to `vended-credentials`.
+
### OSS
Additionally, you need download the [Aliyun OSS
SDK](https://gosspublic.alicdn.com/sdks/java/aliyun_java_sdk_3.10.2.zip), and
copy `aliyun-sdk-oss-3.10.2.jar`, `hamcrest-core-1.1.jar`, `jdom2-2.0.6.jar` to
the Flink classpath.
diff --git a/docs/security/credential-vending.md
b/docs/security/credential-vending.md
index d58dcb78ae..46eccd0306 100755
--- a/docs/security/credential-vending.md
+++ b/docs/security/credential-vending.md
@@ -20,7 +20,7 @@ Gravitino credential vending is used to generate temporary or
static credentials
| JDBC | JDBC user and password |
| Paimon | S3, OSS, JDBC user and password |
-S3 is Amazon S3, OSS is Alibaba Cloud OSS, GCS is Google Cloud Storage, ADLS
is Azure Data Lake Storage, and COS is Tencent Cloud COS. The Gravitino Spark,
Flink, and Trino connectors consume vended credentials automatically for these
catalogs.
+S3 is Amazon S3, OSS is Alibaba Cloud OSS, GCS is Google Cloud Storage, ADLS
is Azure Data Lake Storage, and COS is Tencent Cloud COS. The Gravitino Spark,
Flink, and Trino connectors consume vended credentials automatically for these
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).
## Quick Start
@@ -475,7 +475,7 @@ For a catalog, `{type}` is `catalog` and `{full_name}` is
the catalog name:
GET /api/metalakes/{metalake}/objects/catalog/{catalog}/credentials
```
-The Gravitino Spark and Flink connectors call this for you and inject the
returned credentials, so no client configuration is needed.
+The Gravitino Spark and Flink connectors call this for you and inject the
returned credentials, so no client configuration is needed. 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`.
### Read or Write Scope
diff --git
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/iceberg/GravitinoIcebergCatalog.java
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/iceberg/GravitinoIcebergCatalog.java
index 9e3b19d14d..3ec62cfa15 100644
---
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/iceberg/GravitinoIcebergCatalog.java
+++
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/iceberg/GravitinoIcebergCatalog.java
@@ -18,18 +18,38 @@
*/
package org.apache.gravitino.flink.connector.iceberg;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
import org.apache.flink.table.catalog.AbstractCatalog;
import org.apache.flink.table.catalog.exceptions.CatalogException;
import org.apache.flink.table.factories.Factory;
import org.apache.flink.util.Preconditions;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.credential.ADLSTokenCredential;
+import org.apache.gravitino.credential.AwsIrsaCredential;
+import org.apache.gravitino.credential.COSTokenCredential;
+import org.apache.gravitino.credential.CredentialConstants;
import org.apache.gravitino.credential.CredentialPropertyUtils;
+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.exceptions.NoSuchCatalogException;
import org.apache.gravitino.flink.connector.PartitionConverter;
import org.apache.gravitino.flink.connector.SchemaAndTablePropertiesConverter;
import org.apache.gravitino.flink.connector.catalog.BaseCatalog;
+import org.apache.gravitino.storage.GCSProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,6 +58,14 @@ public abstract class GravitinoIcebergCatalog extends
BaseCatalog {
private static final Logger LOG =
LoggerFactory.getLogger(GravitinoIcebergCatalog.class);
+ private static final Set<String> PATH_SCOPED_TOKEN_PROVIDERS =
+ ImmutableSet.of(
+ S3TokenCredential.S3_TOKEN_CREDENTIAL_TYPE,
+ GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE,
+ ADLSTokenCredential.ADLS_TOKEN_CREDENTIAL_TYPE,
+ OSSTokenCredential.OSS_TOKEN_CREDENTIAL_TYPE,
+ COSTokenCredential.COS_TOKEN_CREDENTIAL_TYPE);
+
private final String icebergCatalogName;
// Mutable copy so credential injection in open() propagates to the inner
Iceberg catalog.
private final Map<String, String> mutableIcebergProperties;
@@ -82,8 +110,20 @@ public abstract class GravitinoIcebergCatalog extends
BaseCatalog {
public void open() throws CatalogException {
if (icebergCatalog == null) {
try {
+ Catalog gravitinoCatalog = catalog();
+ List<String> tokenProviders =
+ checkCredentialProviders(
+ catalogName(), mutableIcebergProperties,
gravitinoCatalog.properties());
+ if (!tokenProviders.isEmpty()) {
+ LOG.warn(
+ "Catalog '{}' uses credential providers {}, which vend no
credential at catalog"
+ + " level. The Flink connector injects no storage credential
for them, so"
+ + " Iceberg FileIO uses the credentials available on the
Flink nodes.",
+ catalogName(),
+ tokenProviders);
+ }
CredentialPropertyUtils.applyIcebergCredentials(
- CredentialPropertyUtils.getCredentials(catalog()),
mutableIcebergProperties);
+ CredentialPropertyUtils.getCredentials(gravitinoCatalog),
mutableIcebergProperties);
} catch (NoSuchCatalogException e) {
LOG.warn(
"Catalog '{}' not found in Gravitino during open(); credential
injection skipped."
@@ -132,4 +172,55 @@ public abstract class GravitinoIcebergCatalog extends
BaseCatalog {
catalog.getClass().getName());
return (AbstractCatalog) catalog;
}
+
+ // Without per-table vended credentials, the inner catalog only receives the
catalog-level
+ // credentials fetched once in open(). A REST backend with vended
credentials enabled is exempt
+ // because the inner REST catalog obtains per-table, refreshable credentials
itself. aws-irsa is
+ // rejected: its credential is injected but never refreshed, so it overrides
the node's own
+ // credentials and then expires. Path-scoped token providers vend nothing at
catalog level, which
+ // leaves the node's own credentials in effect, so they are only returned
for the caller to warn
+ // about. The REST check uses the effective inner catalog-type, since an
explicit catalog-type may
+ // differ from the persisted catalog-backend. When no provider list is set,
the server infers
+ // gcs-token from gcs-service-account-file, which is not reflected in the
catalog properties seen
+ // by the client, so the same inference is applied here.
+ @VisibleForTesting
+ static List<String> checkCredentialProviders(
+ String catalogName,
+ Map<String, String> icebergCatalogProperties,
+ @Nullable Map<String, String> gravitinoCatalogProperties) {
+ boolean restWithVendedCredentials =
+
IcebergPropertiesConstants.ICEBERG_CATALOG_BACKEND_REST.equalsIgnoreCase(
+
icebergCatalogProperties.get(IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE))
+ &&
IcebergPropertiesConstants.ICEBERG_DATA_ACCESS_VENDED_CREDENTIALS.equalsIgnoreCase(
+
icebergCatalogProperties.get(IcebergPropertiesConstants.ICEBERG_ACCESS_DELEGATION));
+ if (restWithVendedCredentials || gravitinoCatalogProperties == null) {
+ return Collections.emptyList();
+ }
+ String providers =
gravitinoCatalogProperties.get(CredentialConstants.CREDENTIAL_PROVIDERS);
+ if (StringUtils.isBlank(providers)) {
+ return StringUtils.isNotBlank(
+
gravitinoCatalogProperties.get(GCSProperties.GRAVITINO_GCS_SERVICE_ACCOUNT_FILE))
+ ?
Collections.singletonList(GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE)
+ : Collections.emptyList();
+ }
+ List<String> providerList =
+
Splitter.on(',').trimResults().omitEmptyStrings().splitToList(providers);
+ if (providerList.stream()
+
.anyMatch(AwsIrsaCredential.AWS_IRSA_CREDENTIAL_TYPE::equalsIgnoreCase)) {
+ throw new CatalogException(
+ String.format(
+ "Catalog '%s' uses credential provider %s, which the Flink
connector does not"
+ + " support for Iceberg catalogs with a non-REST backend:
its credential is"
+ + " never refreshed and expires. Use a static provider such
as %s, or use a"
+ + " REST backend with %s=%s.",
+ catalogName,
+ AwsIrsaCredential.AWS_IRSA_CREDENTIAL_TYPE,
+ S3SecretKeyCredential.S3_SECRET_KEY_CREDENTIAL_TYPE,
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_DATA_ACCESS,
+
IcebergPropertiesConstants.ICEBERG_DATA_ACCESS_VENDED_CREDENTIALS));
+ }
+ return providerList.stream()
+ .filter(provider ->
PATH_SCOPED_TOKEN_PROVIDERS.contains(provider.toLowerCase(Locale.ROOT)))
+ .collect(Collectors.toList());
+ }
}
diff --git
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/iceberg/IcebergPropertiesConstants.java
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/iceberg/IcebergPropertiesConstants.java
index d879bb10e0..0d286c5d0c 100644
---
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/iceberg/IcebergPropertiesConstants.java
+++
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/iceberg/IcebergPropertiesConstants.java
@@ -52,4 +52,13 @@ public class IcebergPropertiesConstants {
@VisibleForTesting
public static final String ICEBERG_CATALOG_BACKEND_REST =
CatalogUtil.ICEBERG_CATALOG_TYPE_REST;
+
+ /** Gravitino Iceberg catalog property that selects how clients access table
data. */
+ public static final String GRAVITINO_ICEBERG_DATA_ACCESS =
IcebergConstants.DATA_ACCESS;
+
+ /** Iceberg catalog property that the data access mode is passed to the REST
backend as. */
+ public static final String ICEBERG_ACCESS_DELEGATION =
IcebergConstants.ICEBERG_ACCESS_DELEGATION;
+
+ /** Data access mode in which the REST backend vends per-table storage
credentials. */
+ public static final String ICEBERG_DATA_ACCESS_VENDED_CREDENTIALS =
"vended-credentials";
}
diff --git
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/iceberg/TestGravitinoIcebergCatalog.java
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/iceberg/TestGravitinoIcebergCatalog.java
index d801841b53..b5c241ae94 100644
---
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/iceberg/TestGravitinoIcebergCatalog.java
+++
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/iceberg/TestGravitinoIcebergCatalog.java
@@ -19,16 +19,31 @@
package org.apache.gravitino.flink.connector.iceberg;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.Locale;
+import java.util.Map;
import org.apache.flink.table.catalog.AbstractCatalog;
import org.apache.flink.table.catalog.Catalog;
+import org.apache.flink.table.catalog.exceptions.CatalogException;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.GCSTokenCredential;
+import org.apache.gravitino.storage.GCSProperties;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
class TestGravitinoIcebergCatalog {
+ private static final Map<String, String> HIVE_CATALOG =
+ ImmutableMap.of(IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE, "hive");
+
@Test
void testRequireAbstractCatalogAcceptsAbstractCatalog() {
AbstractCatalog catalog = mock(AbstractCatalog.class);
@@ -43,4 +58,148 @@ class TestGravitinoIcebergCatalog {
assertThrows(
IllegalStateException.class, () ->
GravitinoIcebergCatalog.asAbstractCatalog(catalog));
}
+
+ @Test
+ void testCheckCredentialProvidersRejectsAwsIrsaForNonRestBackend() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
+ "hive",
+ CredentialConstants.CREDENTIAL_PROVIDERS,
+ "s3-secret-key, AWS-IRSA");
+
+ CatalogException exception =
+ assertThrows(
+ CatalogException.class,
+ () -> GravitinoIcebergCatalog.checkCredentialProviders("c1",
HIVE_CATALOG, properties));
+ assertTrue(exception.getMessage().contains("aws-irsa"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"s3-token", "gcs-token", "adls-token", "oss-token",
"cos-token"})
+ void
testCheckCredentialProvidersReturnsTokenProvidersForNonRestBackend(String
provider) {
+ String upperCaseProvider = provider.toUpperCase(Locale.ROOT);
+ Map<String, String> properties =
+ ImmutableMap.of(
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
+ "hive",
+ CredentialConstants.CREDENTIAL_PROVIDERS,
+ "s3-secret-key, " + upperCaseProvider);
+
+ assertEquals(
+ ImmutableList.of(upperCaseProvider),
+ GravitinoIcebergCatalog.checkCredentialProviders("c1", HIVE_CATALOG,
properties));
+ }
+
+ @Test
+ void testCheckCredentialProvidersIgnoresStaticProviders() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
+ "jdbc",
+ CredentialConstants.CREDENTIAL_PROVIDERS,
+ "s3-secret-key,jdbc-user-password");
+
+ assertTrue(
+ GravitinoIcebergCatalog.checkCredentialProviders("c1",
ImmutableMap.of(), properties)
+ .isEmpty());
+ }
+
+ @Test
+ void testCheckCredentialProvidersIgnoresMissingProviders() {
+ Map<String, String> properties =
+
ImmutableMap.of(IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
"hive");
+
+ assertTrue(
+ GravitinoIcebergCatalog.checkCredentialProviders("c1", HIVE_CATALOG,
properties).isEmpty());
+ }
+
+ @Test
+ void testCheckCredentialProvidersInfersGcsTokenFromServiceAccountFile() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
+ "hive",
+ GCSProperties.GRAVITINO_GCS_SERVICE_ACCOUNT_FILE,
+ "/path/to/sa.json");
+
+ assertEquals(
+ ImmutableList.of(GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE),
+ GravitinoIcebergCatalog.checkCredentialProviders("c1", HIVE_CATALOG,
properties));
+ }
+
+ @Test
+ void testCheckCredentialProvidersUsesExplicitProvidersOverGcsInference() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
+ "hive",
+ GCSProperties.GRAVITINO_GCS_SERVICE_ACCOUNT_FILE,
+ "/path/to/sa.json",
+ CredentialConstants.CREDENTIAL_PROVIDERS,
+ "s3-secret-key");
+
+ assertTrue(
+ GravitinoIcebergCatalog.checkCredentialProviders("c1", HIVE_CATALOG,
properties).isEmpty());
+ }
+
+ @Test
+ void testCheckCredentialProvidersSkipsRestBackendWithVendedCredentials() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
+ "REST",
+ CredentialConstants.CREDENTIAL_PROVIDERS,
+ "s3-token,aws-irsa");
+
+ assertTrue(
+ GravitinoIcebergCatalog.checkCredentialProviders(
+ "c1", restCatalog("REST", true), properties)
+ .isEmpty());
+ }
+
+ @Test
+ void testCheckCredentialProvidersUsesEffectiveCatalogType() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
+ "hive",
+ CredentialConstants.CREDENTIAL_PROVIDERS,
+ "aws-irsa");
+
+ assertTrue(
+ GravitinoIcebergCatalog.checkCredentialProviders(
+ "c1", restCatalog("rest", true), properties)
+ .isEmpty());
+ }
+
+ @Test
+ void
testCheckCredentialProvidersRejectsAwsIrsaForRestBackendWithoutVendedCredentials()
{
+ Map<String, String> properties =
+ ImmutableMap.of(CredentialConstants.CREDENTIAL_PROVIDERS, "aws-irsa");
+
+ assertThrows(
+ CatalogException.class,
+ () ->
+ GravitinoIcebergCatalog.checkCredentialProviders(
+ "c1", restCatalog("rest", false), properties));
+ }
+
+ @Test
+ void testCheckCredentialProvidersIgnoresNullProperties() {
+ assertTrue(
+ GravitinoIcebergCatalog.checkCredentialProviders("c1", HIVE_CATALOG,
null).isEmpty());
+ }
+
+ private static Map<String, String> restCatalog(String catalogType, boolean
vendedCredentials) {
+ ImmutableMap.Builder<String, String> builder =
+ ImmutableMap.<String, String>builder()
+ .put(IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE, catalogType);
+ if (vendedCredentials) {
+ builder.put(
+ IcebergPropertiesConstants.ICEBERG_ACCESS_DELEGATION,
+ IcebergPropertiesConstants.ICEBERG_DATA_ACCESS_VENDED_CREDENTIALS);
+ }
+ return builder.build();
+ }
}
diff --git
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/iceberg/TestGravitinoIcebergCatalogOpen.java
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/iceberg/TestGravitinoIcebergCatalogOpen.java
new file mode 100644
index 0000000000..dc3c606db3
--- /dev/null
+++
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/iceberg/TestGravitinoIcebergCatalogOpen.java
@@ -0,0 +1,133 @@
+/*
+ * 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.flink.connector.iceberg;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.flink.table.catalog.AbstractCatalog;
+import org.apache.flink.table.catalog.exceptions.CatalogException;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.SupportsCredentials;
+import org.apache.gravitino.flink.connector.PartitionConverter;
+import org.apache.gravitino.flink.connector.SchemaAndTablePropertiesConverter;
+import org.apache.gravitino.flink.connector.utils.CatalogCompat;
+import org.junit.jupiter.api.Test;
+
+class TestGravitinoIcebergCatalogOpen {
+
+ private static final Map<String, String> HIVE_BACKEND_WITH_AWS_IRSA =
+ ImmutableMap.of(
+ IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND,
+ "hive",
+ CredentialConstants.CREDENTIAL_PROVIDERS,
+ "aws-irsa");
+
+ @Test
+ void
testOpenAllowsAwsIrsaWhenEffectiveCatalogTypeIsRestWithVendedCredentials() {
+ AbstractCatalog innerCatalog = mock(AbstractCatalog.class);
+ TestableIcebergCatalog catalog =
+ new TestableIcebergCatalog(
+ ImmutableMap.of(
+ IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE,
+ "rest",
+ IcebergPropertiesConstants.ICEBERG_ACCESS_DELEGATION,
+
IcebergPropertiesConstants.ICEBERG_DATA_ACCESS_VENDED_CREDENTIALS),
+ mockGravitinoCatalog(HIVE_BACKEND_WITH_AWS_IRSA),
+ innerCatalog);
+
+ assertDoesNotThrow(catalog::open);
+ verify(innerCatalog).open();
+ }
+
+ @Test
+ void testOpenRejectsAwsIrsaWhenEffectiveCatalogTypeIsNotRest() {
+ TestableIcebergCatalog catalog =
+ new TestableIcebergCatalog(
+ ImmutableMap.of(IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE,
"hive"),
+ mockGravitinoCatalog(HIVE_BACKEND_WITH_AWS_IRSA),
+ mock(AbstractCatalog.class));
+
+ assertThrows(CatalogException.class, catalog::open);
+ }
+
+ @Test
+ void testOpenRejectsAwsIrsaWhenRestBackendHasNoVendedCredentials() {
+ TestableIcebergCatalog catalog =
+ new TestableIcebergCatalog(
+ ImmutableMap.of(IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE,
"rest"),
+ mockGravitinoCatalog(HIVE_BACKEND_WITH_AWS_IRSA),
+ mock(AbstractCatalog.class));
+
+ assertThrows(CatalogException.class, catalog::open);
+ }
+
+ private static Catalog mockGravitinoCatalog(Map<String, String> properties) {
+ Catalog gravitinoCatalog = mock(Catalog.class);
+ SupportsCredentials supportsCredentials = mock(SupportsCredentials.class);
+ when(gravitinoCatalog.properties()).thenReturn(properties);
+
when(gravitinoCatalog.supportsCredentials()).thenReturn(supportsCredentials);
+ when(supportsCredentials.getCredentials()).thenReturn(new Credential[0]);
+ return gravitinoCatalog;
+ }
+
+ private static class TestableIcebergCatalog extends GravitinoIcebergCatalog {
+
+ private final Catalog gravitinoCatalog;
+ private final AbstractCatalog innerCatalog;
+
+ TestableIcebergCatalog(
+ Map<String, String> icebergProperties,
+ Catalog gravitinoCatalog,
+ AbstractCatalog innerCatalog) {
+ super(
+ "c1",
+ "default",
+ mock(SchemaAndTablePropertiesConverter.class),
+ mock(PartitionConverter.class),
+ icebergProperties,
+ icebergProperties);
+ this.gravitinoCatalog = gravitinoCatalog;
+ this.innerCatalog = innerCatalog;
+ }
+
+ @Override
+ protected Catalog catalog() {
+ return gravitinoCatalog;
+ }
+
+ @Override
+ protected Object createInnerIcebergCatalog(String catalogName, Map<String,
String> properties) {
+ return innerCatalog;
+ }
+
+ @Override
+ protected CatalogCompat catalogCompat() {
+ return mock(CatalogCompat.class);
+ }
+ }
+}