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

jerryshao 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 c0491090e0 [Cherry-pick to branch-1.3] [#13927] fix(credential): 
handle AWS IRSA fallback and ADLS paths (#13928) (#13961)
c0491090e0 is described below

commit c0491090e01a054f833bb18e28e7f11fc36d031f
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Oct 21 10:47:12 2026 +0800

    [Cherry-pick to branch-1.3] [#13927] fix(credential): handle AWS IRSA 
fallback and ADLS paths (#13928) (#13961)
    
    **Cherry-pick Information:**
    - Original commit: 026a660350666bb740c4f53c1b34b8e2707b2d88
    - Target branch: `branch-1.3`
    - Status: Conflicts resolved in `docs/security/credential-vending.md`
    
    The credential vending documentation now places the IRSA fallback
    details in the S3 IRSA section and the SAS path scope details in the
    ADLS Token section.
    
    Validation: `./gradlew :docs:build --console=plain` and `./gradlew
    :bundles:aws:test :bundles:azure:test -PskipITs --console=plain`.
    
    ---------
    
    Co-authored-by: roryqi <[email protected]>
    Co-authored-by: roryqi <[email protected]>
---
 .../s3/credential/AwsIrsaCredentialGenerator.java  |  18 +-
 .../credential/TestAwsIrsaCredentialGenerator.java |  56 ++++++
 .../abs/credential/ADLSTokenGenerator.java         | 111 +++++++++---
 .../abs/credential/TestADLSTokenGenerator.java     | 193 +++++++++++++++++++++
 docs/security/credential-vending.md                |   8 +-
 5 files changed, 356 insertions(+), 30 deletions(-)

diff --git 
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
 
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
index 2d5fe0f6ba..2026afbb56 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
@@ -28,6 +28,7 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
 import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.credential.AwsIrsaCredential;
@@ -55,6 +56,7 @@ import software.amazon.awssdk.services.sts.model.Credentials;
 /** Generate AWS IRSA credentials according to the read and write paths. */
 public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCredential> {
 
+  private final Supplier<String> environmentRoleArnSupplier;
   private WebIdentityTokenSource tokenSource;
   private String roleArn;
   private int tokenExpireSecs;
@@ -64,12 +66,24 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
   private StsClient basicModeStsClient;
   private Map<String, StsAssumeRoleWithWebIdentityCredentialsProvider> 
basicModeProviders;
 
+  /** Creates an AWS IRSA credential generator. */
+  public AwsIrsaCredentialGenerator() {
+    this(() -> System.getenv("AWS_ROLE_ARN"));
+  }
+
+  AwsIrsaCredentialGenerator(Supplier<String> environmentRoleArnSupplier) {
+    this.environmentRoleArnSupplier = 
Objects.requireNonNull(environmentRoleArnSupplier);
+  }
+
   @Override
   public void initialize(Map<String, String> properties) {
     this.tokenSource = WebIdentityTokenSources.create(properties);
 
     S3CredentialConfig s3CredentialConfig = new S3CredentialConfig(properties);
-    this.roleArn = s3CredentialConfig.s3RoleArn();
+    this.roleArn =
+        
StringUtils.isNotBlank(properties.get(S3CredentialConfig.S3_ROLE_ARN.getKey()))
+            ? s3CredentialConfig.s3RoleArn()
+            : null;
     this.tokenExpireSecs = s3CredentialConfig.tokenExpireInSecs();
     this.region = s3CredentialConfig.region();
     this.stsEndpoint = s3CredentialConfig.stsEndpoint();
@@ -330,7 +344,7 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
 
   private String getValidatedRoleArn(String configRoleArn) {
     String effectiveRoleArn =
-        StringUtils.isNotBlank(configRoleArn) ? configRoleArn : 
System.getenv("AWS_ROLE_ARN");
+        StringUtils.isNotBlank(configRoleArn) ? configRoleArn : 
environmentRoleArnSupplier.get();
     if (StringUtils.isBlank(effectiveRoleArn)) {
       throw new IllegalStateException(
           "No role ARN available. Either configure s3-role-arn or ensure 
AWS_ROLE_ARN environment variable is set.");
diff --git 
a/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestAwsIrsaCredentialGenerator.java
 
b/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestAwsIrsaCredentialGenerator.java
index f8c48b5c0d..ca261f02d8 100644
--- 
a/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestAwsIrsaCredentialGenerator.java
+++ 
b/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestAwsIrsaCredentialGenerator.java
@@ -84,6 +84,62 @@ class TestAwsIrsaCredentialGenerator {
     assertFalse(body.contains("Policy="));
   }
 
+  @Test
+  void 
basicModeFallsBackToEnvironmentRoleArnWhenConfiguredRoleIsBlank(@TempDir Path 
dir)
+      throws IOException {
+    Map<String, String> properties = createProperties(dir);
+    properties.put("s3-role-arn", " ");
+
+    try (AwsIrsaCredentialGenerator generator =
+        new AwsIrsaCredentialGenerator(() -> 
"arn:aws:iam::123456789012:role/environment-role")) {
+      generator.initialize(properties);
+
+      AwsIrsaCredential credential = generator.generate(() -> "test-user");
+
+      assertEquals("access-key", credential.accessKeyId());
+    }
+
+    String body = URLDecoder.decode(requestBody.get(), 
StandardCharsets.UTF_8.name());
+    
assertTrue(body.contains("RoleArn=arn:aws:iam::123456789012:role/environment-role"));
+  }
+
+  @Test
+  void pathModeFallsBackToEnvironmentRoleArn(@TempDir Path dir) throws 
IOException {
+    Map<String, String> properties = createProperties(dir);
+    properties.remove("s3-role-arn");
+
+    try (AwsIrsaCredentialGenerator generator =
+        new AwsIrsaCredentialGenerator(() -> 
"arn:aws:iam::123456789012:role/environment-role")) {
+      generator.initialize(properties);
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user", Collections.singleton("s3://bucket/table"), 
Collections.emptySet());
+
+      AwsIrsaCredential credential = generator.generate(context);
+
+      assertEquals("access-key", credential.accessKeyId());
+    }
+
+    String body = URLDecoder.decode(requestBody.get(), 
StandardCharsets.UTF_8.name());
+    
assertTrue(body.contains("RoleArn=arn:aws:iam::123456789012:role/environment-role"));
+    assertTrue(body.contains("Policy="));
+  }
+
+  @Test
+  void basicModeFailsWhenNoRoleArnIsAvailable(@TempDir Path dir) throws 
IOException {
+    Map<String, String> properties = createProperties(dir);
+    properties.remove("s3-role-arn");
+
+    try (AwsIrsaCredentialGenerator generator = new 
AwsIrsaCredentialGenerator(() -> null)) {
+      generator.initialize(properties);
+
+      IllegalStateException exception =
+          assertThrows(IllegalStateException.class, () -> 
generator.generate(() -> "test-user"));
+
+      assertTrue(exception.getMessage().contains("No role ARN available"));
+    }
+  }
+
   @Test
   void basicModeReusesCachedStsCredentials(@TempDir Path dir) throws 
IOException {
     try (AwsIrsaCredentialGenerator generator = new 
AwsIrsaCredentialGenerator()) {
diff --git 
a/bundles/azure/src/main/java/org/apache/gravitino/abs/credential/ADLSTokenGenerator.java
 
b/bundles/azure/src/main/java/org/apache/gravitino/abs/credential/ADLSTokenGenerator.java
index f3cbb5b1fe..fbc69c343a 100644
--- 
a/bundles/azure/src/main/java/org/apache/gravitino/abs/credential/ADLSTokenGenerator.java
+++ 
b/bundles/azure/src/main/java/org/apache/gravitino/abs/credential/ADLSTokenGenerator.java
@@ -29,9 +29,13 @@ import 
com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues;
 import com.azure.storage.file.datalake.sas.PathSasPermission;
 import java.io.IOException;
 import java.time.OffsetDateTime;
-import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
 import org.apache.gravitino.credential.ADLSTokenCredential;
 import org.apache.gravitino.credential.CredentialContext;
 import org.apache.gravitino.credential.CredentialGenerator;
@@ -70,34 +74,14 @@ public class ADLSTokenGenerator implements 
CredentialGenerator<ADLSTokenCredenti
 
     Set<String> writePaths = pathContext.getWritePaths();
     Set<String> readPaths = pathContext.getReadPaths();
-    Set<String> combinedPaths = new HashSet<>(writePaths);
+    Set<String> combinedPaths = new TreeSet<>(writePaths);
     combinedPaths.addAll(readPaths);
 
-    if (combinedPaths.size() != 1) {
-      throw new IllegalArgumentException(
-          "ADLS should contain exactly one unique path, but found: "
-              + combinedPaths.size()
-              + " paths: "
-              + combinedPaths);
-    }
-    String uniquePath = combinedPaths.iterator().next();
-
-    ClientSecretCredential clientSecretCredential =
-        new ClientSecretCredentialBuilder()
-            .tenantId(tenantId)
-            .clientId(clientId)
-            .clientSecret(clientSecret)
-            .build();
-
-    DataLakeServiceClient dataLakeServiceClient =
-        new DataLakeServiceClientBuilder()
-            .endpoint(endpoint)
-            .credential(clientSecretCredential)
-            .buildClient();
+    ADLSLocationUtils.ADLSLocationParts locationParts = 
findCredentialLocation(combinedPaths);
 
     OffsetDateTime start = OffsetDateTime.now();
     OffsetDateTime expiry = OffsetDateTime.now().plusSeconds(tokenExpireSecs);
-    UserDelegationKey userDelegationKey = 
dataLakeServiceClient.getUserDelegationKey(start, expiry);
+    UserDelegationKey userDelegationKey = getUserDelegationKey(start, expiry);
 
     PathSasPermission pathSasPermission =
         new 
PathSasPermission().setReadPermission(true).setListPermission(true);
@@ -111,7 +95,6 @@ public class ADLSTokenGenerator implements 
CredentialGenerator<ADLSTokenCredenti
 
     DataLakeServiceSasSignatureValues signatureValues =
         new DataLakeServiceSasSignatureValues(expiry, pathSasPermission);
-    ADLSLocationUtils.ADLSLocationParts locationParts = 
ADLSLocationUtils.parseLocation(uniquePath);
     String sasToken =
         new DataLakeSasImplUtil(
                 signatureValues,
@@ -125,6 +108,84 @@ public class ADLSTokenGenerator implements 
CredentialGenerator<ADLSTokenCredenti
         locationParts.getAccountName(), sasToken, 
expiry.toInstant().toEpochMilli());
   }
 
+  UserDelegationKey getUserDelegationKey(OffsetDateTime start, OffsetDateTime 
expiry) {
+    ClientSecretCredential clientSecretCredential =
+        new ClientSecretCredentialBuilder()
+            .tenantId(tenantId)
+            .clientId(clientId)
+            .clientSecret(clientSecret)
+            .build();
+
+    DataLakeServiceClient dataLakeServiceClient =
+        new DataLakeServiceClientBuilder()
+            .endpoint(endpoint)
+            .credential(clientSecretCredential)
+            .buildClient();
+    return dataLakeServiceClient.getUserDelegationKey(start, expiry);
+  }
+
+  private ADLSLocationUtils.ADLSLocationParts 
findCredentialLocation(Set<String> paths) {
+    if (paths.isEmpty()) {
+      throw new IllegalArgumentException(
+          "ADLS credential context should contain at least one path");
+    }
+
+    List<ADLSLocationUtils.ADLSLocationParts> parsedLocations =
+        
paths.stream().map(ADLSLocationUtils::parseLocation).collect(Collectors.toList());
+    ADLSLocationUtils.ADLSLocationParts first = parsedLocations.get(0);
+    boolean hasDifferentScope =
+        parsedLocations.stream()
+            .anyMatch(
+                location ->
+                    
!storageAccountName.equalsIgnoreCase(location.getAccountName())
+                        || 
!first.getAccountName().equalsIgnoreCase(location.getAccountName())
+                        || 
!first.getContainer().equalsIgnoreCase(location.getContainer()));
+    if (hasDifferentScope) {
+      throw new IllegalArgumentException(
+          "ADLS paths must belong to the configured storage account and a 
single container, but found: "
+              + paths);
+    }
+
+    Map<String, ADLSLocationUtils.ADLSLocationParts> uniqueLocations = new 
LinkedHashMap<>();
+    parsedLocations.forEach(
+        location ->
+            uniqueLocations.putIfAbsent(
+                location.getContainer() + "\0" + 
ADLSLocationUtils.trimSlashes(location.getPath()),
+                location));
+    List<ADLSLocationUtils.ADLSLocationParts> locations = new 
ArrayList<>(uniqueLocations.values());
+
+    List<ADLSLocationUtils.ADLSLocationParts> rootLocations =
+        locations.stream()
+            .filter(
+                location ->
+                    locations.stream()
+                        .noneMatch(
+                            possibleParent ->
+                                possibleParent != location
+                                    && isDescendantOf(location, 
possibleParent)))
+            .collect(Collectors.toList());
+    if (rootLocations.size() != 1) {
+      throw new IllegalArgumentException(
+          "ADLS credential paths must resolve to exactly one root location, 
but found: "
+              + rootLocations.size()
+              + " roots in paths: "
+              + paths);
+    }
+
+    return rootLocations.get(0);
+  }
+
+  private static boolean isDescendantOf(
+      ADLSLocationUtils.ADLSLocationParts location,
+      ADLSLocationUtils.ADLSLocationParts possibleParent) {
+    String path = ADLSLocationUtils.trimSlashes(location.getPath());
+    String parentPath = 
ADLSLocationUtils.trimSlashes(possibleParent.getPath());
+    if (parentPath.isEmpty()) {
+      return !path.isEmpty();
+    }
+    return path.startsWith(parentPath + "/");
+  }
+
   @Override
   public void close() throws IOException {}
 }
diff --git 
a/bundles/azure/src/test/java/org/apache/gravitino/abs/credential/TestADLSTokenGenerator.java
 
b/bundles/azure/src/test/java/org/apache/gravitino/abs/credential/TestADLSTokenGenerator.java
new file mode 100644
index 0000000000..98bd98c56d
--- /dev/null
+++ 
b/bundles/azure/src/test/java/org/apache/gravitino/abs/credential/TestADLSTokenGenerator.java
@@ -0,0 +1,193 @@
+/*
+ * 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.abs.credential;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.azure.storage.file.datalake.models.UserDelegationKey;
+import java.time.OffsetDateTime;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import org.apache.gravitino.credential.ADLSTokenCredential;
+import org.apache.gravitino.credential.PathBasedCredentialContext;
+import org.apache.gravitino.storage.AzureProperties;
+import org.junit.jupiter.api.Test;
+
+class TestADLSTokenGenerator {
+
+  @Test
+  void generatesTokenForParentDirectoryOfMultiplePaths() throws Exception {
+    try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+      generator.initialize(createProperties());
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user",
+              Collections.emptySet(),
+              new HashSet<>(
+                  Arrays.asList(
+                      
"abfss://[email protected]/warehouse/table",
+                      
"abfss://[email protected]/warehouse/table/data",
+                      
"abfss://[email protected]/warehouse/table/metadata")));
+
+      ADLSTokenCredential credential = generator.generate(context);
+
+      assertEquals("account", credential.accountName());
+      assertTrue(credential.sasToken().contains("sr=d"));
+      assertTrue(credential.sasToken().contains("sdd=2"));
+    }
+  }
+
+  @Test
+  void treatsEquivalentLocationsAsOneRoot() throws Exception {
+    try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+      generator.initialize(createProperties());
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user",
+              Collections.emptySet(),
+              new HashSet<>(
+                  Arrays.asList(
+                      
"abfss://[email protected]/warehouse/table",
+                      
"abfs://[email protected]/warehouse/table/",
+                      
"abfss://[email protected]/warehouse/table/data")));
+
+      ADLSTokenCredential credential = generator.generate(context);
+
+      assertTrue(credential.sasToken().contains("sr=d"));
+      assertTrue(credential.sasToken().contains("sdd=2"));
+    }
+  }
+
+  @Test
+  void rejectsPathsWithoutProvidedCommonRoot() throws Exception {
+    String first = 
"abfss://[email protected]/warehouse/table/data";
+    String second = 
"abfss://[email protected]/warehouse/table/metadata";
+    try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+      generator.initialize(createProperties());
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user", Collections.emptySet(), new 
HashSet<>(Arrays.asList(first, second)));
+
+      IllegalArgumentException exception =
+          assertThrows(IllegalArgumentException.class, () -> 
generator.generate(context));
+
+      assertTrue(exception.getMessage().contains("exactly one root location"));
+      assertTrue(exception.getMessage().contains(first));
+      assertTrue(exception.getMessage().contains(second));
+    }
+  }
+
+  @Test
+  void rejectsSiblingPathsWithCommonStringPrefix() throws Exception {
+    String first = 
"abfss://[email protected]/warehouse/table";
+    String second = 
"abfss://[email protected]/warehouse/table2";
+    try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+      generator.initialize(createProperties());
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user", Collections.emptySet(), new 
HashSet<>(Arrays.asList(first, second)));
+
+      IllegalArgumentException exception =
+          assertThrows(IllegalArgumentException.class, () -> 
generator.generate(context));
+
+      assertTrue(exception.getMessage().contains("exactly one root location"));
+    }
+  }
+
+  @Test
+  void rejectsPathsFromDifferentContainers() throws Exception {
+    String first = 
"abfss://[email protected]/warehouse/table";
+    String second = 
"abfss://[email protected]/warehouse/table/data";
+    try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+      generator.initialize(createProperties());
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user", Collections.emptySet(), new 
HashSet<>(Arrays.asList(first, second)));
+
+      IllegalArgumentException exception =
+          assertThrows(IllegalArgumentException.class, () -> 
generator.generate(context));
+
+      assertTrue(exception.getMessage().contains(first));
+      assertTrue(exception.getMessage().contains(second));
+    }
+  }
+
+  @Test
+  void rejectsPathsFromDifferentStorageAccounts() throws Exception {
+    String first = 
"abfss://[email protected]/warehouse/table";
+    String second = 
"abfss://[email protected]/warehouse/table/data";
+    try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+      generator.initialize(createProperties());
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user", Collections.emptySet(), new 
HashSet<>(Arrays.asList(first, second)));
+
+      IllegalArgumentException exception =
+          assertThrows(IllegalArgumentException.class, () -> 
generator.generate(context));
+
+      assertTrue(exception.getMessage().contains(first));
+      assertTrue(exception.getMessage().contains(second));
+    }
+  }
+
+  @Test
+  void rejectsSinglePathFromUnconfiguredStorageAccount() throws Exception {
+    String path = 
"abfss://[email protected]/warehouse/table";
+    try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+      generator.initialize(createProperties());
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user", Collections.emptySet(), 
Collections.singleton(path));
+
+      IllegalArgumentException exception =
+          assertThrows(IllegalArgumentException.class, () -> 
generator.generate(context));
+
+      assertTrue(exception.getMessage().contains(path));
+    }
+  }
+
+  private static Map<String, String> createProperties() {
+    Map<String, String> properties = new HashMap<>();
+    properties.put(AzureProperties.GRAVITINO_AZURE_STORAGE_ACCOUNT_NAME, 
"account");
+    properties.put(AzureProperties.GRAVITINO_AZURE_TENANT_ID, "tenant");
+    properties.put(AzureProperties.GRAVITINO_AZURE_CLIENT_ID, "client");
+    properties.put(AzureProperties.GRAVITINO_AZURE_CLIENT_SECRET, "secret");
+    return properties;
+  }
+
+  private static class TestableADLSTokenGenerator extends ADLSTokenGenerator {
+    @Override
+    UserDelegationKey getUserDelegationKey(OffsetDateTime start, 
OffsetDateTime expiry) {
+      return new UserDelegationKey()
+          .setSignedObjectId("object")
+          .setSignedTenantId("tenant")
+          .setSignedStart(start)
+          .setSignedExpiry(expiry)
+          .setSignedService("b")
+          .setSignedVersion("2020-02-10")
+          .setValue(Base64.getEncoder().encodeToString(new byte[32]));
+    }
+  }
+}
diff --git a/docs/security/credential-vending.md 
b/docs/security/credential-vending.md
index 20b56966ad..09d2c0aab1 100644
--- a/docs/security/credential-vending.md
+++ b/docs/security/credential-vending.md
@@ -56,17 +56,17 @@ A credential using AWS IAM Roles for Service Accounts 
(IRSA) to access S3 with t
 | Gravitino server catalog properties | Gravitino Iceberg REST server 
configurations       | Description                                              
                                                 | Default value | Required |
 
|-------------------------------------|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------|---------------|----------|
 | `credential-providers`              | 
`gravitino.iceberg-rest.credential-providers`      | `aws-irsa` for AWS IRSA 
credential provider.                                                            
  | (none)        | Yes      |
-| `s3-role-arn`                       | `gravitino.iceberg-rest.s3-role-arn`   
            | The ARN of the IAM role to assume. Required for fine-grained 
path-based access control.                   | (none)        | Yes*     |
+| `s3-role-arn`                       | `gravitino.iceberg-rest.s3-role-arn`   
            | The ARN of the IAM role to assume. Falls back to the pod's 
`AWS_ROLE_ARN` when unset or blank.             | (none)        | No       |
 | `s3-region`                         | `gravitino.iceberg-rest.s3-region`     
            | The AWS region for STS operations. Used for fine-grained access 
control.                                  | (none)        | No       |
 | `s3-token-expire-in-secs`           | 
`gravitino.iceberg-rest.s3-token-expire-in-secs`   | Token expiration time in 
seconds for fine-grained credentials. Cannot exceed role's max session 
duration. | 3600          | No       |
 | `s3-token-service-endpoint`         | 
`gravitino.iceberg-rest.s3-token-service-endpoint` | Alternative STS endpoint 
for fine-grained credential generation. Useful for S3-compatible services.      
 | (none)        | No       |
 
-**Note**: `s3-role-arn` is required only when using fine-grained path-based 
access control with vended credentials. For basic IRSA usage without path 
restrictions, only `credential-providers=aws-irsa` is needed.
+**Note**: Set `s3-role-arn` or provide `AWS_ROLE_ARN` in the pod environment. 
Gravitino selects that role when credentials are requested. For table-path 
requests, it adds a session policy covering the requested data, metadata, and 
write locations. Catalog-level requests have no path to scope and return 
credentials with the selected role's full permissions.
 
 **Prerequisites for fine-grained mode:**
 - EKS cluster with IRSA properly configured
 - `AWS_WEB_IDENTITY_TOKEN_FILE` environment variable pointing to the service 
account token
-- IAM role with permissions to assume the target role specified in 
`s3-role-arn`
+- Target IAM role selected by `s3-role-arn` or `AWS_ROLE_ARN` with a trust 
policy allowing `sts:AssumeRoleWithWebIdentity` from the pod's Kubernetes 
service account
 - Target IAM role with necessary S3 permissions for the data locations
 
 #### S3 Secret Key Credential
@@ -136,6 +136,8 @@ A credential with static Azure storage account name and key.
 
 An ADLS token is a token credential with scoped privileges, by leveraging 
Azure [User Delegation 
Sas](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas).
 To use an ADLS token credential, you should create a Microsoft Entra ID 
service principal and grant it proper privileges.
 
+The SAS is scoped to one requested root directory that covers the table 
location and any requested write paths. A custom write path that is the parent 
of the table path results in a broader SAS scope. Requests with independent 
roots or paths in different storage accounts or containers are rejected.
+
 | Gravitino server catalog properties | Gravitino Iceberg REST server 
configurations        | Description                                             
            | Default value | Required |
 
|-------------------------------------|-----------------------------------------------------|---------------------------------------------------------------------|---------------|----------|
 | `credential-providers`              | 
`gravitino.iceberg-rest.credential-providers`       | `adls-token` for ADLS 
token credential.                             | (none)        | Yes      |

Reply via email to