Skip to content

Commit d73091d

Browse files
authored
add support for presigned head urls (#6158)
* adding headobject and headbucket functionality and test coverage * Fixing identation and adding changelog and attribution * adding javadoc * Adding default implementation to maintain backwards compatibility
1 parent 942ac8b commit d73091d

File tree

9 files changed

+784
-1
lines changed

9 files changed

+784
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS S3",
4+
"contributor": "tmccombs",
5+
"description": "Adds the ability to presign HeadObject and HeadBucket requests with the S3 Presigner"
6+
}

services/s3/src/it/java/software/amazon/awssdk/services/s3/S3PresignerIntegrationTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import software.amazon.awssdk.services.s3.presigner.model.PresignedCreateMultipartUploadRequest;
5353
import software.amazon.awssdk.services.s3.presigner.model.PresignedDeleteObjectRequest;
5454
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
55+
import software.amazon.awssdk.services.s3.presigner.model.PresignedHeadBucketRequest;
56+
import software.amazon.awssdk.services.s3.presigner.model.PresignedHeadObjectRequest;
5557
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
5658
import software.amazon.awssdk.services.s3.presigner.model.PresignedUploadPartRequest;
5759
import software.amazon.awssdk.services.s3.utils.S3TestUtils;
@@ -363,6 +365,50 @@ public void abortMultipartUpload_CanBePresigned() throws IOException {
363365
assertThat(getMultipartUpload(objectKey)).isNotPresent();
364366
}
365367

368+
@Test
369+
public void headObject_CanBePresigned() throws IOException {
370+
PresignedHeadObjectRequest presigned =
371+
presigner.presignHeadObject(r -> r.signatureDuration(Duration.ofMinutes(5))
372+
.headObjectRequest(hor -> hor.bucket(testBucket)
373+
.key(testGetObjectKey)));
374+
375+
assertThat(presigned.isBrowserExecutable()).isFalse();
376+
377+
SdkHttpClient httpClient = ApacheHttpClient.builder().build(); // or UrlConnectionHttpClient.builder().build()
378+
379+
HttpExecuteRequest request = HttpExecuteRequest.builder()
380+
.request(presigned.httpRequest())
381+
.build();
382+
383+
HttpExecuteResponse response = httpClient.prepareRequest(request).call();
384+
385+
assertThat(response.httpResponse().isSuccessful()).isTrue();
386+
assertThat(response.httpResponse().firstMatchingHeader("Content-Length")).isPresent();
387+
assertThat(response.httpResponse().firstMatchingHeader("ETag")).isPresent();
388+
assertThat(response.httpResponse().firstMatchingHeader("Last-Modified")).isPresent();
389+
390+
}
391+
392+
@Test
393+
public void headBucket_CanBePresigned() throws IOException {
394+
PresignedHeadBucketRequest presigned =
395+
presigner.presignHeadBucket(r -> r.signatureDuration(Duration.ofMinutes(5))
396+
.headBucketRequest(hbr -> hbr.bucket(testBucket)));
397+
398+
assertThat(presigned.isBrowserExecutable()).isFalse();
399+
400+
SdkHttpClient httpClient = ApacheHttpClient.builder().build(); // or UrlConnectionHttpClient.builder().build()
401+
402+
HttpExecuteRequest request = HttpExecuteRequest.builder()
403+
.request(presigned.httpRequest())
404+
.build();
405+
406+
HttpExecuteResponse response = httpClient.prepareRequest(request).call();
407+
408+
assertThat(response.httpResponse().isSuccessful()).isTrue();
409+
assertThat(response.httpResponse().firstMatchingHeader("x-amz-bucket-region")).isPresent();
410+
}
411+
366412
private Consumer<CreateMultipartUploadRequest.Builder> createMultipartUploadRequest(String objectKey) {
367413
return r -> r.bucket(testBucket).key(objectKey);
368414
}

services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@
9797
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
9898
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
9999
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
100+
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
101+
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
100102
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
101103
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
102104
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
@@ -105,11 +107,15 @@
105107
import software.amazon.awssdk.services.s3.presigner.model.CreateMultipartUploadPresignRequest;
106108
import software.amazon.awssdk.services.s3.presigner.model.DeleteObjectPresignRequest;
107109
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
110+
import software.amazon.awssdk.services.s3.presigner.model.HeadBucketPresignRequest;
111+
import software.amazon.awssdk.services.s3.presigner.model.HeadObjectPresignRequest;
108112
import software.amazon.awssdk.services.s3.presigner.model.PresignedAbortMultipartUploadRequest;
109113
import software.amazon.awssdk.services.s3.presigner.model.PresignedCompleteMultipartUploadRequest;
110114
import software.amazon.awssdk.services.s3.presigner.model.PresignedCreateMultipartUploadRequest;
111115
import software.amazon.awssdk.services.s3.presigner.model.PresignedDeleteObjectRequest;
112116
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
117+
import software.amazon.awssdk.services.s3.presigner.model.PresignedHeadBucketRequest;
118+
import software.amazon.awssdk.services.s3.presigner.model.PresignedHeadObjectRequest;
113119
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
114120
import software.amazon.awssdk.services.s3.presigner.model.PresignedUploadPartRequest;
115121
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
@@ -120,6 +126,8 @@
120126
import software.amazon.awssdk.services.s3.transform.CreateMultipartUploadRequestMarshaller;
121127
import software.amazon.awssdk.services.s3.transform.DeleteObjectRequestMarshaller;
122128
import software.amazon.awssdk.services.s3.transform.GetObjectRequestMarshaller;
129+
import software.amazon.awssdk.services.s3.transform.HeadBucketRequestMarshaller;
130+
import software.amazon.awssdk.services.s3.transform.HeadObjectRequestMarshaller;
123131
import software.amazon.awssdk.services.s3.transform.PutObjectRequestMarshaller;
124132
import software.amazon.awssdk.services.s3.transform.UploadPartRequestMarshaller;
125133
import software.amazon.awssdk.utils.AttributeMap;
@@ -141,6 +149,8 @@ public final class DefaultS3Presigner extends DefaultSdkPresigner implements S3P
141149
private final S3Configuration serviceConfiguration;
142150
private final List<ExecutionInterceptor> clientInterceptors;
143151
private final GetObjectRequestMarshaller getObjectRequestMarshaller;
152+
private final HeadObjectRequestMarshaller headObjectRequestMarshaller;
153+
private final HeadBucketRequestMarshaller headBucketRequestMarshaller;
144154
private final PutObjectRequestMarshaller putObjectRequestMarshaller;
145155
private final CreateMultipartUploadRequestMarshaller createMultipartUploadRequestMarshaller;
146156
private final UploadPartRequestMarshaller uploadPartRequestMarshaller;
@@ -193,6 +203,10 @@ private DefaultS3Presigner(Builder b) {
193203
// Copied from DefaultS3Client#getObject
194204
this.getObjectRequestMarshaller = new GetObjectRequestMarshaller(protocolFactory);
195205

206+
this.headObjectRequestMarshaller = new HeadObjectRequestMarshaller(protocolFactory);
207+
208+
this.headBucketRequestMarshaller = new HeadBucketRequestMarshaller(protocolFactory);
209+
196210
// Copied from DefaultS3Client#putObject
197211
this.putObjectRequestMarshaller = new PutObjectRequestMarshaller(protocolFactory);
198212

@@ -273,6 +287,28 @@ public PresignedGetObjectRequest presignGetObject(GetObjectPresignRequest reques
273287
.build();
274288
}
275289

290+
@Override
291+
public PresignedHeadObjectRequest presignHeadObject(HeadObjectPresignRequest request) {
292+
return presign(PresignedHeadObjectRequest.builder(),
293+
request,
294+
request.headObjectRequest(),
295+
HeadObjectRequest.class,
296+
headObjectRequestMarshaller::marshall,
297+
"HeadObject")
298+
.build();
299+
}
300+
301+
@Override
302+
public PresignedHeadBucketRequest presignHeadBucket(HeadBucketPresignRequest request) {
303+
return presign(PresignedHeadBucketRequest.builder(),
304+
request,
305+
request.headBucketRequest(),
306+
HeadBucketRequest.class,
307+
headBucketRequestMarshaller::marshall,
308+
"HeadBucket")
309+
.build();
310+
}
311+
276312
@Override
277313
public PresignedPutObjectRequest presignPutObject(PutObjectPresignRequest request) {
278314
return presign(PresignedPutObjectRequest.builder(),

services/s3/src/main/java/software/amazon/awssdk/services/s3/presigner/S3Presigner.java

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,24 @@
4040
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
4141
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
4242
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
43+
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
44+
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
4345
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
4446
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
4547
import software.amazon.awssdk.services.s3.presigner.model.AbortMultipartUploadPresignRequest;
4648
import software.amazon.awssdk.services.s3.presigner.model.CompleteMultipartUploadPresignRequest;
4749
import software.amazon.awssdk.services.s3.presigner.model.CreateMultipartUploadPresignRequest;
4850
import software.amazon.awssdk.services.s3.presigner.model.DeleteObjectPresignRequest;
4951
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
52+
import software.amazon.awssdk.services.s3.presigner.model.HeadBucketPresignRequest;
53+
import software.amazon.awssdk.services.s3.presigner.model.HeadObjectPresignRequest;
5054
import software.amazon.awssdk.services.s3.presigner.model.PresignedAbortMultipartUploadRequest;
5155
import software.amazon.awssdk.services.s3.presigner.model.PresignedCompleteMultipartUploadRequest;
5256
import software.amazon.awssdk.services.s3.presigner.model.PresignedCreateMultipartUploadRequest;
5357
import software.amazon.awssdk.services.s3.presigner.model.PresignedDeleteObjectRequest;
5458
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
59+
import software.amazon.awssdk.services.s3.presigner.model.PresignedHeadBucketRequest;
60+
import software.amazon.awssdk.services.s3.presigner.model.PresignedHeadObjectRequest;
5561
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
5662
import software.amazon.awssdk.services.s3.presigner.model.PresignedUploadPartRequest;
5763
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
@@ -328,7 +334,7 @@ static Builder builder() {
328334
* <p />
329335
* This is a shorter method of invoking {@link #presignGetObject(GetObjectPresignRequest)} without needing
330336
* to call {@code GetObjectPresignRequest.builder()} or {@code .build()}.
331-
*
337+
*
332338
* @see #presignGetObject(GetObjectPresignRequest)
333339
*/
334340
default PresignedGetObjectRequest presignGetObject(Consumer<GetObjectPresignRequest.Builder> request) {
@@ -337,6 +343,128 @@ default PresignedGetObjectRequest presignGetObject(Consumer<GetObjectPresignRequ
337343
return presignGetObject(builder.build());
338344
}
339345

346+
/**
347+
* Presign a {@link HeadObjectRequest} so that it can be executed at a later time without requiring additional
348+
* signing or authentication.
349+
* <p/>
350+
*
351+
* <b>Example Usage</b>
352+
* <p/>
353+
*
354+
* <pre>
355+
* {@code
356+
* S3Presigner presigner = ...;
357+
*
358+
* // Create a HeadObjectRequest to be pre-signed
359+
* HeadObjectRequest headObjectRequest =
360+
* HeadObjectRequest.builder()
361+
* .bucket("my-bucket")
362+
* .key("my-key")
363+
* .build();
364+
*
365+
* // Create a HeadObjectPresignRequest to specify the signature duration
366+
* HeadObjectPresignRequest headObjectPresignRequest =
367+
* HeadObjectPresignRequest.builder()
368+
* .signatureDuration(Duration.ofMinutes(10))
369+
* .headObjectRequest(headObjectRequest)
370+
* .build();
371+
*
372+
* // Generate the presigned request
373+
* PresignedHeadObjectRequest presignedHeadObjectRequest =
374+
* presigner.presignHeadObject(headObjectPresignRequest);
375+
*
376+
* // The presigned URL can be used with an HTTP client to retrieve object metadata
377+
* SdkHttpClient httpClient = ApacheHttpClient.builder().build();
378+
* HttpExecuteRequest request = HttpExecuteRequest.builder()
379+
* .request(presignedHeadObjectRequest.httpRequest())
380+
* .build();
381+
* HttpExecuteResponse response = httpClient.prepareRequest(request).call();
382+
*
383+
* // Extract metadata from response headers
384+
* String contentLength = response.httpResponse().firstMatchingHeader("Content-Length").orElse("0");
385+
* }
386+
* </pre>
387+
*/
388+
default PresignedHeadObjectRequest presignHeadObject(HeadObjectPresignRequest request) {
389+
throw new UnsupportedOperationException();
390+
}
391+
392+
/**
393+
* Presign a {@link HeadObjectRequest} so that it can be executed at a later time without requiring additional
394+
* signing or authentication.
395+
* <p />
396+
* This is a shorter method of invoking {@link #presignHeadObject(HeadObjectPresignRequest)} without needing
397+
* to call {@code HeadObjectPresignRequest.builder()} or {@code .build()}.
398+
*
399+
* @see #presignHeadObject(HeadObjectPresignRequest)
400+
*/
401+
default PresignedHeadObjectRequest presignHeadObject(Consumer<HeadObjectPresignRequest.Builder> request) {
402+
HeadObjectPresignRequest.Builder builder = HeadObjectPresignRequest.builder();
403+
request.accept(builder);
404+
return presignHeadObject(builder.build());
405+
}
406+
407+
/**
408+
* Presign a {@link HeadBucketRequest} so that it can be executed at a later time without requiring additional
409+
* signing or authentication.
410+
* <p/>
411+
*
412+
* <b>Example Usage</b>
413+
* <p/>
414+
*
415+
* <pre>
416+
* {@code
417+
* S3Presigner presigner = ...;
418+
*
419+
* // Create a HeadBucketRequest to be pre-signed
420+
* HeadBucketRequest headBucketRequest =
421+
* HeadBucketRequest.builder()
422+
* .bucket("my-bucket")
423+
* .build();
424+
*
425+
* // Create a HeadBucketPresignRequest to specify the signature duration
426+
* HeadBucketPresignRequest headBucketPresignRequest =
427+
* HeadBucketPresignRequest.builder()
428+
* .signatureDuration(Duration.ofMinutes(10))
429+
* .headBucketRequest(headBucketRequest)
430+
* .build();
431+
*
432+
* // Generate the presigned request
433+
* PresignedHeadBucketRequest presignedHeadBucketRequest =
434+
* presigner.presignHeadBucket(headBucketPresignRequest);
435+
*
436+
* // The presigned URL can be used with an HTTP client to check bucket existence and access
437+
* SdkHttpClient httpClient = ApacheHttpClient.builder().build();
438+
* HttpExecuteRequest request = HttpExecuteRequest.builder()
439+
* .request(presignedHeadBucketRequest.httpRequest())
440+
* .build();
441+
* HttpExecuteResponse response = httpClient.prepareRequest(request).call();
442+
*
443+
* // Check if bucket exists and is accessible
444+
* boolean bucketExists = response.httpResponse().isSuccessful();
445+
* String region = response.httpResponse().firstMatchingHeader("x-amz-bucket-region").orElse("");
446+
* }
447+
* </pre>
448+
*/
449+
default PresignedHeadBucketRequest presignHeadBucket(HeadBucketPresignRequest request) {
450+
throw new UnsupportedOperationException();
451+
}
452+
453+
/**
454+
* Presign a {@link HeadBucketRequest} so that it can be executed at a later time without requiring additional
455+
* signing or authentication.
456+
* <p>
457+
* This is a shorter method of invoking {@link #presignHeadBucket(HeadBucketPresignRequest)} without needing
458+
* to call {@code HeadBucketPresignRequest.builder()} or {@code .build()}.
459+
*
460+
* @see #presignHeadBucket(HeadBucketPresignRequest)
461+
*/
462+
default PresignedHeadBucketRequest presignHeadBucket(Consumer<HeadBucketPresignRequest.Builder> request) {
463+
HeadBucketPresignRequest.Builder builder = HeadBucketPresignRequest.builder();
464+
request.accept(builder);
465+
return presignHeadBucket(builder.build());
466+
}
467+
340468
/**
341469
* Presign a {@link PutObjectRequest} so that it can be executed at a later time without requiring additional
342470
* signing or authentication.

0 commit comments

Comments
 (0)