Skip to content

Commit e353f6f

Browse files
committed
test(seaweedfs): add deterministic SigV4 signature-verification test
The quota tests only checked that an Authorization header exists; they did not verify the SigV4 canonical query, payload hash, or signed headers against a known signature. A signing mismatch would therefore pass the suite and make every quota operation fail against SeaweedFS. Add a test that independently signs the same request through AWSS3V4Signer and asserts the Authorization, x-amz-content-sha256, and x-amz-date headers match exactly.
1 parent 7eb6938 commit e353f6f

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

‎plugins/storage/object/seaweedfs/src/test/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImplTest.java‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,87 @@ public void testSetBucketQuotaRejects3xx() throws Exception {
357357
assertThrows(CloudRuntimeException.class, () -> driver.setBucketQuota(bucketTO, TEST_STORE_ID, 10));
358358
}
359359

360+
/**
361+
* Deterministic SigV4 signature-verification test.
362+
*
363+
* Signs the same request through the AWS SDK v1 AWSS3V4Signer (the same
364+
* signer the production code uses) and asserts that the Authorization
365+
* header, signed headers, x-amz-content-sha256, and x-amz-date produced
366+
* by the driver's request match. This catches signing regressions (e.g.
367+
* the query parameter not being in the canonical query string) that a
368+
* mere "header exists" check would miss.
369+
*/
370+
@Test
371+
public void testSetBucketQuotaSigV4SignatureVerification() throws Exception {
372+
String accessKey = "AKIAIOSFODNN7EXAMPLE";
373+
String secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
374+
String bucketName = "quota-sig-test";
375+
String s3Url = "http://s3.example.com:8333";
376+
long quotaGiB = 5;
377+
378+
BucketTO bucketTO = mock(BucketTO.class);
379+
when(bucketTO.getName()).thenReturn(bucketName);
380+
doReturn(s3Url).when(driver).getS3Url(TEST_STORE_ID);
381+
doReturn(accessKey).when(driver).getAccessKey(TEST_STORE_ID);
382+
doReturn(secretKey).when(driver).getSecretKey(TEST_STORE_ID);
383+
384+
HttpClient mockHttpClient = mock(HttpClient.class);
385+
HttpResponse<String> mockResponse = mock(HttpResponse.class);
386+
when(mockResponse.statusCode()).thenReturn(200);
387+
when(mockResponse.body()).thenReturn("");
388+
when(mockHttpClient.send(ArgumentMatchers.<HttpRequest>any(),
389+
ArgumentMatchers.<HttpResponse.BodyHandler<String>>any())).thenReturn(mockResponse);
390+
doReturn(mockHttpClient).when(driver).getS3ExtensionHttpClient();
391+
392+
driver.setBucketQuota(bucketTO, TEST_STORE_ID, quotaGiB);
393+
394+
ArgumentCaptor<HttpRequest> reqCaptor = ArgumentCaptor.forClass(HttpRequest.class);
395+
verify(mockHttpClient, times(1)).send(reqCaptor.capture(),
396+
ArgumentMatchers.<HttpResponse.BodyHandler<String>>any());
397+
HttpRequest sent = reqCaptor.getValue();
398+
399+
// Build the expected signed request the same way the production code does
400+
String expectedBody = String.format("{\"quota_size\":%d,\"quota_unit\":\"GB\",\"quota_enabled\":true}", quotaGiB);
401+
byte[] bodyBytes = expectedBody.getBytes(StandardCharsets.UTF_8);
402+
403+
com.amazonaws.DefaultRequest<?> expectedRequest = new com.amazonaws.DefaultRequest<>("s3");
404+
expectedRequest.setEndpoint(java.net.URI.create(s3Url));
405+
expectedRequest.setHttpMethod(com.amazonaws.http.HttpMethodName.PUT);
406+
expectedRequest.setResourcePath("/" + bucketName);
407+
expectedRequest.addParameter("seaweedfs-quota", "");
408+
expectedRequest.setContent(new java.io.ByteArrayInputStream(bodyBytes));
409+
expectedRequest.getHeaders().put("Content-Length", String.valueOf(bodyBytes.length));
410+
expectedRequest.getHeaders().put("Content-Type", "application/json");
411+
412+
com.amazonaws.auth.AWSCredentials credentials = new com.amazonaws.auth.BasicAWSCredentials(accessKey, secretKey);
413+
com.amazonaws.services.s3.internal.AWSS3V4Signer signer = new com.amazonaws.services.s3.internal.AWSS3V4Signer();
414+
signer.setServiceName("s3");
415+
signer.setRegionName("us-east-1");
416+
signer.sign(expectedRequest, credentials);
417+
418+
// The Authorization header must match exactly — proves the canonical
419+
// query string (including seaweedfs-quota), payload hash, and signed
420+
// headers all match the independently signed reference request.
421+
String expectedAuth = expectedRequest.getHeaders().get("Authorization");
422+
String actualAuth = sent.headers().firstValue("Authorization").orElse(null);
423+
assertNotNull("Authorization header must be present", actualAuth);
424+
assertEquals("SigV4 Authorization header must match the reference signature", expectedAuth, actualAuth);
425+
426+
// The payload hash must be present and match
427+
String expectedContentSha = expectedRequest.getHeaders().get("x-amz-content-sha256");
428+
String actualContentSha = sent.headers().firstValue("x-amz-content-sha256").orElse(null);
429+
assertEquals("x-amz-content-sha256 must match", expectedContentSha, actualContentSha);
430+
431+
// The signed headers list must include the query-signing-relevant headers
432+
String expectedDate = expectedRequest.getHeaders().get("x-amz-date");
433+
String actualDate = sent.headers().firstValue("x-amz-date").orElse(null);
434+
assertEquals("x-amz-date must match", expectedDate, actualDate);
435+
436+
// The query string must carry the subresource
437+
assertNotNull("URI must have a query string", sent.uri().getQuery());
438+
assertTrue("query must carry seaweedfs-quota", sent.uri().getQuery().contains("seaweedfs-quota"));
439+
}
440+
360441
@Test
361442
public void testSetBucketQuotaNoS3ConfigThrows() {
362443
BucketTO bucketTO = mock(BucketTO.class);

0 commit comments

Comments
 (0)