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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 38392b2187 [oss] Support configurable server-side encryption 
(AES256/KMS/SM4) (#8460)
38392b2187 is described below

commit 38392b218724c73cfd137c40bde79670165cc932
Author: Jiajia Li <[email protected]>
AuthorDate: Mon Jul 6 16:38:56 2026 +0800

    [oss] Support configurable server-side encryption (AES256/KMS/SM4) (#8460)
    
    OSS server-side encryption couldn't be configured from Paimon
    (hadoop-aliyun only exposes `fs.oss.server-side-encryption-algorithm`),
    so KMS BYOK and SM4 data encryption were unreachable. This PR adds three
    options mapping 1:1 to the OSS headers:
    
      | Option | OSS header | Values |
      |---|---|---|
    | `fs.oss.server-side-encryption` | `x-oss-server-side-encryption` |
    `AES256` / `KMS` / `SM4` |
    | `fs.oss.server-side-encryption-key-id` |
    `x-oss-server-side-encryption-key-id` | KMS CMK id (requires `KMS`) |
    | `fs.oss.server-side-data-encryption` |
    `x-oss-server-side-data-encryption` | `SM4` (requires `KMS`) |
    
    Invalid values fail fast at init. Headers are stamped on all write paths
    (put, multipart, copy, atomic write). Paimon SSE takes precedence over
    the hadoop-native `-algorithm`, which stays as an atomic-write fallback.
---
 docs/docs/maintenance/filesystems.mdx              |  11 +-
 .../main/java/org/apache/paimon/oss/OSSFileIO.java | 250 ++++++++++++++-------
 .../java/org/apache/paimon/oss/OSSFileIOTest.java  | 159 ++++++++++++-
 3 files changed, 337 insertions(+), 83 deletions(-)

diff --git a/docs/docs/maintenance/filesystems.mdx 
b/docs/docs/maintenance/filesystems.mdx
index 8f1cc844a5..182bc37ab4 100644
--- a/docs/docs/maintenance/filesystems.mdx
+++ b/docs/docs/maintenance/filesystems.mdx
@@ -332,15 +332,20 @@ Download 
[paimon-jindo-@@VERSION@@.jar](https://repository.apache.org/snapshots/
 
 </Unstable>
 
-### Server-Side Encryption with Customer CMK (SSE-KMS)
+### Server-Side Encryption
 
-To encrypt OSS writes with a customer-managed CMK (BYOK) via SSE-KMS, 
configure the CMK key id:
+Paimon can stamp OSS server-side-encryption headers on the writes it performs 
(`PutObject`, server-side `CopyObject` used by rename/commit, and 
multipart-upload initiation) via three keys that map 1:1 to the OSS request 
headers:
 
 ```yaml
+# Encryption method: AES256, KMS or SM4 (-> x-oss-server-side-encryption)
+fs.oss.server-side-encryption: KMS
+# Customer CMK key id, only valid with KMS (-> 
x-oss-server-side-encryption-key-id)
 fs.oss.server-side-encryption-key-id: your-cmk-key-id
+# Data-encryption algorithm SM4, only valid with KMS (-> 
x-oss-server-side-data-encryption)
+fs.oss.server-side-data-encryption: SM4
 ```
 
-When set, Paimon stamps the given CMK on the OSS write paths it performs: 
`PutObject`, server-side `CopyObject` (used by rename/commit), and 
multipart-upload initiation (parts inherit the CMK).
+When any of these is set, Paimon takes over the SSE headers on its write paths 
(these keys take precedence over hadoop-aliyun's native 
`fs.oss.server-side-encryption-algorithm`). A key id or `SM4` data-encryption 
requires `fs.oss.server-side-encryption=KMS` (it defaults to `KMS` when unset), 
and is rejected otherwise.
 
 ## S3
 
diff --git 
a/paimon-filesystems/paimon-oss-impl/src/main/java/org/apache/paimon/oss/OSSFileIO.java
 
b/paimon-filesystems/paimon-oss-impl/src/main/java/org/apache/paimon/oss/OSSFileIO.java
index e126277288..410c460bb4 100644
--- 
a/paimon-filesystems/paimon-oss-impl/src/main/java/org/apache/paimon/oss/OSSFileIO.java
+++ 
b/paimon-filesystems/paimon-oss-impl/src/main/java/org/apache/paimon/oss/OSSFileIO.java
@@ -32,6 +32,7 @@ import com.aliyun.oss.OSSClient;
 import com.aliyun.oss.OSSException;
 import com.aliyun.oss.common.auth.CredentialsProvider;
 import com.aliyun.oss.common.comm.ServiceClient;
+import com.aliyun.oss.internal.OSSHeaders;
 import com.aliyun.oss.internal.OSSMultipartOperation;
 import com.aliyun.oss.internal.OSSObjectOperation;
 import com.aliyun.oss.model.CopyObjectRequest;
@@ -60,6 +61,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Supplier;
 
 import static org.apache.paimon.options.CatalogOptions.FILE_IO_ALLOW_CACHE;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
 
 /** OSS {@link FileIO}. */
 public class OSSFileIO extends HadoopCompliantFileIO implements 
HadoopOptionsProvider {
@@ -78,10 +80,27 @@ public class OSSFileIO extends HadoopCompliantFileIO 
implements HadoopOptionsPro
     private static final String OSS_ACCESS_KEY_SECRET = 
"fs.oss.accessKeySecret";
     private static final String OSS_SECURITY_TOKEN = "fs.oss.securityToken";
     private static final String OSS_SECOND_LEVEL_DOMAIN_ENABLED = 
"fs.oss.sld.enabled";
+    // Paimon OSS SSE keys, mapping 1:1 to the OSS headers; they take 
precedence over hadoop's
+    // server-side-encryption-algorithm.
+    /** SSE method -> x-oss-server-side-encryption (AES256 / KMS / SM4). */
+    private static final String OSS_SSE_METHOD = 
"fs.oss.server-side-encryption";
+
+    /** CMK key id -> x-oss-server-side-encryption-key-id (valid only when the 
method is KMS). */
     private static final String OSS_SSE_KMS_KEY_ID = 
"fs.oss.server-side-encryption-key-id";
 
-    /** OSS-native value for the {@code x-oss-server-side-encryption} header 
when using a CMK. */
-    private static final String SSE_KMS_ALGORITHM = "KMS";
+    /**
+     * Data encryption -> x-oss-server-side-data-encryption (SM4; valid only 
when method is KMS).
+     */
+    private static final String OSS_SSE_DATA_ENCRYPTION = 
"fs.oss.server-side-data-encryption";
+
+    /**
+     * hadoop-aliyun's native SSE method key; only used as the {@code 
tryToWriteAtomic} fallback.
+     */
+    private static final String OSS_SSE_ALGORITHM = 
"fs.oss.server-side-encryption-algorithm";
+
+    private static final String SSE_METHOD_AES256 = "AES256";
+    private static final String SSE_METHOD_KMS = "KMS";
+    private static final String SSE_DATA_SM4 = "SM4";
 
     private static final Map<String, String> CASE_SENSITIVE_KEYS =
             new HashMap<String, String>() {
@@ -187,20 +206,9 @@ public class OSSFileIO extends HadoopCompliantFileIO 
implements HadoopOptionsPro
                         enableSecondLevelDomain(fs);
                     }
 
-                    String sseKmsKeyId = hadoopOptions.get(OSS_SSE_KMS_KEY_ID);
-                    if (sseKmsKeyId != null) {
-                        String trimmed = sseKmsKeyId.trim();
-                        // Reject what can never be a key id, so config errors 
fail fast at
-                        // init instead of mid-job.
-                        if (trimmed.isEmpty()
-                                || 
trimmed.chars().anyMatch(Character::isWhitespace)) {
-                            throw new IllegalArgumentException(
-                                    "Invalid value for '"
-                                            + OSS_SSE_KMS_KEY_ID
-                                            + "': the CMK key id must not be 
blank or contain"
-                                            + " whitespace/newlines.");
-                        }
-                        enableSseKms(fs, trimmed);
+                    SseConfig sse = configuredSse();
+                    if (sse != null) {
+                        enableSse(fs, sse);
                     }
 
                     return fs;
@@ -224,10 +232,9 @@ public class OSSFileIO extends HadoopCompliantFileIO 
implements HadoopOptionsPro
         ObjectMetadata metadata = new ObjectMetadata();
         metadata.setContentLength(bytes.length);
         metadata.setHeader("x-oss-forbid-overwrite", "true");
-
-        String sseAlgorithm =
-                
hadoopOptions.getString("fs.oss.server-side-encryption-algorithm", "");
-        if (StringUtils.isNotEmpty(sseAlgorithm)) {
+        // Fall back to native SSE only when Paimon SSE is unset (the swap 
stamps it otherwise).
+        String sseAlgorithm = hadoopOptions.getString(OSS_SSE_ALGORITHM, "");
+        if (StringUtils.isNotEmpty(sseAlgorithm) && configuredSse() == null) {
             metadata.setServerSideEncryption(sseAlgorithm);
         }
 
@@ -273,112 +280,205 @@ public class OSSFileIO extends HadoopCompliantFileIO 
implements HadoopOptionsPro
         return ReflectionUtils.getPrivateFieldValue(store, "ossClient");
     }
 
-    /**
-     * Enable SSE-KMS with a customer CMK. hadoop-aliyun exposes no CMK key-id 
option, so swap the
-     * OSS client's object/multipart operations for subclasses that stamp the 
OSS-native SSE-KMS
-     * headers on the write paths: PutObject, server-side CopyObject 
(rename/commit) and
-     * InitiateMultipartUpload. UploadPart/HeadObject/GetObject are left 
untouched (stamping SSE
-     * headers on those makes OSS reject the request with 400).
-     */
-    private void enableSseKms(AliyunOSSFileSystem fs, String kmsKeyId) {
+    /** The Paimon SSE config from the three keys, or null when none is set. */
+    private SseConfig configuredSse() {
+        return resolveSse(
+                hadoopOptions.get(OSS_SSE_METHOD),
+                hadoopOptions.get(OSS_SSE_KMS_KEY_ID),
+                hadoopOptions.get(OSS_SSE_DATA_ENCRYPTION));
+    }
+
+    /** Parse the three SSE keys into an {@link SseConfig}, or null. Throws on 
bad input. */
+    static SseConfig resolveSse(String method, String keyId, String 
dataEncryption) {
+        // Reject present-but-blank values fail-fast instead of silently 
writing plaintext.
+        checkNotBlankIfSet(method, OSS_SSE_METHOD);
+        checkNotBlankIfSet(keyId, OSS_SSE_KMS_KEY_ID);
+        checkNotBlankIfSet(dataEncryption, OSS_SSE_DATA_ENCRYPTION);
+        method = method == null ? null : method.trim();
+        keyId = keyId == null ? null : keyId.trim();
+        dataEncryption = dataEncryption == null ? null : dataEncryption.trim();
+        if (method == null && keyId == null && dataEncryption == null) {
+            return null;
+        }
+        // A key id / data-encryption implies KMS; default the method to KMS 
when unset.
+        method = canonicalSseMethod(method == null ? SSE_METHOD_KMS : method);
+        if (keyId != null) {
+            checkArgument(
+                    keyId.chars().noneMatch(Character::isWhitespace),
+                    "Invalid value for '%s': the CMK key id must not contain 
whitespace/newlines.",
+                    OSS_SSE_KMS_KEY_ID);
+            checkArgument(
+                    SSE_METHOD_KMS.equals(method),
+                    "'%s' requires '%s=KMS', but got '%s'.",
+                    OSS_SSE_KMS_KEY_ID,
+                    OSS_SSE_METHOD,
+                    method);
+        }
+        if (dataEncryption != null) {
+            checkArgument(
+                    SSE_METHOD_KMS.equals(method),
+                    "'%s' requires '%s=KMS', but got '%s'.",
+                    OSS_SSE_DATA_ENCRYPTION,
+                    OSS_SSE_METHOD,
+                    method);
+            checkArgument(
+                    SSE_DATA_SM4.equalsIgnoreCase(dataEncryption),
+                    "'%s' only supports 'SM4', but got '%s'.",
+                    OSS_SSE_DATA_ENCRYPTION,
+                    dataEncryption);
+            dataEncryption = SSE_DATA_SM4;
+        }
+        return new SseConfig(method, keyId, dataEncryption);
+    }
+
+    /** Reject a config value that is present but blank (encryption requested 
yet misconfigured). */
+    private static void checkNotBlankIfSet(String value, String key) {
+        checkArgument(
+                value == null || !StringUtils.isNullOrWhitespaceOnly(value),
+                "'%s' is set but blank.",
+                key);
+    }
+
+    /** Canonicalize the SSE method to the exact OSS header value; reject 
unknown values. */
+    private static String canonicalSseMethod(String method) {
+        if (SSE_METHOD_AES256.equalsIgnoreCase(method)) {
+            return SSE_METHOD_AES256;
+        }
+        if (SSE_METHOD_KMS.equalsIgnoreCase(method)) {
+            return SSE_METHOD_KMS;
+        }
+        if (SSE_DATA_SM4.equalsIgnoreCase(method)) {
+            return SSE_DATA_SM4;
+        }
+        throw new IllegalArgumentException(
+                "'"
+                        + OSS_SSE_METHOD
+                        + "' must be one of AES256/KMS/SM4, but got '"
+                        + method
+                        + "'.");
+    }
+
+    /** Resolved OSS server-side-encryption settings. */
+    static final class SseConfig {
+        final String method;
+        final String keyId;
+        final String dataEnc;
+
+        SseConfig(String method, String keyId, String dataEnc) {
+            this.method = method;
+            this.keyId = keyId;
+            this.dataEnc = dataEnc;
+        }
+    }
+
+    /** Swap in the SSE-stamping object/multipart operations (write paths 
only). */
+    private void enableSse(AliyunOSSFileSystem fs, SseConfig sse) {
         try {
-            swapSseKmsOperations(getOssClient(fs), kmsKeyId);
+            swapSseOperations(getOssClient(fs), sse);
         } catch (Exception e) {
-            LOG.error("Failed to enable SSE-KMS BYOK.", e);
-            throw new RuntimeException("Failed to enable SSE-KMS BYOK.", e);
+            LOG.error("Failed to enable OSS server-side encryption.", e);
+            throw new RuntimeException("Failed to enable OSS server-side 
encryption.", e);
         }
     }
 
-    /**
-     * Swap the OSS client's object/multipart operations for the CMK-stamping 
subclasses. Package
-     * private so a unit test can pin the reflected field names and getters 
against the bundled
-     * aliyun-oss-sdk version.
-     */
-    static void swapSseKmsOperations(OSSClient ossClient, String kmsKeyId) 
throws Exception {
+    /** Swap in the SSE-stamping operations; package-private so a test pins 
reflected names. */
+    static void swapSseOperations(OSSClient ossClient, SseConfig sse) throws 
Exception {
         ServiceClient serviceClient =
                 ReflectionUtils.getPrivateFieldValue(ossClient, 
"serviceClient");
         CredentialsProvider credsProvider =
                 ReflectionUtils.getPrivateFieldValue(ossClient, 
"credsProvider");
         // Replacement operations must inherit the endpoint or requests NPE.
         URI endpoint = ossClient.getEndpoint();
-        SseKmsObjectOperation objectOperation =
-                new SseKmsObjectOperation(serviceClient, credsProvider, 
kmsKeyId);
+        SseObjectOperation objectOperation =
+                new SseObjectOperation(serviceClient, credsProvider, sse);
         objectOperation.setEndpoint(endpoint);
-        SseKmsMultipartOperation multipartOperation =
-                new SseKmsMultipartOperation(serviceClient, credsProvider, 
kmsKeyId);
+        SseMultipartOperation multipartOperation =
+                new SseMultipartOperation(serviceClient, credsProvider, sse);
         multipartOperation.setEndpoint(endpoint);
         ReflectionUtils.setPrivateFieldValue(ossClient, "objectOperation", 
objectOperation);
         ReflectionUtils.setPrivateFieldValue(ossClient, "multipartOperation", 
multipartOperation);
-        // Fail closed: if a future SDK change makes the swap a no-op, fail 
loudly
-        // rather than silently writing objects without the customer CMK.
+        // Fail closed: if the swap didn't take effect, refuse to write 
unencrypted.
         if (ossClient.getObjectOperation() != objectOperation
                 || ossClient.getMultipartOperation() != multipartOperation) {
             throw new IllegalStateException(
-                    "SSE-KMS BYOK operation swap did not take effect; refusing 
to write "
-                            + "without server-side encryption. The 
aliyun-oss-sdk internals "
-                            + "may have changed.");
+                    "OSS SSE operation swap did not take effect; refusing to 
write without "
+                            + "server-side encryption. The aliyun-oss-sdk 
internals may have changed.");
         }
     }
 
-    /** Stamps the customer CMK on the given metadata, allocating one if 
absent. */
-    private static ObjectMetadata applySseKms(ObjectMetadata metadata, String 
kmsKeyId) {
+    /** Stamp the SSE headers on object metadata (PutObject / multipart init). 
*/
+    static ObjectMetadata applySse(ObjectMetadata metadata, SseConfig sse) {
         if (metadata == null) {
             metadata = new ObjectMetadata();
         }
         String existing = metadata.getServerSideEncryption();
-        if (existing != null && !SSE_KMS_ALGORITHM.equals(existing)) {
+        if (existing != null && !sse.method.equals(existing)) {
             LOG.warn(
-                    "Customer CMK SSE-KMS is overriding the previously-set 
server-side "
-                            + "encryption algorithm '{}' with 'KMS'.",
-                    existing);
+                    "Paimon SSE is overriding the previously-set server-side 
encryption "
+                            + "method '{}' with '{}'.",
+                    existing,
+                    sse.method);
+        }
+        metadata.setServerSideEncryption(sse.method);
+        if (sse.keyId != null) {
+            metadata.setServerSideEncryptionKeyId(sse.keyId);
+        }
+        if (sse.dataEnc != null) {
+            metadata.setServerSideDataEncryption(sse.dataEnc);
         }
-        metadata.setServerSideEncryption(SSE_KMS_ALGORITHM);
-        metadata.setServerSideEncryptionKeyId(kmsKeyId);
         return metadata;
     }
 
-    /** Stamps the customer CMK on every simple PutObject. */
-    static class SseKmsObjectOperation extends OSSObjectOperation {
-        private final String kmsKeyId;
+    /** Stamp the SSE headers on a server-side CopyObject request 
(rename/commit). */
+    static CopyObjectRequest applySse(CopyObjectRequest request, SseConfig 
sse) {
+        // Use request fields, not newObjectMetadata (forces REPLACE, drops 
source metadata).
+        // No data-encryption field; addHeader works since doOperation merges 
headers pre-sign.
+        request.setServerSideEncryption(sse.method);
+        if (sse.keyId != null) {
+            request.setServerSideEncryptionKeyId(sse.keyId);
+        }
+        if (sse.dataEnc != null) {
+            request.addHeader(OSSHeaders.OSS_SERVER_SIDE_DATA_ENCRYPTION, 
sse.dataEnc);
+        }
+        return request;
+    }
+
+    /** Stamps the SSE headers on every simple PutObject and server-side 
CopyObject. */
+    static class SseObjectOperation extends OSSObjectOperation {
+        private final SseConfig sse;
 
-        SseKmsObjectOperation(
-                ServiceClient serviceClient, CredentialsProvider 
credsProvider, String kmsKeyId) {
+        SseObjectOperation(
+                ServiceClient serviceClient, CredentialsProvider 
credsProvider, SseConfig sse) {
             super(serviceClient, credsProvider);
-            this.kmsKeyId = kmsKeyId;
+            this.sse = sse;
         }
 
         @Override
         public PutObjectResult putObject(PutObjectRequest request) {
-            request.setMetadata(applySseKms(request.getMetadata(), kmsKeyId));
+            request.setMetadata(applySse(request.getMetadata(), sse));
             return super.putObject(request);
         }
 
         @Override
         public CopyObjectResult copyObject(CopyObjectRequest request) {
-            // Rename/commit uses server-side copy. populateCopyObjectHeaders 
reads the SSE
-            // headers straight from these request fields, so setting them 
here stamps the CMK
-            // without touching newObjectMetadata. Setting newObjectMetadata 
would force
-            // metadata-directive REPLACE and drop the source object's 
Content-Type/user metadata.
-            request.setServerSideEncryption(SSE_KMS_ALGORITHM);
-            request.setServerSideEncryptionKeyId(kmsKeyId);
-            return super.copyObject(request);
+            return super.copyObject(applySse(request, sse));
         }
     }
 
-    /** Stamps the customer CMK on the multipart-upload init; parts inherit 
it. */
-    static class SseKmsMultipartOperation extends OSSMultipartOperation {
-        private final String kmsKeyId;
+    /** Stamps the SSE headers on the multipart-upload init; parts inherit 
them. */
+    static class SseMultipartOperation extends OSSMultipartOperation {
+        private final SseConfig sse;
 
-        SseKmsMultipartOperation(
-                ServiceClient serviceClient, CredentialsProvider 
credsProvider, String kmsKeyId) {
+        SseMultipartOperation(
+                ServiceClient serviceClient, CredentialsProvider 
credsProvider, SseConfig sse) {
             super(serviceClient, credsProvider);
-            this.kmsKeyId = kmsKeyId;
+            this.sse = sse;
         }
 
         @Override
         public InitiateMultipartUploadResult initiateMultipartUpload(
                 InitiateMultipartUploadRequest request) {
-            request.setObjectMetadata(applySseKms(request.getObjectMetadata(), 
kmsKeyId));
+            request.setObjectMetadata(applySse(request.getObjectMetadata(), 
sse));
             return super.initiateMultipartUpload(request);
         }
     }
diff --git 
a/paimon-filesystems/paimon-oss-impl/src/test/java/org/apache/paimon/oss/OSSFileIOTest.java
 
b/paimon-filesystems/paimon-oss-impl/src/test/java/org/apache/paimon/oss/OSSFileIOTest.java
index e5100ce3ef..d6b642a024 100644
--- 
a/paimon-filesystems/paimon-oss-impl/src/test/java/org/apache/paimon/oss/OSSFileIOTest.java
+++ 
b/paimon-filesystems/paimon-oss-impl/src/test/java/org/apache/paimon/oss/OSSFileIOTest.java
@@ -18,29 +18,178 @@
 
 package org.apache.paimon.oss;
 
+import com.aliyun.oss.ClientConfiguration;
+import com.aliyun.oss.ClientException;
 import com.aliyun.oss.OSSClient;
 import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.common.auth.DefaultCredentialProvider;
+import com.aliyun.oss.common.comm.ExecutionContext;
+import com.aliyun.oss.common.comm.RequestMessage;
+import com.aliyun.oss.common.comm.ResponseMessage;
+import com.aliyun.oss.common.comm.RetryStrategy;
+import com.aliyun.oss.common.comm.ServiceClient;
+import com.aliyun.oss.model.CopyObjectRequest;
+import com.aliyun.oss.model.ObjectMetadata;
 import org.junit.jupiter.api.Test;
 
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Tests for {@link OSSFileIO}. */
 public class OSSFileIOTest {
 
-    /** The swap must replace both operations with the CMK-stamping 
subclasses. */
+    /** The swap must replace both operations with the SSE-stamping 
subclasses. */
     @Test
-    public void testSseKmsOperationSwapTakesEffect() throws Exception {
+    public void testSseOperationSwapTakesEffect() throws Exception {
         OSSClient ossClient =
                 (OSSClient) new 
OSSClientBuilder().build("http://oss.example.com";, "ak", "sk");
         try {
-            OSSFileIO.swapSseKmsOperations(ossClient, "my-cmk-key-id");
+            OSSFileIO.swapSseOperations(ossClient, OSSFileIO.resolveSse("KMS", 
"my-cmk", "SM4"));
 
             assertThat(ossClient.getObjectOperation())
-                    .isInstanceOf(OSSFileIO.SseKmsObjectOperation.class);
+                    .isInstanceOf(OSSFileIO.SseObjectOperation.class);
             assertThat(ossClient.getMultipartOperation())
-                    .isInstanceOf(OSSFileIO.SseKmsMultipartOperation.class);
+                    .isInstanceOf(OSSFileIO.SseMultipartOperation.class);
         } finally {
             ossClient.shutdown();
         }
     }
+
+    /** applySse must stamp exactly the configured headers onto the metadata. 
*/
+    @Test
+    public void testApplySseStampsHeaders() {
+        ObjectMetadata kms =
+                OSSFileIO.applySse(
+                        (ObjectMetadata) null, OSSFileIO.resolveSse("KMS", 
"my-cmk", "SM4"));
+        assertThat(kms.getServerSideEncryption()).isEqualTo("KMS");
+        assertThat(kms.getServerSideEncryptionKeyId()).isEqualTo("my-cmk");
+        assertThat(kms.getServerSideDataEncryption()).isEqualTo("SM4");
+
+        ObjectMetadata aes =
+                OSSFileIO.applySse(
+                        (ObjectMetadata) null, OSSFileIO.resolveSse("AES256", 
null, null));
+        assertThat(aes.getServerSideEncryption()).isEqualTo("AES256");
+        assertThat(aes.getServerSideEncryptionKeyId()).isNull();
+        assertThat(aes.getServerSideDataEncryption()).isNull();
+    }
+
+    /** applySse must stamp the SSE fields and data-encryption header on a 
CopyObject request. */
+    @Test
+    public void testApplySseStampsCopyRequest() {
+        CopyObjectRequest kms = new CopyObjectRequest("sb", "sk", "db", "dk");
+        OSSFileIO.applySse(kms, OSSFileIO.resolveSse("KMS", "my-cmk", "SM4"));
+        assertThat(kms.getServerSideEncryption()).isEqualTo("KMS");
+        assertThat(kms.getServerSideEncryptionKeyId()).isEqualTo("my-cmk");
+        
assertThat(kms.getHeaders()).containsEntry("x-oss-server-side-data-encryption", 
"SM4");
+
+        CopyObjectRequest aes = new CopyObjectRequest("sb", "sk", "db", "dk");
+        OSSFileIO.applySse(aes, OSSFileIO.resolveSse("AES256", null, null));
+        assertThat(aes.getServerSideEncryption()).isEqualTo("AES256");
+        assertThat(aes.getServerSideEncryptionKeyId()).isNull();
+        
assertThat(aes.getHeaders()).doesNotContainKey("x-oss-server-side-data-encryption");
+    }
+
+    /** copyObject must carry all three SSE headers through to the final 
signed wire request. */
+    @Test
+    public void testCopyObjectSendsSseHeadersOnWire() throws Exception {
+        AtomicReference<Map<String, String>> wire = new AtomicReference<>();
+        ServiceClient capturing =
+                new ServiceClient(new ClientConfiguration()) {
+                    @Override
+                    protected ResponseMessage sendRequestCore(
+                            ServiceClient.Request request, ExecutionContext 
context) {
+                        wire.set(new HashMap<>(request.getHeaders()));
+                        throw new ClientException("captured");
+                    }
+
+                    @Override
+                    protected RetryStrategy getDefaultRetryStrategy() {
+                        return new RetryStrategy() {
+                            @Override
+                            public boolean shouldRetry(
+                                    Exception e,
+                                    RequestMessage request,
+                                    ResponseMessage response,
+                                    int retries) {
+                                return false;
+                            }
+                        };
+                    }
+
+                    @Override
+                    public void shutdown() {}
+                };
+
+        OSSFileIO.SseObjectOperation operation =
+                new OSSFileIO.SseObjectOperation(
+                        capturing,
+                        new DefaultCredentialProvider("ak", "sk"),
+                        OSSFileIO.resolveSse("KMS", "my-cmk", "SM4"));
+        operation.setEndpoint(new URI("http://oss.example.com";));
+
+        assertThatThrownBy(
+                        () -> operation.copyObject(new CopyObjectRequest("sb", 
"sk", "db", "dk")))
+                .isInstanceOf(ClientException.class);
+
+        assertThat(wire.get())
+                .containsEntry("x-oss-server-side-encryption", "KMS")
+                .containsEntry("x-oss-server-side-encryption-key-id", "my-cmk")
+                .containsEntry("x-oss-server-side-data-encryption", "SM4");
+    }
+
+    @Test
+    public void testResolveSse() {
+        // Nothing set -> no SSE.
+        assertThat(OSSFileIO.resolveSse(null, null, null)).isNull();
+
+        // Method only (no key-id / data-encryption).
+        OSSFileIO.SseConfig aes = OSSFileIO.resolveSse("AES256", null, null);
+        assertThat(aes.method).isEqualTo("AES256");
+        assertThat(aes.keyId).isNull();
+        assertThat(aes.dataEnc).isNull();
+
+        // A key id / SM4 data-encryption each default the method to KMS.
+        assertThat(OSSFileIO.resolveSse(null, "my-cmk", 
null).method).isEqualTo("KMS");
+        OSSFileIO.SseConfig dataOnly = OSSFileIO.resolveSse(null, null, "SM4");
+        assertThat(dataOnly.method).isEqualTo("KMS");
+        assertThat(dataOnly.dataEnc).isEqualTo("SM4");
+        OSSFileIO.SseConfig full = OSSFileIO.resolveSse("KMS", "my-cmk", 
"SM4");
+        assertThat(full.method).isEqualTo("KMS");
+        assertThat(full.keyId).isEqualTo("my-cmk");
+        assertThat(full.dataEnc).isEqualTo("SM4");
+
+        // The method is canonicalized to the exact OSS value.
+        assertThat(OSSFileIO.resolveSse("kms", "my-cmk", 
"sm4").method).isEqualTo("KMS");
+        assertThat(OSSFileIO.resolveSse("kms", "my-cmk", 
"sm4").dataEnc).isEqualTo("SM4");
+
+        // Invalid / conflicting config is rejected fail-fast.
+        assertThatThrownBy(() -> OSSFileIO.resolveSse("AES-256", null, null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("AES256/KMS/SM4");
+        assertThatThrownBy(() -> OSSFileIO.resolveSse("AES256", "my-cmk", 
null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("requires");
+        assertThatThrownBy(() -> OSSFileIO.resolveSse("AES256", null, "SM4"))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("requires");
+        assertThatThrownBy(() -> OSSFileIO.resolveSse("KMS", "my-cmk", 
"AES256"))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("SM4");
+        assertThatThrownBy(() -> OSSFileIO.resolveSse("KMS", "bad key", null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("whitespace");
+
+        // A present-but-blank value is a misconfiguration, not "unset".
+        assertThatThrownBy(() -> OSSFileIO.resolveSse(null, "  ", null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("blank");
+        assertThatThrownBy(() -> OSSFileIO.resolveSse(" ", "", null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("blank");
+    }
 }

Reply via email to