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

mchades 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 8ea1e3a90f [Cherry-pick to branch-1.3] [#10990] feat(iceberg-rest): 
Vended credential refresh for S3, GCS, OSS, and ADLS (#11294) (#11449)
8ea1e3a90f is described below

commit 8ea1e3a90fb583828689f95d8a0a3e01c4a17a7a
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Jun 5 15:59:11 2026 +0800

    [Cherry-pick to branch-1.3] [#10990] feat(iceberg-rest): Vended credential 
refresh for S3, GCS, OSS, and ADLS (#11294) (#11449)
    
    **Cherry-pick Information:**
    - Original commit: 91ed7a2e750537276d8ac2f579b81dec162ff307
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: MaSai <[email protected]>
---
 .../gravitino/credential/ADLSTokenCredential.java  |   2 +-
 .../gravitino/credential/AwsIrsaCredential.java    |   9 +-
 .../gravitino/credential/GCSTokenCredential.java   |   2 +-
 .../gravitino/credential/JdbcCredential.java       |   2 +-
 .../credential/OSSSecretKeyCredential.java         |   2 +-
 .../gravitino/credential/OSSTokenCredential.java   |   2 +-
 .../credential/S3SecretKeyCredential.java          |   2 +-
 .../gravitino/credential/S3TokenCredential.java    |   2 +-
 .../s3/credential/AwsIrsaCredentialGenerator.java  |  10 +-
 .../credential/CredentialPropertyUtils.java        | 134 +++++--
 .../credential/TestCredentialFactory.java          |  24 ++
 .../credential/TestCredentialPropertiesUtils.java  |  75 +++-
 .../credential/CatalogCredentialManager.java       |   9 +
 .../iceberg/service/CatalogWrapperForREST.java     | 159 ++++++--
 .../iceberg/service/IcebergRESTUtils.java          | 124 ++++++
 .../iceberg/service/TestCatalogWrapperForREST.java | 416 ++++++++++++++++++++-
 .../iceberg/service/TestIcebergRESTUtils.java      | 231 ++++++++++++
 17 files changed, 1121 insertions(+), 84 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/credential/ADLSTokenCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/ADLSTokenCredential.java
index 6f1c463033..d18aa7f115 100644
--- a/api/src/main/java/org/apache/gravitino/credential/ADLSTokenCredential.java
+++ b/api/src/main/java/org/apache/gravitino/credential/ADLSTokenCredential.java
@@ -112,6 +112,6 @@ public class ADLSTokenCredential implements Credential {
     Preconditions.checkArgument(
         StringUtils.isNotBlank(sasToken), "ADLS SAS token should not be 
empty.");
     Preconditions.checkArgument(
-        expireTimeInMS > 0, "The expire time of ADLSTokenCredential should 
great than 0");
+        expireTimeInMS > 0, "The expiration time of ADLSTokenCredential should 
be greater than 0");
   }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/credential/AwsIrsaCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/AwsIrsaCredential.java
index 0c1888cffc..4fecf9ada1 100644
--- a/api/src/main/java/org/apache/gravitino/credential/AwsIrsaCredential.java
+++ b/api/src/main/java/org/apache/gravitino/credential/AwsIrsaCredential.java
@@ -50,7 +50,7 @@ public class AwsIrsaCredential implements Credential {
    */
   public AwsIrsaCredential(
       String accessKeyId, String secretAccessKey, String sessionToken, long 
expireTimeInMs) {
-    validate(accessKeyId, secretAccessKey, sessionToken);
+    validate(accessKeyId, secretAccessKey, sessionToken, expireTimeInMs);
     this.accessKeyId = accessKeyId;
     this.secretAccessKey = secretAccessKey;
     this.sessionToken = sessionToken;
@@ -84,7 +84,7 @@ public class AwsIrsaCredential implements Credential {
     String accessKeyId = credentialInfo.get(ACCESS_KEY_ID);
     String secretAccessKey = credentialInfo.get(SECRET_ACCESS_KEY);
     String sessionToken = credentialInfo.get(SESSION_TOKEN);
-    validate(accessKeyId, secretAccessKey, sessionToken);
+    validate(accessKeyId, secretAccessKey, sessionToken, expireTimeInMs);
     this.accessKeyId = accessKeyId;
     this.secretAccessKey = secretAccessKey;
     this.sessionToken = sessionToken;
@@ -118,12 +118,15 @@ public class AwsIrsaCredential implements Credential {
     return sessionToken;
   }
 
-  private void validate(String accessKeyId, String secretAccessKey, String 
sessionToken) {
+  private void validate(
+      String accessKeyId, String secretAccessKey, String sessionToken, long 
expireTimeInMs) {
     Preconditions.checkArgument(
         StringUtils.isNotBlank(accessKeyId), "Access key Id should not be 
empty");
     Preconditions.checkArgument(
         StringUtils.isNotBlank(secretAccessKey), "Secret access key should not 
be empty");
     Preconditions.checkArgument(
         StringUtils.isNotBlank(sessionToken), "Session token should not be 
empty");
+    Preconditions.checkArgument(
+        expireTimeInMs > 0, "The expiration time of AwsIrsaCredential should 
be greater than 0");
   }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/credential/GCSTokenCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/GCSTokenCredential.java
index d55799c52a..a3230645fe 100644
--- a/api/src/main/java/org/apache/gravitino/credential/GCSTokenCredential.java
+++ b/api/src/main/java/org/apache/gravitino/credential/GCSTokenCredential.java
@@ -88,6 +88,6 @@ public class GCSTokenCredential implements Credential {
     Preconditions.checkArgument(
         StringUtils.isNotBlank(token), "GCS session token should not be 
empty");
     Preconditions.checkArgument(
-        expireTimeInMs > 0, "The expire time of GcsTokenCredential should 
great than 0");
+        expireTimeInMs > 0, "The expiration time of GcsTokenCredential should 
be greater than 0");
   }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/credential/JdbcCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/JdbcCredential.java
index eeaccc2620..af19110d7e 100644
--- a/api/src/main/java/org/apache/gravitino/credential/JdbcCredential.java
+++ b/api/src/main/java/org/apache/gravitino/credential/JdbcCredential.java
@@ -111,6 +111,6 @@ public class JdbcCredential implements Credential {
         StringUtils.isNotBlank(jdbcPassword), "JDBC password should not be 
empty");
     // JDBC credentials are static (no server-issued expiry). expireTimeInMs 
must always be 0.
     Preconditions.checkArgument(
-        expireTimeInMs == 0, "The expire time of JdbcCredential should be 0");
+        expireTimeInMs == 0, "The expiration time of JdbcCredential should be 
0");
   }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/credential/OSSSecretKeyCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/OSSSecretKeyCredential.java
index fd98a3bfd3..34baa51707 100644
--- 
a/api/src/main/java/org/apache/gravitino/credential/OSSSecretKeyCredential.java
+++ 
b/api/src/main/java/org/apache/gravitino/credential/OSSSecretKeyCredential.java
@@ -115,6 +115,6 @@ public class OSSSecretKeyCredential implements Credential {
     Preconditions.checkArgument(
         StringUtils.isNotBlank(secretAccessKey), "OSS secret access key should 
not empty");
     Preconditions.checkArgument(
-        expireTimeInMs == 0, "The expire time of OSSSecretKeyCredential is not 
0");
+        expireTimeInMs == 0, "The expiration time of OSSSecretKeyCredential is 
not 0");
   }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/credential/OSSTokenCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/OSSTokenCredential.java
index 70e8839489..4f1a871501 100644
--- a/api/src/main/java/org/apache/gravitino/credential/OSSTokenCredential.java
+++ b/api/src/main/java/org/apache/gravitino/credential/OSSTokenCredential.java
@@ -133,6 +133,6 @@ public class OSSTokenCredential implements Credential {
     Preconditions.checkArgument(
         StringUtils.isNotBlank(sessionToken), "OSS session token should not be 
empty");
     Preconditions.checkArgument(
-        expireTimeInMs > 0, "The expire time of OSSTokenCredential should 
great than 0");
+        expireTimeInMs > 0, "The expiration time of OSSTokenCredential should 
be greater than 0");
   }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/credential/S3SecretKeyCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/S3SecretKeyCredential.java
index d9b2b60925..db6de05854 100644
--- 
a/api/src/main/java/org/apache/gravitino/credential/S3SecretKeyCredential.java
+++ 
b/api/src/main/java/org/apache/gravitino/credential/S3SecretKeyCredential.java
@@ -107,6 +107,6 @@ public class S3SecretKeyCredential implements Credential {
     Preconditions.checkArgument(
         StringUtils.isNotBlank(secretAccessKey), "S3 secret access key should 
not empty");
     Preconditions.checkArgument(
-        expireTimeInMs == 0, "The expire time of S3SecretKeyCredential is not 
0");
+        expireTimeInMs == 0, "The expiration time of S3SecretKeyCredential is 
not 0");
   }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/credential/S3TokenCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/S3TokenCredential.java
index 89d7de760f..ce69ab3b45 100644
--- a/api/src/main/java/org/apache/gravitino/credential/S3TokenCredential.java
+++ b/api/src/main/java/org/apache/gravitino/credential/S3TokenCredential.java
@@ -131,6 +131,6 @@ public class S3TokenCredential implements Credential {
     Preconditions.checkArgument(
         StringUtils.isNotBlank(sessionToken), "S3 session token should not be 
empty");
     Preconditions.checkArgument(
-        expireTimeInMs > 0, "The expire time of S3TokenCredential should great 
than 0");
+        expireTimeInMs > 0, "The expiration time of S3TokenCredential should 
be greater than 0");
   }
 }
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 cd5ccad8c5..3c1641506e 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
@@ -81,10 +81,12 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
       AwsCredentials creds = baseCredentialsProvider.resolveCredentials();
       if (creds instanceof AwsSessionCredentials) {
         AwsSessionCredentials sessionCreds = (AwsSessionCredentials) creds;
-        long expiration =
-            sessionCreds.expirationTime().isPresent()
-                ? sessionCreds.expirationTime().get().toEpochMilli()
-                : 0L;
+        if (!sessionCreds.expirationTime().isPresent()) {
+          throw new IllegalStateException(
+              "AWS IRSA session credentials must include an expiration time 
for vended credential"
+                  + " refresh");
+        }
+        long expiration = sessionCreds.expirationTime().get().toEpochMilli();
         return new AwsIrsaCredential(
             sessionCreds.accessKeyId(),
             sessionCreds.secretAccessKey(),
diff --git 
a/common/src/main/java/org/apache/gravitino/credential/CredentialPropertyUtils.java
 
b/common/src/main/java/org/apache/gravitino/credential/CredentialPropertyUtils.java
index b66b650178..d9543b93d3 100644
--- 
a/common/src/main/java/org/apache/gravitino/credential/CredentialPropertyUtils.java
+++ 
b/common/src/main/java/org/apache/gravitino/credential/CredentialPropertyUtils.java
@@ -23,6 +23,7 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
@@ -35,12 +36,22 @@ public class CredentialPropertyUtils {
   @VisibleForTesting static final String ICEBERG_S3_ACCESS_KEY_ID = 
"s3.access-key-id";
   @VisibleForTesting static final String ICEBERG_S3_SECRET_ACCESS_KEY = 
"s3.secret-access-key";
   @VisibleForTesting static final String ICEBERG_S3_TOKEN = "s3.session-token";
-  @VisibleForTesting static final String ICEBERG_GCS_TOKEN = 
"gcs.oauth2.token";
+
+  @VisibleForTesting
+  static final String ICEBERG_S3_TOKEN_EXPIRES_AT_MS = 
"s3.session-token-expires-at-ms";
+
+  @VisibleForTesting
+  static final String ICEBERG_CLIENT_REFRESH_CREDENTIALS_ENDPOINT =
+      "client.refresh-credentials-endpoint";
 
   @VisibleForTesting static final String ICEBERG_OSS_ACCESS_KEY_ID = 
"client.access-key-id";
   @VisibleForTesting static final String ICEBERG_OSS_ACCESS_KEY_SECRET = 
"client.access-key-secret";
   @VisibleForTesting static final String ICEBERG_OSS_SECURITY_TOKEN = 
"client.security-token";
 
+  @VisibleForTesting
+  static final String ICEBERG_OSS_SECURITY_TOKEN_EXPIRES_AT_MS =
+      "client.security-token-expires-at-ms";
+
   @VisibleForTesting static final String ICEBERG_ADLS_TOKEN = "adls.sas-token";
 
   @VisibleForTesting
@@ -49,7 +60,21 @@ public class CredentialPropertyUtils {
   @VisibleForTesting
   static final String ICEBERG_ADLS_ACCOUNT_KEY = 
"adls.auth.shared-key.account.key";
 
-  private static final String GCS_OAUTH_2_TOKEN_EXPIRES_AT = 
"gcs.oauth2.token-expires-at";
+  @VisibleForTesting
+  static final String ICEBERG_ADLS_SAS_TOKEN_EXPIRES_AT_MS_PREFIX = 
"adls.sas-token-expires-at-ms.";
+
+  @VisibleForTesting
+  static final String ICEBERG_ADLS_REFRESH_CREDENTIALS_ENDPOINT =
+      "adls.refresh-credentials-endpoint";
+
+  @VisibleForTesting static final String ICEBERG_GCS_TOKEN = 
"gcs.oauth2.token";
+
+  @VisibleForTesting
+  static final String ICEBERG_GCS_TOKEN_EXPIRES_AT = 
"gcs.oauth2.token-expires-at";
+
+  @VisibleForTesting
+  static final String ICEBERG_GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT =
+      "gcs.oauth2.refresh-credentials-endpoint";
 
   private static Map<String, String> icebergCredentialPropertyMap =
       ImmutableMap.<String, String>builder()
@@ -82,36 +107,40 @@ public class CredentialPropertyUtils {
    * @return a map of Iceberg properties derived from the credential
    */
   public static Map<String, String> toIcebergProperties(Credential credential) 
{
-    if (credential instanceof S3TokenCredential
-        || credential instanceof S3SecretKeyCredential
-        || credential instanceof OSSTokenCredential
-        || credential instanceof OSSSecretKeyCredential
-        || credential instanceof AzureAccountKeyCredential
-        || credential instanceof AwsIrsaCredential) {
-      return transformProperties(credential.credentialInfo(), 
icebergCredentialPropertyMap);
-    }
-
-    if (credential instanceof GCSTokenCredential) {
-      Map<String, String> icebergGCSCredentialProperties =
-          transformProperties(credential.credentialInfo(), 
icebergCredentialPropertyMap);
-      icebergGCSCredentialProperties.put(
-          GCS_OAUTH_2_TOKEN_EXPIRES_AT, 
String.valueOf(credential.expireTimeInMs()));
-      return icebergGCSCredentialProperties;
-    }
-
     if (credential instanceof ADLSTokenCredential) {
       ADLSTokenCredential adlsCredential = (ADLSTokenCredential) credential;
-      String sasTokenKey =
-          String.format(
-              "%s.%s.%s",
-              ICEBERG_ADLS_TOKEN, adlsCredential.accountName(), 
ADLSTokenCredential.ADLS_DOMAIN);
+      String adlsHost = adlsCredential.accountName() + "." + 
ADLSTokenCredential.ADLS_DOMAIN;
 
       Map<String, String> icebergADLSCredentialProperties = new HashMap<>();
-      icebergADLSCredentialProperties.put(sasTokenKey, 
adlsCredential.sasToken());
+      icebergADLSCredentialProperties.put(
+          ICEBERG_ADLS_TOKEN + "." + adlsHost, adlsCredential.sasToken());
+      icebergADLSCredentialProperties.put(
+          ICEBERG_ADLS_SAS_TOKEN_EXPIRES_AT_MS_PREFIX + adlsHost,
+          String.valueOf(adlsCredential.expireTimeInMs()));
       return icebergADLSCredentialProperties;
     }
 
-    return credential.toProperties();
+    Map<String, String> icebergProperties =
+        transformProperties(credential.credentialInfo(), 
icebergCredentialPropertyMap);
+    if (credential instanceof S3TokenCredential || credential instanceof 
AwsIrsaCredential) {
+      icebergProperties.put(
+          ICEBERG_S3_TOKEN_EXPIRES_AT_MS, 
String.valueOf(credential.expireTimeInMs()));
+      return icebergProperties;
+    } else if (credential instanceof OSSTokenCredential) {
+      icebergProperties.put(
+          ICEBERG_OSS_SECURITY_TOKEN_EXPIRES_AT_MS, 
String.valueOf(credential.expireTimeInMs()));
+      return icebergProperties;
+    } else if (credential instanceof GCSTokenCredential) {
+      icebergProperties.put(
+          ICEBERG_GCS_TOKEN_EXPIRES_AT, 
String.valueOf(credential.expireTimeInMs()));
+      return icebergProperties;
+    } else if (credential instanceof S3SecretKeyCredential
+        || credential instanceof OSSSecretKeyCredential
+        || credential instanceof AzureAccountKeyCredential) {
+      return icebergProperties;
+    } else {
+      return credential.toProperties();
+    }
   }
 
   /**
@@ -126,20 +155,69 @@ public class CredentialPropertyUtils {
    */
   public static Map<String, String> filterCredentialProperties(Map<String, 
String> properties) {
     Set<String> credentialPropertyKeys = 
Sets.newHashSet(icebergCredentialPropertyMap.values());
-    credentialPropertyKeys.add(GCS_OAUTH_2_TOKEN_EXPIRES_AT);
+    credentialPropertyKeys.add(ICEBERG_S3_TOKEN_EXPIRES_AT_MS);
+    credentialPropertyKeys.add(ICEBERG_OSS_SECURITY_TOKEN_EXPIRES_AT_MS);
+    credentialPropertyKeys.add(ICEBERG_GCS_TOKEN_EXPIRES_AT);
     Map<String, String> filteredProperties = Maps.newHashMap(properties);
     filteredProperties
         .entrySet()
         .removeIf(
             entry ->
                 !credentialPropertyKeys.contains(entry.getKey())
-                    && !entry.getKey().startsWith(ICEBERG_ADLS_TOKEN));
+                    && !entry.getKey().startsWith(ICEBERG_ADLS_TOKEN)
+                    && 
!entry.getKey().startsWith(ICEBERG_ADLS_SAS_TOKEN_EXPIRES_AT_MS_PREFIX));
     return filteredProperties;
   }
 
+  /**
+   * Builds refresh credential endpoint properties for Iceberg credential 
properties.
+   *
+   * @param encodedCatalogName Iceberg REST encoded catalog name
+   * @param encodedNamespace Iceberg REST encoded namespace
+   * @param encodedTableName Iceberg REST encoded table name
+   * @param credentialProperties Iceberg credential properties used to 
determine refresh keys
+   * @return refresh endpoint properties keyed by Iceberg client config names
+   */
+  public static Map<String, String> buildRefreshProps(
+      String encodedCatalogName,
+      String encodedNamespace,
+      String encodedTableName,
+      Map<String, String> credentialProperties) {
+    if (credentialProperties == null || credentialProperties.isEmpty()) {
+      return Collections.emptyMap();
+    }
+
+    String credentialsRefreshPath =
+        String.format(
+            "v1/%s/namespaces/%s/tables/%s/credentials",
+            encodedCatalogName, encodedNamespace, encodedTableName);
+    Map<String, String> refreshEndpointProperties = Maps.newHashMap();
+    if (credentialProperties.containsKey(ICEBERG_S3_TOKEN)) {
+      refreshEndpointProperties.put(
+          ICEBERG_CLIENT_REFRESH_CREDENTIALS_ENDPOINT, credentialsRefreshPath);
+    }
+    if (credentialProperties.containsKey(ICEBERG_GCS_TOKEN)) {
+      refreshEndpointProperties.put(
+          ICEBERG_GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT, 
credentialsRefreshPath);
+    }
+    if (credentialProperties.keySet().stream()
+        .anyMatch(key -> key.startsWith(ICEBERG_ADLS_TOKEN + "."))) {
+      refreshEndpointProperties.put(
+          ICEBERG_ADLS_REFRESH_CREDENTIALS_ENDPOINT, credentialsRefreshPath);
+    }
+    return ImmutableMap.copyOf(refreshEndpointProperties);
+  }
+
+  /**
+   * Transforms Gravitino credential keys to Iceberg property keys.
+   *
+   * @param originProperties the source credential info map
+   * @param transformMap mapping from Gravitino keys to Iceberg keys
+   * @return a new mutable map of transformed Iceberg properties
+   */
   private static Map<String, String> transformProperties(
       Map<String, String> originProperties, Map<String, String> transformMap) {
-    HashMap<String, String> properties = new HashMap();
+    Map<String, String> properties = new HashMap<>();
     originProperties.forEach(
         (k, v) -> {
           if (transformMap.containsKey(k)) {
diff --git 
a/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
 
b/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
index 443399daf6..259949ae2b 100644
--- 
a/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
+++ 
b/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
@@ -26,6 +26,30 @@ import org.junit.jupiter.api.Test;
 
 public class TestCredentialFactory {
 
+  @Test
+  void testAwsIrsaCredential() {
+    Map<String, String> awsIrsaCredentialInfo =
+        ImmutableMap.of(
+            AwsIrsaCredential.ACCESS_KEY_ID,
+            "accessKeyId",
+            AwsIrsaCredential.SECRET_ACCESS_KEY,
+            "secretAccessKey",
+            AwsIrsaCredential.SESSION_TOKEN,
+            "token");
+    long expireTime = 1000;
+    Credential awsIrsaCredential =
+        CredentialFactory.create(
+            AwsIrsaCredential.AWS_IRSA_CREDENTIAL_TYPE, awsIrsaCredentialInfo, 
expireTime);
+    Assertions.assertEquals(
+        AwsIrsaCredential.AWS_IRSA_CREDENTIAL_TYPE, 
awsIrsaCredential.credentialType());
+    Assertions.assertInstanceOf(AwsIrsaCredential.class, awsIrsaCredential);
+    AwsIrsaCredential awsIrsaCredential1 = (AwsIrsaCredential) 
awsIrsaCredential;
+    Assertions.assertEquals("accessKeyId", awsIrsaCredential1.accessKeyId());
+    Assertions.assertEquals("secretAccessKey", 
awsIrsaCredential1.secretAccessKey());
+    Assertions.assertEquals("token", awsIrsaCredential1.sessionToken());
+    Assertions.assertEquals(expireTime, awsIrsaCredential1.expireTimeInMs());
+  }
+
   @Test
   void testS3TokenCredential() {
     Map<String, String> s3TokenCredentialInfo =
diff --git 
a/common/src/test/java/org/apache/gravitino/credential/TestCredentialPropertiesUtils.java
 
b/common/src/test/java/org/apache/gravitino/credential/TestCredentialPropertiesUtils.java
index cb5eaabe76..0ee13adac5 100644
--- 
a/common/src/test/java/org/apache/gravitino/credential/TestCredentialPropertiesUtils.java
+++ 
b/common/src/test/java/org/apache/gravitino/credential/TestCredentialPropertiesUtils.java
@@ -38,7 +38,9 @@ public class TestCredentialPropertiesUtils {
             CredentialPropertyUtils.ICEBERG_S3_SECRET_ACCESS_KEY,
             "secret",
             CredentialPropertyUtils.ICEBERG_S3_TOKEN,
-            "token");
+            "token",
+            CredentialPropertyUtils.ICEBERG_S3_TOKEN_EXPIRES_AT_MS,
+            "100");
     Assertions.assertEquals(expectedProperties, icebergProperties);
 
     S3SecretKeyCredential secretKeyCredential = new 
S3SecretKeyCredential("key", "secret");
@@ -65,7 +67,9 @@ public class TestCredentialPropertiesUtils {
             CredentialPropertyUtils.ICEBERG_OSS_ACCESS_KEY_SECRET,
             "secret",
             CredentialPropertyUtils.ICEBERG_OSS_SECURITY_TOKEN,
-            "security-token");
+            "security-token",
+            CredentialPropertyUtils.ICEBERG_OSS_SECURITY_TOKEN_EXPIRES_AT_MS,
+            "100");
     Assertions.assertEquals(expectedProperties, icebergProperties);
   }
 
@@ -80,14 +84,67 @@ public class TestCredentialPropertiesUtils {
     Map<String, String> icebergProperties =
         CredentialPropertyUtils.toIcebergProperties(adlsTokenCredential);
 
-    String sasTokenKey =
-        String.format(
-            "%s.%s.%s",
-            CredentialPropertyUtils.ICEBERG_ADLS_TOKEN,
-            storageAccountName,
-            ADLSTokenCredential.ADLS_DOMAIN);
+    String adlsHost = storageAccountName + "." + 
ADLSTokenCredential.ADLS_DOMAIN;
+    String sasTokenKey = CredentialPropertyUtils.ICEBERG_ADLS_TOKEN + "." + 
adlsHost;
 
-    Map<String, String> expectedProperties = ImmutableMap.of(sasTokenKey, 
sasToken);
+    Map<String, String> expectedProperties =
+        ImmutableMap.of(
+            sasTokenKey,
+            sasToken,
+            
CredentialPropertyUtils.ICEBERG_ADLS_SAS_TOKEN_EXPIRES_AT_MS_PREFIX + adlsHost,
+            String.valueOf(expireTimeInMS));
     Assertions.assertEquals(expectedProperties, icebergProperties);
   }
+
+  @Test
+  void testBuildRefreshEndpointsForS3() {
+    Map<String, String> credentialProperties =
+        ImmutableMap.of(
+            CredentialPropertyUtils.ICEBERG_S3_TOKEN, "token",
+            CredentialPropertyUtils.ICEBERG_S3_TOKEN_EXPIRES_AT_MS, "123");
+
+    Map<String, String> refreshEndpointProperties =
+        CredentialPropertyUtils.buildRefreshProps("irc1", "db", "tbl", 
credentialProperties);
+
+    Assertions.assertEquals(
+        "v1/irc1/namespaces/db/tables/tbl/credentials",
+        refreshEndpointProperties.get(
+            
CredentialPropertyUtils.ICEBERG_CLIENT_REFRESH_CREDENTIALS_ENDPOINT));
+  }
+
+  @Test
+  void testBuildRefreshEndpointsForGCS() {
+    Map<String, String> credentialProperties =
+        ImmutableMap.of(
+            CredentialPropertyUtils.ICEBERG_GCS_TOKEN, "token",
+            CredentialPropertyUtils.ICEBERG_GCS_TOKEN_EXPIRES_AT, "123");
+
+    Map<String, String> refreshEndpointProperties =
+        CredentialPropertyUtils.buildRefreshProps("irc1", "db", "tbl", 
credentialProperties);
+
+    Assertions.assertEquals(
+        "v1/irc1/namespaces/db/tables/tbl/credentials",
+        refreshEndpointProperties.get(
+            
CredentialPropertyUtils.ICEBERG_GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT));
+  }
+
+  @Test
+  void testBuildRefreshEndpointsForADLS() {
+    String adlsHost = "storage.dfs.core.windows.net";
+    String sasTokenKey = CredentialPropertyUtils.ICEBERG_ADLS_TOKEN + "." + 
adlsHost;
+    Map<String, String> credentialProperties =
+        ImmutableMap.of(
+            sasTokenKey,
+            "sas-token",
+            
CredentialPropertyUtils.ICEBERG_ADLS_SAS_TOKEN_EXPIRES_AT_MS_PREFIX + adlsHost,
+            "123");
+
+    Map<String, String> refreshEndpointProperties =
+        CredentialPropertyUtils.buildRefreshProps("irc1", "db", "tbl", 
credentialProperties);
+
+    Assertions.assertEquals(
+        "v1/irc1/namespaces/db/tables/tbl/credentials",
+        refreshEndpointProperties.get(
+            
CredentialPropertyUtils.ICEBERG_ADLS_REFRESH_CREDENTIALS_ENDPOINT));
+  }
 }
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 88892391c4..9fb5a9eecf 100644
--- 
a/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
+++ 
b/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
@@ -49,6 +49,15 @@ public class CatalogCredentialManager implements Closeable {
     credentialCache.initialize(catalogProperties);
   }
 
+  /**
+   * Returns the catalog name this manager was created for.
+   *
+   * @return catalog name
+   */
+  public String catalogName() {
+    return catalogName;
+  }
+
   public Optional<Credential> getCredential(String credentialType, 
CredentialContext context) {
     CredentialCacheKey credentialCacheKey = new 
CredentialCacheKey(credentialType, context);
     return credentialCache.getCredential(credentialCacheKey, cacheKey -> 
doGetCredential(cacheKey));
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 39fc375d7b..7e1aac0c0d 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
@@ -57,6 +57,7 @@ import org.apache.gravitino.utils.PrincipalUtils;
 import org.apache.iceberg.BaseMetadataTable;
 import org.apache.iceberg.BaseTable;
 import org.apache.iceberg.BaseTransaction;
+import org.apache.iceberg.CatalogProperties;
 import org.apache.iceberg.FileScanTask;
 import org.apache.iceberg.IncrementalAppendScan;
 import org.apache.iceberg.MetadataUpdate;
@@ -81,8 +82,16 @@ import org.apache.iceberg.inmemory.InMemoryFileIO;
 import org.apache.iceberg.io.CloseableIterable;
 import org.apache.iceberg.io.FileIO;
 import org.apache.iceberg.rest.CatalogHandlers;
+import org.apache.iceberg.rest.ErrorHandlers;
+import org.apache.iceberg.rest.HTTPClient;
 import org.apache.iceberg.rest.PlanStatus;
 import org.apache.iceberg.rest.RESTCatalog;
+import org.apache.iceberg.rest.RESTClient;
+import org.apache.iceberg.rest.RESTUtil;
+import org.apache.iceberg.rest.ResourcePaths;
+import org.apache.iceberg.rest.auth.AuthManager;
+import org.apache.iceberg.rest.auth.AuthManagers;
+import org.apache.iceberg.rest.auth.AuthSession;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
 import org.apache.iceberg.rest.requests.PlanTableScanRequest;
 import org.apache.iceberg.rest.requests.RegisterTableRequest;
@@ -192,38 +201,92 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
   /**
    * Get table credentials.
    *
-   * @param identifier The table identifier for which to load credentials
-   * @return A {@link 
org.apache.iceberg.rest.responses.LoadCredentialsResponse} object containing
-   *     the credentials.
+   * @param identifier table identifier
+   * @param privilege used for local credential vending; ignored for REST 
catalog backends
+   * @return table credentials response
    */
   public LoadCredentialsResponse getTableCredentials(
       TableIdentifier identifier, CredentialPrivilege privilege) {
+    if (isRESTCatalog()) {
+      return getRESTTableCredentials((RESTCatalog) getCatalog(), identifier);
+    } else {
+      return getLocalTableCredentials(identifier, privilege);
+    }
+  }
+
+  private LoadCredentialsResponse getLocalTableCredentials(
+      TableIdentifier identifier, CredentialPrivilege privilege) {
     try {
       LoadTableResponse loadTableResponse = super.loadTable(identifier);
-      Credential credential = getCredential(loadTableResponse, privilege);
-      org.apache.iceberg.rest.credentials.Credential icebergCredential =
-          new org.apache.iceberg.rest.credentials.Credential() {
-            @Override
-            public String prefix() {
-              return "";
-            }
-
-            @Override
-            public Map<String, String> config() {
-              // Convert Gravitino credentials to the Iceberg REST credential 
payload format.
-              return CredentialPropertyUtils.toIcebergProperties(credential);
-            }
-
-            @Override
-            public void validate() {}
-          };
-      return 
ImmutableLoadCredentialsResponse.builder().addCredentials(icebergCredential).build();
+      Credential credential = getCredential(loadTableResponse.tableMetadata(), 
privilege);
+      return ImmutableLoadCredentialsResponse.builder()
+          .addCredentials(
+              IcebergRESTUtils.toRESTCredential(
+                  catalogCredentialManager.catalogName(),
+                  identifier,
+                  credential,
+                  loadTableResponse.tableMetadata()))
+          .build();
     } catch (ServiceUnavailableException e) {
       LOG.warn("Service unavailable when loading table credentials for table: 
{}", identifier, e);
       return ImmutableLoadCredentialsResponse.builder().build();
     }
   }
 
+  private static LoadCredentialsResponse getRESTTableCredentials(
+      RESTCatalog restCatalog, TableIdentifier identifier) {
+    Map<String, String> properties = Maps.newHashMap(restCatalog.properties());
+    String credentialsPath =
+        ResourcePaths.forCatalogProperties(properties).table(identifier) + 
"/credentials";
+
+    AuthManager authManager = null;
+    RESTClient client = null;
+    AuthSession authSession = null;
+    try {
+      authManager = AuthManagers.loadAuthManager(restCatalog.name(), 
properties);
+      client =
+          HTTPClient.builder(properties)
+              .uri(properties.get(CatalogProperties.URI))
+              .withHeaders(RESTUtil.configHeaders(properties))
+              .build();
+      authSession = authManager.catalogSession(client, properties);
+      return client
+          .withAuthSession(authSession)
+          .get(
+              credentialsPath,
+              LoadCredentialsResponse.class,
+              Collections.emptyMap(),
+              ErrorHandlers.tableErrorHandler());
+    } finally {
+      if (authSession != null) {
+        try {
+          authSession.close();
+        } catch (Exception e) {
+          LOG.warn(
+              "Failed to close auth session when loading credentials for 
table: {}", identifier, e);
+        }
+      }
+
+      if (client != null) {
+        try {
+          client.close();
+        } catch (Exception e) {
+          LOG.warn(
+              "Failed to close REST client when loading credentials for table: 
{}", identifier, e);
+        }
+      }
+
+      if (authManager != null) {
+        try {
+          authManager.close();
+        } catch (Exception e) {
+          LOG.warn(
+              "Failed to close auth manager when loading credentials for 
table: {}", identifier, e);
+        }
+      }
+    }
+  }
+
   @Override
   public void close() throws Exception {
     try {
@@ -318,7 +381,7 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
       TableIdentifier tableIdentifier,
       LoadTableResponse loadTableResponse,
       CredentialPrivilege privilege) {
-    final Credential credential = getCredential(loadTableResponse, privilege);
+    final Credential credential = 
getCredential(loadTableResponse.tableMetadata(), privilege);
 
     LOG.info(
         "Generate credential: {} for Iceberg table: {}",
@@ -327,7 +390,13 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
 
     // Merge temporary credential fields as Iceberg client config entries in 
the load-table
     // response.
-    Map<String, String> credentialConfig = 
CredentialPropertyUtils.toIcebergProperties(credential);
+    Map<String, String> credentialConfig =
+        IcebergRESTUtils.toRESTCredential(
+                catalogCredentialManager.catalogName(),
+                tableIdentifier,
+                credential,
+                loadTableResponse.tableMetadata())
+            .config();
     return LoadTableResponse.builder()
         .withTableMetadata(loadTableResponse.tableMetadata())
         .addAllConfig(loadTableResponse.config())
@@ -336,9 +405,7 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
         .build();
   }
 
-  private Credential getCredential(
-      LoadTableResponse loadTableResponse, CredentialPrivilege privilege) {
-    TableMetadata tableMetadata = loadTableResponse.tableMetadata();
+  private Credential getCredential(TableMetadata tableMetadata, 
CredentialPrivilege privilege) {
     String[] path =
         Stream.of(
                 tableMetadata.location(),
@@ -685,13 +752,22 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
 
     if (table instanceof BaseTable) {
       Map<String, String> properties = retrieveFileIOProperties(table.io());
+      Map<String, String> filteredCredentialProperties =
+          CredentialPropertyUtils.filterCredentialProperties(properties);
       return LoadTableResponse.builder()
           .withTableMetadata(((BaseTable) table).operations().current())
           .addAllConfig(
               MapUtils.getFilteredMap(
                   properties, key -> 
catalogPropertiesToClientKeys.contains(key)))
-          // Keep only credential fields from FileIO properties before 
returning them to the client.
-          
.addAllConfig(CredentialPropertyUtils.filterCredentialProperties(properties))
+          // Keep only credential fields from FileIO properties before 
returning them to the
+          // client.
+          .addAllConfig(filteredCredentialProperties)
+          .addAllConfig(
+              IcebergRESTUtils.buildRefreshProps(
+                  catalogCredentialManager.catalogName(), ident, 
filteredCredentialProperties))
+          .addAllCredentials(
+              IcebergRESTUtils.buildStorageCreds(
+                  catalogCredentialManager.catalogName(), ident, table.io()))
           .build();
     }
 
@@ -726,10 +802,19 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
     }
 
     Map<String, String> tableProperties = retrieveFileIOProperties(table.io());
+    Map<String, String> filteredCredentialProperties =
+        CredentialPropertyUtils.filterCredentialProperties(tableProperties);
     config.putAll(
         MapUtils.getFilteredMap(
             tableProperties, key -> 
catalogPropertiesToClientKeys.contains(key)));
-    
config.putAll(CredentialPropertyUtils.filterCredentialProperties(tableProperties));
+    config.putAll(filteredCredentialProperties);
+    config.putAll(
+        IcebergRESTUtils.buildRefreshProps(
+            catalogCredentialManager.catalogName(), ident, 
filteredCredentialProperties));
+
+    List<org.apache.iceberg.rest.credentials.Credential> credentials =
+        IcebergRESTUtils.buildStorageCreds(
+            catalogCredentialManager.catalogName(), ident, table.io());
 
     TableMetadata metadata =
         TableMetadata.newTableMetadata(
@@ -739,7 +824,11 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
             table.location(),
             properties);
 
-    return 
LoadTableResponse.builder().withTableMetadata(metadata).addAllConfig(config).build();
+    return LoadTableResponse.builder()
+        .withTableMetadata(metadata)
+        .addAllConfig(config)
+        .addAllCredentials(credentials)
+        .build();
   }
 
   private LoadTableResponse tableUpdateInternal(TableIdentifier ident, 
UpdateTableRequest request) {
@@ -793,13 +882,21 @@ public class CatalogWrapperForREST extends 
IcebergCatalogWrapper {
 
     if (table instanceof BaseTable) {
       Map<String, String> properties = retrieveFileIOProperties(table.io());
+      Map<String, String> filteredCredentialProperties =
+          CredentialPropertyUtils.filterCredentialProperties(properties);
       return LoadTableResponse.builder()
           .withTableMetadata(((BaseTable) table).operations().current())
           .addAllConfig(
               MapUtils.getFilteredMap(
                   properties, key -> 
catalogPropertiesToClientKeys.contains(key)))
           // Keep only credential fields from FileIO properties before 
returning them to the client.
-          
.addAllConfig(CredentialPropertyUtils.filterCredentialProperties(properties))
+          .addAllConfig(filteredCredentialProperties)
+          .addAllConfig(
+              IcebergRESTUtils.buildRefreshProps(
+                  catalogCredentialManager.catalogName(), ident, 
filteredCredentialProperties))
+          .addAllCredentials(
+              IcebergRESTUtils.buildStorageCreds(
+                  catalogCredentialManager.catalogName(), ident, table.io()))
           .build();
     } else if (table instanceof BaseMetadataTable) {
       // metadata tables are loaded on the client side, return 
NoSuchTableException for now
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergRESTUtils.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergRESTUtils.java
index c095ac231b..465ebfc138 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergRESTUtils.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergRESTUtils.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.iceberg.service;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
@@ -27,9 +28,12 @@ import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Stream;
@@ -40,9 +44,15 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.CredentialPropertyUtils;
 import 
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
+import org.apache.iceberg.TableMetadata;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.StorageCredential;
+import org.apache.iceberg.io.SupportsStorageCredentials;
 import org.apache.iceberg.rest.RESTUtil;
 import org.apache.iceberg.rest.responses.ErrorResponse;
 import org.apache.iceberg.rest.responses.LoadTableResponse;
@@ -83,6 +93,101 @@ public class IcebergRESTUtils {
 
   private IcebergRESTUtils() {}
 
+  /**
+   * Builds an Iceberg REST {@link 
org.apache.iceberg.rest.credentials.Credential} for load-table,
+   * scan-plan, or credentials API responses.
+   *
+   * <p>Refresh credential endpoints are added only for credential types whose 
Iceberg client
+   * modules support vended credential refresh: S3 ({@link
+   * org.apache.gravitino.credential.S3TokenCredential}, {@link
+   * org.apache.gravitino.credential.AwsIrsaCredential}), GCS ({@link
+   * org.apache.gravitino.credential.GCSTokenCredential}), and ADLS ({@link
+   * org.apache.gravitino.credential.ADLSTokenCredential}). OSS temporary 
credentials ({@link
+   * org.apache.gravitino.credential.OSSTokenCredential}) are omitted because 
Iceberg's Aliyun OSS
+   * FileIO does not consume {@code client.refresh-credentials-endpoint}; only 
the initial STS
+   * properties are returned.
+   *
+   * @param catalogName IRC catalog name used in the refresh path
+   * @param tableIdentifier table receiving the credential
+   * @param credential Gravitino credential to vend
+   * @param tableMetadata table metadata used to derive the storage prefix
+   * @return Iceberg REST credential with prefix and config
+   */
+  public static org.apache.iceberg.rest.credentials.Credential 
toRESTCredential(
+      String catalogName,
+      TableIdentifier tableIdentifier,
+      Credential credential,
+      TableMetadata tableMetadata) {
+    Map<String, String> config =
+        new HashMap<>(CredentialPropertyUtils.toIcebergProperties(credential));
+    config.putAll(buildRefreshProps(catalogName, tableIdentifier, config));
+
+    return toRESTCredential(tableMetadata.location(), config);
+  }
+
+  /**
+   * Builds an Iceberg REST {@link 
org.apache.iceberg.rest.credentials.Credential} from a storage
+   * prefix and credential config map.
+   *
+   * @param prefix storage location prefix for the credential
+   * @param config Iceberg credential config properties
+   * @return Iceberg REST credential
+   */
+  public static org.apache.iceberg.rest.credentials.Credential 
toRESTCredential(
+      String prefix, Map<String, String> config) {
+    Map<String, String> credentialConfig = ImmutableMap.copyOf(config);
+    return new org.apache.iceberg.rest.credentials.Credential() {
+      @Override
+      public String prefix() {
+        return prefix;
+      }
+
+      @Override
+      public Map<String, String> config() {
+        return credentialConfig;
+      }
+
+      @Override
+      public void validate() {}
+    };
+  }
+
+  /**
+   * Builds Iceberg REST 1.11 {@code storage-credentials} from an upstream 
REST catalog proxy.
+   *
+   * <p>Upstream credentials are read from {@link 
SupportsStorageCredentials#credentials()}. When
+   * present, they are filtered and rewritten with IRC-local refresh endpoints 
via {@link
+   * CredentialPropertyUtils}. Credential properties in {@link 
FileIO#properties()} must be handled
+   * separately by the caller.
+   *
+   * @param catalogName IRC catalog name used to build refresh paths
+   * @param tableIdentifier table receiving the credentials
+   * @param fileIO table FileIO returned by the upstream catalog
+   * @return rewritten storage credentials for the downstream client, or an 
empty list
+   */
+  public static List<org.apache.iceberg.rest.credentials.Credential> 
buildStorageCreds(
+      String catalogName, TableIdentifier tableIdentifier, FileIO fileIO) {
+    if (!(fileIO instanceof SupportsStorageCredentials)) {
+      return Collections.emptyList();
+    }
+
+    List<StorageCredential> credentials = ((SupportsStorageCredentials) 
fileIO).credentials();
+    if (credentials == null || credentials.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    List<org.apache.iceberg.rest.credentials.Credential> restCredentials = new 
ArrayList<>();
+    for (StorageCredential credential : credentials) {
+      Map<String, String> filteredConfig =
+          
CredentialPropertyUtils.filterCredentialProperties(credential.config());
+      filteredConfig.putAll(buildRefreshProps(catalogName, tableIdentifier, 
filteredConfig));
+      restCredentials.add(
+          toRESTCredential(credential.prefix(), 
ImmutableMap.copyOf(filteredConfig)));
+    }
+
+    return List.copyOf(restCredentials);
+  }
+
   public static <T> Response ok(T t) {
     return 
Response.status(Response.Status.OK).entity(t).type(MediaType.APPLICATION_JSON).build();
   }
@@ -262,6 +367,25 @@ public class IcebergRESTUtils {
     return headers;
   }
 
+  /**
+   * Builds refresh credential endpoint properties for a table, with Iceberg 
REST path encoding.
+   *
+   * @param catalogName IRC catalog name used in the refresh path
+   * @param tableIdentifier table receiving the credentials
+   * @param credentialProperties Iceberg credential properties used to 
determine refresh keys
+   * @return refresh endpoint properties keyed by Iceberg client config names
+   */
+  public static Map<String, String> buildRefreshProps(
+      String catalogName,
+      TableIdentifier tableIdentifier,
+      Map<String, String> credentialProperties) {
+    return CredentialPropertyUtils.buildRefreshProps(
+        RESTUtil.encodeString(catalogName),
+        RESTUtil.encodeNamespace(tableIdentifier.namespace(), 
NAMESPACE_SEPARATOR_URLENCODED_UTF_8),
+        RESTUtil.encodeString(tableIdentifier.name()),
+        credentialProperties);
+  }
+
   // remove the last '/' from the prefix, for example transform 
'iceberg_catalog/' to
   // 'iceberg_catalog'
   private static String normalizePrefix(String rawPrefix) {
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 b7432cdc31..55d30018a0 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
@@ -27,8 +27,13 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
 
 import com.google.common.collect.ImmutableMap;
+import com.sun.net.httpserver.HttpServer;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -38,8 +43,13 @@ import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.CredentialPrivilege;
 import org.apache.gravitino.iceberg.common.IcebergConfig;
+import org.apache.gravitino.iceberg.service.extension.DummyCredentialProvider;
+import org.apache.iceberg.BaseTable;
 import org.apache.iceberg.BaseTransaction;
+import org.apache.iceberg.CatalogProperties;
 import org.apache.iceberg.MetadataUpdate;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
@@ -51,12 +61,22 @@ import org.apache.iceberg.Transaction;
 import org.apache.iceberg.UpdateRequirement;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.SupportsNamespaces;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.exceptions.NoSuchTableException;
+import org.apache.iceberg.exceptions.NotAuthorizedException;
+import org.apache.iceberg.exceptions.ServiceFailureException;
 import org.apache.iceberg.io.FileIO;
 import org.apache.iceberg.io.ResolvingFileIO;
+import org.apache.iceberg.io.StorageCredential;
+import org.apache.iceberg.io.SupportsStorageCredentials;
 import org.apache.iceberg.rest.RESTCatalog;
+import org.apache.iceberg.rest.auth.AuthProperties;
+import org.apache.iceberg.rest.credentials.Credential;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
 import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.rest.responses.LoadCredentialsResponse;
 import org.apache.iceberg.rest.responses.LoadTableResponse;
 import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Assertions;
@@ -101,6 +121,292 @@ public class TestCatalogWrapperForREST {
     Assertions.assertFalse(CatalogWrapperForREST.isLocalOrHdfsLocation("   "));
   }
 
+  @Test
+  void testRESTTableCredentials() throws Exception {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("db"), "tbl");
+    String expectedPath = "/v1/upstream/namespaces/db/tables/tbl/credentials";
+    String upstreamJson =
+        
"{\"storage-credentials\":[{\"prefix\":\"s3://upstream/db/tbl/\",\"config\":{"
+            + "\"s3.access-key-id\":\"upstream-key\","
+            + "\"s3.secret-access-key\":\"upstream-secret\","
+            + "\"s3.session-token\":\"upstream-token\"}}]}";
+
+    AtomicReference<String> requestPath = new AtomicReference<>();
+    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
+    server.createContext(
+        "/",
+        exchange -> {
+          requestPath.set(exchange.getRequestURI().getPath());
+          byte[] body = upstreamJson.getBytes(StandardCharsets.UTF_8);
+          exchange.getResponseHeaders().add("Content-Type", 
"application/json");
+          exchange.sendResponseHeaders(200, body.length);
+          try (OutputStream os = exchange.getResponseBody()) {
+            os.write(body);
+          }
+        });
+    server.start();
+    try {
+      String uri = "http://127.0.0.1:"; + server.getAddress().getPort();
+      RESTCatalog restCatalog = mock(RESTCatalog.class);
+      when(restCatalog.name()).thenReturn("upstream");
+      when(restCatalog.properties())
+          .thenReturn(
+              ImmutableMap.of(
+                  CatalogProperties.URI,
+                  uri,
+                  AuthProperties.AUTH_TYPE,
+                  AuthProperties.AUTH_TYPE_NONE,
+                  "prefix",
+                  "upstream"));
+
+      IcebergConfig config =
+          new IcebergConfig(
+              ImmutableMap.of(
+                  IcebergConstants.CATALOG_BACKEND,
+                  "memory",
+                  IcebergConstants.WAREHOUSE,
+                  "/tmp/warehouse"));
+      CatalogWrapperForREST wrapper = new StaticCatalogWrapperForREST("local", 
config, restCatalog);
+
+      LoadCredentialsResponse response =
+          wrapper.getTableCredentials(table, CredentialPrivilege.READ);
+
+      Assertions.assertEquals(expectedPath, requestPath.get());
+      Assertions.assertEquals(1, response.credentials().size());
+      Credential credential = response.credentials().get(0);
+      Assertions.assertEquals("s3://upstream/db/tbl/", credential.prefix());
+      Assertions.assertEquals("upstream-key", 
credential.config().get("s3.access-key-id"));
+      Assertions.assertEquals("upstream-secret", 
credential.config().get("s3.secret-access-key"));
+      Assertions.assertEquals("upstream-token", 
credential.config().get("s3.session-token"));
+    } finally {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void testRESTTableCredentialsOnFailure() throws Exception {
+    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
+    server.createContext(
+        "/",
+        exchange -> {
+          exchange.sendResponseHeaders(500, -1);
+          exchange.close();
+        });
+    server.start();
+    try {
+      String uri = "http://127.0.0.1:"; + server.getAddress().getPort();
+      RESTCatalog restCatalog = mock(RESTCatalog.class);
+      when(restCatalog.name()).thenReturn("upstream");
+      when(restCatalog.properties())
+          .thenReturn(
+              ImmutableMap.of(
+                  CatalogProperties.URI,
+                  uri,
+                  AuthProperties.AUTH_TYPE,
+                  AuthProperties.AUTH_TYPE_NONE,
+                  "prefix",
+                  "upstream"));
+
+      IcebergConfig config =
+          new IcebergConfig(
+              ImmutableMap.of(
+                  IcebergConstants.CATALOG_BACKEND,
+                  "memory",
+                  IcebergConstants.WAREHOUSE,
+                  "/tmp/warehouse"));
+      CatalogWrapperForREST wrapper = new StaticCatalogWrapperForREST("local", 
config, restCatalog);
+
+      Assertions.assertThrows(
+          ServiceFailureException.class,
+          () ->
+              wrapper.getTableCredentials(
+                  TableIdentifier.of(Namespace.of("db"), "tbl"), 
CredentialPrivilege.READ));
+    } finally {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void testRESTTableCredentialsOnForbidden() throws Exception {
+    String errorJson =
+        
"{\"error\":{\"message\":\"Forbidden\",\"type\":\"ForbiddenException\","
+            + "\"code\":403,\"stack\":[]}}";
+    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
+    server.createContext(
+        "/",
+        exchange -> {
+          byte[] body = errorJson.getBytes(StandardCharsets.UTF_8);
+          exchange.getResponseHeaders().add("Content-Type", 
"application/json");
+          exchange.sendResponseHeaders(403, body.length);
+          try (OutputStream os = exchange.getResponseBody()) {
+            os.write(body);
+          }
+        });
+    server.start();
+    try {
+      String uri = "http://127.0.0.1:"; + server.getAddress().getPort();
+      RESTCatalog restCatalog = mock(RESTCatalog.class);
+      when(restCatalog.name()).thenReturn("upstream");
+      when(restCatalog.properties())
+          .thenReturn(
+              ImmutableMap.of(
+                  CatalogProperties.URI,
+                  uri,
+                  AuthProperties.AUTH_TYPE,
+                  AuthProperties.AUTH_TYPE_NONE,
+                  "prefix",
+                  "upstream"));
+
+      IcebergConfig config =
+          new IcebergConfig(
+              ImmutableMap.of(
+                  IcebergConstants.CATALOG_BACKEND,
+                  "memory",
+                  IcebergConstants.WAREHOUSE,
+                  "/tmp/warehouse"));
+      CatalogWrapperForREST wrapper = new StaticCatalogWrapperForREST("local", 
config, restCatalog);
+
+      Assertions.assertThrows(
+          ForbiddenException.class,
+          () ->
+              wrapper.getTableCredentials(
+                  TableIdentifier.of(Namespace.of("db"), "tbl"), 
CredentialPrivilege.READ));
+    } finally {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void testRESTTableCredentialsOnUnauthorized() throws Exception {
+    String errorJson =
+        "{\"error\":{\"message\":\"Not 
authorized\",\"type\":\"NotAuthorizedException\","
+            + "\"code\":401,\"stack\":[]}}";
+    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
+    server.createContext(
+        "/",
+        exchange -> {
+          byte[] body = errorJson.getBytes(StandardCharsets.UTF_8);
+          exchange.getResponseHeaders().add("Content-Type", 
"application/json");
+          exchange.sendResponseHeaders(401, body.length);
+          try (OutputStream os = exchange.getResponseBody()) {
+            os.write(body);
+          }
+        });
+    server.start();
+    try {
+      String uri = "http://127.0.0.1:"; + server.getAddress().getPort();
+      RESTCatalog restCatalog = mock(RESTCatalog.class);
+      when(restCatalog.name()).thenReturn("upstream");
+      when(restCatalog.properties())
+          .thenReturn(
+              ImmutableMap.of(
+                  CatalogProperties.URI,
+                  uri,
+                  AuthProperties.AUTH_TYPE,
+                  AuthProperties.AUTH_TYPE_NONE,
+                  "prefix",
+                  "upstream"));
+
+      IcebergConfig config =
+          new IcebergConfig(
+              ImmutableMap.of(
+                  IcebergConstants.CATALOG_BACKEND,
+                  "memory",
+                  IcebergConstants.WAREHOUSE,
+                  "/tmp/warehouse"));
+      CatalogWrapperForREST wrapper = new StaticCatalogWrapperForREST("local", 
config, restCatalog);
+
+      Assertions.assertThrows(
+          NotAuthorizedException.class,
+          () ->
+              wrapper.getTableCredentials(
+                  TableIdentifier.of(Namespace.of("db"), "tbl"), 
CredentialPrivilege.READ));
+    } finally {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void testRESTTableCredentialsNoSuchTable() throws Exception {
+    String errorJson =
+        "{\"error\":{\"message\":\"Table not 
found\",\"type\":\"NoSuchTableException\","
+            + "\"code\":404,\"stack\":[]}}";
+    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
+    server.createContext(
+        "/",
+        exchange -> {
+          byte[] body = errorJson.getBytes(StandardCharsets.UTF_8);
+          exchange.getResponseHeaders().add("Content-Type", 
"application/json");
+          exchange.sendResponseHeaders(404, body.length);
+          try (OutputStream os = exchange.getResponseBody()) {
+            os.write(body);
+          }
+        });
+    server.start();
+    try {
+      String uri = "http://127.0.0.1:"; + server.getAddress().getPort();
+      RESTCatalog restCatalog = mock(RESTCatalog.class);
+      when(restCatalog.name()).thenReturn("upstream");
+      when(restCatalog.properties())
+          .thenReturn(
+              ImmutableMap.of(
+                  CatalogProperties.URI,
+                  uri,
+                  AuthProperties.AUTH_TYPE,
+                  AuthProperties.AUTH_TYPE_NONE,
+                  "prefix",
+                  "upstream"));
+
+      IcebergConfig config =
+          new IcebergConfig(
+              ImmutableMap.of(
+                  IcebergConstants.CATALOG_BACKEND,
+                  "memory",
+                  IcebergConstants.WAREHOUSE,
+                  "/tmp/warehouse"));
+      CatalogWrapperForREST wrapper = new StaticCatalogWrapperForREST("local", 
config, restCatalog);
+
+      Assertions.assertThrows(
+          NoSuchTableException.class,
+          () ->
+              wrapper.getTableCredentials(
+                  TableIdentifier.of(Namespace.of("db"), "missing"), 
CredentialPrivilege.READ));
+    } finally {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void testLocalTableCredentials() {
+    IcebergConfig config =
+        new IcebergConfig(
+            ImmutableMap.of(
+                IcebergConstants.CATALOG_BACKEND,
+                "memory",
+                IcebergConstants.WAREHOUSE,
+                "/tmp/warehouse",
+                CredentialConstants.CREDENTIAL_PROVIDERS,
+                DummyCredentialProvider.DUMMY_CREDENTIAL_TYPE));
+
+    CatalogWrapperForREST wrapper = new CatalogWrapperForREST("local-catalog", 
config);
+    Namespace namespace = Namespace.of("db");
+    Catalog catalog = wrapper.getCatalog();
+    ((SupportsNamespaces) catalog).createNamespace(namespace);
+    TableIdentifier table = TableIdentifier.of(namespace, "tbl");
+    Schema schema = new Schema(Types.NestedField.required(1, "id", 
Types.IntegerType.get()));
+    catalog.createTable(
+        table,
+        schema,
+        PartitionSpec.unpartitioned(),
+        "s3://bucket/wh/db/tbl",
+        Collections.emptyMap());
+
+    LoadCredentialsResponse response = wrapper.getTableCredentials(table, 
CredentialPrivilege.READ);
+
+    Assertions.assertEquals(1, response.credentials().size());
+    Assertions.assertEquals("s3://bucket/wh/db/tbl", 
response.credentials().get(0).prefix());
+  }
+
   @Test
   void testValidateCredentialLocation() {
     Assertions.assertDoesNotThrow(
@@ -116,7 +422,113 @@ public class TestCatalogWrapperForREST {
   }
 
   @Test
-  void testRestCatalogClientConfigMergesRemote() {
+  void testLoadTableRefreshEndpoint() {
+    TableIdentifier ident = TableIdentifier.of(Namespace.of("db"), "tbl");
+    RESTCatalog catalog = mock(RESTCatalog.class);
+    BaseTable baseTable = mock(BaseTable.class);
+    TableOperations ops = mock(TableOperations.class);
+    FileIO fileIO = mock(FileIO.class);
+    TableMetadata metadata =
+        TableMetadata.newTableMetadata(
+            new Schema(Types.NestedField.required(1, "id", 
Types.IntegerType.get())),
+            PartitionSpec.unpartitioned(),
+            SortOrder.unsorted(),
+            "s3://bucket/db/tbl",
+            Collections.emptyMap());
+
+    when(catalog.loadTable(ident)).thenReturn(baseTable);
+    when(baseTable.operations()).thenReturn(ops);
+    when(ops.current()).thenReturn(metadata);
+    when(baseTable.io()).thenReturn(fileIO);
+    when(fileIO.properties())
+        .thenReturn(
+            ImmutableMap.of(
+                "s3.session-token",
+                "token",
+                "s3.session-token-expires-at-ms",
+                "123",
+                "client.refresh-credentials-endpoint",
+                "v1/upstream/namespaces/db/tables/tbl/credentials"));
+
+    IcebergConfig config =
+        new IcebergConfig(
+            ImmutableMap.of(
+                IcebergConstants.CATALOG_BACKEND,
+                "memory",
+                IcebergConstants.WAREHOUSE,
+                "/tmp/warehouse"));
+    CatalogWrapperForREST wrapper = new StaticCatalogWrapperForREST("irc1", 
config, catalog);
+
+    LoadTableResponse response = wrapper.loadTable(ident, false, 
CredentialPrivilege.READ);
+
+    Assertions.assertEquals(
+        "v1/irc1/namespaces/db/tables/tbl/credentials",
+        response.config().get("client.refresh-credentials-endpoint"));
+    Assertions.assertEquals("token", 
response.config().get("s3.session-token"));
+  }
+
+  @Test
+  void testLoadTableStorageCreds() {
+    TableIdentifier ident = TableIdentifier.of(Namespace.of("db"), "tbl");
+    RESTCatalog catalog = mock(RESTCatalog.class);
+    BaseTable baseTable = mock(BaseTable.class);
+    TableOperations ops = mock(TableOperations.class);
+    FileIO fileIO =
+        mock(FileIO.class, 
withSettings().extraInterfaces(SupportsStorageCredentials.class));
+    SupportsStorageCredentials storageCredentialsFileIO = 
(SupportsStorageCredentials) fileIO;
+    TableMetadata metadata =
+        TableMetadata.newTableMetadata(
+            new Schema(Types.NestedField.required(1, "id", 
Types.IntegerType.get())),
+            PartitionSpec.unpartitioned(),
+            SortOrder.unsorted(),
+            "s3://bucket/db/tbl",
+            Collections.emptyMap());
+
+    StorageCredential upstreamCredential =
+        StorageCredential.create(
+            "s3://bucket/db/tbl/",
+            ImmutableMap.of(
+                "s3.access-key-id",
+                "upstream-key",
+                "s3.secret-access-key",
+                "upstream-secret",
+                "s3.session-token",
+                "upstream-token",
+                "s3.session-token-expires-at-ms",
+                "123",
+                "client.refresh-credentials-endpoint",
+                "v1/upstream/namespaces/db/tables/tbl/credentials"));
+
+    when(catalog.loadTable(ident)).thenReturn(baseTable);
+    when(baseTable.operations()).thenReturn(ops);
+    when(ops.current()).thenReturn(metadata);
+    when(baseTable.io()).thenReturn(fileIO);
+    when(fileIO.properties()).thenReturn(Collections.emptyMap());
+    
when(storageCredentialsFileIO.credentials()).thenReturn(List.of(upstreamCredential));
+
+    IcebergConfig config =
+        new IcebergConfig(
+            ImmutableMap.of(
+                IcebergConstants.CATALOG_BACKEND,
+                "memory",
+                IcebergConstants.WAREHOUSE,
+                "/tmp/warehouse"));
+    CatalogWrapperForREST wrapper = new StaticCatalogWrapperForREST("irc1", 
config, catalog);
+
+    LoadTableResponse response = wrapper.loadTable(ident, false, 
CredentialPrivilege.READ);
+
+    Assertions.assertEquals(1, response.credentials().size());
+    Credential credential = response.credentials().get(0);
+    Assertions.assertEquals("s3://bucket/db/tbl/", credential.prefix());
+    Assertions.assertEquals("upstream-token", 
credential.config().get("s3.session-token"));
+    Assertions.assertEquals(
+        "v1/irc1/namespaces/db/tables/tbl/credentials",
+        credential.config().get("client.refresh-credentials-endpoint"));
+    
Assertions.assertFalse(response.config().containsKey("client.refresh-credentials-endpoint"));
+  }
+
+  @Test
+  void testRESTCatalogClientConfigMergesRemote() {
     IcebergConfig config =
         new IcebergConfig(
             ImmutableMap.of(
@@ -172,7 +584,7 @@ public class TestCatalogWrapperForREST {
   }
 
   @Test
-  void testNonRestCatalogClientConfig() {
+  void testNonRESTCatalogClientConfig() {
     Catalog catalog = mock(Catalog.class);
     IcebergConfig config =
         new IcebergConfig(
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestIcebergRESTUtils.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestIcebergRESTUtils.java
index 7c2e05c80b..f7dd7cb2f0 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestIcebergRESTUtils.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestIcebergRESTUtils.java
@@ -19,12 +19,31 @@
 
 package org.apache.gravitino.iceberg.service;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
+import org.apache.gravitino.credential.ADLSTokenCredential;
+import org.apache.gravitino.credential.AwsIrsaCredential;
+import org.apache.gravitino.credential.GCSTokenCredential;
+import org.apache.gravitino.credential.OSSTokenCredential;
+import org.apache.gravitino.credential.S3SecretKeyCredential;
+import org.apache.gravitino.credential.S3TokenCredential;
 import 
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
 import org.apache.gravitino.iceberg.service.provider.IcebergConfigProvider;
 import org.apache.iceberg.Schema;
+import org.apache.iceberg.TableMetadata;
+import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.StorageCredential;
+import org.apache.iceberg.io.SupportsStorageCredentials;
+import org.apache.iceberg.rest.credentials.Credential;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
 import org.apache.iceberg.types.Types.IntegerType;
 import org.apache.iceberg.types.Types.NestedField;
@@ -87,4 +106,216 @@ public class TestIcebergRESTUtils {
       Assertions.assertEquals(field, clonedField);
     }
   }
+
+  @Test
+  void testTableCredentialsPath() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    TableMetadata tableMetadata = mock(TableMetadata.class);
+    when(tableMetadata.location()).thenReturn("s3://bucket/t/");
+    org.apache.iceberg.rest.credentials.Credential credential =
+        IcebergRESTUtils.toRESTCredential(
+            "my_catalog", table, new S3TokenCredential("k", "s", "t", 99L), 
tableMetadata);
+    Assertions.assertEquals(
+        "v1/my_catalog/namespaces/ns/tables/tbl/credentials",
+        credential.config().get("client.refresh-credentials-endpoint"));
+  }
+
+  @Test
+  void testToRESTCredential() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    String refreshPath = "v1/cat/namespaces/ns/tables/tbl/credentials";
+    TableMetadata metadataWithSlash = mock(TableMetadata.class);
+    when(metadataWithSlash.location()).thenReturn("s3://bucket/t/");
+    org.apache.iceberg.rest.credentials.Credential credentialWithSlash =
+        IcebergRESTUtils.toRESTCredential(
+            "cat", table, new S3TokenCredential("k", "s", "t", 99L), 
metadataWithSlash);
+    Assertions.assertEquals("s3://bucket/t/", credentialWithSlash.prefix());
+    Assertions.assertEquals(
+        "99", 
credentialWithSlash.config().get("s3.session-token-expires-at-ms"));
+    Assertions.assertEquals(
+        refreshPath, 
credentialWithSlash.config().get("client.refresh-credentials-endpoint"));
+
+    TableMetadata metadataWithoutSlash = mock(TableMetadata.class);
+    
when(metadataWithoutSlash.location()).thenReturn("s3://bucket/path/to/table");
+    org.apache.iceberg.rest.credentials.Credential credentialWithoutSlash =
+        IcebergRESTUtils.toRESTCredential(
+            "cat", table, new S3TokenCredential("k", "s", "t", 99L), 
metadataWithoutSlash);
+    Assertions.assertEquals("s3://bucket/path/to/table", 
credentialWithoutSlash.prefix());
+  }
+
+  @Test
+  void testToRESTCredentialForS3Token() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    TableMetadata tableMetadata = mock(TableMetadata.class);
+    when(tableMetadata.location()).thenReturn("s3://bucket/t/");
+    Map<String, String> config =
+        IcebergRESTUtils.toRESTCredential(
+                "aws", table, new S3TokenCredential("key", "secret", "token", 
1234L), tableMetadata)
+            .config();
+
+    Assertions.assertEquals(
+        "v1/aws/namespaces/ns/tables/tbl/credentials",
+        config.get("client.refresh-credentials-endpoint"));
+    Assertions.assertEquals("1234", 
config.get("s3.session-token-expires-at-ms"));
+  }
+
+  @Test
+  void testToRESTCredentialForAwsIrsa() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    TableMetadata tableMetadata = mock(TableMetadata.class);
+    when(tableMetadata.location()).thenReturn("s3://bucket/t/");
+    Map<String, String> config =
+        IcebergRESTUtils.toRESTCredential(
+                "aws", table, new AwsIrsaCredential("key", "secret", "token", 
4321L), tableMetadata)
+            .config();
+
+    Assertions.assertEquals(
+        "v1/aws/namespaces/ns/tables/tbl/credentials",
+        config.get("client.refresh-credentials-endpoint"));
+    Assertions.assertEquals("4321", 
config.get("s3.session-token-expires-at-ms"));
+  }
+
+  @Test
+  void testToRESTCredentialConfigIsUnmodifiable() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    TableMetadata tableMetadata = mock(TableMetadata.class);
+    when(tableMetadata.location()).thenReturn("s3://bucket/t/");
+    Map<String, String> config =
+        IcebergRESTUtils.toRESTCredential(
+                "aws", table, new S3TokenCredential("k", "s", "t", 99L), 
tableMetadata)
+            .config();
+
+    Assertions.assertThrows(UnsupportedOperationException.class, () -> 
config.put("k", "v"));
+  }
+
+  @Test
+  void testToRESTCredentialForGcsToken() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    TableMetadata tableMetadata = mock(TableMetadata.class);
+    when(tableMetadata.location()).thenReturn("gs://bucket/t/");
+    Map<String, String> config =
+        IcebergRESTUtils.toRESTCredential(
+                "gcs", table, new GCSTokenCredential("gcs-token", 5678L), 
tableMetadata)
+            .config();
+
+    Assertions.assertEquals(
+        "v1/gcs/namespaces/ns/tables/tbl/credentials",
+        config.get("gcs.oauth2.refresh-credentials-endpoint"));
+    Assertions.assertEquals("5678", config.get("gcs.oauth2.token-expires-at"));
+  }
+
+  @Test
+  void testToRESTCredentialForOssToken() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    TableMetadata tableMetadata = mock(TableMetadata.class);
+    when(tableMetadata.location()).thenReturn("oss://bucket/t/");
+    Map<String, String> config =
+        IcebergRESTUtils.toRESTCredential(
+                "oss",
+                table,
+                new OSSTokenCredential("key", "secret", "oss-token", 9012L),
+                tableMetadata)
+            .config();
+
+    
Assertions.assertFalse(config.containsKey("client.refresh-credentials-endpoint"));
+    Assertions.assertEquals("9012", 
config.get("client.security-token-expires-at-ms"));
+    Assertions.assertEquals("key", config.get("client.access-key-id"));
+    Assertions.assertEquals("secret", config.get("client.access-key-secret"));
+    Assertions.assertEquals("oss-token", config.get("client.security-token"));
+  }
+
+  @Test
+  void testToRESTCredentialForAdlsToken() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    TableMetadata tableMetadata = mock(TableMetadata.class);
+    
when(tableMetadata.location()).thenReturn("abfss://[email protected]/t/");
+    Map<String, String> config =
+        IcebergRESTUtils.toRESTCredential(
+                "adls",
+                table,
+                new ADLSTokenCredential("storageacct", "sas-token", 3456L),
+                tableMetadata)
+            .config();
+
+    Assertions.assertEquals(
+        "v1/adls/namespaces/ns/tables/tbl/credentials",
+        config.get("adls.refresh-credentials-endpoint"));
+    Assertions.assertEquals(
+        "3456", 
config.get("adls.sas-token-expires-at-ms.storageacct.dfs.core.windows.net"));
+  }
+
+  @Test
+  void testBuildStorageCreds() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("db"), "tbl");
+    FileIO fileIO =
+        mock(FileIO.class, 
withSettings().extraInterfaces(SupportsStorageCredentials.class));
+    SupportsStorageCredentials storageCredentialsFileIO = 
(SupportsStorageCredentials) fileIO;
+    StorageCredential upstreamCredential =
+        StorageCredential.create(
+            "s3://bucket/db/tbl/",
+            ImmutableMap.of(
+                "s3.access-key-id",
+                "upstream-key",
+                "s3.secret-access-key",
+                "upstream-secret",
+                "s3.session-token",
+                "upstream-token",
+                "s3.session-token-expires-at-ms",
+                "123",
+                "client.refresh-credentials-endpoint",
+                "v1/upstream/namespaces/db/tables/tbl/credentials"));
+    
when(storageCredentialsFileIO.credentials()).thenReturn(List.of(upstreamCredential));
+
+    List<Credential> credentials = IcebergRESTUtils.buildStorageCreds("irc1", 
table, fileIO);
+
+    Assertions.assertEquals(1, credentials.size());
+    Credential credential = credentials.get(0);
+    Assertions.assertEquals("s3://bucket/db/tbl/", credential.prefix());
+    Assertions.assertEquals("upstream-token", 
credential.config().get("s3.session-token"));
+    Assertions.assertEquals(
+        "v1/irc1/namespaces/db/tables/tbl/credentials",
+        credential.config().get("client.refresh-credentials-endpoint"));
+    Assertions.assertFalse(
+        
credential.config().containsValue("v1/upstream/namespaces/db/tables/tbl/credentials"));
+  }
+
+  @Test
+  void testBuildStorageCredsPreservesSchemePrefix() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("db"), "tbl");
+    FileIO fileIO =
+        mock(FileIO.class, 
withSettings().extraInterfaces(SupportsStorageCredentials.class));
+    SupportsStorageCredentials storageCredentialsFileIO = 
(SupportsStorageCredentials) fileIO;
+    StorageCredential upstreamCredential =
+        StorageCredential.create(
+            "s3",
+            ImmutableMap.of(
+                "s3.session-token", "upstream-token",
+                "s3.session-token-expires-at-ms", "123"));
+    
when(storageCredentialsFileIO.credentials()).thenReturn(List.of(upstreamCredential));
+
+    Credential credential = IcebergRESTUtils.buildStorageCreds("irc1", table, 
fileIO).get(0);
+
+    Assertions.assertEquals("s3", credential.prefix());
+  }
+
+  @Test
+  void testBuildStorageCredsEmpty() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("db"), "tbl");
+    FileIO fileIO = mock(FileIO.class);
+
+    Assertions.assertTrue(IcebergRESTUtils.buildStorageCreds("irc1", table, 
fileIO).isEmpty());
+  }
+
+  @Test
+  void testToRESTCredentialOmitsStaticSecretKey() {
+    TableIdentifier table = TableIdentifier.of(Namespace.of("ns"), "tbl");
+    TableMetadata tableMetadata = mock(TableMetadata.class);
+    when(tableMetadata.location()).thenReturn("s3://bucket/t/");
+    Map<String, String> config =
+        IcebergRESTUtils.toRESTCredential(
+                "aws", table, new S3SecretKeyCredential("key", "secret"), 
tableMetadata)
+            .config();
+
+    
Assertions.assertFalse(config.containsKey("client.refresh-credentials-endpoint"));
+  }
 }

Reply via email to