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 1b9c807f3d [Cherry-pick to branch-1.3] [#13369] fix(catalog-glue): 
Report rejected AWS credentials clearly (#13370) (#13379)
1b9c807f3d is described below

commit 1b9c807f3d7993d0698b6424f78c1fc3bf681933
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Sep 21 20:51:42 2026 +0800

    [Cherry-pick to branch-1.3] [#13369] fix(catalog-glue): Report rejected AWS 
credentials clearly (#13370) (#13379)
    
    **Cherry-pick Information:**
    - Original commit: 197e1adba9b073c7df890f8526e2102f232ac005
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: Yuhui <[email protected]>
---
 .../catalog/glue/GlueCatalogOperations.java        |  3 +-
 .../gravitino/catalog/glue/GlueClientProvider.java | 30 +++-----
 .../catalog/glue/GlueExceptionConverter.java       | 87 ++++++++++++++++++++++
 .../glue/TestGlueCatalogSchemaOperations.java      | 60 +++++++++++++++
 .../catalog/glue/TestGlueClientProvider.java       | 13 +++-
 .../catalog/glue/TestGlueExceptionConverter.java   | 41 ++++++++++
 6 files changed, 208 insertions(+), 26 deletions(-)

diff --git 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
index 8b36af4129..898e6964a2 100644
--- 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
+++ 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
@@ -46,7 +46,6 @@ import org.apache.gravitino.connector.CatalogInfo;
 import org.apache.gravitino.connector.CatalogOperations;
 import org.apache.gravitino.connector.HasPropertyMetadata;
 import org.apache.gravitino.connector.SupportsSchemas;
-import org.apache.gravitino.exceptions.ConnectionFailedException;
 import org.apache.gravitino.exceptions.NoSuchCatalogException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
@@ -166,7 +165,7 @@ public class GlueCatalogOperations implements 
CatalogOperations, SupportsSchemas
       applyCatalogId(catalogId, req::catalogId);
       glueClient.getDatabases(req.build());
     } catch (SdkException e) {
-      throw new ConnectionFailedException(e, "Failed to connect to AWS Glue: 
%s", e.getMessage());
+      throw GlueExceptionConverter.toConnectionException(e);
     }
   }
 
diff --git 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
index 0cc5bb9e90..be0dbe1925 100644
--- 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
+++ 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
@@ -23,6 +23,7 @@ import com.google.common.base.Preconditions;
 import java.net.URI;
 import java.util.Map;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
 import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
@@ -55,8 +56,9 @@ public final class GlueClientProvider {
    * @param config Catalog configuration properties.
    * @return A configured and ready-to-use {@link GlueClient}.
    * @throws IllegalArgumentException if {@code aws-region} is missing or 
blank, if only one of the
-   *     credential keys is provided, if {@code aws-glue-endpoint} is not a 
valid URI, or if no
-   *     usable AWS credential source can be resolved.
+   *     credential keys is provided, or if {@code aws-glue-endpoint} is not a 
valid URI
+   * @throws ConnectionFailedException if the configured credential provider 
cannot resolve
+   *     credentials
    */
   public static GlueClient buildClient(Map<String, String> config) {
     String region = config.get(GlueConstants.AWS_REGION);
@@ -97,31 +99,19 @@ public final class GlueClientProvider {
   }
 
   /**
-   * Eagerly resolves {@code credentialsProvider} to confirm a usable 
credential source exists,
-   * instead of leaving resolution to the first real Glue API call. Without 
this check, a catalog
-   * created with no static credentials and no usable default-chain source 
(env vars, instance
-   * profile, etc.) is stored successfully and then fails on every operation 
with a raw AWS SDK
-   * error that never mentions this connector's own credential properties.
+   * Eagerly resolves {@code credentialsProvider} when Glue operations are 
initialized, instead of
+   * leaving resolution to the first real Glue API call. This makes an 
explicit connection test or
+   * the first operation fail with an actionable connection error when no 
credential source is
+   * available. It does not authenticate static credentials; only an AWS API 
request can do that.
    *
-   * @throws IllegalArgumentException if no credentials can be resolved
+   * @throws ConnectionFailedException if no credentials can be resolved
    */
   @VisibleForTesting
   static void validateCredentials(AwsCredentialsProvider credentialsProvider) {
     try {
       credentialsProvider.resolveCredentials();
     } catch (SdkClientException e) {
-      if (!GlueExceptionConverter.isCredentialFailure(e)) {
-        throw new IllegalArgumentException(
-            "Failed to resolve AWS credentials for the Glue catalog: " + 
e.getMessage(), e);
-      }
-      throw new IllegalArgumentException(
-          String.format(
-              "No usable AWS credentials found for the Glue catalog. Set both 
'%s' and '%s' "
-                  + "catalog properties for static authentication, or ensure 
the default AWS "
-                  + "credential chain (environment variables, instance 
profile, web identity "
-                  + "token, etc.) can resolve credentials.",
-              GlueConstants.AWS_ACCESS_KEY_ID, 
GlueConstants.AWS_SECRET_ACCESS_KEY),
-          e);
+      throw GlueExceptionConverter.toConnectionException(e);
     }
   }
 
diff --git 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
index 4ab3ed9c9b..5725c75fb1 100644
--- 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
+++ 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
@@ -18,7 +18,9 @@
  */
 package org.apache.gravitino.catalog.glue;
 
+import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
 import org.apache.gravitino.exceptions.ForbiddenException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
@@ -27,6 +29,7 @@ import 
org.apache.gravitino.exceptions.TableAlreadyExistsException;
 import org.apache.gravitino.utils.ExceptionMessages;
 import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
 import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.exception.SdkException;
 import software.amazon.awssdk.services.glue.model.AccessDeniedException;
 import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
 import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
@@ -39,6 +42,20 @@ final class GlueExceptionConverter {
   private static final String NO_CREDENTIALS_MARKER =
       "Unable to load credentials from any of the providers";
 
+  private static final Set<String> AUTHENTICATION_ERROR_CODES =
+      Set.of(
+          "AuthFailure",
+          "ExpiredToken",
+          "ExpiredTokenException",
+          "IncompleteSignature",
+          "InvalidAccessKeyId",
+          "InvalidClientTokenId",
+          "InvalidSignatureException",
+          "RequestExpired",
+          "SignatureDoesNotMatch",
+          "TokenRefreshRequired",
+          "UnrecognizedClientException");
+
   private GlueExceptionConverter() {}
 
   /**
@@ -72,6 +89,57 @@ final class GlueExceptionConverter {
         e);
   }
 
+  /**
+   * Whether AWS Glue rejected credentials that were successfully resolved by 
the configured
+   * provider. Static credential providers can return any nonblank access-key 
pair locally, so only
+   * an AWS service response can establish whether that pair is authentic.
+   *
+   * @param e the service exception raised by AWS Glue
+   * @return true if AWS classified the failure as an authentication error
+   */
+  static boolean isAuthenticationFailure(GlueException e) {
+    AwsErrorDetails details = e.awsErrorDetails();
+    return details != null
+        && StringUtils.isNotBlank(details.errorCode())
+        && AUTHENTICATION_ERROR_CODES.contains(details.errorCode());
+  }
+
+  /**
+   * Converts an AWS Glue SDK failure raised by a connection probe into a 
connection error. Known
+   * credential failures name the connector properties that an operator can 
correct; authorization
+   * and transport failures retain the AWS or SDK detail without claiming the 
credentials are
+   * invalid.
+   *
+   * @param e the SDK failure raised by the connection probe
+   * @return an actionable connection failure
+   */
+  static ConnectionFailedException toConnectionException(SdkException e) {
+    if (e instanceof SdkClientException && 
isCredentialFailure((SdkClientException) e)) {
+      return new ConnectionFailedException(
+          e,
+          "Failed to authenticate with AWS Glue. No usable AWS credentials 
were found. Set both "
+              + "'%s' and '%s' catalog properties, or ensure the default AWS 
credential chain can "
+              + "resolve credentials.",
+          GlueConstants.AWS_ACCESS_KEY_ID,
+          GlueConstants.AWS_SECRET_ACCESS_KEY);
+    }
+    if (e instanceof GlueException && isAuthenticationFailure((GlueException) 
e)) {
+      return new ConnectionFailedException(
+          e,
+          "AWS Glue rejected the configured credentials. Verify the '%s' and 
'%s' catalog "
+              + "properties, or the configured default AWS credential source. 
AWS error: %s",
+          GlueConstants.AWS_ACCESS_KEY_ID,
+          GlueConstants.AWS_SECRET_ACCESS_KEY,
+          awsErrorDetail((GlueException) e));
+    }
+
+    String detail =
+        e instanceof GlueException
+            ? awsErrorDetail((GlueException) e)
+            : StringUtils.defaultIfBlank(e.getMessage(), 
e.getClass().getSimpleName());
+    return new ConnectionFailedException(e, "Failed to connect to AWS Glue: 
%s", detail);
+  }
+
   /**
    * Converts a {@link GlueException} to the appropriate Gravitino schema 
exception.
    *
@@ -80,6 +148,9 @@ final class GlueExceptionConverter {
    * @return a Gravitino or standard Java runtime exception
    */
   static RuntimeException toSchemaException(GlueException e, String context) {
+    if (isAuthenticationFailure(e)) {
+      return toAuthenticationException(e, context);
+    }
     if (e instanceof EntityNotFoundException) {
       return new NoSuchSchemaException(e, "%s does not exist", context);
     }
@@ -103,6 +174,9 @@ final class GlueExceptionConverter {
    * @return a Gravitino or standard Java runtime exception
    */
   static RuntimeException toTableException(GlueException e, String context) {
+    if (isAuthenticationFailure(e)) {
+      return toAuthenticationException(e, context);
+    }
     if (e instanceof EntityNotFoundException) {
       return new NoSuchTableException(e, "%s does not exist", context);
     }
@@ -118,6 +192,19 @@ final class GlueExceptionConverter {
     return new RuntimeException("Glue error: " + context + ": " + 
awsErrorDetail(e), e);
   }
 
+  private static RuntimeException toAuthenticationException(GlueException e, 
String context) {
+    return new RuntimeException(
+        String.format(
+            "Failed to authenticate with AWS Glue for %s. AWS rejected the 
configured "
+                + "credentials. Verify the '%s' and '%s' catalog properties, 
or the configured "
+                + "default AWS credential source. AWS error: %s",
+            context,
+            GlueConstants.AWS_ACCESS_KEY_ID,
+            GlueConstants.AWS_SECRET_ACCESS_KEY,
+            awsErrorDetail(e)),
+        e);
+  }
+
   /**
    * Renders the AWS-side detail of a Glue exception. AWS names the failing 
action and the resource
    * there, which is what the caller needs to act on; the error code is 
prefixed so the failure can
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
index 7c03aaeddb..6bebc1baac 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
@@ -42,6 +42,7 @@ import 
org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
 import software.amazon.awssdk.core.exception.SdkClientException;
 import software.amazon.awssdk.services.glue.GlueClient;
 import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
@@ -55,6 +56,7 @@ import 
software.amazon.awssdk.services.glue.model.GetDatabasesRequest;
 import software.amazon.awssdk.services.glue.model.GetDatabasesResponse;
 import software.amazon.awssdk.services.glue.model.GetTablesRequest;
 import software.amazon.awssdk.services.glue.model.GetTablesResponse;
+import software.amazon.awssdk.services.glue.model.GlueException;
 import software.amazon.awssdk.services.glue.model.Table;
 import software.amazon.awssdk.services.glue.model.UpdateDatabaseRequest;
 import software.amazon.awssdk.services.glue.model.UpdateDatabaseResponse;
@@ -86,6 +88,38 @@ class TestGlueCatalogSchemaOperations {
     assertTrue(exception.getMessage().contains("connection refused"));
   }
 
+  @Test
+  void testConnectionMapsMissingCredentialsToActionableMessage() {
+    SdkClientException cause =
+        SdkClientException.create("Unable to load credentials from any of the 
providers");
+    
when(mockClient.getDatabases(any(GetDatabasesRequest.class))).thenThrow(cause);
+
+    ConnectionFailedException exception =
+        assertThrows(
+            ConnectionFailedException.class,
+            () -> ops.testConnection(NameIdentifier.of("metalake", 
"catalog")));
+
+    assertEquals(cause, exception.getCause());
+    
assertTrue(exception.getMessage().contains(GlueConstants.AWS_ACCESS_KEY_ID));
+    
assertTrue(exception.getMessage().contains(GlueConstants.AWS_SECRET_ACCESS_KEY));
+  }
+
+  @Test
+  void testConnectionMapsRejectedCredentialsToActionableMessage() {
+    GlueException cause = invalidCredentialsException();
+    
when(mockClient.getDatabases(any(GetDatabasesRequest.class))).thenThrow(cause);
+
+    ConnectionFailedException exception =
+        assertThrows(
+            ConnectionFailedException.class,
+            () -> ops.testConnection(NameIdentifier.of("metalake", 
"catalog")));
+
+    assertEquals(cause, exception.getCause());
+    
assertTrue(exception.getMessage().contains(GlueConstants.AWS_ACCESS_KEY_ID));
+    
assertTrue(exception.getMessage().contains(GlueConstants.AWS_SECRET_ACCESS_KEY));
+    assertTrue(exception.getMessage().contains("UnrecognizedClientException"));
+  }
+
   // -------------------------------------------------------------------------
   // listSchemas
   // -------------------------------------------------------------------------
@@ -139,6 +173,20 @@ class TestGlueCatalogSchemaOperations {
     assertTrue(ex.getMessage().contains("aws-secret-access-key"));
   }
 
+  @Test
+  void testListSchemasMapsRejectedCredentialsToActionableMessage() {
+    Namespace ns = Namespace.of("metalake", "catalog");
+    GlueException cause = invalidCredentialsException();
+    
when(mockClient.getDatabases(any(GetDatabasesRequest.class))).thenThrow(cause);
+
+    RuntimeException ex = assertThrows(RuntimeException.class, () -> 
ops.listSchemas(ns));
+
+    assertEquals(cause, ex.getCause());
+    assertTrue(ex.getMessage().contains(GlueConstants.AWS_ACCESS_KEY_ID));
+    assertTrue(ex.getMessage().contains(GlueConstants.AWS_SECRET_ACCESS_KEY));
+    assertTrue(ex.getMessage().contains("UnrecognizedClientException"));
+  }
+
   @Test
   void testListSchemasRethrowsNonCredentialSdkClientException() {
     Namespace ns = Namespace.of("metalake", "catalog");
@@ -352,4 +400,16 @@ class TestGlueCatalogSchemaOperations {
     verify(mockClient).deleteDatabase(captor.capture());
     assertEquals("123456789012", captor.getValue().catalogId());
   }
+
+  private static GlueException invalidCredentialsException() {
+    return (GlueException)
+        GlueException.builder()
+            .message("The security token included in the request is invalid")
+            .awsErrorDetails(
+                AwsErrorDetails.builder()
+                    .errorCode("UnrecognizedClientException")
+                    .errorMessage("The security token included in the request 
is invalid")
+                    .build())
+            .build();
+  }
 }
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
index e3b1a2cb1c..665e76d72b 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
@@ -32,6 +32,7 @@ import static org.mockito.Mockito.mock;
 
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
 import org.junit.jupiter.api.Test;
 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
@@ -74,9 +75,10 @@ class TestGlueClientProvider {
         .when(provider)
         .resolveCredentials();
 
-    IllegalArgumentException ex =
+    ConnectionFailedException ex =
         assertThrows(
-            IllegalArgumentException.class, () -> 
GlueClientProvider.validateCredentials(provider));
+            ConnectionFailedException.class,
+            () -> GlueClientProvider.validateCredentials(provider));
     assertTrue(ex.getMessage().contains(AWS_ACCESS_KEY_ID));
     assertTrue(ex.getMessage().contains(AWS_SECRET_ACCESS_KEY));
   }
@@ -89,9 +91,10 @@ class TestGlueClientProvider {
     SdkClientException cause = SdkClientException.create("connection refused");
     doThrow(cause).when(provider).resolveCredentials();
 
-    IllegalArgumentException ex =
+    ConnectionFailedException ex =
         assertThrows(
-            IllegalArgumentException.class, () -> 
GlueClientProvider.validateCredentials(provider));
+            ConnectionFailedException.class,
+            () -> GlueClientProvider.validateCredentials(provider));
     assertEquals(cause, ex.getCause());
     assertTrue(ex.getMessage().contains("connection refused"));
     assertFalse(ex.getMessage().contains("No usable AWS credentials"));
@@ -155,6 +158,8 @@ class TestGlueClientProvider {
   void testBuildClientInvalidEndpointThrows() {
     Map<String, String> config = new HashMap<>();
     config.put(AWS_REGION, "us-east-1");
+    config.put(AWS_ACCESS_KEY_ID, "test");
+    config.put(AWS_SECRET_ACCESS_KEY, "test");
     config.put(AWS_GLUE_ENDPOINT, "not a valid uri ://");
 
     assertThrows(IllegalArgumentException.class, () -> 
GlueClientProvider.buildClient(config));
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
index 23a32114a7..bb6ff91378 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
@@ -71,6 +71,23 @@ public class TestGlueExceptionConverter {
     assertFalse(GlueExceptionConverter.isCredentialFailure(e));
   }
 
+  @Test
+  public void testIsAuthenticationFailureMatchesAwsErrorCode() {
+    GlueException e = invalidCredentialsException();
+
+    assertTrue(GlueExceptionConverter.isAuthenticationFailure(e));
+  }
+
+  @Test
+  public void testIsAuthenticationFailureRejectsAuthorizationError() {
+    AccessDeniedException e =
+        AccessDeniedException.builder()
+            
.awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build())
+            .build();
+
+    assertFalse(GlueExceptionConverter.isAuthenticationFailure(e));
+  }
+
   @Test
   public void testToCredentialExceptionIncludesContextAndPropertyNames() {
     SdkClientException cause =
@@ -84,6 +101,18 @@ public class TestGlueExceptionConverter {
     assertTrue(ex.getMessage().contains(GlueConstants.AWS_SECRET_ACCESS_KEY));
   }
 
+  @Test
+  public void testRejectedCredentialsIncludePropertyNamesAndAwsError() {
+    GlueException cause = invalidCredentialsException();
+
+    RuntimeException converted = 
GlueExceptionConverter.toSchemaException(cause, "listing schemas");
+
+    assertSame(cause, converted.getCause());
+    
assertTrue(converted.getMessage().contains(GlueConstants.AWS_ACCESS_KEY_ID));
+    
assertTrue(converted.getMessage().contains(GlueConstants.AWS_SECRET_ACCESS_KEY));
+    assertTrue(converted.getMessage().contains("UnrecognizedClientException"));
+  }
+
   @Test
   public void testSchemaAccessDeniedKeepsAwsMessage() {
     AccessDeniedException e =
@@ -218,4 +247,16 @@ public class TestGlueExceptionConverter {
         IllegalArgumentException.class,
         GlueExceptionConverter.toTableException(invalid, "table ctas_test"));
   }
+
+  private static GlueException invalidCredentialsException() {
+    return (GlueException)
+        GlueException.builder()
+            .message("The security token included in the request is invalid")
+            .awsErrorDetails(
+                AwsErrorDetails.builder()
+                    .errorCode("UnrecognizedClientException")
+                    .errorMessage("The security token included in the request 
is invalid")
+                    .build())
+            .build();
+  }
 }

Reply via email to