Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
020c466
feat: implement response body SHA-256 verification
hevinhsu Nov 13, 2025
0b76f03
feat: add integration test
hevinhsu Nov 13, 2025
41cf5c4
feat: add end to end test
hevinhsu Nov 13, 2025
a677197
fix typo
hevinhsu Nov 13, 2025
0a6bb3b
recover non necessary change
hevinhsu Nov 14, 2025
25c7f78
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Nov 14, 2025
a820e9d
Update comments
hevinhsu Nov 14, 2025
0bee1d3
Add integration test to verify x-amz-content-sha256 mismatch
hevinhsu Nov 14, 2025
99a120a
revert implementation and relative unit tests
hevinhsu Nov 18, 2025
1132cbf
feat: implement multi-digest input stream
hevinhsu Nov 18, 2025
e16ebad
feat: rename class name and method name
hevinhsu Nov 18, 2025
a149a36
feat: add utils to verify x-amz-content-sha256
hevinhsu Nov 18, 2025
0563f7d
feat: use MultiDigestInputStream to verify x-amz-content-sha256
hevinhsu Nov 18, 2025
ea5e0f4
fix: findbugs and checkstyle
hevinhsu Nov 18, 2025
c5f5f56
fix: end to end tests
hevinhsu Nov 19, 2025
61bd0bb
feat: add object checking when sha256 mismatch
hevinhsu Nov 19, 2025
f0e32d0
fix: end to end test failed
hevinhsu Nov 20, 2025
0e72c78
feat: expose cleanup method to prevent memory leak
hevinhsu Nov 20, 2025
44ce54f
feat: add sha-256 verification for streaming mode
hevinhsu Nov 20, 2025
0c803c5
chore: remove test code
hevinhsu Nov 20, 2025
c3294c2
fix checkstyle and findbugs
hevinhsu Nov 20, 2025
b4f08d3
fix comments from github copilot
hevinhsu Nov 20, 2025
056c90a
refactor: use const for "SHA-256"
hevinhsu Nov 20, 2025
d0de195
fix compiler error in java11
hevinhsu Nov 20, 2025
50ad20f
try to fix compile error
hevinhsu Nov 20, 2025
f68b526
chore: update comment to fix current implementation
hevinhsu Nov 21, 2025
37cdf22
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Nov 21, 2025
1399fd4
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Dec 1, 2025
9c4176d
revert close output stream in streaming mode
hevinhsu Dec 1, 2025
acbdd0e
revert close output stream manually
hevinhsu Dec 1, 2025
02d6c67
feature: add preCommit for sha256 validation
hevinhsu Dec 2, 2025
e38799d
feat: address comments about sha256 calc and reset digest api call
hevinhsu Dec 2, 2025
6bd7391
feat: add sha256 validation in preCommit and use existed method to do…
hevinhsu Dec 2, 2025
e45a4f6
chore: revert non-relative changes
hevinhsu Dec 2, 2025
0a7dd2d
fix: pmd error
hevinhsu Dec 2, 2025
2e5858b
fix: acceptance test failed and update comments
hevinhsu Dec 2, 2025
6493d84
fix: put object failed
hevinhsu Dec 2, 2025
eca2982
feat: add preCommit in `ECKeyOuptputStream`
hevinhsu Dec 2, 2025
91ad95e
refactor: remove duplicate validation
hevinhsu Dec 4, 2025
a441e0e
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Dec 4, 2025
b889489
fix: checkstyle
hevinhsu Dec 4, 2025
d99e470
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Dec 4, 2025
1298fb8
chore: revert non-relative changes
hevinhsu Dec 5, 2025
be04ba3
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Dec 5, 2025
a8be8d2
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Dec 5, 2025
2ea5ea7
feat: address comments
hevinhsu Dec 21, 2025
0c6c947
merge upstream changes
hevinhsu Dec 21, 2025
99e7aa0
fix checkstyle
hevinhsu Dec 21, 2025
2609b87
Merge branch 'master' into HDDS-13668
hevinhsu Dec 22, 2025
de81605
feat: add missing tests
hevinhsu Dec 22, 2025
32bc363
fix compile error
hevinhsu Dec 22, 2025
ba58459
feat: use list to contain preCommit hooks
hevinhsu Dec 22, 2025
185a7e0
Fix checkstyle
hevinhsu Dec 22, 2025
9f9af72
recover non-related change
hevinhsu Dec 29, 2025
2e65af6
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Dec 29, 2025
86d1230
Merge remote-tracking branch 'origin/master' into HDDS-13668
hevinhsu Dec 30, 2025
1f460cf
feat: change OS3Exception to extend runtime exception
hevinhsu Dec 30, 2025
a889dc1
feat: use CheckedRunnable<IOException> for preCommit hook
hevinhsu Dec 30, 2025
b40ffb4
feat: update comment
hevinhsu Dec 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import jakarta.annotation.Nonnull;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
Expand All @@ -47,6 +49,7 @@
import org.apache.ozone.erasurecode.rawcoder.RawErasureEncoder;
import org.apache.ozone.erasurecode.rawcoder.util.CodecUtil;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
import org.apache.ratis.util.function.CheckedRunnable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -86,6 +89,13 @@ public final class ECKeyOutputStream extends KeyOutputStream
// how much data has been ingested into the stream
private long writeOffset;

private List<CheckedRunnable<IOException>> preCommits = Collections.emptyList();

@Override
public void setPreCommits(@Nonnull List<CheckedRunnable<IOException>> preCommits) {
this.preCommits = preCommits;
}

@VisibleForTesting
public void insertFlushCheckpoint(long version) throws IOException {
addStripeToQueue(new CheckpointDummyStripe(version));
Expand Down Expand Up @@ -485,6 +495,9 @@ public void close() throws IOException {
"Expected: %d and actual %d write sizes do not match",
expectedSize, offset));
}
for (CheckedRunnable<IOException> preCommit : preCommits) {
preCommit.run();
}
blockOutputStreamEntryPool.commitKey(offset);
}
} catch (ExecutionException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import jakarta.annotation.Nonnull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand All @@ -44,6 +46,7 @@
import org.apache.hadoop.ozone.om.helpers.OmMultipartCommitUploadPartInfo;
import org.apache.hadoop.ozone.om.helpers.OpenKeySession;
import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
import org.apache.ratis.util.function.CheckedRunnable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -82,6 +85,12 @@ public class KeyDataStreamOutput extends AbstractDataStreamOutput
*/
private boolean atomicKeyCreation;

private List<CheckedRunnable<IOException>> preCommits = Collections.emptyList();

public void setPreCommits(@Nonnull List<CheckedRunnable<IOException>> preCommits) {
this.preCommits = preCommits;
}

@VisibleForTesting
public List<BlockDataStreamOutputEntry> getStreamEntries() {
return blockDataStreamOutputEntryPool.getStreamEntries();
Expand Down Expand Up @@ -431,6 +440,9 @@ public void close() throws IOException {
String.format("Expected: %d and actual %d write sizes do not match",
expectedSize, offset));
}
for (CheckedRunnable<IOException> preCommit : preCommits) {
preCommit.run();
}
blockDataStreamOutputEntryPool.commitKey(offset);
} finally {
blockDataStreamOutputEntryPool.cleanup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import jakarta.annotation.Nonnull;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -110,6 +112,11 @@ public class KeyOutputStream extends OutputStream

private final int maxConcurrentWritePerKey;
private final KeyOutputStreamSemaphore keyOutputStreamSemaphore;
private List<CheckedRunnable<IOException>> preCommits = Collections.emptyList();

public void setPreCommits(@Nonnull List<CheckedRunnable<IOException>> preCommits) {
this.preCommits = preCommits;
}

@VisibleForTesting
KeyOutputStreamSemaphore getRequestSemaphore() {
Expand Down Expand Up @@ -655,6 +662,9 @@ private void closeInternal() throws IOException {
String.format("Expected: %d and actual %d write sizes do not match",
expectedSize, offset));
}
for (CheckedRunnable<IOException> preCommit : preCommits) {
preCommit.run();
}
blockOutputStreamEntryPool.commitKey(offset);
} finally {
blockOutputStreamEntryPool.cleanup();
Expand Down
73 changes: 73 additions & 0 deletions hadoop-ozone/dist/src/main/smoketest/s3/presigned_url_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import hashlib


def generate_presigned_put_object_url(
aws_access_key_id=None,
aws_secret_access_key=None,
bucket_name=None,
object_key=None,
region_name='us-east-1',
expiration=3600,
content_type=None,
endpoint_url=None,
):
"""
Generate a presigned URL for PUT Object. This function creates the S3 client internally.
"""
try:
import boto3

client_args = {
'service_name': 's3',
'region_name': region_name,
}

if aws_access_key_id and aws_secret_access_key:
client_args['aws_access_key_id'] = aws_access_key_id
client_args['aws_secret_access_key'] = aws_secret_access_key

if endpoint_url:
client_args['endpoint_url'] = endpoint_url

s3_client = boto3.client(**client_args)

params = {
'Bucket': bucket_name,
'Key': object_key,
}

if content_type:
params['ContentType'] = content_type

presigned_url = s3_client.generate_presigned_url(
ClientMethod='put_object',
Params=params,
ExpiresIn=expiration
)

return presigned_url

except Exception as e:
raise Exception(f"Failed to generate presigned URL: {str(e)}")


def compute_sha256_file(path):
"""Compute SHA256 hex digest for the entire file content at path."""
with open(path, 'rb') as f:
return hashlib.sha256(f.read()).hexdigest()
54 changes: 54 additions & 0 deletions hadoop-ozone/dist/src/main/smoketest/s3/presignedurl.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

*** Settings ***
Documentation S3 gateway test with aws cli
Library OperatingSystem
Library String
Library ./presigned_url_helper.py
Resource ../commonlib.robot
Resource commonawslib.robot
Test Timeout 5 minutes
Suite Setup Setup s3 tests

*** Variables ***
${ENDPOINT_URL} http://s3g:9878
${OZONE_TEST} true
${BUCKET} generated

*** Test Cases ***
Presigned URL PUT Object
[Documentation] Test presigned URL PUT object
Execute echo "Randomtext" > /tmp/testfile
${ACCESS_KEY} = Execute aws configure get aws_access_key_id
${SECRET_ACCESS_KEY} = Execute aws configure get aws_secret_access_key
${presigned_url}= Generate Presigned Put Object Url ${ACCESS_KEY} ${SECRET_ACCESS_KEY} ${BUCKET} test-presigned-put us-east-1 3600 ${EMPTY} ${ENDPOINT_URL}
${SHA256} = Compute Sha256 File /tmp/testfile
${result} = Execute curl -X PUT -T "/tmp/testfile" -H "x-amz-content-sha256: ${SHA256}" "${presigned_url}"
Should Not Contain ${result} Error
${head_result} = Execute AWSS3ApiCli head-object --bucket ${BUCKET} --key test-presigned-put
Should Not Contain ${head_result} Error

Presigned URL PUT Object using wrong x-amz-content-sha256
[Documentation] Test presigned URL PUT object with wrong x-amz-content-sha256
Execute echo "Randomtext" > /tmp/testfile
${ACCESS_KEY} = Execute aws configure get aws_access_key_id
${SECRET_ACCESS_KEY} = Execute aws configure get aws_secret_access_key
${presigned_url}= Generate Presigned Put Object Url ${ACCESS_KEY} ${SECRET_ACCESS_KEY} ${BUCKET} test-presigned-put-wrong-sha us-east-1 3600 ${EMPTY} ${ENDPOINT_URL}
${result} = Execute curl -X PUT -T "/tmp/testfile" -H "x-amz-content-sha256: wronghash" "${presigned_url}"
Should Contain ${result} The provided 'x-amz-content-sha256' header does not match the computed hash.
${head_result} = Execute AWSS3APICli and ignore error head-object --bucket ${BUCKET} --key test-presigned-put-wrong-sha
Should contain ${head_result} 404
Should contain ${head_result} Not Found
5 changes: 5 additions & 0 deletions hadoop-ozone/integration-test-s3/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
<artifactId>hadoop-common</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerby-util</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.ozone</groupId>
<artifactId>hdds-common</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,41 @@ public void testPresignedUrlPutObject() throws Exception {
}
}

@Test
public void testPresignedUrlPutSingleChunkWithWrongSha256() throws Exception {
final String keyName = getKeyName();

// Test PutObjectRequest presigned URL
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(BUCKET_NAME, keyName).withMethod(HttpMethod.PUT).withExpiration(expiration);
URL presignedUrl = s3Client.generatePresignedUrl(generatePresignedUrlRequest);

Map<String, List<String>> headers = new HashMap<>();
List<String> sha256Value = new ArrayList<>();
sha256Value.add("wrong-sha256-value");
headers.put("x-amz-content-sha256", sha256Value);

HttpURLConnection connection = null;
try {
connection = S3SDKTestUtils.openHttpURLConnection(presignedUrl, "PUT",
headers, CONTENT.getBytes(StandardCharsets.UTF_8));
int responseCode = connection.getResponseCode();
assertEquals(400, responseCode, "PutObject presigned URL should return 400 because of wrong SHA256");
} finally {
if (connection != null) {
connection.disconnect();
}
}

// Verify the object was not uploaded
AmazonServiceException ase = assertThrows(AmazonServiceException.class,
() -> s3Client.getObject(BUCKET_NAME, keyName));

assertEquals(ErrorType.Client, ase.getErrorType());
assertEquals(404, ase.getStatusCode());
assertEquals("NoSuchKey", ase.getErrorCode());
}

@Test
public void testPresignedUrlMultipartUpload(@TempDir Path tempDir) throws Exception {
final String keyName = getKeyName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import org.apache.hadoop.ozone.s3.S3ClientFactory;
import org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils;
import org.apache.hadoop.ozone.s3.endpoint.S3Owner;
import org.apache.hadoop.ozone.s3.util.S3Consts;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.ozone.test.NonHATests;
import org.apache.ozone.test.OzoneTestBase;
Expand Down Expand Up @@ -645,6 +646,41 @@ public void testPresignedUrlPut() throws Exception {
assertEquals(CONTENT, actualContent);
}

@Test
public void testPresignedUrlPutSingleChunkWithWrongSha256() throws Exception {
final String keyName = getKeyName();

PutObjectRequest objectRequest = PutObjectRequest.builder().bucket(BUCKET_NAME).key(keyName).build();

PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(duration)
.putObjectRequest(objectRequest)
.build();

PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);

Map<String, List<String>> headers = presignedRequest.signedHeaders();
List<String> sha256 = new ArrayList<>();
sha256.add("wrong-sha256-value");
headers.put(S3Consts.X_AMZ_CONTENT_SHA256, sha256);

// use http url connection
HttpURLConnection connection = null;
try {
connection = S3SDKTestUtils.openHttpURLConnection(presignedRequest.url(), "PUT",
headers, CONTENT.getBytes(StandardCharsets.UTF_8));
int responseCode = connection.getResponseCode();
assertEquals(400, responseCode, "PutObject presigned URL should return 400 because of wrong SHA256");
} finally {
if (connection != null) {
connection.disconnect();
}
}

// Verify the object was not uploaded
assertThrows(NoSuchKeyException.class, () -> s3Client.headObject(b -> b.bucket(BUCKET_NAME).key(keyName)));
}

@Test
public void testPresignedUrlMultipartUpload(@TempDir Path tempDir) throws Exception {
final String keyName = getKeyName();
Expand Down
Loading