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