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

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 8f0903de28 [Minor] Just a minor (#11229)
8f0903de28 is described below

commit 8f0903de2839ba2680ec1b7e66ef521d6beb37de
Author: roryqi <[email protected]>
AuthorDate: Wed May 27 19:14:42 2026 +0800

    [Minor] Just a minor (#11229)
    
    Just a minor.
    
    Co-authored-by: Claude Opus 4.7 <[email protected]>
---
 .../s3/credential/AwsIrsaCredentialGenerator.java  | 28 ++++++--
 .../gravitino/s3/credential/S3TokenGenerator.java  | 28 ++++++--
 .../s3/credential/TestS3PolicyPrefix.java          | 37 ++++++++---
 .../gravitino/credential/CredentialConstants.java  | 11 ++++
 .../credential/config/S3CredentialConfig.java      | 18 +++++
 .../catalog/fileset/FilesetCatalogImpl.java        | 16 +++++
 .../fileset/TestFilesetCatalogCredential.java      | 77 ++++++++++++++++++++++
 .../catalog/lakehouse/iceberg/IcebergCatalog.java  |  4 ++
 .../lakehouse/iceberg/TestIcebergCatalog.java      | 28 ++++++++
 9 files changed, 223 insertions(+), 24 deletions(-)

diff --git 
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
 
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
index 3e83c57e98..cd5ccad8c5 100644
--- 
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
+++ 
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
@@ -59,6 +59,7 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
   private int tokenExpireSecs;
   private String region;
   private String stsEndpoint;
+  private boolean listLocationPrefix;
 
   @Override
   public void initialize(Map<String, String> properties) {
@@ -70,6 +71,7 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
     this.tokenExpireSecs = s3CredentialConfig.tokenExpireInSecs();
     this.region = s3CredentialConfig.region();
     this.stsEndpoint = s3CredentialConfig.stsEndpoint();
+    this.listLocationPrefix = s3CredentialConfig.listLocationPrefix();
   }
 
   @Override
@@ -213,7 +215,9 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
                               .addAction("s3:ListBucket")
                               .addResource(key))
                   .addConditions(
-                      IamConditionOperator.STRING_LIKE, "s3:prefix", 
listPrefixes(rawPath));
+                      IamConditionOperator.STRING_LIKE,
+                      "s3:prefix",
+                      listPrefixes(rawPath, listLocationPrefix));
 
               bucketGetLocationStatementBuilder.computeIfAbsent(
                   bucketArn,
@@ -253,17 +257,27 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
   }
 
   /**
-   * Builds the {@code s3:prefix} condition values for a {@code ListBucket} 
statement. The prefixes
-   * are restricted to the location and its descendants by requiring a 
trailing slash, so a vended
-   * credential cannot enumerate keys in adjacent locations (e.g. {@code 
path/to/table_new}) that
-   * merely share the location's string prefix. The empty path (bucket root) 
is preserved so listing
-   * the whole bucket still works.
+   * Builds the {@code s3:prefix} condition values for a {@code ListBucket} 
statement. By default
+   * the prefixes are restricted to the location and its descendants by 
requiring a trailing slash,
+   * so a vended credential cannot enumerate keys in adjacent locations (e.g. 
{@code
+   * path/to/table_new}) that merely share the location's string prefix. The 
empty path (bucket
+   * root) is preserved so listing the whole bucket still works.
+   *
+   * <p>When {@code includeLocationPrefix} is {@code true}, the bare location 
prefix is also
+   * allowed. This is required for the Hadoop FileSystem API, whose {@code 
getFileStatus} issues a
+   * HEAD on the bare location key: that HEAD returns 404 (instead of 403) 
only if {@code
+   * ListBucket} permits the bare prefix. The trade-off is that the credential 
can then enumerate
+   * sibling key names sharing the prefix.
    *
    * @param rawPath the object key prefix of the location, without a leading 
slash
+   * @param includeLocationPrefix whether to also allow the bare location 
prefix
    * @return the allowed {@code s3:prefix} values
    */
-  static List<String> listPrefixes(String rawPath) {
+  static List<String> listPrefixes(String rawPath, boolean 
includeLocationPrefix) {
     String dirPrefix = rawPath.isEmpty() || rawPath.endsWith("/") ? rawPath : 
rawPath + "/";
+    if (includeLocationPrefix && !rawPath.equals(dirPrefix)) {
+      return Arrays.asList(rawPath, dirPrefix, addWildcardToPath(rawPath));
+    }
     return Arrays.asList(dirPrefix, addWildcardToPath(rawPath));
   }
 
diff --git 
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/S3TokenGenerator.java
 
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/S3TokenGenerator.java
index e0ed31f45e..7cb3bc14e8 100644
--- 
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/S3TokenGenerator.java
+++ 
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/S3TokenGenerator.java
@@ -56,6 +56,7 @@ public class S3TokenGenerator implements 
CredentialGenerator<S3TokenCredential>
   private String roleArn;
   private String externalID;
   private int tokenExpireSecs;
+  private boolean listLocationPrefix;
 
   @Override
   public void initialize(Map<String, String> properties) {
@@ -63,6 +64,7 @@ public class S3TokenGenerator implements 
CredentialGenerator<S3TokenCredential>
     this.roleArn = s3CredentialConfig.s3RoleArn();
     this.externalID = s3CredentialConfig.externalID();
     this.tokenExpireSecs = s3CredentialConfig.tokenExpireInSecs();
+    this.listLocationPrefix = s3CredentialConfig.listLocationPrefix();
     this.stsClient = createStsClient(s3CredentialConfig);
   }
 
@@ -148,7 +150,9 @@ public class S3TokenGenerator implements 
CredentialGenerator<S3TokenCredential>
                               .addAction("s3:ListBucket")
                               .addResource(key))
                   .addConditions(
-                      IamConditionOperator.STRING_LIKE, "s3:prefix", 
listPrefixes(rawPath));
+                      IamConditionOperator.STRING_LIKE,
+                      "s3:prefix",
+                      listPrefixes(rawPath, listLocationPrefix));
 
               bucketGetLocationStatementBuilder.computeIfAbsent(
                   bucketArn,
@@ -203,17 +207,27 @@ public class S3TokenGenerator implements 
CredentialGenerator<S3TokenCredential>
   }
 
   /**
-   * Builds the {@code s3:prefix} condition values for a {@code ListBucket} 
statement. The prefixes
-   * are restricted to the location and its descendants by requiring a 
trailing slash, so a vended
-   * credential cannot enumerate keys in adjacent locations (e.g. {@code 
path/to/table_new}) that
-   * merely share the location's string prefix. The empty path (bucket root) 
is preserved so listing
-   * the whole bucket still works.
+   * Builds the {@code s3:prefix} condition values for a {@code ListBucket} 
statement. By default
+   * the prefixes are restricted to the location and its descendants by 
requiring a trailing slash,
+   * so a vended credential cannot enumerate keys in adjacent locations (e.g. 
{@code
+   * path/to/table_new}) that merely share the location's string prefix. The 
empty path (bucket
+   * root) is preserved so listing the whole bucket still works.
+   *
+   * <p>When {@code includeLocationPrefix} is {@code true}, the bare location 
prefix is also
+   * allowed. This is required for the Hadoop FileSystem API, whose {@code 
getFileStatus} issues a
+   * HEAD on the bare location key: that HEAD returns 404 (instead of 403) 
only if {@code
+   * ListBucket} permits the bare prefix. The trade-off is that the credential 
can then enumerate
+   * sibling key names sharing the prefix.
    *
    * @param rawPath the object key prefix of the location, without a leading 
slash
+   * @param includeLocationPrefix whether to also allow the bare location 
prefix
    * @return the allowed {@code s3:prefix} values
    */
-  static List<String> listPrefixes(String rawPath) {
+  static List<String> listPrefixes(String rawPath, boolean 
includeLocationPrefix) {
     String dirPrefix = rawPath.isEmpty() || rawPath.endsWith("/") ? rawPath : 
rawPath + "/";
+    if (includeLocationPrefix && !rawPath.equals(dirPrefix)) {
+      return Arrays.asList(rawPath, dirPrefix, addWildcardToPath(rawPath));
+    }
     return Arrays.asList(dirPrefix, addWildcardToPath(rawPath));
   }
 
diff --git 
a/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestS3PolicyPrefix.java
 
b/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestS3PolicyPrefix.java
index d9fcc068e1..6e948d8560 100644
--- 
a/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestS3PolicyPrefix.java
+++ 
b/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestS3PolicyPrefix.java
@@ -24,30 +24,47 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 /**
- * Verifies that the {@code s3:prefix} condition for {@code ListBucket} 
statements is scoped to the
- * location and its descendants, so a vended credential cannot enumerate keys 
in adjacent locations
- * sharing the same string prefix (e.g. {@code path/to/table_new} for location 
{@code
- * path/to/table}).
+ * Verifies the {@code s3:prefix} condition for {@code ListBucket} statements. 
By default it is
+ * scoped to the location and its descendants, so a vended credential cannot 
enumerate keys in
+ * adjacent locations sharing the same string prefix (e.g. {@code 
path/to/table_new} for location
+ * {@code path/to/table}). When the location prefix is enabled (fileset 
catalogs), the bare location
+ * prefix is additionally allowed so a directory-root {@code getFileStatus} 
HEAD returns 404.
  */
 public class TestS3PolicyPrefix {
 
   @Test
   void testListPrefixesExcludeAdjacentLocations() {
-    // The bare path "path/to/table" must NOT be allowed, otherwise ListBucket 
with that prefix
-    // would enumerate keys under the sibling "path/to/table_new".
-    List<String> prefixes = S3TokenGenerator.listPrefixes("path/to/table");
+    // Default (secure): the bare path "path/to/table" must NOT be allowed, 
otherwise ListBucket
+    // with that prefix would enumerate keys under the sibling 
"path/to/table_new".
+    List<String> prefixes = S3TokenGenerator.listPrefixes("path/to/table", 
false);
     Assertions.assertEquals(2, prefixes.size());
     Assertions.assertTrue(prefixes.contains("path/to/table/"));
     Assertions.assertTrue(prefixes.contains("path/to/table/*"));
     Assertions.assertFalse(prefixes.contains("path/to/table"));
 
     // The IRSA generator shares the same scoping rule.
-    Assertions.assertEquals(prefixes, 
AwsIrsaCredentialGenerator.listPrefixes("path/to/table"));
+    Assertions.assertEquals(
+        prefixes, AwsIrsaCredentialGenerator.listPrefixes("path/to/table", 
false));
+  }
+
+  @Test
+  void testListPrefixesIncludeLocationPrefix() {
+    // With the location prefix enabled, the bare path is added so the 
directory-root HEAD returns
+    // 404 instead of 403, while the descendant prefixes remain.
+    List<String> prefixes = S3TokenGenerator.listPrefixes("path/to/table", 
true);
+    Assertions.assertEquals(3, prefixes.size());
+    Assertions.assertTrue(prefixes.contains("path/to/table"));
+    Assertions.assertTrue(prefixes.contains("path/to/table/"));
+    Assertions.assertTrue(prefixes.contains("path/to/table/*"));
+
+    Assertions.assertEquals(
+        prefixes, AwsIrsaCredentialGenerator.listPrefixes("path/to/table", 
true));
   }
 
   @Test
   void testListPrefixesWithTrailingSlash() {
-    List<String> prefixes = S3TokenGenerator.listPrefixes("path/to/table/");
+    // A trailing slash already denotes a directory; enabling the location 
prefix adds nothing new.
+    List<String> prefixes = S3TokenGenerator.listPrefixes("path/to/table/", 
true);
     Assertions.assertEquals(2, prefixes.size());
     Assertions.assertTrue(prefixes.contains("path/to/table/"));
     Assertions.assertTrue(prefixes.contains("path/to/table/*"));
@@ -57,7 +74,7 @@ public class TestS3PolicyPrefix {
   void testListPrefixesForBucketRoot() {
     // For the bucket root the empty prefix must be preserved so listing the 
whole bucket still
     // works; "/*" alone would not match the empty list prefix.
-    List<String> prefixes = S3TokenGenerator.listPrefixes("");
+    List<String> prefixes = S3TokenGenerator.listPrefixes("", true);
     Assertions.assertEquals(2, prefixes.size());
     Assertions.assertTrue(prefixes.contains(""));
     Assertions.assertTrue(prefixes.contains("/*"));
diff --git 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
index 220a128e53..ea10725b7b 100644
--- 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
+++ 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
@@ -29,6 +29,17 @@ public class CredentialConstants {
   public static final String CREDENTIAL_CACHE_EXPIRE_RATIO = 
"credential-cache-expire-ratio";
   public static final String CREDENTIAL_CACHE_MAX_SIZE = 
"credential-cache-max-size";
   public static final String S3_TOKEN_EXPIRE_IN_SECS = 
"s3-token-expire-in-secs";
+
+  /**
+   * Whether the vended {@code s3:ListBucket} statement also allows the bare 
location prefix. When
+   * {@code true}, a directory-root {@code getFileStatus} HEAD returns 404 
instead of 403, at the
+   * cost of allowing enumeration of sibling keys that share the location's 
string prefix. Defaults
+   * to {@code false} (secure); fileset catalogs enable it because access goes 
through the Hadoop
+   * FileSystem API.
+   */
+  public static final String S3_CREDENTIAL_LIST_LOCATION_PREFIX =
+      "s3-credential-list-location-prefix";
+
   public static final String OSS_TOKEN_EXPIRE_IN_SECS = 
"oss-token-expire-in-secs";
   public static final String ADLS_TOKEN_EXPIRE_IN_SECS = 
"adls-token-expire-in-secs";
 
diff --git 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/config/S3CredentialConfig.java
 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/config/S3CredentialConfig.java
index 1bdf7b2fad..61547e6746 100644
--- 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/config/S3CredentialConfig.java
+++ 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/config/S3CredentialConfig.java
@@ -83,6 +83,20 @@ public class S3CredentialConfig extends Config {
           .stringConf()
           .create();
 
+  /**
+   * Internal flag, determined by the catalog type rather than configured by 
users. When set, the
+   * vended {@code s3:ListBucket} statement also allows the bare location 
prefix, so a
+   * directory-root {@code getFileStatus} returns 404 instead of 403, at the 
cost of allowing
+   * enumeration of sibling keys sharing the location prefix.
+   */
+  public static final ConfigEntry<Boolean> S3_LIST_LOCATION_PREFIX =
+      new ConfigBuilder(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX)
+          .doc("Whether the vended s3:ListBucket statement also allows the 
bare location prefix")
+          .version(ConfigConstants.VERSION_1_3_0)
+          .internal()
+          .booleanConf()
+          .createWithDefault(false);
+
   public S3CredentialConfig(Map<String, String> properties) {
     super(false);
     loadFromMap(properties, k -> true);
@@ -118,4 +132,8 @@ public class S3CredentialConfig extends Config {
   public String stsEndpoint() {
     return this.get(S3_STS_ENDPOINT);
   }
+
+  public boolean listLocationPrefix() {
+    return this.get(S3_LIST_LOCATION_PREFIX);
+  }
 }
diff --git 
a/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogImpl.java
 
b/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogImpl.java
index 76984f92ca..4402e45127 100644
--- 
a/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogImpl.java
+++ 
b/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogImpl.java
@@ -18,11 +18,13 @@
  */
 package org.apache.gravitino.catalog.fileset;
 
+import java.util.HashMap;
 import java.util.Map;
 import org.apache.gravitino.connector.BaseCatalog;
 import org.apache.gravitino.connector.CatalogOperations;
 import org.apache.gravitino.connector.PropertiesMetadata;
 import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.credential.CredentialConstants;
 
 /**
  * Hadoop catalog is a fileset catalog that can manage filesets on the Hadoop 
Compatible File
@@ -68,4 +70,18 @@ public class FilesetCatalogImpl extends 
BaseCatalog<FilesetCatalogImpl> {
   public PropertiesMetadata filesetPropertiesMetadata() throws 
UnsupportedOperationException {
     return FILESET_PROPERTIES_META;
   }
+
+  /**
+   * Fileset access goes through the Hadoop FileSystem API (e.g. GVFS over 
S3A), whose {@code
+   * getFileStatus} issues a HEAD on the directory-root key. That HEAD returns 
404 (instead of 403)
+   * only when the vended {@code s3:ListBucket} statement allows the bare 
location prefix, so this
+   * catalog enables that internal flag. It is determined by the catalog type 
and is not meant to be
+   * configured by users.
+   */
+  @Override
+  public Map<String, String> propertiesWithCredentialProviders() {
+    Map<String, String> properties = new 
HashMap<>(super.propertiesWithCredentialProviders());
+    properties.put(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX, 
"true");
+    return properties;
+  }
 }
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
new file mode 100644
index 0000000000..11d8978947
--- /dev/null
+++ 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogCredential.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.catalog.fileset;
+
+import com.google.common.collect.Maps;
+import java.time.Instant;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.CatalogEntity;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that the fileset catalog enables the internal S3 location-prefix 
flag so a
+ * directory-root {@code getFileStatus} HEAD returns 404 instead of 403, 
regardless of any
+ * user-provided value.
+ */
+public class TestFilesetCatalogCredential {
+
+  private static FilesetCatalogImpl newCatalog(Map<String, String> properties) 
{
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+    CatalogEntity entity =
+        CatalogEntity.builder()
+            .withId(1L)
+            .withName("catalog")
+            .withNamespace(Namespace.of("metalake"))
+            .withType(Catalog.Type.FILESET)
+            .withProvider("fileset")
+            .withProperties(properties)
+            .withAuditInfo(auditInfo)
+            .build();
+    return new 
FilesetCatalogImpl().withCatalogConf(properties).withCatalogEntity(entity);
+  }
+
+  @Test
+  void testLocationPrefixEnabledByDefault() {
+    FilesetCatalogImpl catalog = newCatalog(Maps.newHashMap());
+    Assertions.assertEquals(
+        "true",
+        catalog
+            .propertiesWithCredentialProviders()
+            .get(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX));
+  }
+
+  @Test
+  void testLocationPrefixNotUserConfigurable() {
+    // A user attempt to disable it is overridden by the catalog type.
+    Map<String, String> properties = Maps.newHashMap();
+    properties.put(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX, 
"false");
+    FilesetCatalogImpl catalog = newCatalog(properties);
+    Assertions.assertEquals(
+        "true",
+        catalog
+            .propertiesWithCredentialProviders()
+            .get(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX));
+  }
+}
diff --git 
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
 
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
index 653831464d..6b6b375596 100644
--- 
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
+++ 
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
@@ -94,6 +94,10 @@ public class IcebergCatalog extends 
BaseCatalog<IcebergCatalog> {
   @Evolving
   public Map<String, String> propertiesWithCredentialProviders() {
     Map<String, String> properties = 
Maps.newHashMap(super.propertiesWithCredentialProviders());
+    // Iceberg is security-first: the vended s3:ListBucket statement keeps the 
bare location prefix
+    // disabled so a credential cannot enumerate sibling keys sharing the 
location prefix. This is
+    // determined by the catalog type and is not meant to be configured by 
users.
+    properties.put(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX, 
"false");
     return applyDefaultCredentialProviders(properties);
   }
 
diff --git 
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
 
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
index ca8fb57c34..9249088593 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
@@ -366,6 +366,34 @@ public class TestIcebergCatalog {
     Assertions.assertEquals("custom-provider", credentialProviders);
   }
 
+  @Test
+  void testS3ListLocationPrefixForcedDisabled() {
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+    // Iceberg is security-first: a user attempt to enable the location prefix 
is overridden.
+    Map<String, String> props = Maps.newHashMap();
+    props.put(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX, "true");
+
+    CatalogEntity entity =
+        CatalogEntity.builder()
+            .withId(5L)
+            .withName("secure-catalog")
+            .withNamespace(Namespace.of("metalake"))
+            .withType(IcebergCatalog.Type.RELATIONAL)
+            .withProvider("iceberg")
+            .withAuditInfo(auditInfo)
+            .withProperties(props)
+            .build();
+
+    IcebergCatalog catalog = new 
IcebergCatalog().withCatalogConf(props).withCatalogEntity(entity);
+    Assertions.assertEquals(
+        "false",
+        catalog
+            .propertiesWithCredentialProviders()
+            .get(CredentialConstants.S3_CREDENTIAL_LIST_LOCATION_PREFIX));
+  }
+
   @Test
   void testJdbcBackendWithOSSCredentialProviders() {
     AuditInfo auditInfo =

Reply via email to