This is an automated email from the ASF dual-hosted git repository.

diqiu50 pushed a commit to branch dell-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git

commit 6214c3469346115527f9bfea48cbdc4a4e81b8fd
Author: diqiu50 <[email protected]>
AuthorDate: Sat Aug 22 21:38:49 2026 +0800

    [Cherry-pick to branch-1.3] [#12554] improvement(trino-connector): Address 
review findings on the Iceberg REST routing
    
    Surface the missing rest-uri at connector startup instead of only when a
    catalog load fails inside the refresh loop, where the error is swallowed.
    
    Correct the claim that the REST path is the only one supporting credential
    vending: it is the only one supporting temporary credentials, while static
    keys are still injected on the backend path by applyCredentials.
    
    Derive s3.path-style-access, which S3-compatible stores such as MinIO need,
    and warn when a warehouse scheme has no Trino native file system but the
    catalog vends credentials. Log ignored reserved keys and the ambiguity of
    routing by catalog name alone in multi-metalake mode.
    
    Match Gravitino's case-insensitive handling of catalog-backend, and set the
    Iceberg REST config from an instance initializer so two @BeforeAll methods
    on the same test class no longer race.
    
    (cherry picked from commit a1b67fbd7f56f9522f64f62182696da130beaf93)
---
 docs/trino-connector/catalog-iceberg.md            |  22 ++-
 docs/trino-connector/configuration.md              |   4 +-
 .../docker-script/init/trino/init.sh               |   5 +-
 .../test/container/TrinoITContainers.java          |   4 +
 .../gravitino/integration/test/util/BaseIT.java    |   3 +-
 .../integration/test/TrinoConnectorIT.java         |  24 +--
 .../gravitino/trino/connector/GravitinoConfig.java |  26 ++++
 .../iceberg/IcebergCatalogPropertyConverter.java   | 173 +++++++++++++++------
 .../catalog/iceberg/IcebergConnectorAdapter.java   |  16 +-
 .../trino/connector/TestGravitinoConfig.java       |  12 ++
 .../TestIcebergCatalogPropertyConverter.java       | 163 ++++++++++++++++++-
 11 files changed, 383 insertions(+), 69 deletions(-)

diff --git a/docs/trino-connector/catalog-iceberg.md 
b/docs/trino-connector/catalog-iceberg.md
index 223b597fe9..dd00d8cab1 100644
--- a/docs/trino-connector/catalog-iceberg.md
+++ b/docs/trino-connector/catalog-iceberg.md
@@ -50,9 +50,20 @@ them, and one catalog could not serve two Trino clusters 
that reach the IRC by d
 The connector derives everything else from the catalog itself. The Gravitino 
catalog name is passed
 as both `iceberg.rest-catalog.warehouse` and `iceberg.rest-catalog.prefix` — 
the Iceberg client
 selects the catalog twice over, first as the query parameter of the `GET 
/v1/config` call that
-discovers it, then as the path segment of every request after that. The Trino 
native file system
-(`fs.native-s3.enabled` and `s3.region`, or the GCS and Azure equivalents) is 
derived from the
-catalog's `warehouse` scheme and storage properties.
+discovers it, then as the path segment of every request after that.
+
+The Trino native file system is derived from the catalog's `warehouse` scheme, 
because vended
+credentials are only consumed by Trino's native file systems:
+
+| Warehouse scheme                          | Derived properties               
                                                                     |
+|:------------------------------------------|:------------------------------------------------------------------------------------------------------|
+| `s3://`, `s3a://`, `s3n://`               | `fs.native-s3.enabled`, plus 
`s3.region`, `s3.endpoint` and `s3.path-style-access` where the catalog defines 
`s3-region`, `s3-endpoint` and `s3-path-style-access` |
+| `gs://`                                   | `fs.native-gcs.enabled`          
                                                                      |
+| `abfs://`, `abfss://`, `wasb://`, `wasbs://` | `fs.native-azure.enabled`     
                                                                      |
+| anything else (`hdfs://`, `file://`, `oss://`) | none — only 
`fs.hadoop.enabled`                                                             
      |
+
+A scheme in the last row has no Trino native file system, so a catalog that 
vends credentials for it
+cannot have them applied; the connector logs a warning when it detects that 
combination.
 
 On the Gravitino side, the IRC must run with the dynamic config provider so it 
can serve catalogs
 defined in Gravitino:
@@ -81,6 +92,11 @@ gravitino.iceberg.rest-catalog.oauth2.scope=email
 Omitting this block against an authenticated IRC surfaces as an authentication 
error rather than a
 missing-configuration error, which is easy to misdiagnose.
 
+Four keys are reserved: `iceberg.rest-catalog.uri`, `.warehouse`, `.prefix` and
+`iceberg.catalog.type`. The connector always derives these itself, so setting 
them through either
+`gravitino.iceberg.rest-catalog.` or a catalog's `trino.bypass.` has no effect 
— the connector logs
+when it ignores one.
+
 When `gravitino.client.session.forwardUser=true`, the connector also sets
 `iceberg.rest-catalog.session=USER` so that each query carries the end user's 
identity to the IRC,
 keeping per-user credential vending and per-user authorization intact. Set
diff --git a/docs/trino-connector/configuration.md 
b/docs/trino-connector/configuration.md
index 1d756be8de..6d225a6cec 100644
--- a/docs/trino-connector/configuration.md
+++ b/docs/trino-connector/configuration.md
@@ -30,8 +30,8 @@ license: "This software is licensed under the Apache License 
version 2."
 | gravitino.trino.skip-catalog-patterns       | string  | (none)               
 | The `gravitino.trino.skip-catalog-patterns` defines a comma-separated list 
of catalog name regex patterns that should be excluded from loading. For 
example, `test_.*, .*_tmp` excludes all catalogs starting with `test_` or 
ending with `_tmp`.                                                             
                                   | No       |
 | gravitino.use-single-metalake               | boolean | true                 
 | If `true`, only one metalake is used and catalogs are identified by 
`<catalog_name>`. If `false`, multi-metalake mode is enabled and catalogs are 
identified by `<metalake_name>.<catalog_name>`.                                 
                                                                                
                               | No       |
 | gravitino.iceberg.rest-enabled              | boolean | true                 
 | If `true`, `lakehouse-iceberg` catalogs are loaded through the Gravitino 
Iceberg REST server (IRC) instead of being translated into Trino's `jdbc` or 
`hive_metastore` Iceberg catalog type. This is what makes credential vending 
work. Requires `gravitino.iceberg.rest-uri`. Set to `false` for a deployment 
that does not run the IRC.                                    | No       |
-| gravitino.iceberg.rest-uri                  | string  | (none)               
 | The endpoint of the Gravitino Iceberg REST server, for example 
`http://gravitino-host:9001/iceberg`. Required when 
`gravitino.iceberg.rest-enabled` is `true` and the metalake contains 
`lakehouse-iceberg` catalogs.                                                   
                                            | No       |
-| gravitino.iceberg.rest-catalog.             | string  | (none)               
 | The configuration key prefix for properties passed through to the internal 
Trino Iceberg REST catalog. The prefix is rewritten to `iceberg.rest-catalog.`, 
so `gravitino.iceberg.rest-catalog.security=OAUTH2` becomes 
`iceberg.rest-catalog.security=OAUTH2`.                                         
             | No       |
+| gravitino.iceberg.rest-uri                  | string  | (none)               
 | The endpoint of the Gravitino Iceberg REST server, for example 
`http://gravitino-host:9001/iceberg`. Required when 
`gravitino.iceberg.rest-enabled` is `true` and the metalake contains 
`lakehouse-iceberg` catalogs whose `catalog-backend` is not `rest`.             
                                             | No       |
+| gravitino.iceberg.rest-catalog.             | string  | (none)               
 | The configuration key prefix for properties passed through to the internal 
Trino Iceberg REST catalog. The prefix is rewritten to `iceberg.rest-catalog.`, 
so `gravitino.iceberg.rest-catalog.security=OAUTH2` becomes 
`iceberg.rest-catalog.security=OAUTH2`. The `uri`, `warehouse` and `prefix` 
keys are reserved and always derived by the connector.                     | No 
      |
 
 To configure the Gravitino client, use properties prefixed with 
`gravitino.client.`. These properties will directly passed to the Gravitino 
client.
 
diff --git a/integration-test-common/docker-script/init/trino/init.sh 
b/integration-test-common/docker-script/init/trino/init.sh
index d713708e52..a65ec5ab90 100644
--- a/integration-test-common/docker-script/init/trino/init.sh
+++ b/integration-test-common/docker-script/init/trino/init.sh
@@ -34,8 +34,9 @@ cp /usr/lib/trino/plugin/mysql/*mysql-connector-j-*.jar 
/usr/lib/trino/plugin/ic
 sed -i 
"s/GRAVITINO_HOST_IP:GRAVITINO_HOST_PORT/${GRAVITINO_HOST_IP}:${GRAVITINO_HOST_PORT}/g"
 /etc/trino/catalog/gravitino.properties
 # Update `gravitino.metalake = GRAVITINO_METALAKE_NAME` in the 
`conf/catalog/gravitino.properties`
 sed -i "s/GRAVITINO_METALAKE_NAME/${GRAVITINO_METALAKE_NAME}/g" 
/etc/trino/catalog/gravitino.properties
-# Update `gravitino.iceberg.rest-uri` in the 
`conf/catalog/gravitino.properties`. It uses its own
-# placeholders so that the substitution above cannot leave a half-replaced 
host behind.
+# Update `gravitino.iceberg.rest-uri` in the 
`conf/catalog/gravitino.properties`. The Iceberg REST
+# server runs on the same host as the Gravitino server but on a different 
port, so it needs its own
+# placeholder pair — reusing GRAVITINO_HOST_IP:GRAVITINO_HOST_PORT would 
substitute the server port.
 sed -i 
"s/GRAVITINO_ICEBERG_REST_HOST:GRAVITINO_ICEBERG_REST_PORT/${GRAVITINO_HOST_IP}:${GRAVITINO_ICEBERG_REST_PORT}/g"
 /etc/trino/catalog/gravitino.properties
 # Update `node.id=NODE_ID` in the `/conf/node.properties`
 sed -i "s/NODE_ID/${RANDOM}-${RANDOM}-${RANDOM}-${RANDOM}-${RANDOM}/g" 
/etc/trino/node.properties
diff --git 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoITContainers.java
 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoITContainers.java
index 14fe768c0b..572b948d55 100644
--- 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoITContainers.java
+++ 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoITContainers.java
@@ -84,6 +84,10 @@ public class TrinoITContainers implements AutoCloseable {
     env.put("HIVE_RUNTIME_VERSION", hiveRuntimeVersion);
     if (icebergRestServerPort > 0) {
       env.put("GRAVITINO_ICEBERG_REST_PORT", 
String.valueOf(icebergRestServerPort));
+    } else {
+      LOG.info(
+          "No Iceberg REST server port supplied; Trino falls back to the 
compose default. "
+              + "Catalogs routed through the Iceberg REST server will not work 
in this container.");
     }
     env.put("TRINO_CONNECTOR_TEST", String.valueOf(isTrinoConnectorTest));
     if (System.getProperty("gravitino.log.path") != null) {
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 f3a0724ef0..0c247a22f1 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
@@ -688,7 +688,8 @@ public class BaseIT {
   }
 
   /**
-   * Returns the port the Iceberg REST auxiliary service listens on.
+   * Returns the port the Iceberg REST auxiliary service listens on. Only 
valid once {@link
+   * #startIntegrationTest()} has run, since it reads the started server's 
config.
    *
    * @return the Iceberg REST service port
    */
diff --git 
a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoConnectorIT.java
 
b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoConnectorIT.java
index d0796e044f..1be5ef43bc 100644
--- 
a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoConnectorIT.java
+++ 
b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoConnectorIT.java
@@ -23,7 +23,6 @@ import static 
org.testcontainers.shaded.org.awaitility.Awaitility.await;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -94,18 +93,19 @@ public class TrinoConnectorIT extends BaseIT {
   private static GravitinoMetalake metalake;
   private static Catalog catalog;
 
-  @BeforeAll
-  @Override
-  public void startIntegrationTest() throws Exception {
+  // Set from an instance initializer rather than an overridden @BeforeAll: 
BaseIT is PER_CLASS, so
+  // the instance exists before any lifecycle method runs, and JUnit only 
guarantees that a
+  // superclass @BeforeAll precedes a subclass one — two @BeforeAll methods 
declared on this same
+  // class would have no defined order relative to each other.
+  {
     // The Trino connector loads every lakehouse-iceberg catalog through the 
Iceberg REST server,
     // so the auxiliary service has to run and serve this test's metalake.
-    ignoreIcebergAuxRestService = false;
-    customConfigs.put(
-        GRAVITINO_ICEBERG_REST_PREFIX + 
IcebergConstants.ICEBERG_REST_CATALOG_CONFIG_PROVIDER,
-        IcebergConstants.DYNAMIC_ICEBERG_CATALOG_CONFIG_PROVIDER_NAME);
-    customConfigs.put(
-        GRAVITINO_ICEBERG_REST_PREFIX + IcebergConstants.GRAVITINO_METALAKE, 
metalakeName);
-    super.startIntegrationTest();
+    enableIcebergAuxRestService(
+        ImmutableMap.of(
+            GRAVITINO_ICEBERG_REST_PREFIX + 
IcebergConstants.ICEBERG_REST_CATALOG_CONFIG_PROVIDER,
+            IcebergConstants.DYNAMIC_ICEBERG_CATALOG_CONFIG_PROVIDER_NAME,
+            GRAVITINO_ICEBERG_REST_PREFIX + 
IcebergConstants.GRAVITINO_METALAKE,
+            metalakeName));
   }
 
   @BeforeAll
@@ -131,7 +131,7 @@ public class TrinoConnectorIT extends BaseIT {
         trinoConfDir,
         System.getenv("GRAVITINO_ROOT_DIR") + "/trino-connector/build/libs",
         getGravitinoServerPort(),
-        URI.create(getIcebergRestServiceUri()).getPort(),
+        getIcebergRestServicePort(),
         metalakeName);
     Assertions.assertTrue(
         containerSuite.getTrinoContainer().checkSyncCatalogFromGravitino(5, 
catalogName),
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java
index b6a518584a..50af268066 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java
@@ -35,10 +35,14 @@ import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.trino.connector.security.GravitinoAuthProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Gravitino config. */
 public class GravitinoConfig {
 
+  private static final Logger LOG = 
LoggerFactory.getLogger(GravitinoConfig.class);
+
   // Trino config keys
   /** The Trino discovery URI. */
   private static final String TRINO_DISCOVERY_URI = "discovery.uri";
@@ -321,6 +325,28 @@ public class GravitinoConfig {
           "Config `gravitino.trino.skip-catalog-patterns` is invalid because 
it contains an illegal regular expression",
           e);
     }
+    warnOnMissingIcebergRestUri();
+  }
+
+  /**
+   * Warns at startup when the Iceberg REST routing is on but has no endpoint. 
Without this, the
+   * failure only surfaces once a lakehouse-iceberg catalog is loaded, and 
that error is swallowed
+   * by the catalog refresh loop, leaving the user with an unexplained missing 
catalog.
+   */
+  private void warnOnMissingIcebergRestUri() {
+    // Only the statically configured connector warns; the dynamic per-catalog 
ones would repeat it.
+    if (isDynamicConnector()
+        || !isIcebergRestEnabled()
+        || StringUtils.isNotBlank(getIcebergRestUri())) {
+      return;
+    }
+    LOG.warn(
+        "'{}' is enabled but '{}' is not set, so every lakehouse-iceberg 
catalog will fail to "
+            + "load. Set the Iceberg REST server endpoint, or set '{}=false' 
to load Iceberg "
+            + "catalogs through their catalog backend instead.",
+        GRAVITINO_ICEBERG_REST_ENABLED.key,
+        GRAVITINO_ICEBERG_REST_URI.key,
+        GRAVITINO_ICEBERG_REST_ENABLED.key);
   }
 
   /**
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java
index 8073f1e1c2..dd8a492e1d 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java
@@ -22,6 +22,7 @@ package org.apache.gravitino.trino.connector.catalog.iceberg;
 import com.google.common.collect.Sets;
 import io.trino.spi.TrinoException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
@@ -30,6 +31,7 @@ import 
org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
 import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergPropertiesUtils;
 import org.apache.gravitino.credential.AzureAccountKeyCredential;
 import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.CredentialConstants;
 import org.apache.gravitino.credential.JdbcCredential;
 import org.apache.gravitino.credential.S3SecretKeyCredential;
 import org.apache.gravitino.storage.S3Properties;
@@ -37,6 +39,8 @@ import org.apache.gravitino.trino.connector.GravitinoConfig;
 import org.apache.gravitino.trino.connector.GravitinoErrorCode;
 import org.apache.gravitino.trino.connector.catalog.CatalogPropertyConverter;
 import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * A property converter for Iceberg catalogs that handles the conversion 
between Trino and Gravitino
@@ -45,6 +49,8 @@ import 
org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
  */
 public class IcebergCatalogPropertyConverter extends CatalogPropertyConverter {
 
+  private static final Logger LOG = 
LoggerFactory.getLogger(IcebergCatalogPropertyConverter.class);
+
   private static final Set<String> JDBC_BACKEND_REQUIRED_PROPERTIES = 
Set.of("jdbc-driver", "uri");
 
   private static final Set<String> HIVE_BACKEND_REQUIRED_PROPERTIES = 
Set.of("uri");
@@ -65,6 +71,15 @@ public class IcebergCatalogPropertyConverter extends 
CatalogPropertyConverter {
   private static final String TRINO_FS_NATIVE_AZURE_ENABLED = 
"fs.native-azure.enabled";
   private static final String TRINO_S3_REGION = "s3.region";
   private static final String TRINO_S3_ENDPOINT = "s3.endpoint";
+  private static final String TRINO_S3_PATH_STYLE_ACCESS = 
"s3.path-style-access";
+
+  /** Routing keys the connector always derives itself; they cannot be 
overridden. */
+  private static final List<String> RESERVED_REST_PROPERTIES =
+      List.of(
+          TRINO_ICEBERG_CATALOG_TYPE,
+          TRINO_ICEBERG_REST_URI,
+          TRINO_ICEBERG_REST_WAREHOUSE,
+          TRINO_ICEBERG_REST_PREFIX);
 
   /**
    * Injects credentials from credential vending into the Iceberg catalog 
config. Applies JDBC
@@ -100,7 +115,9 @@ public class IcebergCatalogPropertyConverter extends 
CatalogPropertyConverter {
       throw new TrinoException(
           GravitinoErrorCode.GRAVITINO_MISSING_REQUIRED_PROPERTY,
           "Missing required property 'catalog-backend'");
-    switch (backend) {
+    // Gravitino accepts the backend as an enum value, and its own consumers 
upper-case it before
+    // resolving, so the value reaching here may be in any case.
+    switch (backend.toLowerCase(Locale.ROOT)) {
       case "hive":
         stringStringMap = buildHiveBackendProperties(properties);
         break;
@@ -121,6 +138,73 @@ public class IcebergCatalogPropertyConverter extends 
CatalogPropertyConverter {
     return config;
   }
 
+  /**
+   * Builds the Trino Iceberg connector config that reaches this catalog 
through the Gravitino
+   * Iceberg REST server, regardless of the catalog backend Gravitino uses to 
store its metadata.
+   *
+   * <p>This is the only path on which <em>temporary</em> credentials work — 
{@code s3-token} and
+   * its GCS and ADLS counterparts. The Iceberg REST protocol issues a fresh 
one per table access,
+   * while Trino's jdbc and hive_metastore Iceberg catalog types have nowhere 
to put the session
+   * token of an STS credential. Static-key credentials are unaffected: they 
are still injected on
+   * the backend path by {@link #applyCredentials}.
+   *
+   * @param catalog the Gravitino catalog to load
+   * @param gravitinoConfig the connector configuration holding the Iceberg 
REST server endpoint
+   * @return the Trino Iceberg connector config
+   * @throws TrinoException if {@code gravitino.iceberg.rest-uri} is not 
configured
+   */
+  public Map<String, String> buildIcebergRestProperties(
+      GravitinoCatalog catalog, GravitinoConfig gravitinoConfig) {
+    String restUri = gravitinoConfig.getIcebergRestUri();
+    if (StringUtils.isBlank(restUri)) {
+      throw new TrinoException(
+          GravitinoErrorCode.GRAVITINO_MISSING_CONFIG,
+          "Missing required config 'gravitino.iceberg.rest-uri'. Set it to the 
Gravitino Iceberg "
+              + "REST server endpoint, for example 
http://localhost:9001/iceberg, or set "
+              + "'gravitino.iceberg.rest-enabled=false' to load Iceberg 
catalogs through their "
+              + "catalog backend instead.");
+    }
+
+    Map<String, String> config = new HashMap<>();
+    // The order of put operations determines the priority of parameters.
+    config.putAll(buildStorageProperties(catalog.getProperties()));
+    config.put(TRINO_ICEBERG_REST_VENDED_CREDENTIALS, "true");
+    if (gravitinoConfig.isForwardUser()) {
+      config.put(TRINO_ICEBERG_REST_SESSION, "USER");
+    }
+    // The catalog's own trino.bypass properties override the defaults above, 
so that a Trino
+    // release renaming one of them can be worked around without a connector 
change.
+    config.putAll(super.gravitinoToEngineProperties(catalog.getProperties()));
+    // The IRC's own authentication is a cluster-level operational setting, so 
it takes precedence
+    // over anything set on a single catalog.
+    config.putAll(gravitinoConfig.getIcebergRestCatalogConfig());
+
+    warnOnReservedOverrides(catalog, config);
+
+    config.put(TRINO_ICEBERG_CATALOG_TYPE, TRINO_ICEBERG_CATALOG_TYPE_REST);
+    config.put(TRINO_ICEBERG_REST_URI, restUri);
+    // The Iceberg client selects the catalog twice over: `warehouse` is the 
query parameter of the
+    // initial GET /v1/config that discovers the catalog, and `prefix` is the 
path segment of every
+    // request after it. Setting only the latter leaves the config call 
without a catalog name.
+    // The server echoes `prefix` back as a config default, so setting it here 
is belt-and-braces.
+    config.put(TRINO_ICEBERG_REST_WAREHOUSE, catalog.getName());
+    config.put(TRINO_ICEBERG_REST_PREFIX, catalog.getName());
+
+    if (!gravitinoConfig.singleMetalakeMode()) {
+      // The IRC serves exactly one metalake, and the prefix carries only the 
catalog name, so two
+      // metalakes holding a same-named catalog both resolve to whichever one 
the IRC was started
+      // with.
+      LOG.warn(
+          "Catalog '{}' in metalake '{}' is routed to the Iceberg REST server 
using the catalog "
+              + "name alone. In multi-metalake mode this is ambiguous: the 
Iceberg REST server "
+              + "serves a single metalake, so a same-named catalog in another 
metalake resolves to "
+              + "the same prefix.",
+          catalog.getName(),
+          catalog.getMetalake());
+    }
+    return config;
+  }
+
   private Map<String, String> buildHiveBackendProperties(Map<String, String> 
properties) {
     Set<String> missingProperty =
         Sets.difference(HIVE_BACKEND_REQUIRED_PROPERTIES, properties.keySet());
@@ -167,51 +251,16 @@ public class IcebergCatalogPropertyConverter extends 
CatalogPropertyConverter {
     return jdbcProperties;
   }
 
-  /**
-   * Builds the Trino Iceberg connector config that reaches this catalog 
through the Gravitino
-   * Iceberg REST server, regardless of the catalog backend Gravitino uses to 
store its metadata.
-   *
-   * <p>This is the only path on which credential vending works: the Iceberg 
REST protocol issues a
-   * fresh temporary credential per table access, while Trino's jdbc and 
hive_metastore Iceberg
-   * catalog types have nowhere to put the session token of an STS credential.
-   *
-   * @param catalog the Gravitino catalog to load
-   * @param gravitinoConfig the connector configuration holding the Iceberg 
REST server endpoint
-   * @return the Trino Iceberg connector config
-   */
-  public Map<String, String> buildIcebergRestProperties(
-      GravitinoCatalog catalog, GravitinoConfig gravitinoConfig) {
-    String restUri = gravitinoConfig.getIcebergRestUri();
-    if (StringUtils.isBlank(restUri)) {
-      throw new TrinoException(
-          GravitinoErrorCode.GRAVITINO_MISSING_CONFIG,
-          "Missing required config 'gravitino.iceberg.rest-uri'. Set it to the 
Gravitino Iceberg "
-              + "REST server endpoint, for example 
http://localhost:9001/iceberg, or set "
-              + "'gravitino.iceberg.rest-enabled=false' to load Iceberg 
catalogs through their "
-              + "catalog backend instead.");
-    }
-
-    Map<String, String> config = new HashMap<>();
-    // The order of put operations determines the priority of parameters.
-    config.putAll(buildStorageProperties(catalog.getProperties()));
-    config.put(TRINO_ICEBERG_REST_VENDED_CREDENTIALS, "true");
-    if (gravitinoConfig.isForwardUser()) {
-      config.put(TRINO_ICEBERG_REST_SESSION, "USER");
+  private void warnOnReservedOverrides(GravitinoCatalog catalog, Map<String, 
String> config) {
+    for (String reserved : RESERVED_REST_PROPERTIES) {
+      if (config.containsKey(reserved)) {
+        LOG.info(
+            "Property '{}' set on catalog '{}' is ignored; the connector 
always derives it when "
+                + "routing through the Iceberg REST server.",
+            reserved,
+            catalog.getName());
+      }
     }
-    // The catalog's own trino.bypass properties override the defaults above, 
so that a Trino
-    // release renaming one of them can be worked around without a connector 
change.
-    config.putAll(super.gravitinoToEngineProperties(catalog.getProperties()));
-    // The Iceberg REST server endpoint and its authentication are 
cluster-level operational
-    // settings, so they take precedence over anything set on a single catalog.
-    config.putAll(gravitinoConfig.getIcebergRestCatalogConfig());
-    config.put(TRINO_ICEBERG_CATALOG_TYPE, TRINO_ICEBERG_CATALOG_TYPE_REST);
-    config.put(TRINO_ICEBERG_REST_URI, restUri);
-    // The Iceberg client selects the catalog twice over: `warehouse` is the 
query parameter of the
-    // initial GET /v1/config that discovers the catalog, and `prefix` is the 
path segment of every
-    // request after it. Setting only the latter leaves the config call 
without a catalog name.
-    config.put(TRINO_ICEBERG_REST_WAREHOUSE, catalog.getName());
-    config.put(TRINO_ICEBERG_REST_PREFIX, catalog.getName());
-    return config;
   }
 
   /**
@@ -221,6 +270,8 @@ public class IcebergCatalogPropertyConverter extends 
CatalogPropertyConverter {
    */
   private Map<String, String> buildStorageProperties(Map<String, String> 
properties) {
     Map<String, String> storageProperties = new HashMap<>();
+    // Always available as the fallback; where a native file system is also 
enabled below, that one
+    // takes precedence for its own scheme.
     storageProperties.put(TRINO_FS_HADOOP_ENABLED, "true");
 
     String warehouse = properties.get(IcebergConstants.WAREHOUSE);
@@ -238,6 +289,12 @@ public class IcebergCatalogPropertyConverter extends 
CatalogPropertyConverter {
             properties, S3Properties.GRAVITINO_S3_REGION, storageProperties, 
TRINO_S3_REGION);
         copyProperty(
             properties, S3Properties.GRAVITINO_S3_ENDPOINT, storageProperties, 
TRINO_S3_ENDPOINT);
+        // Required by S3-compatible stores such as MinIO and Ceph, which a 
custom endpoint implies.
+        copyProperty(
+            properties,
+            S3Properties.GRAVITINO_S3_PATH_STYLE_ACCESS,
+            storageProperties,
+            TRINO_S3_PATH_STYLE_ACCESS);
         break;
       case "gs":
         storageProperties.put(TRINO_FS_NATIVE_GCS_ENABLED, "true");
@@ -249,12 +306,36 @@ public class IcebergCatalogPropertyConverter extends 
CatalogPropertyConverter {
         storageProperties.put(TRINO_FS_NATIVE_AZURE_ENABLED, "true");
         break;
       default:
-        // hdfs, file and any scheme without a Trino native file system stay 
on fs.hadoop.enabled.
+        warnOnSchemeWithoutNativeFileSystem(properties, warehouse, scheme);
         break;
     }
     return storageProperties;
   }
 
+  /**
+   * Warns when a warehouse scheme has no Trino native file system but the 
catalog vends
+   * credentials. The Hadoop file system cannot consume a vended credential, 
so table access would
+   * fail at read time with a storage authentication error far from its cause.
+   */
+  private void warnOnSchemeWithoutNativeFileSystem(
+      Map<String, String> properties, String warehouse, String scheme) {
+    // hdfs and file legitimately run on the Hadoop file system, and a 
warehouse without a scheme
+    // is a plain path. None of them vend credentials.
+    if (!warehouse.contains("://") || "hdfs".equals(scheme) || 
"file".equals(scheme)) {
+      return;
+    }
+    if 
(StringUtils.isBlank(properties.get(CredentialConstants.CREDENTIAL_PROVIDERS))) 
{
+      return;
+    }
+    LOG.warn(
+        "Warehouse scheme '{}' has no Trino native file system, so the 
credentials vended by "
+            + "'{}' cannot be applied. Table access falls back to the Hadoop 
file system and may "
+            + "fail to authenticate. Schemes with a native file system: 
s3/s3a/s3n, gs, "
+            + "abfs/abfss/wasb/wasbs.",
+        scheme,
+        properties.get(CredentialConstants.CREDENTIAL_PROVIDERS));
+  }
+
   private void copyProperty(
       Map<String, String> source, String sourceKey, Map<String, String> 
target, String targetKey) {
     String value = source.get(sourceKey);
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergConnectorAdapter.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergConnectorAdapter.java
index 102c15d0f5..1480dc13d4 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergConnectorAdapter.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergConnectorAdapter.java
@@ -30,6 +30,8 @@ import org.apache.gravitino.trino.connector.GravitinoConfig;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorAdapter;
 import 
org.apache.gravitino.trino.connector.catalog.CatalogConnectorMetadataAdapter;
 import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Transforming Apache Iceberg connector configuration and components into 
Apache Gravitino
@@ -37,6 +39,8 @@ import 
org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
  */
 public class IcebergConnectorAdapter implements CatalogConnectorAdapter {
 
+  private static final Logger LOG = 
LoggerFactory.getLogger(IcebergConnectorAdapter.class);
+
   private static final String CONNECTOR_ICEBERG = "iceberg";
   private static final String REST_CATALOG_BACKEND = "rest";
 
@@ -61,11 +65,19 @@ public class IcebergConnectorAdapter implements 
CatalogConnectorAdapter {
       GravitinoCatalog catalog, Credential[] credentials) throws Exception {
     // The catalog backend describes how Gravitino stores the metadata; it 
does not decide how
     // Trino reaches the data. Unless the routing is disabled, every catalog 
is loaded through the
-    // Gravitino Iceberg REST server, which is the only path that supports 
credential vending.
+    // Gravitino Iceberg REST server, the only path that supports temporary 
credentials.
     // A catalog that already has a REST backend keeps pointing at its own 
configured endpoint.
     if (config.isIcebergRestEnabled()
-        && !REST_CATALOG_BACKEND.equals(
+        && !REST_CATALOG_BACKEND.equalsIgnoreCase(
             catalog.getProperty(IcebergConstants.CATALOG_BACKEND, null))) {
+      // `credentials` is intentionally unused here: with vended credentials 
enabled, Trino obtains
+      // a fresh one per table access over the REST protocol, rather than the 
catalog-level
+      // snapshot applyCredentials installs.
+      LOG.debug(
+          "Routing catalog '{}' through the Iceberg REST server; its {} 
catalog-level credential(s)"
+              + " are not applied because the REST protocol vends one per 
table access.",
+          catalog.getName(),
+          credentials.length);
       return catalogConverter.buildIcebergRestProperties(catalog, config);
     }
 
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java
index c31c594be0..357280cf90 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java
@@ -353,6 +353,18 @@ public class TestGravitinoConfig {
     
assertTrue(catalogConfig.contains("\"gravitino.iceberg.rest-catalog.security\"='OAUTH2'"));
   }
 
+  @Test
+  public void testToCatalogConfigPropagatesIcebergRestEnabled() {
+    // The switch rides the exact-key loop rather than the prefix filter; if 
it fails to propagate,
+    // the coordinator and the workers build different configs for the same 
catalog.
+    GravitinoConfig config =
+        new GravitinoConfig(
+            ImmutableMap.of(
+                "gravitino.metalake", "user_001", 
"gravitino.iceberg.rest-enabled", "false"));
+
+    
assertTrue(config.toCatalogConfig().contains("\"gravitino.iceberg.rest-enabled\"='false'"));
+  }
+
   private static boolean skipCatalog(String catalogName, GravitinoConfig 
config) {
     for (Pattern pattern : config.getSkipCatalogPatterns()) {
       if (pattern.matcher(catalogName).matches()) {
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java
index fc3571c8e8..f09e4d7469 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.Catalog;
 import org.apache.gravitino.catalog.property.PropertyConverter;
 import org.apache.gravitino.credential.Credential;
 import org.apache.gravitino.credential.JdbcCredential;
+import org.apache.gravitino.credential.S3SecretKeyCredential;
 import org.apache.gravitino.trino.connector.GravitinoConfig;
 import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
 import org.apache.gravitino.trino.connector.metadata.TestGravitinoCatalog;
@@ -451,11 +452,171 @@ public class TestIcebergCatalogPropertyConverter {
   private static Map<String, String> buildConnectorConfig(
       String catalogName, Map<String, String> properties, GravitinoConfig 
gravitinoConfig)
       throws Exception {
+    return buildConnectorConfig(catalogName, properties, gravitinoConfig, new 
Credential[0]);
+  }
+
+  @Test
+  public void testIcebergRestPathIgnoresCatalogLevelCredentials() throws 
Exception {
+    Map<String, String> properties =
+        ImmutableMap.<String, String>builder()
+            .put("catalog-backend", "jdbc")
+            .put("uri", "jdbc:postgresql://localhost:5432/iceberg")
+            .put("jdbc-driver", "org.postgresql.Driver")
+            .put("warehouse", "s3://bucket/warehouse/")
+            .build();
+    Credential[] credentials = {
+      new JdbcCredential("root", "ds123"), new 
S3SecretKeyCredential("AKIAEXAMPLE", "secret")
+    };
+
+    Map<String, String> config =
+        buildConnectorConfig(
+            "catalog1", properties, 
icebergRestEnabledConfig(ImmutableMap.of()), credentials);
+
+    // The REST protocol vends a fresh credential per table access, so the 
catalog-level snapshot
+    // must not be pinned into the connector config.
+    Assertions.assertNull(config.get("iceberg.jdbc-catalog.connection-user"));
+    
Assertions.assertNull(config.get("iceberg.jdbc-catalog.connection-password"));
+    Assertions.assertNull(config.get("hive.s3.aws-access-key"));
+    Assertions.assertNull(config.get("hive.s3.aws-secret-key"));
+    Assertions.assertEquals("true", 
config.get("iceberg.rest-catalog.vended-credentials-enabled"));
+
+    // The backend path still applies them.
+    Map<String, String> legacyConfig =
+        buildConnectorConfig("catalog1", properties, 
icebergRestDisabledConfig(), credentials);
+    Assertions.assertEquals("root", 
legacyConfig.get("iceberg.jdbc-catalog.connection-user"));
+    Assertions.assertEquals("AKIAEXAMPLE", 
legacyConfig.get("hive.s3.aws-access-key"));
+  }
+
+  @Test
+  public void testIcebergRestReservedPropertiesResistCatalogOverride() throws 
Exception {
+    Map<String, String> properties =
+        ImmutableMap.<String, String>builder()
+            .put("catalog-backend", "jdbc")
+            .put("warehouse", "s3://bucket/warehouse/")
+            .put("trino.bypass.iceberg.catalog.type", "jdbc")
+            .put("trino.bypass.iceberg.rest-catalog.uri", 
"http://elsewhere:9001/iceberg";)
+            .put("trino.bypass.iceberg.rest-catalog.prefix", "other_catalog")
+            .put("trino.bypass.iceberg.rest-catalog.warehouse", 
"other_catalog")
+            .build();
+
+    Map<String, String> config =
+        buildConnectorConfig("catalog1", properties, 
icebergRestEnabledConfig(ImmutableMap.of()));
+
+    // A catalog property must never redirect the connector at another 
endpoint or catalog.
+    Assertions.assertEquals("rest", config.get("iceberg.catalog.type"));
+    Assertions.assertEquals(
+        "http://localhost:9001/iceberg";, 
config.get("iceberg.rest-catalog.uri"));
+    Assertions.assertEquals("catalog1", 
config.get("iceberg.rest-catalog.prefix"));
+    Assertions.assertEquals("catalog1", 
config.get("iceberg.rest-catalog.warehouse"));
+  }
+
+  @Test
+  public void testConnectorRestCatalogConfigOverridesCatalogBypass() throws 
Exception {
+    Map<String, String> properties =
+        ImmutableMap.<String, String>builder()
+            .put("catalog-backend", "jdbc")
+            .put("warehouse", "s3://bucket/warehouse/")
+            .put("trino.bypass.iceberg.rest-catalog.security", "NONE")
+            .build();
+
+    Map<String, String> config =
+        buildConnectorConfig(
+            "catalog1",
+            properties,
+            icebergRestEnabledConfig(
+                ImmutableMap.of("gravitino.iceberg.rest-catalog.security", 
"OAUTH2")));
+
+    // Cluster-level operational settings outrank per-catalog properties.
+    Assertions.assertEquals("OAUTH2", 
config.get("iceberg.rest-catalog.security"));
+  }
+
+  @Test
+  public void testBuildConnectorPropertiesStorageDetectionEdgeCases() throws 
Exception {
+    // s3a/s3n aliases and an uppercase scheme all resolve to the native S3 
file system.
+    for (String warehouse : new String[] {"s3a://bucket/wh", 
"s3n://bucket/wh", "S3://bucket/wh"}) {
+      Map<String, String> config =
+          buildConnectorConfig(
+              "catalog1",
+              ImmutableMap.of("catalog-backend", "jdbc", "warehouse", 
warehouse),
+              icebergRestEnabledConfig(ImmutableMap.of()));
+      Assertions.assertEquals(
+          "true", config.get("fs.native-s3.enabled"), "warehouse: " + 
warehouse);
+    }
+
+    // A warehouse without a scheme is a plain path and stays on the Hadoop 
file system.
+    Map<String, String> schemeless =
+        buildConnectorConfig(
+            "catalog1",
+            ImmutableMap.of("catalog-backend", "jdbc", "warehouse", 
"gt_iceberg_rest"),
+            icebergRestEnabledConfig(ImmutableMap.of()));
+    Assertions.assertEquals("true", schemeless.get("fs.hadoop.enabled"));
+    Assertions.assertNull(schemeless.get("fs.native-s3.enabled"));
+
+    // No warehouse at all.
+    Map<String, String> noWarehouse =
+        buildConnectorConfig(
+            "catalog1",
+            ImmutableMap.of("catalog-backend", "jdbc"),
+            icebergRestEnabledConfig(ImmutableMap.of()));
+    Assertions.assertEquals("true", noWarehouse.get("fs.hadoop.enabled"));
+    Assertions.assertNull(noWarehouse.get("fs.native-s3.enabled"));
+    Assertions.assertNull(noWarehouse.get("fs.native-gcs.enabled"));
+    Assertions.assertNull(noWarehouse.get("fs.native-azure.enabled"));
+
+    // oss:// has no Trino native file system, so nothing is enabled beyond 
Hadoop.
+    Map<String, String> oss =
+        buildConnectorConfig(
+            "catalog1",
+            ImmutableMap.of(
+                "catalog-backend", "jdbc",
+                "warehouse", "oss://bucket/wh",
+                "credential-providers", "oss-token"),
+            icebergRestEnabledConfig(ImmutableMap.of()));
+    Assertions.assertEquals("true", oss.get("fs.hadoop.enabled"));
+    Assertions.assertNull(oss.get("fs.native-s3.enabled"));
+  }
+
+  @Test
+  public void testBuildConnectorPropertiesCopiesS3PathStyleAccess() throws 
Exception {
+    Map<String, String> config =
+        buildConnectorConfig(
+            "catalog1",
+            ImmutableMap.of(
+                "catalog-backend", "jdbc",
+                "warehouse", "s3://bucket/wh",
+                "s3-endpoint", "http://minio:9000";,
+                "s3-path-style-access", "true"),
+            icebergRestEnabledConfig(ImmutableMap.of()));
+
+    // S3-compatible stores such as MinIO need path-style addressing to 
resolve the bucket.
+    Assertions.assertEquals("true", config.get("s3.path-style-access"));
+    Assertions.assertEquals("http://minio:9000";, config.get("s3.endpoint"));
+  }
+
+  @Test
+  public void testRestBackendIsNotReRoutedRegardlessOfCase() throws Exception {
+    Map<String, String> config =
+        buildConnectorConfig(
+            "catalog1",
+            ImmutableMap.of("catalog-backend", "REST", "uri", 
"http://other-irc:9001/iceberg";),
+            icebergRestEnabledConfig(ImmutableMap.of()));
+
+    Assertions.assertEquals(
+        "http://other-irc:9001/iceberg";, 
config.get("iceberg.rest-catalog.uri"));
+    Assertions.assertNull(config.get("iceberg.rest-catalog.prefix"));
+  }
+
+  private static Map<String, String> buildConnectorConfig(
+      String catalogName,
+      Map<String, String> properties,
+      GravitinoConfig gravitinoConfig,
+      Credential[] credentials)
+      throws Exception {
     Catalog mockCatalog =
         TestGravitinoCatalog.mockCatalog(
             catalogName, "lakehouse-iceberg", "test catalog", 
Catalog.Type.RELATIONAL, properties);
     return new IcebergConnectorAdapter(gravitinoConfig)
-        .buildInternalConnectorConfig(new GravitinoCatalog("test", 
mockCatalog), new Credential[0]);
+        .buildInternalConnectorConfig(new GravitinoCatalog("test", 
mockCatalog), credentials);
   }
 
   private static GravitinoConfig icebergRestDisabledConfig() {

Reply via email to