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

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


The following commit(s) were added to refs/heads/main by this push:
     new 0f070dee3a GCP: Support CMEK (kms-key-name) for GCS server-side 
encryption (#18151)
0f070dee3a is described below

commit 0f070dee3aea21e963f4dd5f337b0b050f75c569
Author: S N Munendra <[email protected]>
AuthorDate: Mon Oct 12 22:37:51 2026 +0530

    GCP: Support CMEK (kms-key-name) for GCS server-side encryption (#18151)
    
    GCSOutputStream previously supported only CSEK (gcs.encryption-key), where
    the raw key is sent to GCS on every request. This adds CMEK support via a
    new gcs.kms-key-name property: the caller passes only the Cloud KMS key
    resource name, and GCS performs encryption/decryption server-side, so the
    caller's credentials need no KMS permission.
    
    The write path emits BlobWriteOption.kmsKeyName(...) when the property is
    set; reads require no change since CMEK decryption is automatic. CSEK and
    CMEK are mutually exclusive (GCS rejects both on one object), enforced at
    property-parse time with a fail-fast Preconditions check.
---
 .../java/org/apache/iceberg/gcp/GCPProperties.java | 18 ++++++++++++++
 .../apache/iceberg/gcp/gcs/GCSOutputStream.java    |  3 +++
 .../org/apache/iceberg/gcp/TestGCPProperties.java  | 29 ++++++++++++++++++++++
 .../iceberg/gcp/gcs/TestGCSOutputStream.java       | 29 +++++++++++++++++++++-
 4 files changed, 78 insertions(+), 1 deletion(-)

diff --git a/gcp/src/main/java/org/apache/iceberg/gcp/GCPProperties.java 
b/gcp/src/main/java/org/apache/iceberg/gcp/GCPProperties.java
index 2702ce565d..de736dc69b 100644
--- a/gcp/src/main/java/org/apache/iceberg/gcp/GCPProperties.java
+++ b/gcp/src/main/java/org/apache/iceberg/gcp/GCPProperties.java
@@ -44,6 +44,12 @@ public class GCPProperties implements Serializable {
   public static final String GCS_ENCRYPTION_KEY = "gcs.encryption-key";
   public static final String GCS_USER_PROJECT = "gcs.user-project";
 
+  /**
+   * Cloud KMS key resource name for CMEK server-side encryption. Mutually 
exclusive with {@link
+   * #GCS_ENCRYPTION_KEY} (CSEK).
+   */
+  public static final String GCS_KMS_KEY_NAME = "gcs.kms-key-name";
+
   public static final String GCS_CHANNEL_READ_CHUNK_SIZE = 
"gcs.channel.read.chunk-size-bytes";
   public static final String GCS_CHANNEL_WRITE_CHUNK_SIZE = 
"gcs.channel.write.chunk-size-bytes";
 
@@ -87,6 +93,7 @@ public class GCPProperties implements Serializable {
 
   private String gcsDecryptionKey;
   private String gcsEncryptionKey;
+  private String gcsKmsKeyName;
   private String gcsUserProject;
 
   private Integer gcsChannelReadChunkSize;
@@ -151,8 +158,15 @@ public class GCPProperties implements Serializable {
 
     gcsDecryptionKey = properties.get(GCS_DECRYPTION_KEY);
     gcsEncryptionKey = properties.get(GCS_ENCRYPTION_KEY);
+    gcsKmsKeyName = properties.get(GCS_KMS_KEY_NAME);
     gcsUserProject = properties.get(GCS_USER_PROJECT);
 
+    Preconditions.checkState(
+        !(gcsEncryptionKey != null && gcsKmsKeyName != null),
+        "Invalid encryption settings: must not configure both %s (CSEK) and %s 
(CMEK)",
+        GCS_ENCRYPTION_KEY,
+        GCS_KMS_KEY_NAME);
+
     if (properties.containsKey(GCS_CHANNEL_READ_CHUNK_SIZE)) {
       gcsChannelReadChunkSize = 
Integer.parseInt(properties.get(GCS_CHANNEL_READ_CHUNK_SIZE));
     }
@@ -219,6 +233,10 @@ public class GCPProperties implements Serializable {
     return Optional.ofNullable(gcsEncryptionKey);
   }
 
+  public Optional<String> kmsKeyName() {
+    return Optional.ofNullable(gcsKmsKeyName);
+  }
+
   public Optional<String> projectId() {
     return Optional.ofNullable(projectId);
   }
diff --git a/gcp/src/main/java/org/apache/iceberg/gcp/gcs/GCSOutputStream.java 
b/gcp/src/main/java/org/apache/iceberg/gcp/gcs/GCSOutputStream.java
index 3fb8aa4801..fa7d5a9196 100644
--- a/gcp/src/main/java/org/apache/iceberg/gcp/gcs/GCSOutputStream.java
+++ b/gcp/src/main/java/org/apache/iceberg/gcp/gcs/GCSOutputStream.java
@@ -106,6 +106,9 @@ class GCSOutputStream extends PositionOutputStream {
     gcpProperties
         .encryptionKey()
         .ifPresent(key -> 
writeOptions.add(BlobWriteOption.encryptionKey(key)));
+    gcpProperties
+        .kmsKeyName()
+        .ifPresent(kmsKeyName -> 
writeOptions.add(BlobWriteOption.kmsKeyName(kmsKeyName)));
     gcpProperties
         .userProject()
         .ifPresent(userProject -> 
writeOptions.add(BlobWriteOption.userProject(userProject)));
diff --git a/gcp/src/test/java/org/apache/iceberg/gcp/TestGCPProperties.java 
b/gcp/src/test/java/org/apache/iceberg/gcp/TestGCPProperties.java
index 0ec2183fc3..d0fb1979ca 100644
--- a/gcp/src/test/java/org/apache/iceberg/gcp/TestGCPProperties.java
+++ b/gcp/src/test/java/org/apache/iceberg/gcp/TestGCPProperties.java
@@ -18,6 +18,8 @@
  */
 package org.apache.iceberg.gcp;
 
+import static org.apache.iceberg.gcp.GCPProperties.GCS_ENCRYPTION_KEY;
+import static org.apache.iceberg.gcp.GCPProperties.GCS_KMS_KEY_NAME;
 import static org.apache.iceberg.gcp.GCPProperties.GCS_NO_AUTH;
 import static 
org.apache.iceberg.gcp.GCPProperties.GCS_OAUTH2_REFRESH_CREDENTIALS_ENABLED;
 import static 
org.apache.iceberg.gcp.GCPProperties.GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT;
@@ -50,6 +52,33 @@ public class TestGCPProperties {
     assertThat(gcpProperties.oauth2Token()).isNotPresent();
   }
 
+  @Test
+  public void testKmsKeyName() {
+    GCPProperties gcpProperties =
+        new GCPProperties(
+            ImmutableMap.of(GCS_KMS_KEY_NAME, 
"projects/p/locations/l/keyRings/r/cryptoKeys/k"));
+    assertThat(gcpProperties.kmsKeyName())
+        .get()
+        .isEqualTo("projects/p/locations/l/keyRings/r/cryptoKeys/k");
+    assertThat(gcpProperties.encryptionKey()).isNotPresent();
+
+    assertThat(new GCPProperties().kmsKeyName()).isNotPresent();
+  }
+
+  @Test
+  public void testKmsKeyNameWithEncryptionKey() {
+    assertThatIllegalStateException()
+        .isThrownBy(
+            () ->
+                new GCPProperties(
+                    ImmutableMap.of(
+                        GCS_ENCRYPTION_KEY, "csek", GCS_KMS_KEY_NAME, 
"projects/p/cryptoKeys/k")))
+        .withMessage(
+            String.format(
+                "Invalid encryption settings: must not configure both %s 
(CSEK) and %s (CMEK)",
+                GCS_ENCRYPTION_KEY, GCS_KMS_KEY_NAME));
+  }
+
   @Test
   public void refreshCredentialsEndpointSet() {
     GCPProperties gcpProperties =
diff --git 
a/gcp/src/test/java/org/apache/iceberg/gcp/gcs/TestGCSOutputStream.java 
b/gcp/src/test/java/org/apache/iceberg/gcp/gcs/TestGCSOutputStream.java
index ebf56ffb7e..8444688a0f 100644
--- a/gcp/src/test/java/org/apache/iceberg/gcp/gcs/TestGCSOutputStream.java
+++ b/gcp/src/test/java/org/apache/iceberg/gcp/gcs/TestGCSOutputStream.java
@@ -19,9 +19,14 @@
 package org.apache.iceberg.gcp.gcs;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
 import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.BlobInfo;
 import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BlobWriteOption;
 import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -30,7 +35,9 @@ import java.util.UUID;
 import java.util.stream.Stream;
 import org.apache.iceberg.gcp.GCPProperties;
 import org.apache.iceberg.metrics.MetricsContext;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 
 public class TestGCSOutputStream {
   private static final String BUCKET = "test-bucket";
@@ -53,6 +60,21 @@ public class TestGCSOutputStream {
             });
   }
 
+  @Test
+  public void testWriteWithKmsKeyName() {
+    String kmsKeyName = "projects/p/locations/l/keyRings/r/cryptoKeys/k";
+    GCPProperties cmekProperties =
+        new GCPProperties(ImmutableMap.of(GCPProperties.GCS_KMS_KEY_NAME, 
kmsKeyName));
+    Storage spyStorage = spy(storage);
+    BlobId blobId = randomBlobId();
+
+    writeAndVerify(spyStorage, blobId, randomData(1024), true, cmekProperties);
+
+    ArgumentCaptor<BlobWriteOption> options = 
ArgumentCaptor.forClass(BlobWriteOption.class);
+    verify(spyStorage).writer(any(BlobInfo.class), options.capture());
+    
assertThat(options.getAllValues()).contains(BlobWriteOption.kmsKeyName(kmsKeyName));
+  }
+
   @Test
   public void testMultipleClose() throws IOException {
     GCSOutputStream stream =
@@ -62,8 +84,13 @@ public class TestGCSOutputStream {
   }
 
   private void writeAndVerify(Storage client, BlobId uri, byte[] data, boolean 
arrayWrite) {
+    writeAndVerify(client, uri, data, arrayWrite, properties);
+  }
+
+  private void writeAndVerify(
+      Storage client, BlobId uri, byte[] data, boolean arrayWrite, 
GCPProperties gcpProperties) {
     try (GCSOutputStream stream =
-        new GCSOutputStream(client, uri, properties, 
MetricsContext.nullMetrics())) {
+        new GCSOutputStream(client, uri, gcpProperties, 
MetricsContext.nullMetrics())) {
       if (arrayWrite) {
         stream.write(data);
         assertThat(stream.getPos()).isEqualTo(data.length);

Reply via email to