Skip to content

Commit 4b4e2a8

Browse files
Merge branch 'main' into registry/update-example-for-aws
2 parents 9d128f9 + 077997e commit 4b4e2a8

File tree

14 files changed

+871
-64
lines changed

14 files changed

+871
-64
lines changed

blob/blob-aws/src/main/java/com/salesforce/multicloudj/blob/aws/AwsTransformer.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,11 +508,14 @@ public PutObjectPresignRequest toPutObjectPresignRequest(PresignedUrlRequest req
508508
}
509509

510510
public GetObjectPresignRequest toGetObjectPresignRequest(PresignedUrlRequest request) {
511-
GetObjectRequest getObjectRequest =
512-
GetObjectRequest.builder().bucket(getBucket()).key(request.getKey()).build();
511+
GetObjectRequest.Builder getObjectBuilder =
512+
GetObjectRequest.builder().bucket(getBucket()).key(request.getKey());
513+
if (request.getContentDisposition() != null) {
514+
getObjectBuilder.responseContentDisposition(request.getContentDisposition());
515+
}
513516
return GetObjectPresignRequest.builder()
514517
.signatureDuration(request.getDuration())
515-
.getObjectRequest(getObjectRequest)
518+
.getObjectRequest(getObjectBuilder.build())
516519
.build();
517520
}
518521

blob/blob-aws/src/test/java/com/salesforce/multicloudj/blob/aws/AwsTransformerTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,43 @@ void testToGetObjectPresignRequest() {
587587
assertEquals(BUCKET, actualRequest.getObjectRequest().bucket());
588588
assertEquals("object-1", actualRequest.getObjectRequest().key());
589589
assertEquals(Duration.ofHours(4), actualRequest.signatureDuration());
590+
assertNull(actualRequest.getObjectRequest().responseContentDisposition());
591+
}
592+
593+
@Test
594+
void testToGetObjectPresignRequest_WithContentDisposition() {
595+
PresignedUrlRequest presignedUrlRequest =
596+
PresignedUrlRequest.builder()
597+
.type(PresignedOperation.DOWNLOAD)
598+
.key("object-1")
599+
.duration(Duration.ofHours(4))
600+
.contentDisposition("attachment; filename=\"report.pdf\"")
601+
.build();
602+
GetObjectPresignRequest actualRequest =
603+
transformer.toGetObjectPresignRequest(presignedUrlRequest);
604+
assertEquals(BUCKET, actualRequest.getObjectRequest().bucket());
605+
assertEquals("object-1", actualRequest.getObjectRequest().key());
606+
assertEquals(Duration.ofHours(4), actualRequest.signatureDuration());
607+
assertEquals(
608+
"attachment; filename=\"report.pdf\"",
609+
actualRequest.getObjectRequest().responseContentDisposition());
610+
}
611+
612+
@Test
613+
void testToPutObjectPresignRequest_IgnoresContentDisposition() {
614+
PresignedUrlRequest presignedUrlRequest =
615+
PresignedUrlRequest.builder()
616+
.type(PresignedOperation.UPLOAD)
617+
.key("object-1")
618+
.duration(Duration.ofHours(4))
619+
.contentDisposition("attachment; filename=\"report.pdf\"")
620+
.build();
621+
PutObjectPresignRequest actualRequest =
622+
transformer.toPutObjectPresignRequest(presignedUrlRequest);
623+
assertEquals(BUCKET, actualRequest.putObjectRequest().bucket());
624+
assertEquals("object-1", actualRequest.putObjectRequest().key());
625+
assertEquals(Duration.ofHours(4), actualRequest.signatureDuration());
626+
assertNull(actualRequest.putObjectRequest().contentDisposition());
590627
}
591628

592629
@Test

blob/blob-client/src/main/java/com/salesforce/multicloudj/blob/driver/PresignedUrlRequest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ public class PresignedUrlRequest {
3030

3131
/** Optional: Specify the KMS key ID to be used for encryption in a presignedUrl upload */
3232
private final String kmsKeyId;
33+
34+
/**
35+
* Optional: Specify the Content-Disposition header to override in a presigned download URL.
36+
*/
37+
private final String contentDisposition;
3338
}

blob/blob-gcp/src/main/java/com/salesforce/multicloudj/blob/gcp/GcpBlobStore.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import com.salesforce.multicloudj.blob.driver.MultipartUploadRequest;
5858
import com.salesforce.multicloudj.blob.driver.MultipartUploadResponse;
5959
import com.salesforce.multicloudj.blob.driver.ObjectLockInfo;
60+
import com.salesforce.multicloudj.blob.driver.PresignedOperation;
6061
import com.salesforce.multicloudj.blob.driver.PresignedUrlRequest;
6162
import com.salesforce.multicloudj.blob.driver.RetentionMode;
6263
import com.salesforce.multicloudj.blob.driver.UploadRequest;
@@ -109,6 +110,7 @@ public class GcpBlobStore extends AbstractBlobStore {
109110
private final MultipartUploadClient multipartUploadClient;
110111
private final GcpTransformer transformer;
111112
private static final String TAG_PREFIX = "gcp-tag-";
113+
private static final String RESPONSE_CONTENT_DISPOSITION = "response-content-disposition";
112114

113115
public GcpBlobStore() {
114116
this(new Builder(), null, null);
@@ -565,6 +567,12 @@ protected URL doGeneratePresignedUrl(PresignedUrlRequest request) {
565567
if (request.getMetadata() != null) {
566568
options.add(Storage.SignUrlOption.withExtHeaders(request.getMetadata()));
567569
}
570+
if (request.getContentDisposition() != null
571+
&& request.getType() == PresignedOperation.DOWNLOAD) {
572+
Map<String, String> queryParams = new HashMap<>();
573+
queryParams.put(RESPONSE_CONTENT_DISPOSITION, request.getContentDisposition());
574+
options.add(Storage.SignUrlOption.withQueryParams(queryParams));
575+
}
568576

569577
return storage.signUrl(
570578
blobInfo,

blob/blob-gcp/src/test/java/com/salesforce/multicloudj/blob/gcp/GcpBlobStoreTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,84 @@ void testDoGeneratePresignedUrl_WithoutKmsKey() throws Exception {
12681268
any(Storage.SignUrlOption[].class));
12691269
}
12701270

1271+
@Test
1272+
void testDoGeneratePresignedUrl_WithContentDisposition() throws Exception {
1273+
Duration duration = Duration.ofHours(4);
1274+
String contentDisposition = "attachment; filename=\"report.pdf\"";
1275+
PresignedUrlRequest presignedUrlRequest =
1276+
PresignedUrlRequest.builder()
1277+
.type(PresignedOperation.DOWNLOAD)
1278+
.key(TEST_KEY)
1279+
.duration(duration)
1280+
.contentDisposition(contentDisposition)
1281+
.build();
1282+
1283+
URL expectedUrl = new URL("https://signed-url-with-cd.example.com");
1284+
1285+
when(mockTransformer.toBlobInfo(presignedUrlRequest)).thenReturn(mockBlobInfo);
1286+
when(mockStorage.signUrl(
1287+
eq(mockBlobInfo),
1288+
any(Long.class),
1289+
eq(TimeUnit.MILLISECONDS),
1290+
any(Storage.SignUrlOption[].class)))
1291+
.thenReturn(expectedUrl);
1292+
1293+
URL actualUrl = gcpBlobStore.doGeneratePresignedUrl(presignedUrlRequest);
1294+
1295+
assertEquals(expectedUrl, actualUrl);
1296+
ArgumentCaptor<Storage.SignUrlOption[]> optionsCaptor =
1297+
ArgumentCaptor.forClass(Storage.SignUrlOption[].class);
1298+
verify(mockStorage)
1299+
.signUrl(
1300+
eq(mockBlobInfo),
1301+
eq(duration.toMillis()),
1302+
eq(TimeUnit.MILLISECONDS),
1303+
optionsCaptor.capture());
1304+
Storage.SignUrlOption[] options = optionsCaptor.getValue();
1305+
assertEquals(
1306+
3, options.length, "Download with contentDisposition should have httpMethod, v4Signature,"
1307+
+ " and queryParams options");
1308+
}
1309+
1310+
@Test
1311+
void testDoGeneratePresignedUrl_UploadIgnoresContentDisposition() throws Exception {
1312+
Duration duration = Duration.ofHours(4);
1313+
String contentDisposition = "attachment; filename=\"report.pdf\"";
1314+
PresignedUrlRequest presignedUrlRequest =
1315+
PresignedUrlRequest.builder()
1316+
.type(PresignedOperation.UPLOAD)
1317+
.key(TEST_KEY)
1318+
.duration(duration)
1319+
.contentDisposition(contentDisposition)
1320+
.build();
1321+
1322+
URL expectedUrl = new URL("https://signed-url-for-upload.example.com");
1323+
1324+
when(mockTransformer.toBlobInfo(presignedUrlRequest)).thenReturn(mockBlobInfo);
1325+
when(mockStorage.signUrl(
1326+
eq(mockBlobInfo),
1327+
any(Long.class),
1328+
eq(TimeUnit.MILLISECONDS),
1329+
any(Storage.SignUrlOption[].class)))
1330+
.thenReturn(expectedUrl);
1331+
1332+
URL actualUrl = gcpBlobStore.doGeneratePresignedUrl(presignedUrlRequest);
1333+
1334+
assertEquals(expectedUrl, actualUrl);
1335+
ArgumentCaptor<Storage.SignUrlOption[]> optionsCaptor =
1336+
ArgumentCaptor.forClass(Storage.SignUrlOption[].class);
1337+
verify(mockStorage)
1338+
.signUrl(
1339+
eq(mockBlobInfo),
1340+
eq(duration.toMillis()),
1341+
eq(TimeUnit.MILLISECONDS),
1342+
optionsCaptor.capture());
1343+
Storage.SignUrlOption[] options = optionsCaptor.getValue();
1344+
assertEquals(
1345+
2, options.length,
1346+
"Upload should only have httpMethod and v4Signature options, no queryParams");
1347+
}
1348+
12711349
// Test class to access protected methods
12721350
private static class TestGcpBlobStore extends GcpBlobStore {
12731351
public TestGcpBlobStore(Builder builder, Storage storage, MultipartUploadClient client) {

registry/registry-aws/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@
9090
<version>5.12.1</version>
9191
<scope>test</scope>
9292
</dependency>
93+
<dependency>
94+
<groupId>org.junit.jupiter</groupId>
95+
<artifactId>junit-jupiter-params</artifactId>
96+
<version>5.12.1</version>
97+
<scope>test</scope>
98+
</dependency>
9399
<dependency>
94100
<groupId>org.mockito</groupId>
95101
<artifactId>mockito-junit-jupiter</artifactId>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.salesforce.multicloudj.registry.aws;
2+
3+
import java.net.URI;
4+
import org.apache.http.HttpHeaders;
5+
import org.apache.http.HttpHost;
6+
import org.apache.http.HttpRequest;
7+
import org.apache.http.HttpRequestInterceptor;
8+
import org.apache.http.client.protocol.HttpClientContext;
9+
import org.apache.http.protocol.HttpContext;
10+
11+
/**
12+
* HTTP request interceptor that strips Authorization headers when the request target is not the
13+
* registry host.
14+
*
15+
* <p>This is necessary because AWS ECR redirects blob downloads to S3 pre-signed URLs, which
16+
* already contain authentication in query parameters. Sending an Authorization header to S3 causes
17+
* a 400 error ("Only one auth mechanism allowed").
18+
*/
19+
public class AuthStrippingInterceptor implements HttpRequestInterceptor {
20+
private final String registryHost;
21+
22+
/**
23+
* @param registryEndpoint the registry base URL
24+
*/
25+
public AuthStrippingInterceptor(String registryEndpoint) {
26+
this.registryHost = extractHost(registryEndpoint);
27+
}
28+
29+
@Override
30+
public void process(HttpRequest request, HttpContext context) {
31+
HttpHost targetHost = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
32+
if (targetHost == null) {
33+
return;
34+
}
35+
if (!registryHost.equalsIgnoreCase(targetHost.getHostName())) {
36+
request.removeHeaders(HttpHeaders.AUTHORIZATION);
37+
}
38+
}
39+
40+
/** Extracts the hostname from a URL, stripping scheme, port, and path. */
41+
private static String extractHost(String url) {
42+
return URI.create(url).getHost();
43+
}
44+
}

0 commit comments

Comments
 (0)