Skip to content

Commit 99cce95

Browse files
authored
fix: Support all PutObjectRequest fields (#458)
* fix: Support all PutObjectRequest fields All possible values of a PutObjectRequest need to be supported when enabling multipart put object. This PR adds support and the S3EC will now fail closed in the case of new options. This is preferred to silently dropping the value.
1 parent 92aee3c commit 99cce95

File tree

4 files changed

+611
-13
lines changed

4 files changed

+611
-13
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package software.amazon.encryption.s3.internal;
2+
3+
import software.amazon.awssdk.services.s3.model.ChecksumType;
4+
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
5+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
6+
import java.time.Instant;
7+
import java.util.Map;
8+
9+
public class ConvertSDKRequests {
10+
11+
public static CreateMultipartUploadRequest convert(PutObjectRequest request) {
12+
13+
final CreateMultipartUploadRequest.Builder output = CreateMultipartUploadRequest.builder();
14+
request
15+
.toBuilder()
16+
.sdkFields()
17+
.forEach(f -> {
18+
final Object value = f.getValueOrDefault(request);
19+
if (value != null) {
20+
switch (f.memberName()) {
21+
case "ACL":
22+
output.acl((String) value);
23+
break;
24+
case "Bucket":
25+
output.bucket((String) value);
26+
break;
27+
case "BucketKeyEnabled":
28+
output.bucketKeyEnabled((Boolean) value);
29+
break;
30+
case "CacheControl":
31+
output.cacheControl((String) value);
32+
break;
33+
case "ChecksumAlgorithm":
34+
output.checksumAlgorithm((String) value);
35+
break;
36+
case "ChecksumType":
37+
output.checksumType((ChecksumType) value);
38+
case "ContentDisposition":
39+
assert value instanceof String;
40+
output.contentDisposition((String) value);
41+
break;
42+
case "ContentEncoding":
43+
output.contentEncoding((String) value);
44+
break;
45+
case "ContentLanguage":
46+
output.contentLanguage((String) value);
47+
break;
48+
case "ContentType":
49+
output.contentType((String) value);
50+
break;
51+
case "ExpectedBucketOwner":
52+
output.expectedBucketOwner((String) value);
53+
break;
54+
case "Expires":
55+
output.expires((Instant) value);
56+
break;
57+
case "GrantFullControl":
58+
output.grantFullControl((String) value);
59+
break;
60+
case "GrantRead":
61+
output.grantRead((String) value);
62+
break;
63+
case "GrantReadACP":
64+
output.grantReadACP((String) value);
65+
break;
66+
case "GrantWriteACP":
67+
output.grantWriteACP((String) value);
68+
break;
69+
case "Key":
70+
output.key((String) value);
71+
break;
72+
case "Metadata":
73+
// The PutObjectRequest.builder().metadata(value)
74+
// only takes Map<String, String> therefore it should not be possible
75+
// to get here with anything other than a Map<String, String>
76+
// This may be overkill, but this map should be small
77+
// so the performance hit to verify this is worth the correctness.
78+
if (!isStringStringMap(value)) {
79+
throw new IllegalArgumentException("Metadata must be a Map<String, String>");
80+
}
81+
@SuppressWarnings("unchecked")
82+
Map<String, String> metadata = (Map<String, String>) value;
83+
output.metadata(metadata);
84+
break;
85+
case "ObjectLockLegalHoldStatus":
86+
output.objectLockLegalHoldStatus((String) value);
87+
break;
88+
case "ObjectLockMode":
89+
output.objectLockMode((String) value);
90+
break;
91+
case "ObjectLockRetainUntilDate":
92+
output.objectLockRetainUntilDate((Instant) value);
93+
break;
94+
case "RequestPayer":
95+
output.requestPayer((String) value);
96+
break;
97+
case "ServerSideEncryption":
98+
output.serverSideEncryption((String) value);
99+
break;
100+
case "SSECustomerAlgorithm":
101+
output.sseCustomerAlgorithm((String) value);
102+
break;
103+
case "SSECustomerKey":
104+
output.sseCustomerKey((String) value);
105+
break;
106+
case "SSEKMSEncryptionContext":
107+
output.ssekmsEncryptionContext((String) value);
108+
break;
109+
case "SSEKMSKeyId":
110+
output.ssekmsKeyId((String) value);
111+
break;
112+
case "StorageClass":
113+
output.storageClass((String) value);
114+
break;
115+
case "Tagging":
116+
output.tagging((String) value);
117+
break;
118+
case "WebsiteRedirectLocation":
119+
output.websiteRedirectLocation((String) value);
120+
break;
121+
default:
122+
// Rather than silently dropping the value,
123+
// we loudly signal that we don't know how to handle this field.
124+
throw new IllegalArgumentException("Unknown PutObjectRequest field " + f.locationName() + ".");
125+
}
126+
}
127+
});
128+
return output
129+
// OverrideConfiguration is not as SDKField but still needs to be supported
130+
.overrideConfiguration(request.overrideConfiguration().orElse(null))
131+
.build();
132+
}
133+
134+
private static boolean isStringStringMap(Object value) {
135+
if (!(value instanceof Map)) {
136+
return false;
137+
}
138+
Map<?, ?> map = (Map<?, ?>) value;
139+
return map.entrySet().stream()
140+
.allMatch(entry -> entry != null
141+
&& ((Map.Entry<?, ?>) entry).getKey() instanceof String
142+
&& ((Map.Entry<?, ?>) entry).getValue() instanceof String);
143+
}
144+
}

src/main/java/software/amazon/encryption/s3/internal/UploadObjectObserver.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import software.amazon.awssdk.services.s3.S3AsyncClient;
88
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse;
99
import software.amazon.awssdk.services.s3.model.CompletedPart;
10-
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
1110
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse;
1211
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
1312
import software.amazon.awssdk.services.s3.model.SdkPartType;
@@ -42,20 +41,10 @@ public UploadObjectObserver init(PutObjectRequest req,
4241
this.es = es;
4342
return this;
4443
}
45-
46-
protected CreateMultipartUploadRequest newCreateMultipartUploadRequest(
47-
PutObjectRequest request) {
48-
return CreateMultipartUploadRequest.builder()
49-
.bucket(request.bucket())
50-
.key(request.key())
51-
.metadata(request.metadata())
52-
.overrideConfiguration(request.overrideConfiguration().orElse(null))
53-
.build();
54-
}
55-
44+
5645
public String onUploadCreation(PutObjectRequest req) {
5746
CreateMultipartUploadResponse res =
58-
s3EncryptionClient.createMultipartUpload(newCreateMultipartUploadRequest(req));
47+
s3EncryptionClient.createMultipartUpload(ConvertSDKRequests.convert(req));
5948
return this.uploadId = res.uploadId();
6049
}
6150

src/test/java/software/amazon/encryption/s3/S3EncryptionClientMultipartUploadTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import software.amazon.awssdk.services.s3.model.SdkPartType;
2121
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
2222
import software.amazon.awssdk.services.s3.model.UploadPartResponse;
23+
import software.amazon.awssdk.services.s3.model.StorageClass;
2324
import software.amazon.awssdk.utils.IoUtils;
2425
import software.amazon.encryption.s3.utils.BoundedInputStream;
2526

@@ -522,4 +523,43 @@ public void multipartUploadV3OutputStreamPartSizeMismatch() throws IOException {
522523
v3Client.close();
523524
}
524525

526+
@Test
527+
public void multipartPutObjectWithOptions() throws IOException {
528+
final String objectKey = appendTestSuffix("multipart-put-object-with-options");
529+
530+
final long fileSizeLimit = 1024 * 1024 * 10;
531+
final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
532+
final InputStream objectStreamForResult = new BoundedInputStream(fileSizeLimit);
533+
534+
final S3Client v3Client = S3EncryptionClient.builder()
535+
.kmsKeyId(KMS_KEY_ID)
536+
.enableMultipartPutObject(true)
537+
.enableDelayedAuthenticationMode(true)
538+
.cryptoProvider(PROVIDER)
539+
.build();
540+
541+
final Map<String, String> encryptionContext = new HashMap<>();
542+
encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3");
543+
544+
final StorageClass storageClass = StorageClass.INTELLIGENT_TIERING;
545+
546+
v3Client.putObject(builder -> builder
547+
.bucket(BUCKET)
548+
.overrideConfiguration(withAdditionalConfiguration(encryptionContext))
549+
.storageClass(storageClass)
550+
.key(objectKey), RequestBody.fromInputStream(inputStream, fileSizeLimit));
551+
552+
// Asserts
553+
final ResponseInputStream<GetObjectResponse> output = v3Client.getObject(builder -> builder
554+
.bucket(BUCKET)
555+
.overrideConfiguration(S3EncryptionClient.withAdditionalConfiguration(encryptionContext))
556+
.key(objectKey));
557+
558+
assertTrue(IOUtils.contentEquals(objectStreamForResult, output));
559+
assertEquals(storageClass, output.response().storageClass());
560+
561+
v3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
562+
v3Client.close();
563+
}
564+
525565
}

0 commit comments

Comments
 (0)