Make KMS endpoint configurable via kms.endpoint AWS property (#14246)

Co-authored-by: Thomas Powell <tpowell@palantir.com>
diff --git a/aws/src/integration/java/org/apache/iceberg/aws/TestDefaultAwsClientFactory.java b/aws/src/integration/java/org/apache/iceberg/aws/TestDefaultAwsClientFactory.java
index e9803c8..7a7e34f 100644
--- a/aws/src/integration/java/org/apache/iceberg/aws/TestDefaultAwsClientFactory.java
+++ b/aws/src/integration/java/org/apache/iceberg/aws/TestDefaultAwsClientFactory.java
@@ -30,6 +30,7 @@
 import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
 import software.amazon.awssdk.services.glue.GlueClient;
 import software.amazon.awssdk.services.glue.model.GetDatabaseRequest;
+import software.amazon.awssdk.services.kms.KmsClient;
 import software.amazon.awssdk.services.s3.S3Client;
 import software.amazon.awssdk.services.s3.model.GetObjectRequest;
 import software.amazon.awssdk.services.s3.model.S3Exception;
@@ -99,4 +100,16 @@
         .isInstanceOf(SdkClientException.class)
         .hasMessageContaining("Unable to execute HTTP request: unknown");
   }
+
+  @Test
+  public void testKmsEndpointOverride() {
+    Map<String, String> properties = Maps.newHashMap();
+    properties.put(AwsProperties.KMS_ENDPOINT, "https://unknown:1234");
+    AwsClientFactory factory = AwsClientFactories.from(properties);
+    KmsClient kmsClient = factory.kms();
+    assertThatThrownBy(kmsClient::listKeys)
+        .cause()
+        .isInstanceOf(SdkClientException.class)
+        .hasMessageContaining("Unable to execute HTTP request: unknown");
+  }
 }
diff --git a/aws/src/main/java/org/apache/iceberg/aws/AwsClientFactories.java b/aws/src/main/java/org/apache/iceberg/aws/AwsClientFactories.java
index cd5715b..0ee4bf2 100644
--- a/aws/src/main/java/org/apache/iceberg/aws/AwsClientFactories.java
+++ b/aws/src/main/java/org/apache/iceberg/aws/AwsClientFactories.java
@@ -39,6 +39,7 @@
 import software.amazon.awssdk.services.glue.GlueClient;
 import software.amazon.awssdk.services.glue.GlueClientBuilder;
 import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.awssdk.services.kms.KmsClientBuilder;
 import software.amazon.awssdk.services.s3.S3AsyncClient;
 import software.amazon.awssdk.services.s3.S3BaseClientBuilder;
 import software.amazon.awssdk.services.s3.S3Client;
@@ -155,6 +156,7 @@
       return KmsClient.builder()
           .applyMutation(awsClientProperties::applyClientRegionConfiguration)
           .applyMutation(httpClientProperties::applyHttpClientConfigurations)
+          .applyMutation(awsProperties::applyKmsEndpointConfigurations)
           .applyMutation(awsClientProperties::applyClientCredentialConfigurations)
           .applyMutation(awsClientProperties::applyRetryConfigurations)
           .build();
@@ -208,8 +210,9 @@
    * @deprecated Not for public use. To configure the endpoint for a client, please use {@link
    *     S3FileIOProperties#applyEndpointConfigurations(S3BaseClientBuilder)}, {@link
    *     AwsProperties#applyGlueEndpointConfigurations(GlueClientBuilder)}, or {@link
-   *     AwsProperties#applyDynamoDbEndpointConfigurations(DynamoDbClientBuilder)} accordingly. It
-   *     will be removed in 2.0.0
+   *     AwsProperties#applyDynamoDbEndpointConfigurations(DynamoDbClientBuilder)}, or {@link
+   *     AwsProperties#applyKmsEndpointConfigurations(KmsClientBuilder)} accordingly. It will be
+   *     removed in 2.0.0
    */
   @Deprecated
   public static <T extends SdkClientBuilder> void configureEndpoint(T builder, String endpoint) {
diff --git a/aws/src/main/java/org/apache/iceberg/aws/AwsProperties.java b/aws/src/main/java/org/apache/iceberg/aws/AwsProperties.java
index 62d541d..a37406e 100644
--- a/aws/src/main/java/org/apache/iceberg/aws/AwsProperties.java
+++ b/aws/src/main/java/org/apache/iceberg/aws/AwsProperties.java
@@ -41,6 +41,7 @@
 import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
 import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder;
 import software.amazon.awssdk.services.glue.GlueClientBuilder;
+import software.amazon.awssdk.services.kms.KmsClientBuilder;
 import software.amazon.awssdk.services.kms.model.DataKeySpec;
 import software.amazon.awssdk.services.kms.model.EncryptionAlgorithmSpec;
 
@@ -208,6 +209,14 @@
    */
   public static final String REST_SESSION_TOKEN = "rest.session-token";
 
+  /**
+   * Configure an alternative endpoint of the KMS service for AwsKeyManagementClient to access.
+   *
+   * <p>This could be used to use KMS key management with any KMS-compatible service that has a
+   * different endpoint
+   */
+  public static final String KMS_ENDPOINT = "kms.endpoint";
+
   /** Encryption algorithm used to encrypt/decrypt master table keys */
   public static final String KMS_ENCRYPTION_ALGORITHM_SPEC = "kms.encryption-algorithm-spec";
 
@@ -243,6 +252,7 @@
   private String restAccessKeyId;
   private String restSecretAccessKey;
   private String restSessionToken;
+  private final String kmsEndpoint;
   private EncryptionAlgorithmSpec kmsEncryptionAlgorithmSpec;
   private DataKeySpec kmsDataKeySpec;
 
@@ -268,6 +278,7 @@
 
     this.restSigningName = REST_SIGNING_NAME_DEFAULT;
 
+    this.kmsEndpoint = null;
     this.kmsEncryptionAlgorithmSpec = KMS_ENCRYPTION_ALGORITHM_SPEC_DEFAULT;
     this.kmsDataKeySpec = KMS_DATA_KEY_SPEC_DEFAULT;
   }
@@ -312,6 +323,7 @@
     this.restSecretAccessKey = properties.get(REST_SECRET_ACCESS_KEY);
     this.restSessionToken = properties.get(REST_SESSION_TOKEN);
 
+    this.kmsEndpoint = properties.get(KMS_ENDPOINT);
     this.kmsEncryptionAlgorithmSpec =
         EncryptionAlgorithmSpec.fromValue(
             properties.getOrDefault(
@@ -411,6 +423,19 @@
     configureEndpoint(builder, dynamoDbEndpoint);
   }
 
+  /**
+   * Override the endpoint for a KMS client.
+   *
+   * <p>Sample usage:
+   *
+   * <pre>
+   *     KmsClient.builder().applyMutation(awsProperties::applyKmsEndpointConfigurations)
+   * </pre>
+   */
+  public <T extends KmsClientBuilder> void applyKmsEndpointConfigurations(T builder) {
+    configureEndpoint(builder, kmsEndpoint);
+  }
+
   public Region restSigningRegion() {
     if (restSigningRegion == null) {
       this.restSigningRegion = DefaultAwsRegionProviderChain.builder().build().getRegion().id();
@@ -428,6 +453,10 @@
         this.restAccessKeyId, this.restSecretAccessKey, this.restSessionToken);
   }
 
+  public String kmsEndpoint() {
+    return this.kmsEndpoint;
+  }
+
   public EncryptionAlgorithmSpec kmsEncryptionAlgorithmSpec() {
     return this.kmsEncryptionAlgorithmSpec;
   }