From b65e6fcd7f520c763eca0464ebe9515599a73e2c Mon Sep 17 00:00:00 2001 From: Sreeja Chintalapati Date: Tue, 19 May 2026 10:38:27 +0530 Subject: [PATCH] HDDS-15304. DeleteObjects should enforce the request key limit --- .../ozone/s3/endpoint/BucketEndpoint.java | 6 +++ .../apache/hadoop/ozone/s3/util/S3Consts.java | 3 ++ .../s3/endpoint/TestObjectMultiDelete.java | 42 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java index 2fd6dee16db7..b65b52e76023 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java @@ -62,6 +62,7 @@ import org.apache.hadoop.ozone.s3.exception.OS3Exception; import org.apache.hadoop.ozone.s3.exception.S3ErrorTable; import org.apache.hadoop.ozone.s3.util.ContinueToken; +import org.apache.hadoop.ozone.s3.util.S3Consts; import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams; import org.apache.hadoop.ozone.s3.util.S3StorageType; import org.apache.hadoop.util.Time; @@ -341,6 +342,11 @@ public MultiDeleteResponse multiDelete( ) throws OS3Exception, IOException { S3GAction s3GAction = S3GAction.MULTI_DELETE; + if (request.getObjects() != null + && request.getObjects().size() > S3Consts.S3_DELETE_OBJECTS_MAX_KEYS) { + throw newError(S3ErrorTable.MALFORMED_XML, bucketName); + } + OzoneBucket bucket = getVolume().getBucket(bucketName); MultiDeleteResponse result = new MultiDeleteResponse(); List deleteKeys = new ArrayList<>(); diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java index 76addc4b9e3d..f0f5e893467d 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java @@ -95,6 +95,9 @@ public final class S3Consts { public static final Pattern TAG_REGEX_PATTERN = Pattern.compile("^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-]*)$"); public static final String MP_PARTS_COUNT = "x-amz-mp-parts-count"; + /** AWS S3 maximum number of keys per DeleteObjects request. */ + public static final int S3_DELETE_OBJECTS_MAX_KEYS = 1000; + // Bucket owner condition headers // See https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-owner-condition.html public static final String EXPECTED_BUCKET_OWNER_HEADER = "x-amz-expected-bucket-owner"; diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectMultiDelete.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectMultiDelete.java index 8b6c869b5d95..cc687f78c11d 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectMultiDelete.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectMultiDelete.java @@ -17,8 +17,10 @@ package org.apache.hadoop.ozone.s3.endpoint; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.util.Collections.singleton; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.collect.Sets; import java.io.IOException; @@ -32,6 +34,7 @@ import org.apache.hadoop.ozone.client.OzoneKey; import org.apache.hadoop.ozone.s3.endpoint.MultiDeleteRequest.DeleteObject; import org.apache.hadoop.ozone.s3.exception.OS3Exception; +import org.apache.hadoop.ozone.s3.util.S3Consts; import org.junit.jupiter.api.Test; /** @@ -101,6 +104,45 @@ public void deleteQuiet() throws IOException, OS3Exception, JAXBException { assertEquals(0, response.getErrors().size()); } + @Test + public void multiDeleteRejectsMoreThanMaxKeysPerRequest() throws Exception { + OzoneClient client = new OzoneClientStub(); + BucketEndpoint rest = EndpointBuilder.newBucketEndpointBuilder() + .setClient(client) + .build(); + + MultiDeleteRequest mdr = new MultiDeleteRequest(); + for (int i = 0; i < S3Consts.S3_DELETE_OBJECTS_MAX_KEYS + 1; i++) { + mdr.getObjects().add(new DeleteObject("key-" + i)); + } + + OS3Exception ex = assertThrows(OS3Exception.class, + () -> rest.multiDelete("b1", "", mdr)); + assertEquals("MalformedXML", ex.getCode()); + assertEquals(HTTP_BAD_REQUEST, ex.getHttpCode()); + } + + @Test + public void multiDeleteAllowsMaxKeysPerRequest() throws Exception { + OzoneClient client = new OzoneClientStub(); + OzoneBucket bucket = initTestData(client); + BucketEndpoint rest = EndpointBuilder.newBucketEndpointBuilder() + .setClient(client) + .build(); + + MultiDeleteRequest mdr = new MultiDeleteRequest(); + mdr.setQuiet(true); + for (int i = 0; i < S3Consts.S3_DELETE_OBJECTS_MAX_KEYS; i++) { + mdr.getObjects().add(new DeleteObject("missing-" + i)); + } + + MultiDeleteResponse response = rest.multiDelete("b1", "", mdr); + assertEquals(0, response.getDeletedObjects().size()); + assertEquals(0, response.getErrors().size()); + + assertEquals(3, Sets.newHashSet(bucket.listKeys("")).size()); + } + private OzoneBucket initTestData(OzoneClient client) throws IOException { client.getObjectStore().createS3Bucket("b1");