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

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


The following commit(s) were added to refs/heads/main by this push:
     new bbb73f1a65 [#13977] fix(credential): Scope and validate cloud 
credentials (#13987)
bbb73f1a65 is described below

commit bbb73f1a656842969ab603f1a5978a6c5d2acb60
Author: roryqi <[email protected]>
AuthorDate: Thu Oct 22 08:58:56 2026 +0800

    [#13977] fix(credential): Scope and validate cloud credentials (#13987)
    
    ### What changes were proposed in this pull request?
    
    - Do not vend unscoped `aws-irsa` credentials for catalog-level
    requests. Keep table-path requests scoped by an STS session policy.
    - Normalize AWS and OSS STS role session names to fit provider character
    and length limits while retaining a stable user identifier.
    - Reject `wasb(s)` locations before vending an ADLS SAS that Iceberg
    would not look up.
    - Include every requested path in the OSS `ListObjects` policy for a
    bucket.
    - Add regression tests and update the IRSA credential-vending
    documentation.
    
    ### Why are the changes needed?
    
    Catalog-level IRSA requests could expose the assumed role's full
    permissions. Invalid session names prevented credential generation for
    some users. Blob-host ADLS locations could receive unusable SAS
    properties, and OSS requests with multiple paths omitted listing access
    to later paths.
    
    `main` already rejects ADLS paths outside the configured storage
    account, so this PR preserves that existing behavior while addressing
    the remaining issues.
    
    Fix: #13977
    
    ### Does this PR introduce _any_ user-facing change?
    
    Catalog-level requests no longer return `aws-irsa` credentials.
    `wasb(s)` locations now fail with a clear unsupported-location error.
    STS session names change to valid, bounded names. No property keys
    change.
    
    ### How was this patch tested?
    
    - Ran `:common:test`, `:bundles:aws:test`, `:bundles:azure:test`, and
    `:bundles:aliyun:test` with focused regression classes for the changed
    behavior and `-PskipITs`.
    - Ran Spotless on the four changed Java modules.
    - Ran `git diff --cached --check` before committing.
---
 .../oss/credential/OSSTokenGenerator.java          | 42 ++++++---------
 .../oss/credential/TestOSSTokenGenerator.java      | 45 ++++++++++++++++
 .../s3/credential/AwsIrsaCredentialGenerator.java  | 60 ++--------------------
 .../gravitino/s3/credential/S3TokenGenerator.java  |  3 +-
 .../credential/TestAwsIrsaCredentialGenerator.java | 45 ++++++++--------
 .../abs/credential/ADLSTokenGenerator.java         | 17 +++++-
 .../abs/credential/TestADLSTokenGenerator.java     | 34 ++++++++++++
 .../credential/CredentialProviderDelegator.java    |  2 +
 .../gravitino/credential/RoleSessionNameUtils.java | 51 ++++++++++++++++++
 .../credential/TestRoleSessionNameUtils.java       | 53 +++++++++++++++++++
 docs/security/credential-vending.md                |  2 +-
 .../TestMetadataObjectCredentialOperations.java    | 24 +++++++++
 12 files changed, 269 insertions(+), 109 deletions(-)

diff --git 
a/bundles/aliyun/src/main/java/org/apache/gravitino/oss/credential/OSSTokenGenerator.java
 
b/bundles/aliyun/src/main/java/org/apache/gravitino/oss/credential/OSSTokenGenerator.java
index 9ad6b8bf84..915fe08649 100644
--- 
a/bundles/aliyun/src/main/java/org/apache/gravitino/oss/credential/OSSTokenGenerator.java
+++ 
b/bundles/aliyun/src/main/java/org/apache/gravitino/oss/credential/OSSTokenGenerator.java
@@ -37,6 +37,7 @@ import org.apache.gravitino.credential.CredentialContext;
 import org.apache.gravitino.credential.CredentialGenerator;
 import org.apache.gravitino.credential.OSSTokenCredential;
 import org.apache.gravitino.credential.PathBasedCredentialContext;
+import org.apache.gravitino.credential.RoleSessionNameUtils;
 import org.apache.gravitino.credential.config.OSSCredentialConfig;
 import org.apache.gravitino.oss.credential.policy.Condition;
 import org.apache.gravitino.oss.credential.policy.Effect;
@@ -103,8 +104,7 @@ public class OSSTokenGenerator implements 
CredentialGenerator<OSSTokenCredential
     return client.getCredential();
   }
 
-  private String createPolicy(
-      Set<String> readLocations, Set<String> writeLocations, String region) {
+  String createPolicy(Set<String> readLocations, Set<String> writeLocations, 
String region) {
     Policy.Builder policyBuilder = Policy.builder().version("1");
 
     Statement.Builder allowGetObjectStatementBuilder =
@@ -113,7 +113,7 @@ public class OSSTokenGenerator implements 
CredentialGenerator<OSSTokenCredential
             .addAction("oss:GetObject")
             .addAction("oss:GetObjectVersion");
 
-    Map<String, Statement.Builder> bucketListStatementBuilder = new 
HashMap<>();
+    Map<String, StringLike.Builder> bucketListPrefixes = new HashMap<>();
     Map<String, Statement.Builder> bucketMetadataStatementBuilder = new 
HashMap<>();
 
     String arnPrefix = getArnPrefix(region);
@@ -124,14 +124,9 @@ public class OSSTokenGenerator implements 
CredentialGenerator<OSSTokenCredential
               URI uri = URI.create(location);
               
allowGetObjectStatementBuilder.addResource(getOssUriWithArn(arnPrefix, uri));
               String bucketArn = arnPrefix + getBucketName(uri);
-              bucketListStatementBuilder.computeIfAbsent(
-                  bucketArn,
-                  key ->
-                      Statement.builder()
-                          .effect(Effect.ALLOW)
-                          .addAction("oss:ListObjects")
-                          .addResource(key)
-                          .condition(getCondition(uri)));
+              bucketListPrefixes
+                  .computeIfAbsent(bucketArn, key -> StringLike.builder())
+                  
.addPrefix(concatPathWithSep(trimLeadingSlash(uri.getPath()), "*", "/"));
               bucketMetadataStatementBuilder.computeIfAbsent(
                   bucketArn,
                   key ->
@@ -156,10 +151,16 @@ public class OSSTokenGenerator implements 
CredentialGenerator<OSSTokenCredential
       policyBuilder.addStatement(allowPutObjectStatementBuilder.build());
     }
 
-    if (!bucketListStatementBuilder.isEmpty()) {
-      bucketListStatementBuilder
-          .values()
-          .forEach(statementBuilder -> 
policyBuilder.addStatement(statementBuilder.build()));
+    if (!bucketListPrefixes.isEmpty()) {
+      bucketListPrefixes.forEach(
+          (bucketArn, prefixes) ->
+              policyBuilder.addStatement(
+                  Statement.builder()
+                      .effect(Effect.ALLOW)
+                      .addAction("oss:ListObjects")
+                      .addResource(bucketArn)
+                      
.condition(Condition.builder().stringLike(prefixes.build()).build())
+                      .build()));
     } else {
       policyBuilder.addStatement(
           
Statement.builder().effect(Effect.ALLOW).addAction("oss:ListBucket").build());
@@ -176,15 +177,6 @@ public class OSSTokenGenerator implements 
CredentialGenerator<OSSTokenCredential
     }
   }
 
-  private Condition getCondition(URI uri) {
-    return Condition.builder()
-        .stringLike(
-            StringLike.builder()
-                .addPrefix(concatPathWithSep(trimLeadingSlash(uri.getPath()), 
"*", "/"))
-                .build())
-        .build();
-  }
-
   private String getArnPrefix(String region) {
     if (StringUtils.isNotEmpty(region)) {
       return "acs:oss:" + region + ":*:";
@@ -222,7 +214,7 @@ public class OSSTokenGenerator implements 
CredentialGenerator<OSSTokenCredential
   }
 
   private String getRoleName(String userName) {
-    return "gravitino_" + userName;
+    return RoleSessionNameUtils.create("gravitino_", userName);
   }
 
   @Override
diff --git 
a/bundles/aliyun/src/test/java/org/apache/gravitino/oss/credential/TestOSSTokenGenerator.java
 
b/bundles/aliyun/src/test/java/org/apache/gravitino/oss/credential/TestOSSTokenGenerator.java
new file mode 100644
index 0000000000..92678906d0
--- /dev/null
+++ 
b/bundles/aliyun/src/test/java/org/apache/gravitino/oss/credential/TestOSSTokenGenerator.java
@@ -0,0 +1,45 @@
+/*
+ * 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.oss.credential;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import org.junit.jupiter.api.Test;
+
+class TestOSSTokenGenerator {
+  @Test
+  void listPolicyIncludesEveryPathInTheSameBucket() throws Exception {
+    OSSTokenGenerator generator = new OSSTokenGenerator();
+    String policy =
+        generator.createPolicy(
+            Collections.singleton("oss://lake/warehouse/db/orders"),
+            new HashSet<>(Arrays.asList("oss://lake/data/orders", 
"oss://lake/metadata/orders")),
+            "cn-hangzhou");
+    JsonNode prefixes = new 
ObjectMapper().readTree(policy).findValue("oss:Prefix");
+    assertTrue(prefixes.toString().contains("warehouse/db/orders/*"));
+    assertTrue(prefixes.toString().contains("data/orders/*"));
+    assertTrue(prefixes.toString().contains("metadata/orders/*"));
+  }
+}
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 2026afbb56..cc5f33bf64 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
@@ -27,7 +27,6 @@ import java.util.Map;
 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;
@@ -35,11 +34,10 @@ import org.apache.gravitino.credential.AwsIrsaCredential;
 import org.apache.gravitino.credential.CredentialContext;
 import org.apache.gravitino.credential.CredentialGenerator;
 import org.apache.gravitino.credential.PathBasedCredentialContext;
+import org.apache.gravitino.credential.RoleSessionNameUtils;
 import org.apache.gravitino.credential.config.S3CredentialConfig;
 import org.apache.gravitino.s3.credential.webidentity.WebIdentityTokenSource;
 import org.apache.gravitino.s3.credential.webidentity.WebIdentityTokenSources;
-import software.amazon.awssdk.auth.credentials.AwsCredentials;
-import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
 import software.amazon.awssdk.policybuilder.iam.IamConditionOperator;
 import software.amazon.awssdk.policybuilder.iam.IamEffect;
 import software.amazon.awssdk.policybuilder.iam.IamPolicy;
@@ -48,7 +46,6 @@ import software.amazon.awssdk.policybuilder.iam.IamStatement;
 import software.amazon.awssdk.regions.Region;
 import software.amazon.awssdk.services.sts.StsClient;
 import software.amazon.awssdk.services.sts.StsClientBuilder;
-import 
software.amazon.awssdk.services.sts.auth.StsAssumeRoleWithWebIdentityCredentialsProvider;
 import 
software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest;
 import 
software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityResponse;
 import software.amazon.awssdk.services.sts.model.Credentials;
@@ -63,8 +60,6 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
   private String region;
   private String stsEndpoint;
   private boolean listLocationPrefix;
-  private StsClient basicModeStsClient;
-  private Map<String, StsAssumeRoleWithWebIdentityCredentialsProvider> 
basicModeProviders;
 
   /** Creates an AWS IRSA credential generator. */
   public AwsIrsaCredentialGenerator() {
@@ -88,14 +83,12 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
     this.region = s3CredentialConfig.region();
     this.stsEndpoint = s3CredentialConfig.stsEndpoint();
     this.listLocationPrefix = s3CredentialConfig.listLocationPrefix();
-    this.basicModeStsClient = createStsClient();
-    this.basicModeProviders = new ConcurrentHashMap<>();
   }
 
   @Override
   public AwsIrsaCredential generate(CredentialContext context) {
     if (!(context instanceof PathBasedCredentialContext)) {
-      return 
toAwsIrsaCredential(createBasicModeCredentials(context.getUserName()));
+      return null;
     }
 
     PathBasedCredentialContext pathBasedCredentialContext = 
(PathBasedCredentialContext) context;
@@ -122,26 +115,6 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
     return assumeRoleWithWebIdentity(effectiveRoleArn, userName, tokenContent, 
sessionPolicy);
   }
 
-  private AwsCredentials createBasicModeCredentials(String userName) {
-    return basicModeProviders
-        .computeIfAbsent(userName, this::createBasicModeCredentialsProvider)
-        .resolveCredentials();
-  }
-
-  private StsAssumeRoleWithWebIdentityCredentialsProvider 
createBasicModeCredentialsProvider(
-      String userName) {
-    return StsAssumeRoleWithWebIdentityCredentialsProvider.builder()
-        .stsClient(basicModeStsClient)
-        .refreshRequest(
-            () ->
-                createAssumeRoleWithWebIdentityRequest(
-                    getValidatedRoleArn(roleArn),
-                    userName,
-                    tokenSource.getToken(),
-                    Optional.empty()))
-        .build();
-  }
-
   private AwsIrsaCredential toAwsIrsaCredential(Credentials credentials) {
     return new AwsIrsaCredential(
         credentials.accessKeyId(),
@@ -150,26 +123,6 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
         credentials.expiration().toEpochMilli());
   }
 
-  private AwsIrsaCredential toAwsIrsaCredential(AwsCredentials credentials) {
-    if (!(credentials instanceof AwsSessionCredentials)) {
-      throw new IllegalStateException(
-          "AWS IRSA credentials must be of type AwsSessionCredentials. "
-              + "Check your EKS/IRSA configuration. Got: "
-              + credentials.getClass().getName());
-    }
-    AwsSessionCredentials sessionCredentials = (AwsSessionCredentials) 
credentials;
-    if (!sessionCredentials.expirationTime().isPresent()) {
-      throw new IllegalStateException(
-          "AWS IRSA session credentials must include an expiration time for 
vended credential"
-              + " refresh");
-    }
-    return new AwsIrsaCredential(
-        sessionCredentials.accessKeyId(),
-        sessionCredentials.secretAccessKey(),
-        sessionCredentials.sessionToken(),
-        sessionCredentials.expirationTime().get().toEpochMilli());
-  }
-
   private IamPolicy createSessionPolicy(
       Set<String> readLocations, Set<String> writeLocations, String region) {
     IamPolicy.Builder policyBuilder = IamPolicy.builder();
@@ -389,7 +342,7 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
     AssumeRoleWithWebIdentityRequest.Builder requestBuilder =
         AssumeRoleWithWebIdentityRequest.builder()
             .roleArn(roleArn)
-            .roleSessionName("gravitino_irsa_session_" + userName)
+            
.roleSessionName(RoleSessionNameUtils.create("gravitino_irsa_session_", 
userName))
             .durationSeconds(tokenExpireSecs)
             .webIdentityToken(webIdentityToken);
     sessionPolicy.ifPresent(policy -> requestBuilder.policy(policy.toJson()));
@@ -398,13 +351,6 @@ public class AwsIrsaCredentialGenerator implements 
CredentialGenerator<AwsIrsaCr
 
   @Override
   public void close() throws IOException {
-    if (basicModeProviders != null) {
-      
basicModeProviders.values().forEach(StsAssumeRoleWithWebIdentityCredentialsProvider::close);
-      basicModeProviders.clear();
-    }
-    if (basicModeStsClient != null) {
-      basicModeStsClient.close();
-    }
     if (tokenSource != null) {
       tokenSource.close();
     }
diff --git 
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/S3TokenGenerator.java
 
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/S3TokenGenerator.java
index 7cb3bc14e8..d234915e2c 100644
--- 
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/S3TokenGenerator.java
+++ 
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/S3TokenGenerator.java
@@ -32,6 +32,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.credential.CredentialContext;
 import org.apache.gravitino.credential.CredentialGenerator;
 import org.apache.gravitino.credential.PathBasedCredentialContext;
+import org.apache.gravitino.credential.RoleSessionNameUtils;
 import org.apache.gravitino.credential.S3TokenCredential;
 import org.apache.gravitino.credential.config.S3CredentialConfig;
 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
@@ -109,7 +110,7 @@ public class S3TokenGenerator implements 
CredentialGenerator<S3TokenCredential>
     AssumeRoleRequest.Builder builder =
         AssumeRoleRequest.builder()
             .roleArn(roleArn)
-            .roleSessionName("gravitino_" + userName)
+            .roleSessionName(RoleSessionNameUtils.create("gravitino_", 
userName))
             .durationSeconds(tokenExpireSecs)
             .policy(policy.toJson());
 
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 ca261f02d8..08f5369310 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
@@ -19,7 +19,7 @@
 package org.apache.gravitino.s3.credential;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -66,26 +66,16 @@ class TestAwsIrsaCredentialGenerator {
   }
 
   @Test
-  void basicModeUsesConfiguredTokenSourceWithoutSessionPolicy(@TempDir Path 
dir)
-      throws IOException {
+  void catalogRequestDoesNotVendUnscopedCredentials(@TempDir Path dir) throws 
IOException {
     try (AwsIrsaCredentialGenerator generator = new 
AwsIrsaCredentialGenerator()) {
       generator.initialize(createProperties(dir));
-
-      AwsIrsaCredential credential = generator.generate(() -> "test-user");
-
-      assertEquals("access-key", credential.accessKeyId());
-      assertEquals("secret-key", credential.secretAccessKey());
-      assertEquals("session-token", credential.sessionToken());
+      assertNull(generator.generate(() -> "test-user"));
     }
-
-    String body = URLDecoder.decode(requestBody.get(), 
StandardCharsets.UTF_8.name());
-    assertTrue(body.contains("WebIdentityToken=configured-token"));
-    
assertTrue(body.contains("RoleSessionName=gravitino_irsa_session_test-user"));
-    assertFalse(body.contains("Policy="));
+    assertEquals(0, requestCount.get());
   }
 
   @Test
-  void 
basicModeFallsBackToEnvironmentRoleArnWhenConfiguredRoleIsBlank(@TempDir Path 
dir)
+  void pathModeFallsBackToEnvironmentRoleArnWhenConfiguredRoleIsBlank(@TempDir 
Path dir)
       throws IOException {
     Map<String, String> properties = createProperties(dir);
     properties.put("s3-role-arn", " ");
@@ -94,7 +84,7 @@ class TestAwsIrsaCredentialGenerator {
         new AwsIrsaCredentialGenerator(() -> 
"arn:aws:iam::123456789012:role/environment-role")) {
       generator.initialize(properties);
 
-      AwsIrsaCredential credential = generator.generate(() -> "test-user");
+      AwsIrsaCredential credential = generator.generate(pathContext());
 
       assertEquals("access-key", credential.accessKeyId());
     }
@@ -126,7 +116,7 @@ class TestAwsIrsaCredentialGenerator {
   }
 
   @Test
-  void basicModeFailsWhenNoRoleArnIsAvailable(@TempDir Path dir) throws 
IOException {
+  void pathModeFailsWhenNoRoleArnIsAvailable(@TempDir Path dir) throws 
IOException {
     Map<String, String> properties = createProperties(dir);
     properties.remove("s3-role-arn");
 
@@ -134,21 +124,23 @@ class TestAwsIrsaCredentialGenerator {
       generator.initialize(properties);
 
       IllegalStateException exception =
-          assertThrows(IllegalStateException.class, () -> 
generator.generate(() -> "test-user"));
+          assertThrows(IllegalStateException.class, () -> 
generator.generate(pathContext()));
 
       assertTrue(exception.getMessage().contains("No role ARN available"));
     }
   }
 
   @Test
-  void basicModeReusesCachedStsCredentials(@TempDir Path dir) throws 
IOException {
+  void pathRequestUsesSessionPolicy(@TempDir Path dir) throws IOException {
     try (AwsIrsaCredentialGenerator generator = new 
AwsIrsaCredentialGenerator()) {
       generator.initialize(createProperties(dir));
-
-      generator.generate(() -> "test-user");
-      generator.generate(() -> "test-user");
+      AwsIrsaCredential credential = generator.generate(pathContext());
+      assertEquals("access-key", credential.accessKeyId());
     }
-
+    String body = URLDecoder.decode(requestBody.get(), 
StandardCharsets.UTF_8.name());
+    assertTrue(body.contains("WebIdentityToken=configured-token"));
+    
assertTrue(body.contains("RoleSessionName=gravitino_irsa_session_test-user"));
+    assertTrue(body.contains("Policy="));
     assertEquals(1, requestCount.get());
   }
 
@@ -160,7 +152,7 @@ class TestAwsIrsaCredentialGenerator {
     try (AwsIrsaCredentialGenerator generator = new 
AwsIrsaCredentialGenerator()) {
       generator.initialize(properties);
 
-      AwsIrsaCredential credential = generator.generate(() -> "test-user");
+      AwsIrsaCredential credential = generator.generate(pathContext());
 
       assertEquals("access-key", credential.accessKeyId());
       assertEquals("secret-key", credential.secretAccessKey());
@@ -200,6 +192,11 @@ class TestAwsIrsaCredentialGenerator {
     return properties;
   }
 
+  private PathBasedCredentialContext pathContext() {
+    return new PathBasedCredentialContext(
+        "test-user", Collections.emptySet(), 
Collections.singleton("s3://bucket/object"));
+  }
+
   private void handleAssumeRoleWithWebIdentity(HttpExchange exchange) throws 
IOException {
     requestCount.incrementAndGet();
     requestBody.set(new String(exchange.getRequestBody().readAllBytes(), 
StandardCharsets.UTF_8));
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 fbc69c343a..73611b31fc 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
@@ -28,6 +28,7 @@ import 
com.azure.storage.file.datalake.models.UserDelegationKey;
 import com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues;
 import com.azure.storage.file.datalake.sas.PathSasPermission;
 import java.io.IOException;
+import java.net.URI;
 import java.time.OffsetDateTime;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -131,7 +132,7 @@ public class ADLSTokenGenerator implements 
CredentialGenerator<ADLSTokenCredenti
     }
 
     List<ADLSLocationUtils.ADLSLocationParts> parsedLocations =
-        
paths.stream().map(ADLSLocationUtils::parseLocation).collect(Collectors.toList());
+        
paths.stream().map(ADLSTokenGenerator::parseSupportedLocation).collect(Collectors.toList());
     ADLSLocationUtils.ADLSLocationParts first = parsedLocations.get(0);
     boolean hasDifferentScope =
         parsedLocations.stream()
@@ -175,6 +176,20 @@ public class ADLSTokenGenerator implements 
CredentialGenerator<ADLSTokenCredenti
     return rootLocations.get(0);
   }
 
+  private static ADLSLocationUtils.ADLSLocationParts 
parseSupportedLocation(String path) {
+    URI uri = URI.create(path);
+    if (!"abfs".equalsIgnoreCase(uri.getScheme()) && 
!"abfss".equalsIgnoreCase(uri.getScheme())) {
+      throw new IllegalArgumentException(
+          "adls-token requires an abfs or abfss location; unsupported 
location: " + path);
+    }
+    ADLSLocationUtils.ADLSLocationParts location = 
ADLSLocationUtils.parseLocation(path);
+    if (!(location.getAccountName() + "." + ADLSTokenCredential.ADLS_DOMAIN)
+        .equalsIgnoreCase(uri.getHost())) {
+      throw new IllegalArgumentException("Invalid ADLS storage account in 
location: " + path);
+    }
+    return location;
+  }
+
   private static boolean isDescendantOf(
       ADLSLocationUtils.ADLSLocationParts location,
       ADLSLocationUtils.ADLSLocationParts possibleParent) {
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
index 98bd98c56d..da5ac37514 100644
--- 
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
@@ -168,6 +168,40 @@ class TestADLSTokenGenerator {
     }
   }
 
+  @Test
+  void rejectsBlobHostLocationsBeforeVendingSas() throws Exception {
+    String path = 
"wasbs://[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("abfs or abfss"));
+      assertTrue(exception.getMessage().contains(path));
+    }
+  }
+
+  @Test
+  void providerPreservesUnsupportedLocationError() throws Exception {
+    String path = 
"wasbs://[email protected]/warehouse/table";
+    try (ADLSTokenProvider provider = new ADLSTokenProvider()) {
+      provider.initialize(createProperties());
+      PathBasedCredentialContext context =
+          new PathBasedCredentialContext(
+              "test-user", Collections.emptySet(), 
Collections.singleton(path));
+
+      IllegalArgumentException exception =
+          assertThrows(IllegalArgumentException.class, () -> 
provider.getCredential(context));
+
+      assertTrue(exception.getMessage().contains("abfs or abfss"));
+      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");
diff --git 
a/common/src/main/java/org/apache/gravitino/credential/CredentialProviderDelegator.java
 
b/common/src/main/java/org/apache/gravitino/credential/CredentialProviderDelegator.java
index 50da186baa..99a2ceb36c 100644
--- 
a/common/src/main/java/org/apache/gravitino/credential/CredentialProviderDelegator.java
+++ 
b/common/src/main/java/org/apache/gravitino/credential/CredentialProviderDelegator.java
@@ -63,6 +63,8 @@ public abstract class CredentialProviderDelegator<T extends 
Credential>
   public Credential getCredential(CredentialContext context) {
     try {
       return generator.generate(context);
+    } catch (IllegalArgumentException e) {
+      throw e;
     } catch (Exception e) {
       throw new RuntimeException(
           "Failed to generate credential using " + getGeneratorClassName(), e);
diff --git 
a/common/src/main/java/org/apache/gravitino/credential/RoleSessionNameUtils.java
 
b/common/src/main/java/org/apache/gravitino/credential/RoleSessionNameUtils.java
new file mode 100644
index 0000000000..3fb5ede5ae
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/credential/RoleSessionNameUtils.java
@@ -0,0 +1,51 @@
+/*
+ * 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.credential;
+
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+/** Builds cloud STS role session names from arbitrary user names. */
+public final class RoleSessionNameUtils {
+  private static final int MAX_LENGTH = 64;
+
+  private RoleSessionNameUtils() {}
+
+  /**
+   * Builds an ASCII session name with a stable suffix identifying the 
original user.
+   *
+   * @param prefix the provider's session name prefix
+   * @param userName the authenticated user name
+   * @return a session name accepted by AWS and Aliyun STS
+   */
+  public static String create(String prefix, String userName) {
+    String suffix =
+        "_"
+            + UUID.nameUUIDFromBytes(userName.getBytes(StandardCharsets.UTF_8))
+                .toString()
+                .substring(0, 16);
+    String cleaned = userName.replaceAll("[^[email protected]]", "_");
+    int available = MAX_LENGTH - prefix.length() - suffix.length();
+    if (available < 1) {
+      throw new IllegalArgumentException("Role session prefix is too long");
+    }
+    return prefix + cleaned.substring(0, Math.min(cleaned.length(), 
available)) + suffix;
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/credential/TestRoleSessionNameUtils.java
 
b/common/src/test/java/org/apache/gravitino/credential/TestRoleSessionNameUtils.java
new file mode 100644
index 0000000000..1dcf0da0c1
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/credential/TestRoleSessionNameUtils.java
@@ -0,0 +1,53 @@
+/*
+ * 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.credential;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class TestRoleSessionNameUtils {
+  @Test
+  void sessionNamesAreValidAndDistinguishable() {
+    String nonAscii = new String(Character.toChars(0x1F600));
+    String first = RoleSessionNameUtils.create("gravitino_irsa_session_", 
"first name/" + nonAscii);
+    String second =
+        RoleSessionNameUtils.create("gravitino_irsa_session_", "first name:" + 
nonAscii);
+    assertTrue(first.matches("[[email protected]]{2,64}"));
+    assertNotEquals(first, second);
+    assertEquals(
+        first, RoleSessionNameUtils.create("gravitino_irsa_session_", "first 
name/" + nonAscii));
+
+    String longName = "[email protected]";
+    assertTrue(RoleSessionNameUtils.create("gravitino_irsa_session_", 
longName).length() <= 64);
+    assertTrue(RoleSessionNameUtils.create("gravitino_", longName).length() <= 
64);
+  }
+
+  @Test
+  void replacesCharactersRejectedByAliyun() {
+    String sessionName =
+        RoleSessionNameUtils.create("gravitino_", 
"alice+etl=prod,[email protected]");
+
+    assertTrue(sessionName.matches("[[email protected]]{2,64}"));
+    
assertTrue(sessionName.startsWith("[email protected]_"));
+  }
+}
diff --git a/docs/security/credential-vending.md 
b/docs/security/credential-vending.md
index c30cf9012e..d58dcb78ae 100755
--- a/docs/security/credential-vending.md
+++ b/docs/security/credential-vending.md
@@ -230,7 +230,7 @@ For Gravitino running on EKS. Instead of an access key, 
Gravitino uses its pod's
 | `s3-token-expire-in-secs`   | Session lifetime of the vended credentials. 
Cannot exceed the role's maximum session duration.                              
              | 3600          | No       |
 | `s3-token-service-endpoint` | Alternative STS endpoint, for S3-compatible 
storage.                                                                        
              | (none)        | No       |
 
-For path-based requests, Gravitino always scopes the credentials with a 
session policy covering the requested data, metadata, and write locations. 
Catalog-level requests do not include a session policy and inherit the selected 
role's permissions. The role is selected from `s3-role-arn` when configured, or 
from the pod's `AWS_ROLE_ARN` otherwise.
+For path-based requests, Gravitino always scopes the credentials with a 
session policy covering the requested data, metadata, and write locations. 
Catalog-level requests do not vend `aws-irsa` credentials because they provide 
no paths for a session policy. The role is selected from `s3-role-arn` when 
configured, or from the pod's `AWS_ROLE_ARN` otherwise.
 
 The selected role needs the same permission policy as `s3-token` above. Its 
trust policy must allow `sts:AssumeRoleWithWebIdentity` from the pod's 
Kubernetes service account identity.
 
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectCredentialOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectCredentialOperations.java
index c1e814f0d3..8e9d06c95d 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectCredentialOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectCredentialOperations.java
@@ -34,6 +34,7 @@ import org.apache.gravitino.credential.Credential;
 import org.apache.gravitino.credential.CredentialOperationDispatcher;
 import org.apache.gravitino.credential.S3SecretKeyCredential;
 import org.apache.gravitino.dto.responses.CredentialResponse;
+import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.rest.RESTUtils;
 import org.glassfish.jersey.internal.inject.AbstractBinder;
@@ -94,6 +95,29 @@ public class TestMetadataObjectCredentialOperations extends 
JerseyTest {
         MetadataObjects.parse("catalog.schema.fileset", 
MetadataObject.Type.FILESET));
   }
 
+  @Test
+  public void testInvalidCredentialLocationResponse() {
+    String path = 
"wasbs://[email protected]/warehouse/table";
+    when(credentialOperationDispatcher.getCredentials(any(), any()))
+        .thenThrow(
+            new IllegalArgumentException(
+                "adls-token requires an abfs or abfss location; unsupported 
location: " + path));
+
+    Response response =
+        target(basePath(metalake))
+            .path("fileset")
+            .path("catalog.schema.fileset")
+            .path("credentials")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
response.getStatus());
+    ErrorResponse error = response.readEntity(ErrorResponse.class);
+    Assertions.assertTrue(error.getMessage().contains("abfs or abfss"));
+    Assertions.assertTrue(error.getMessage().contains(path));
+  }
+
   private void testGetCredentialsForObject(MetadataObject metadataObject) {
 
     S3SecretKeyCredential credential = new S3SecretKeyCredential("access-id", 
"secret-key");

Reply via email to