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

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


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new a68cbe65b4 [Cherry-pick to branch-1.3] [#14450] fix(iceberg-rest): 
follow the access-delegation spec and vend only where a provider can (#14452)
a68cbe65b4 is described below

commit a68cbe65b4a0b4729a3f3919414a1d253ced44d5
Author: Mark Hoerth <[email protected]>
AuthorDate: Sun Nov 1 19:41:51 2026 -0800

    [Cherry-pick to branch-1.3] [#14450] fix(iceberg-rest): follow the 
access-delegation spec and vend only where a provider can (#14452)
    
    Backport to branch-1.3 of the fix for #14450 (same change as the PR to
    main; branch-1.3 does not vend credentials in scan planning, so that
    part does not apply).
    
    ### What changes were proposed in this pull request?
    
    - `IcebergTableOperations.isCredentialVending`: split the header on
    commas; vended credentials are used when `vended-credentials` is listed.
    Other mechanisms, such as `remote-signing` or an unknown value, are
    ignored instead of refused.
    - New `CatalogCredentialManager.supportsPath`. `CatalogWrapperForREST`
    vends credentials (load, create, register, scan planning) only when a
    configured provider supports the location's scheme, and otherwise serves
    the request without credentials. The check uses each provider's own
    `supportsScheme`, so it holds for every storage provider (S3, GCS, ADLS,
    OSS) and for providers added outside this repository. The explicit
    `/credentials` endpoint is unchanged.
    - `S3TokenGenerator`: with neither static key set, call STS with the AWS
    default credential chain (instance profile, EKS service account,
    environment). With both keys set, behavior is unchanged. Setting only
    one key is refused with a message naming the missing one.
    
    ### Why are the changes needed?
    
    Clients that announce more than one mechanism, or only remote-signing,
    could not load or create tables. Catalogs without a credential provider
    were unusable from pyiceberg, and creates were reported as failed after
    committing. Servers running with instance roles could not use s3-token
    vending.
    
    Fixes #14450
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Requests with other or multiple delegation mechanisms are served.
    Requests for vended credentials on a location no provider covers are
    served without credentials instead of failing. `s3-token` works without
    static keys when the server has AWS credentials from its environment.
    
    ### How was this patch tested?
    
    - `TestIcebergTableOperations`: create and load with `remote-signing`,
    both list orders and an unknown mechanism (200); `isCredentialVending`
    parsing; scan planning with other mechanisms (200). The old tests
    asserting 406/400 for these headers are replaced.
    - `TestIcebergNamespaceOperations`: register with other mechanisms is
    handled as without the header.
    -
    
`TestCatalogWrapperForREST#testVendsOnlyWhereACredentialProviderCoversTheLocation`.
    -
    
`TestCatalogCredentialManager#testSupportsPathOnlyWhenAProviderCoversTheScheme`.
    - `TestS3TokenGeneratorCredentials`: static keys, default chain without
    keys, one key refused.
    
    `./gradlew :core:test :bundles:aws:test
    :iceberg:iceberg-rest-server:test -PskipITs`
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    https://claude.ai/code/session_01Eu6fb9pLfmrSJzGQDVLi1g
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../gravitino/s3/credential/S3TokenGenerator.java  |  37 ++++++--
 .../TestS3TokenGeneratorCredentials.java           |  64 +++++++++++++
 .../credential/CatalogCredentialManager.java       |  11 +++
 .../credential/TestCatalogCredentialManager.java   |  16 ++++
 .../iceberg/service/CatalogWrapperForREST.java     |  22 ++++-
 .../service/rest/IcebergTableOperations.java       |  22 ++---
 .../iceberg/service/TestCatalogWrapperForREST.java |  54 +++++++++++
 .../rest/TestIcebergNamespaceOperations.java       |  38 ++++----
 .../service/rest/TestIcebergTableOperations.java   | 102 ++++++++++++---------
 9 files changed, 283 insertions(+), 83 deletions(-)

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 d234915e2c..ec95898b15 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
@@ -19,6 +19,8 @@
 
 package org.apache.gravitino.s3.credential;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import java.io.IOException;
 import java.net.URI;
 import java.util.Arrays;
@@ -35,8 +37,10 @@ import 
org.apache.gravitino.credential.PathBasedCredentialContext;
 import org.apache.gravitino.credential.RoleSessionNameUtils;
 import org.apache.gravitino.credential.S3TokenCredential;
 import org.apache.gravitino.credential.config.S3CredentialConfig;
+import org.apache.gravitino.storage.S3Properties;
 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
 import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
 import software.amazon.awssdk.policybuilder.iam.IamConditionOperator;
 import software.amazon.awssdk.policybuilder.iam.IamEffect;
@@ -66,7 +70,7 @@ public class S3TokenGenerator implements 
CredentialGenerator<S3TokenCredential>
     this.externalID = s3CredentialConfig.externalID();
     this.tokenExpireSecs = s3CredentialConfig.tokenExpireInSecs();
     this.listLocationPrefix = s3CredentialConfig.listLocationPrefix();
-    this.stsClient = createStsClient(s3CredentialConfig);
+    this.stsClient = createStsClient(s3CredentialConfig, 
stsCallerCredentials(properties));
   }
 
   @Override
@@ -88,11 +92,32 @@ public class S3TokenGenerator implements 
CredentialGenerator<S3TokenCredential>
         s3Token.expiration().toEpochMilli());
   }
 
-  private StsClient createStsClient(S3CredentialConfig s3CredentialConfig) {
-    AwsCredentialsProvider credentialsProvider =
-        StaticCredentialsProvider.create(
-            AwsBasicCredentials.create(
-                s3CredentialConfig.accessKeyID(), 
s3CredentialConfig.secretAccessKey()));
+  /**
+   * Returns the credentials Gravitino uses to call STS. With a static access 
key pair configured,
+   * those keys are used. With neither key configured, the AWS default 
credential chain is used, so
+   * a server running with an instance profile, an EKS service account or 
environment credentials
+   * can assume the vending role without storing keys in the catalog.
+   */
+  @VisibleForTesting
+  static AwsCredentialsProvider stsCallerCredentials(Map<String, String> 
properties) {
+    String accessKeyId = 
properties.get(S3Properties.GRAVITINO_S3_ACCESS_KEY_ID);
+    String secretAccessKey = 
properties.get(S3Properties.GRAVITINO_S3_SECRET_ACCESS_KEY);
+    boolean hasAccessKeyId = StringUtils.isNotBlank(accessKeyId);
+    boolean hasSecretAccessKey = StringUtils.isNotBlank(secretAccessKey);
+    if (!hasAccessKeyId && !hasSecretAccessKey) {
+      return DefaultCredentialsProvider.builder().build();
+    }
+    Preconditions.checkArgument(
+        hasAccessKeyId && hasSecretAccessKey,
+        "Set both %s and %s, or neither to use the AWS default credential 
chain",
+        S3Properties.GRAVITINO_S3_ACCESS_KEY_ID,
+        S3Properties.GRAVITINO_S3_SECRET_ACCESS_KEY);
+    return StaticCredentialsProvider.create(
+        AwsBasicCredentials.create(accessKeyId, secretAccessKey));
+  }
+
+  private StsClient createStsClient(
+      S3CredentialConfig s3CredentialConfig, AwsCredentialsProvider 
credentialsProvider) {
     StsClientBuilder builder = 
StsClient.builder().credentialsProvider(credentialsProvider);
 
     if (StringUtils.isNotBlank(s3CredentialConfig.region())) {
diff --git 
a/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestS3TokenGeneratorCredentials.java
 
b/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestS3TokenGeneratorCredentials.java
new file mode 100644
index 0000000000..6169cc129e
--- /dev/null
+++ 
b/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestS3TokenGeneratorCredentials.java
@@ -0,0 +1,64 @@
+/*
+ *  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.s3.credential;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.gravitino.storage.S3Properties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+
+/** Tests which credentials the S3 token generator uses to call STS. */
+public class TestS3TokenGeneratorCredentials {
+
+  @Test
+  public void testStaticKeysAreUsedWhenConfigured() {
+    AwsCredentialsProvider provider =
+        S3TokenGenerator.stsCallerCredentials(
+            ImmutableMap.of(
+                S3Properties.GRAVITINO_S3_ACCESS_KEY_ID, "AKIAEXAMPLE",
+                S3Properties.GRAVITINO_S3_SECRET_ACCESS_KEY, "secret"));
+    Assertions.assertInstanceOf(StaticCredentialsProvider.class, provider);
+    Assertions.assertEquals("AKIAEXAMPLE", 
provider.resolveCredentials().accessKeyId());
+  }
+
+  @Test
+  public void testDefaultChainIsUsedWithoutKeys() {
+    // A server running with an instance profile or service account needs no 
keys in the catalog.
+    Assertions.assertInstanceOf(
+        DefaultCredentialsProvider.class,
+        S3TokenGenerator.stsCallerCredentials(
+            ImmutableMap.of(S3Properties.GRAVITINO_S3_ROLE_ARN, 
"arn:aws:iam::123:role/vend")));
+  }
+
+  @Test
+  public void testOnlyOneKeyIsRefused() {
+    IllegalArgumentException e =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                S3TokenGenerator.stsCallerCredentials(
+                    ImmutableMap.of(S3Properties.GRAVITINO_S3_ACCESS_KEY_ID, 
"AKIAEXAMPLE")));
+    Assertions.assertTrue(
+        e.getMessage().contains(S3Properties.GRAVITINO_S3_SECRET_ACCESS_KEY), 
e.getMessage());
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
 
b/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
index 9fb5a9eecf..fcfb9a4e43 100644
--- 
a/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
+++ 
b/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
@@ -67,6 +67,17 @@ public class CatalogCredentialManager implements Closeable {
     return Optional.ofNullable(credentialProviders.get(credentialType));
   }
 
+  /**
+   * Returns whether a credential provider of this catalog supports the scheme 
of the path.
+   *
+   * @param path a storage location
+   * @return true if at least one configured provider supports the path's 
scheme
+   */
+  public boolean supportsPath(String path) {
+    String scheme = extractScheme(path);
+    return credentialProviders.values().stream().anyMatch(p -> 
p.supportsScheme(scheme));
+  }
+
   public Optional<Credential> getCredentialByPath(String path, 
CredentialContext context) {
     String scheme = extractScheme(path);
     String matchedCredentialType = null;
diff --git 
a/core/src/test/java/org/apache/gravitino/credential/TestCatalogCredentialManager.java
 
b/core/src/test/java/org/apache/gravitino/credential/TestCatalogCredentialManager.java
index 88f4bc6404..e00324a60f 100644
--- 
a/core/src/test/java/org/apache/gravitino/credential/TestCatalogCredentialManager.java
+++ 
b/core/src/test/java/org/apache/gravitino/credential/TestCatalogCredentialManager.java
@@ -55,4 +55,20 @@ public class TestCatalogCredentialManager {
           
credentialManager.getCredentialProvider(JdbcCredential.JDBC_CREDENTIAL_TYPE).isPresent());
     }
   }
+
+  @Test
+  void testSupportsPathOnlyWhenAProviderCoversTheScheme() {
+    try (CatalogCredentialManager noProviders =
+        new CatalogCredentialManager("no-providers", ImmutableMap.of())) {
+      
Assertions.assertFalse(noProviders.supportsPath("s3://bucket/warehouse/table"));
+    }
+    try (CatalogCredentialManager storageProvider =
+        new CatalogCredentialManager(
+            "storage",
+            ImmutableMap.of(
+                CredentialConstants.CREDENTIAL_PROVIDERS,
+                DummyCredentialProvider.CREDENTIAL_TYPE))) {
+      
Assertions.assertTrue(storageProvider.supportsPath("s3://bucket/warehouse/table"));
+    }
+  }
 }
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/CatalogWrapperForREST.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/CatalogWrapperForREST.java
index a8aff3076c..b5b877564e 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/CatalogWrapperForREST.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/CatalogWrapperForREST.java
@@ -341,7 +341,27 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
     }
 
     validateCredentialLocation(loadTableResponse.tableMetadata().location());
-    return !isLocalOrHdfsTable(loadTableResponse.tableMetadata());
+    return canVendCredentials(loadTableResponse.tableMetadata().location());
+  }
+
+  /**
+   * Whether this catalog can vend credentials for a location. The 
access-delegation header only
+   * tells the server what the client supports, and the server may supply none 
of the mechanisms, so
+   * a location no configured provider covers is served without credentials 
instead of failing the
+   * request.
+   */
+  private boolean canVendCredentials(String location) {
+    if (isLocalOrHdfsLocation(location)) {
+      return false;
+    }
+    if (!catalogCredentialManager.supportsPath(location)) {
+      LOG.debug(
+          "No credential provider of catalog {} covers {}, serving without 
vended credentials",
+          catalogCredentialManager.catalogName(),
+          location);
+      return false;
+    }
+    return true;
   }
 
   private boolean isLocalOrHdfsTable(TableMetadata tableMetadata) {
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
index d468207259..4408ae9b20 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
@@ -637,25 +637,21 @@ public class IcebergTableOperations {
    * requesting credential vending. Package-private and static so that {@link
    * IcebergNamespaceOperations#registerTable} can reuse the same parsing 
logic from the same
    * package.
+   *
+   * <p>The header is a comma-separated list of the mechanisms the client 
supports, and the server
+   * may serve the request with any or none of them, so mechanisms this server 
does not offer (such
+   * as remote-signing) are ignored.
    */
   static boolean isCredentialVending(String accessDelegation) {
     if (StringUtils.isBlank(accessDelegation)) {
       return false;
     }
-    if ("vended-credentials".equalsIgnoreCase(accessDelegation)) {
-      return true;
-    }
-    if ("remote-signing".equalsIgnoreCase(accessDelegation)) {
-      throw new UnsupportedOperationException(
-          "Gravitino IcebergRESTServer doesn't support remote signing");
-    } else {
-      throw new IllegalArgumentException(
-          X_ICEBERG_ACCESS_DELEGATION
-              + ": "
-              + accessDelegation
-              + " is illegal, Iceberg REST spec supports: 
[vended-credentials,remote-signing], "
-              + "Gravitino Iceberg REST server supports: vended-credentials");
+    for (String mechanism : accessDelegation.split(",")) {
+      if ("vended-credentials".equalsIgnoreCase(mechanism.trim())) {
+        return true;
+      }
     }
+    return false;
   }
 
   private NameIdentifier[] toNameIdentifiers(
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestCatalogWrapperForREST.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestCatalogWrapperForREST.java
index 612a618524..9d94ef4942 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestCatalogWrapperForREST.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestCatalogWrapperForREST.java
@@ -379,6 +379,60 @@ public class TestCatalogWrapperForREST {
     }
   }
 
+  @Test
+  void testVendsOnlyWhereACredentialProviderCoversTheLocation() {
+    Namespace namespace = Namespace.of("db");
+    Schema schema = new Schema(Types.NestedField.required(1, "id", 
Types.IntegerType.get()));
+    TableIdentifier table = TableIdentifier.of(namespace, "tbl");
+
+    // No credential provider: a request asking for vended credentials is 
served without them.
+    CatalogWrapperForREST noProvider =
+        new CatalogWrapperForREST(
+            "no-provider",
+            new IcebergConfig(
+                ImmutableMap.of(
+                    IcebergConstants.CATALOG_BACKEND, "memory",
+                    IcebergConstants.WAREHOUSE, "/tmp/warehouse")));
+    ((SupportsNamespaces) noProvider.getCatalog()).createNamespace(namespace);
+    noProvider
+        .getCatalog()
+        .createTable(
+            table,
+            schema,
+            PartitionSpec.unpartitioned(),
+            "s3://bucket/wh/db/tbl",
+            Collections.emptyMap());
+    LoadTableResponse loaded = noProvider.loadTable(table, true, 
CredentialPrivilege.READ);
+    Assertions.assertFalse(noProvider.shouldGenerateCredential(loaded, true));
+    Assertions.assertTrue(loaded.credentials().isEmpty());
+    Assertions.assertTrue(
+        loaded.config().keySet().stream().noneMatch(key -> 
key.startsWith("s3.")),
+        loaded.config().toString());
+
+    // With a provider covering the scheme, credentials are vended as before.
+    CatalogWrapperForREST withProvider =
+        new CatalogWrapperForREST(
+            "with-provider",
+            new IcebergConfig(
+                ImmutableMap.of(
+                    IcebergConstants.CATALOG_BACKEND, "memory",
+                    IcebergConstants.WAREHOUSE, "/tmp/warehouse",
+                    CredentialConstants.CREDENTIAL_PROVIDERS,
+                        DummyCredentialProvider.DUMMY_CREDENTIAL_TYPE)));
+    ((SupportsNamespaces) 
withProvider.getCatalog()).createNamespace(namespace);
+    withProvider
+        .getCatalog()
+        .createTable(
+            table,
+            schema,
+            PartitionSpec.unpartitioned(),
+            "s3://bucket/wh/db/tbl",
+            Collections.emptyMap());
+    Assertions.assertTrue(
+        withProvider.shouldGenerateCredential(
+            withProvider.loadTable(table, false, CredentialPrivilege.READ), 
true));
+  }
+
   @Test
   void testLocalTableCredentials() {
     IcebergConfig config =
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
index fb491d260a..06f7e9c792 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
@@ -415,36 +415,30 @@ public class TestIcebergNamespaceOperations extends 
IcebergNamespaceTestBase {
   }
 
   @Test
-  void testRegisterTableRemoteSigningNotSupported() {
+  void testRegisterTableWithOtherAccessDelegationIsNotRefused() {
     RegisterTableRequest request =
         ImmutableRegisterTableRequest.builder()
             .name("remote_signing_test")
             .metadataLocation("mock")
             .build();
-    Response response =
+    // A delegation mechanism the server does not offer does not change the 
outcome: the request
+    // is handled exactly as without the header.
+    int withoutHeader =
         getNamespaceClientBuilder(
                 Optional.of(Namespace.of("register_remote_ns")),
                 Optional.of("register"),
                 Optional.empty())
-            .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
"remote-signing")
-            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
-    Assertions.assertEquals(406, response.getStatus());
-  }
-
-  @Test
-  void testRegisterTableInvalidAccessDelegation() {
-    RegisterTableRequest request =
-        ImmutableRegisterTableRequest.builder()
-            .name("invalid_delegation_test")
-            .metadataLocation("mock")
-            .build();
-    Response response =
-        getNamespaceClientBuilder(
-                Optional.of(Namespace.of("register_invalid_ns")),
-                Optional.of("register"),
-                Optional.empty())
-            .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
"invalid-value")
-            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
-    Assertions.assertEquals(400, response.getStatus());
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))
+            .getStatus();
+    for (String header : new String[] {"remote-signing", "invalid-value"}) {
+      Response response =
+          getNamespaceClientBuilder(
+                  Optional.of(Namespace.of("register_remote_ns")),
+                  Optional.of("register"),
+                  Optional.empty())
+              .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
header)
+              .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+      Assertions.assertEquals(withoutHeader, response.getStatus(), header);
+    }
   }
 }
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
index db8a253f4b..5226ee10ef 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
@@ -925,27 +925,36 @@ public class TestIcebergTableOperations extends 
IcebergNamespaceTestBase {
 
   @ParameterizedTest
   
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
-  void testRemoteSigningNotSupported(Namespace namespace) {
+  void testAccessDelegationListIsServed(Namespace namespace) {
     verifyCreateNamespaceSucc(namespace);
 
-    // Attempt to create table with "remote-signing" access delegation
-    // This should fail with UnsupportedOperationException -> 406 Not 
Acceptable
-    CreateTableRequest createTableRequest =
-        CreateTableRequest.builder()
-            .withName("test_remote_signing")
-            .withSchema(tableSchema)
-            .build();
-
-    Response response =
-        getTableClientBuilder(namespace, Optional.empty())
-            .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
"remote-signing")
-            .post(Entity.entity(createTableRequest, 
MediaType.APPLICATION_JSON_TYPE));
-
-    Assertions.assertEquals(406, response.getStatus());
-    String errorBody = response.readEntity(String.class);
-    Assertions.assertTrue(
-        errorBody.contains("remote signing") || 
errorBody.contains("remote-signing"),
-        "Error message should mention remote signing: " + errorBody);
+    // The header lists the mechanisms the client supports; the server serves 
the request with the
+    // ones it offers, or none. remote-signing alone, a comma-separated list 
in either order, and an
+    // unknown mechanism are all served.
+    String[] headers = {
+      "remote-signing",
+      "vended-credentials,remote-signing",
+      "remote-signing, vended-credentials",
+      "invalid-value"
+    };
+    for (int i = 0; i < headers.length; i++) {
+      CreateTableRequest createTableRequest =
+          CreateTableRequest.builder()
+              .withName("delegation_list_" + i)
+              .withSchema(tableSchema)
+              .build();
+      Response create =
+          getTableClientBuilder(namespace, Optional.empty())
+              .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
headers[i])
+              .post(Entity.entity(createTableRequest, 
MediaType.APPLICATION_JSON_TYPE));
+      Assertions.assertEquals(Status.OK.getStatusCode(), create.getStatus(), 
headers[i]);
+
+      Response load =
+          getTableClientBuilder(namespace, Optional.of("delegation_list_" + i))
+              .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
headers[i])
+              .get();
+      Assertions.assertEquals(Status.OK.getStatusCode(), load.getStatus(), 
headers[i]);
+    }
   }
 
   @ParameterizedTest
@@ -982,29 +991,18 @@ public class TestIcebergTableOperations extends 
IcebergNamespaceTestBase {
     verifyDropNamespaceSucc(namespace);
   }
 
-  @ParameterizedTest
-  
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
-  void testInvalidAccessDelegation(Namespace namespace) {
-    verifyCreateNamespaceSucc(namespace);
-
-    // Attempt to create table with invalid access delegation value
-    // This should fail with IllegalArgumentException -> 400 Bad Request
-    CreateTableRequest createTableRequest =
-        CreateTableRequest.builder()
-            .withName("test_invalid_delegation")
-            .withSchema(tableSchema)
-            .build();
-
-    Response response =
-        getTableClientBuilder(namespace, Optional.empty())
-            .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
"invalid-value")
-            .post(Entity.entity(createTableRequest, 
MediaType.APPLICATION_JSON_TYPE));
-
-    Assertions.assertEquals(400, response.getStatus());
-    String errorBody = response.readEntity(String.class);
+  @Test
+  void testIsCredentialVendingParsesTheMechanismList() {
+    Assertions.assertFalse(IcebergTableOperations.isCredentialVending(null));
+    Assertions.assertFalse(IcebergTableOperations.isCredentialVending(" "));
+    
Assertions.assertFalse(IcebergTableOperations.isCredentialVending("remote-signing"));
+    
Assertions.assertFalse(IcebergTableOperations.isCredentialVending("invalid-value"));
+    
Assertions.assertTrue(IcebergTableOperations.isCredentialVending("vended-credentials"));
+    
Assertions.assertTrue(IcebergTableOperations.isCredentialVending("VENDED-CREDENTIALS"));
+    Assertions.assertTrue(
+        
IcebergTableOperations.isCredentialVending("vended-credentials,remote-signing"));
     Assertions.assertTrue(
-        errorBody.contains("vended-credentials") && 
errorBody.contains("illegal"),
-        "Error message should mention valid values: " + errorBody);
+        IcebergTableOperations.isCredentialVending("remote-signing , 
vended-credentials"));
   }
 
   @ParameterizedTest
@@ -1578,4 +1576,26 @@ public class TestIcebergTableOperations extends 
IcebergNamespaceTestBase {
             NameIdentifier.of("metalake", "catalog", "schema"));
     Assertions.assertEquals(-1, event.resultCount());
   }
+
+  @ParameterizedTest
+  
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
+  void testPlanTableScanWithOtherAccessDelegationIsServed(Namespace namespace) 
{
+    verifyCreateNamespaceSucc(namespace);
+    verifyCreateTableSucc(namespace, "scan_remote_signing");
+    PlanTableScanRequest request = PlanTableScanRequest.builder().build();
+    for (String header : new String[] {"remote-signing", "invalid-value"}) {
+      Response response =
+          getTableClientBuilder(namespace, 
Optional.of("scan_remote_signing/plan"))
+              .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
header)
+              .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+      Assertions.assertEquals(Status.OK.getStatusCode(), response.getStatus(), 
header);
+    }
+  }
+
+  private Response doPlanTableScanWithCredentialVending(
+      Namespace ns, String tableName, PlanTableScanRequest request) {
+    return getTableClientBuilder(ns, Optional.of(tableName + "/plan"))
+        .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, 
"vended-credentials")
+        .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+  }
 }

Reply via email to