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();
+ }
}