Skip to content

Commit 5fd2e6e

Browse files
authored
Decouple policy logic from resource url for getSignedUrlWithCustomPolicy (#5862)
* Decouple policy logic from resource url for getSignedUrlWithCustomPolicy * Adding validation for resourceUrl to avoid NPE * Add changelog * Fix checkstyle * Renaming resource policy, adding documentation, refactoring * Adding not null validations * Changing resources name for intg test suite recreation * Renaming policyResource, adding a test * Adding parameterized test * Fix checkstyle --------- Co-authored-by: Ran Vaknin <[email protected]>
1 parent 36c89a0 commit 5fd2e6e

File tree

6 files changed

+242
-12
lines changed

6 files changed

+242
-12
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon Cloudfront",
4+
"contributor": "",
5+
"description": "Decouple policy logic from resource url for getSignedUrlWithCustomPolicy"
6+
}

services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilities.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import software.amazon.awssdk.services.cloudfront.model.CannedSignerRequest;
3434
import software.amazon.awssdk.services.cloudfront.model.CustomSignerRequest;
3535
import software.amazon.awssdk.services.cloudfront.url.SignedUrl;
36+
import software.amazon.awssdk.utils.StringUtils;
3637

3738
/**
3839
*
@@ -216,7 +217,13 @@ public SignedUrl getSignedUrlWithCustomPolicy(Consumer<CustomSignerRequest.Build
216217
*
217218
* @param request
218219
* A {@link CustomSignerRequest} configured with the following values:
219-
* resourceUrl, privateKey, keyPairId, expirationDate, activeDate (optional), ipRange (optional)
220+
* resourceUrl,
221+
* privateKey,
222+
* keyPairId,
223+
* expirationDate,
224+
* activeDate (optional),
225+
* ipRange (optional),
226+
* resourceUrlPattern (optional)
220227
* @return A signed URL that will permit access to distribution and S3
221228
* objects as specified in the policy document.
222229
*
@@ -233,6 +240,7 @@ public SignedUrl getSignedUrlWithCustomPolicy(Consumer<CustomSignerRequest.Build
233240
* Path keyFile = myKeyFile;
234241
* Instant activeDate = Instant.now().plus(Duration.ofDays(2));
235242
* String ipRange = "192.168.0.1/24";
243+
* String resourceUrlPattern = "*"; // If not supplied, defaults to the value of resourceUrl.
236244
*
237245
* CustomSignerRequest customRequest = CustomSignerRequest.builder()
238246
* .resourceUrl(resourceUrl)
@@ -241,16 +249,24 @@ public SignedUrl getSignedUrlWithCustomPolicy(Consumer<CustomSignerRequest.Build
241249
* .expirationDate(expirationDate)
242250
* .activeDate(activeDate)
243251
* .ipRange(ipRange)
252+
* .resourceUrlPattern(resourceUrlPattern)
244253
* .build();
245254
* SignedUrl signedUrl = utilities.getSignedUrlWithCustomPolicy(customRequest);
246255
* String url = signedUrl.url();
247256
* }
248257
*/
249258
public SignedUrl getSignedUrlWithCustomPolicy(CustomSignerRequest request) {
259+
String resourceUrl = request.resourceUrl();
250260
try {
251-
String resourceUrl = request.resourceUrl();
252-
String policy = SigningUtils.buildCustomPolicyForSignedUrl(request.resourceUrl(), request.activeDate(),
253-
request.expirationDate(), request.ipRange());
261+
String resourceUrlPattern = StringUtils.isEmpty(request.resourceUrlPattern())
262+
? request.resourceUrl()
263+
: request.resourceUrlPattern();
264+
265+
String policy = SigningUtils.buildCustomPolicyForSignedUrl(resourceUrlPattern,
266+
request.activeDate(),
267+
request.expirationDate(),
268+
request.ipRange());
269+
254270
byte[] signatureBytes = SigningUtils.signWithSha1Rsa(policy.getBytes(UTF_8), request.privateKey());
255271
String urlSafePolicy = SigningUtils.makeStringUrlSafe(policy);
256272
String urlSafeSignature = SigningUtils.makeBytesUrlSafe(signatureBytes);

services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/internal/utils/SigningUtils.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import software.amazon.awssdk.services.cloudfront.internal.auth.Rsa;
3535
import software.amazon.awssdk.utils.IoUtils;
3636
import software.amazon.awssdk.utils.StringUtils;
37+
import software.amazon.awssdk.utils.Validate;
3738

3839
@SdkInternalApi
3940
public final class SigningUtils {
@@ -175,12 +176,13 @@ public static String buildCustomPolicyForSignedUrl(String resourceUrl,
175176
Instant activeDate,
176177
Instant expirationDate,
177178
String limitToIpAddressCidr) {
178-
if (expirationDate == null) {
179-
throw SdkClientException.create("Expiration date must be provided to sign CloudFront URLs");
180-
}
179+
180+
Validate.notNull(expirationDate, "Expiration date must be provided to sign CloudFront URLs");
181+
181182
if (resourceUrl == null) {
182183
resourceUrl = "*";
183184
}
185+
184186
return buildCustomPolicy(resourceUrl, activeDate, expirationDate, limitToIpAddressCidr);
185187
}
186188

services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/model/CustomSignerRequest.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import software.amazon.awssdk.annotations.SdkPublicApi;
2525
import software.amazon.awssdk.annotations.ThreadSafe;
2626
import software.amazon.awssdk.services.cloudfront.internal.utils.SigningUtils;
27+
import software.amazon.awssdk.utils.Validate;
2728
import software.amazon.awssdk.utils.builder.CopyableBuilder;
2829
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
2930

@@ -35,21 +36,22 @@
3536
@SdkPublicApi
3637
public final class CustomSignerRequest implements CloudFrontSignerRequest,
3738
ToCopyableBuilder<CustomSignerRequest.Builder, CustomSignerRequest> {
38-
3939
private final String resourceUrl;
4040
private final PrivateKey privateKey;
4141
private final String keyPairId;
4242
private final Instant expirationDate;
4343
private final Instant activeDate;
4444
private final String ipRange;
45+
private final String resourceUrlPattern;
4546

4647
private CustomSignerRequest(DefaultBuilder builder) {
47-
this.resourceUrl = builder.resourceUrl;
48+
this.resourceUrl = Validate.notNull(builder.resourceUrl, "resourceUrl must not be null");
4849
this.privateKey = builder.privateKey;
4950
this.keyPairId = builder.keyPairId;
5051
this.expirationDate = builder.expirationDate;
5152
this.activeDate = builder.activeDate;
5253
this.ipRange = builder.ipRange;
54+
this.resourceUrlPattern = builder.resourceUrlPattern;
5355
}
5456

5557
/**
@@ -99,6 +101,10 @@ public String ipRange() {
99101
return ipRange;
100102
}
101103

104+
public String resourceUrlPattern() {
105+
return resourceUrlPattern;
106+
}
107+
102108
@Override
103109
public boolean equals(Object o) {
104110
if (this == o) {
@@ -114,7 +120,8 @@ public boolean equals(Object o) {
114120
&& Objects.equals(keyPairId, cookie.keyPairId)
115121
&& Objects.equals(expirationDate, cookie.expirationDate)
116122
&& Objects.equals(activeDate, cookie.activeDate)
117-
&& Objects.equals(ipRange, cookie.ipRange);
123+
&& Objects.equals(ipRange, cookie.ipRange)
124+
&& Objects.equals(resourceUrlPattern, cookie.resourceUrlPattern);
118125
}
119126

120127
@Override
@@ -125,6 +132,7 @@ public int hashCode() {
125132
result = 31 * result + (expirationDate != null ? expirationDate.hashCode() : 0);
126133
result = 31 * result + (activeDate != null ? activeDate.hashCode() : 0);
127134
result = 31 * result + (ipRange != null ? ipRange.hashCode() : 0);
135+
result = 31 * result + (resourceUrlPattern != null ? resourceUrlPattern.hashCode() : 0);
128136
return result;
129137
}
130138

@@ -179,6 +187,16 @@ public interface Builder extends CopyableBuilder<CustomSignerRequest.Builder, Cu
179187
* IPv6 format is not supported.
180188
*/
181189
Builder ipRange(String ipRange);
190+
191+
/**
192+
* Configure the resource URL pattern to be used in the policy
193+
* <p>
194+
* For custom policies, this specifies the URL pattern that determines which files
195+
* can be accessed with this signed URL. This can include wildcard characters (*) to
196+
* grant access to multiple files or paths. If not specified, the resourceUrl value
197+
* will be used in the policy.
198+
*/
199+
Builder resourceUrlPattern(String resourceUrlPattern);
182200
}
183201

184202
private static final class DefaultBuilder implements Builder {
@@ -188,6 +206,7 @@ private static final class DefaultBuilder implements Builder {
188206
private Instant expirationDate;
189207
private Instant activeDate;
190208
private String ipRange;
209+
private String resourceUrlPattern;
191210

192211
private DefaultBuilder() {
193212
}
@@ -199,6 +218,7 @@ private DefaultBuilder(CustomSignerRequest request) {
199218
this.expirationDate = request.expirationDate;
200219
this.activeDate = request.activeDate;
201220
this.ipRange = request.ipRange;
221+
this.resourceUrlPattern = request.resourceUrlPattern;
202222
}
203223

204224
@Override
@@ -243,6 +263,12 @@ public Builder ipRange(String ipRange) {
243263
return this;
244264
}
245265

266+
@Override
267+
public Builder resourceUrlPattern(String resourceUrlPattern) {
268+
this.resourceUrlPattern = resourceUrlPattern;
269+
return this;
270+
}
271+
246272
@Override
247273
public CustomSignerRequest build() {
248274
return new CustomSignerRequest(this);

services/cloudfront/src/test/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilitiesIntegrationTest.java

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.FileWriter;
2222
import java.io.IOException;
2323
import java.io.InputStream;
24+
import java.net.URI;
2425
import java.nio.file.Files;
2526
import java.nio.file.Path;
2627
import java.nio.file.StandardOpenOption;
@@ -74,9 +75,12 @@
7475

7576
public class CloudFrontUtilitiesIntegrationTest extends IntegrationTestBase {
7677
private static final Base64.Encoder ENCODER = Base64.getEncoder();
77-
private static final String RESOURCE_PREFIX = "do-not-delete-cf-test-";
78+
private static final String RESOURCE_PREFIX = "do-not-delete-cf-test-v2";
7879
private static final String CALLER_REFERENCE = UUID.randomUUID().toString();
7980
private static final String S3_OBJECT_KEY = "s3ObjectKey";
81+
private static final String S3_OBJECT_KEY_ON_SUB_PATH = "foo/specific-file";
82+
private static final String S3_OBJECT_KEY_ON_SUB_PATH_OTHER = "foo/other-file";
83+
8084

8185
private static String bucket;
8286
private static String domainName;
@@ -267,6 +271,114 @@ void getCookiesForCustomPolicy_withFutureActiveDate_shouldReturn403Response() th
267271
assertThat(response.httpResponse().statusCode()).isEqualTo(expectedStatus);
268272
}
269273

274+
@Test
275+
void getSignedUrlWithCustomPolicy_shouldAllowQueryParametersWhenUsingWildcard() throws Exception {
276+
Instant expirationDate = LocalDate.of(2050, 1, 1)
277+
.atStartOfDay()
278+
.toInstant(ZoneOffset.of("Z"));
279+
280+
Instant activeDate = LocalDate.of(2022, 1, 1)
281+
.atStartOfDay()
282+
.toInstant(ZoneOffset.of("Z"));
283+
284+
CustomSignerRequest request = CustomSignerRequest.builder()
285+
.resourceUrl(resourceUrl)
286+
.privateKey(keyFilePath)
287+
.keyPairId(keyPairId)
288+
.resourceUrlPattern(resourceUrl + "*")
289+
.activeDate(activeDate)
290+
.expirationDate(expirationDate)
291+
.build();
292+
293+
SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(request);
294+
295+
String urlWithDynamicParam = signedUrl.url() + "&foo=bar";
296+
URI modifiedUri = URI.create(urlWithDynamicParam);
297+
298+
299+
SdkHttpClient client = ApacheHttpClient.create();
300+
HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder()
301+
.request(SdkHttpRequest.builder()
302+
.encodedPath(modifiedUri.getRawPath() + "?" + modifiedUri.getRawQuery())
303+
.host(modifiedUri.getHost())
304+
.method(SdkHttpMethod.GET)
305+
.protocol("https")
306+
.build())
307+
.build()).call();
308+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
309+
}
310+
311+
@Test
312+
void getSignedUrlWithCustomPolicy_wildCardPath() throws Exception {
313+
String resourceUri = "https://" + domainName;
314+
Instant expirationDate = LocalDate.of(2050, 1, 1)
315+
.atStartOfDay()
316+
.toInstant(ZoneOffset.of("Z"));
317+
318+
Instant activeDate = LocalDate.of(2022, 1, 1)
319+
.atStartOfDay()
320+
.toInstant(ZoneOffset.of("Z"));
321+
322+
CustomSignerRequest request = CustomSignerRequest.builder()
323+
.resourceUrl(resourceUri + "/foo/specific-file")
324+
.privateKey(keyFilePath)
325+
.keyPairId(keyPairId)
326+
.resourceUrlPattern(resourceUri + "/foo/*")
327+
.activeDate(activeDate)
328+
.expirationDate(expirationDate)
329+
.build();
330+
331+
SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(request);
332+
333+
334+
URI modifiedUri = URI.create(signedUrl.url().replace("/specific-file","/other-file"));
335+
SdkHttpClient client = ApacheHttpClient.create();
336+
HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder()
337+
.request(SdkHttpRequest.builder()
338+
.encodedPath(modifiedUri.getRawPath() + "?" + modifiedUri.getRawQuery())
339+
.host(modifiedUri.getHost())
340+
.method(SdkHttpMethod.GET)
341+
.protocol("https")
342+
.build())
343+
.build()).call();
344+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
345+
}
346+
347+
@Test
348+
void getSignedUrlWithCustomPolicy_wildCardPolicyResource_allowsAnyPath() throws Exception {
349+
Instant expirationDate = LocalDate.of(2050, 1, 1)
350+
.atStartOfDay()
351+
.toInstant(ZoneOffset.of("Z"));
352+
353+
Instant activeDate = LocalDate.of(2022, 1, 1)
354+
.atStartOfDay()
355+
.toInstant(ZoneOffset.of("Z"));
356+
357+
CustomSignerRequest request = CustomSignerRequest.builder()
358+
.resourceUrl(resourceUrl)
359+
.privateKey(keyFilePath)
360+
.keyPairId(keyPairId)
361+
.resourceUrlPattern("*")
362+
.activeDate(activeDate)
363+
.expirationDate(expirationDate)
364+
.build();
365+
366+
SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(request);
367+
368+
369+
URI modifiedUri = URI.create(signedUrl.url().replace("/s3ObjectKey","/foo/other-file"));
370+
SdkHttpClient client = ApacheHttpClient.create();
371+
HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder()
372+
.request(SdkHttpRequest.builder()
373+
.encodedPath(modifiedUri.getRawPath() + "?" + modifiedUri.getRawQuery())
374+
.host(modifiedUri.getHost())
375+
.method(SdkHttpMethod.GET)
376+
.protocol("https")
377+
.build())
378+
.build()).call();
379+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
380+
}
381+
270382
private static void initStaticFields() throws Exception {
271383
initializeKeyFileAndPair();
272384
originAccessId = getOrCreateOriginAccessIdentity();
@@ -409,7 +521,11 @@ private static String getOrCreateBucket() throws IOException {
409521
s3Client.waiter().waitUntilBucketExists(r -> r.bucket(newBucketName));
410522

411523
File content = new RandomTempFile("testFile", 1000L);
524+
File content2 = new RandomTempFile("testFile2", 500L);
412525
s3Client.putObject(PutObjectRequest.builder().bucket(newBucketName).key(S3_OBJECT_KEY).build(), RequestBody.fromFile(content));
526+
s3Client.putObject(PutObjectRequest.builder().bucket(newBucketName).key(S3_OBJECT_KEY_ON_SUB_PATH).build(), RequestBody.fromFile(content2));
527+
s3Client.putObject(PutObjectRequest.builder().bucket(newBucketName).key(S3_OBJECT_KEY_ON_SUB_PATH_OTHER).build(), RequestBody.fromFile(content2));
528+
413529

414530
String bucketPolicy = "{\n"
415531
+ "\"Version\":\"2012-10-17\",\n"

0 commit comments

Comments
 (0)