This is an automated email from the ASF dual-hosted git repository.

adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 4a832912fea HDDS-16591. Reject not implemented S3 subresource APIs 
(#11313)
4a832912fea is described below

commit 4a832912feae49664e05479d0ea9ba2c182e905e
Author: fmorg-git <[email protected]>
AuthorDate: Wed Sep 30 23:55:58 2026 -0700

    HDDS-16591. Reject not implemented S3 subresource APIs (#11313)
---
 .../ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java   |  48 +++++++--
 .../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java   |  86 ++++++++++++++--
 .../org/apache/hadoop/ozone/audit/S3GAction.java   |   6 +-
 .../hadoop/ozone/s3/endpoint/BucketAclHandler.java |   5 +
 .../hadoop/ozone/s3/endpoint/BucketEndpoint.java   |   6 ++
 .../s3/endpoint/BucketGetLocationHandler.java      |   5 +
 .../ozone/s3/endpoint/BucketOperationHandler.java  |  21 +++-
 .../BucketOperationNotImplementedHandler.java      |  64 ++++++++++++
 .../hadoop/ozone/s3/endpoint/EndpointBase.java     |  25 +++++
 .../s3/endpoint/ListMultipartUploadsHandler.java   |   5 +
 .../hadoop/ozone/s3/endpoint/ObjectAclHandler.java |  15 ++-
 .../ozone/s3/endpoint/ObjectAttributesHandler.java |   5 +
 .../hadoop/ozone/s3/endpoint/ObjectEndpoint.java   |   1 +
 .../ozone/s3/endpoint/ObjectGetTorrentHandler.java |   5 +
 .../ozone/s3/endpoint/ObjectOperationHandler.java  |  17 +++-
 .../ObjectOperationNotImplementedHandler.java      |  65 ++++++++++++
 .../ozone/s3/endpoint/ObjectTaggingHandler.java    |  12 ++-
 .../org/apache/hadoop/ozone/s3/util/S3Consts.java  |  35 +++++++
 .../hadoop/ozone/s3/util/S3GActionIamMapper.java   |   2 +
 .../s3/endpoint/TestBucketNotImplemented.java      | 102 +++++++++++++++++++
 .../ozone/s3/endpoint/TestObjectMultiDelete.java   |  24 +++++
 .../s3/endpoint/TestObjectNotImplemented.java      | 113 +++++++++++++++++++++
 .../ozone/s3/endpoint/TestObjectTaggingDelete.java |  12 +++
 .../ozone/s3/endpoint/TestObjectTaggingPut.java    |   8 ++
 .../ozone/s3/util/TestS3GActionIamMapper.java      |   2 +
 25 files changed, 661 insertions(+), 28 deletions(-)

diff --git 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
index 07dae405a12..246eae25011 100644
--- 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
+++ 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
@@ -54,8 +54,10 @@
 import com.amazonaws.services.s3.model.CreateBucketRequest;
 import com.amazonaws.services.s3.model.DeleteBucketTaggingConfigurationRequest;
 import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
+import com.amazonaws.services.s3.model.GetBucketAccelerateConfigurationRequest;
 import com.amazonaws.services.s3.model.GetBucketLifecycleConfigurationRequest;
 import com.amazonaws.services.s3.model.GetBucketTaggingConfigurationRequest;
+import com.amazonaws.services.s3.model.GetObjectAclRequest;
 import com.amazonaws.services.s3.model.GetObjectRequest;
 import com.amazonaws.services.s3.model.GetObjectTaggingRequest;
 import com.amazonaws.services.s3.model.GetObjectTaggingResult;
@@ -162,6 +164,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.function.Executable;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
@@ -1394,11 +1397,7 @@ public void testPutObjectACL() throws Exception {
 
     SetObjectAclRequest setObjectAclRequest = new 
SetObjectAclRequest(bucketName, keyName, aclList);
 
-    AmazonServiceException ase = assertThrows(AmazonServiceException.class,
-        () -> s3Client.setObjectAcl(setObjectAclRequest));
-    assertEquals("NotImplemented", ase.getErrorCode());
-    assertEquals(501, ase.getStatusCode());
-    assertEquals(ErrorType.Service, ase.getErrorType());
+    assertNotImplemented(() -> s3Client.setObjectAcl(setObjectAclRequest));
 
     // Ensure that the object content remains unchanged
     ObjectMetadata updatedObjectMetadata = 
s3Client.getObjectMetadata(bucketName, keyName);
@@ -1416,6 +1415,27 @@ public void testPutObjectACL() throws Exception {
     }
   }
 
+  @Test
+  public void testGetObjectAclNotImplemented() throws Exception {
+    final String bucketName = getBucketName();
+    final String keyName = getKeyName();
+    final String content = "bar";
+    s3Client.createBucket(bucketName);
+    s3Client.putObject(
+        bucketName, keyName, new 
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), new 
ObjectMetadata());
+
+    assertNotImplemented(() -> s3Client.getObjectAcl(new 
GetObjectAclRequest(bucketName, keyName)));
+  }
+
+  @Test
+  public void testGetBucketAccelerateConfigurationNotImplemented() {
+    final String bucketName = getBucketName();
+    s3Client.createBucket(bucketName);
+
+    assertNotImplemented(
+        () -> s3Client.getBucketAccelerateConfiguration(new 
GetBucketAccelerateConfigurationRequest(bucketName)));
+  }
+
   @Test
   public void testGetObject() throws Exception {
     final String bucketName = getBucketName();
@@ -2561,19 +2581,20 @@ public void testPresignedUrlGet() throws IOException {
       }
     }
 
-    @Test
-    public void testPresignedUrlGetObjectTorrentNotImplemented() throws 
Exception {
+    @ParameterizedTest
+    @ValueSource(strings = {"annotation", "torrent"})
+    public void testPresignedUrlGetObjectSubresourceNotImplemented(String 
subresource) throws Exception {
       final String keyName = getKeyName();
 
       InputStream is = new 
ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8));
       s3Client.putObject(BUCKET_NAME, keyName, is, new ObjectMetadata());
 
-      // AmazonS3 (SDK v1) has no getObjectTorrent API, so exercise the same 
HTTP behavior
-      // via a presigned URL with the torrent query parameter, as with other 
request shapes
+      // AmazonS3 (SDK v1) has no getObjectTorrent or listObjectAnnotations 
API, so exercise the same HTTP behavior
+      // via a presigned URL with the subresource query parameter, as with 
other request shapes
       // the typed v1 API doesn't expose.
       GeneratePresignedUrlRequest generatePresignedUrlRequest =
           new GeneratePresignedUrlRequest(BUCKET_NAME, 
keyName).withMethod(HttpMethod.GET).withExpiration(expiration);
-      generatePresignedUrlRequest.addRequestParameter("torrent", "");
+      generatePresignedUrlRequest.addRequestParameter(subresource, "");
       URL presignedUrl = 
s3Client.generatePresignedUrl(generatePresignedUrlRequest);
 
       HttpURLConnection connection = null;
@@ -2977,6 +2998,13 @@ private String getKeyName(String ignored) {
     return uniqueObjectName();
   }
 
+  private static void assertNotImplemented(Executable request) {
+    final AmazonServiceException ase = 
assertThrows(AmazonServiceException.class, request);
+    assertEquals(S3ErrorTable.NOT_IMPLEMENTED.getCode(), ase.getErrorCode());
+    assertEquals(501, ase.getStatusCode());
+    assertEquals(ErrorType.Service, ase.getErrorType());
+  }
+
   private String multipartUpload(String bucketName, String key, File file, 
long partSize, String contentType,
                                  Map<String, String> userMetadata, List<Tag> 
tags) throws Exception {
     String uploadId = initiateMultipartUpload(bucketName, key, contentType, 
userMetadata, tags);
diff --git 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
index 28c4efad510..5a1577c7890 100644
--- 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
+++ 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
@@ -168,6 +168,7 @@
 import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
 import software.amazon.awssdk.services.s3.model.ObjectAttributes;
 import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
+import software.amazon.awssdk.services.s3.model.ObjectLockLegalHoldStatus;
 import software.amazon.awssdk.services.s3.model.PutBucketAclRequest;
 import software.amazon.awssdk.services.s3.model.PutBucketTaggingRequest;
 import software.amazon.awssdk.services.s3.model.PutObjectRequest;
@@ -677,18 +678,84 @@ public void testGetObjectTorrentNotImplemented() {
     s3Client.createBucket(b -> b.bucket(bucketName));
     s3Client.putObject(b -> b.bucket(bucketName).key(keyName), 
RequestBody.fromString(content));
 
-    S3Exception exception = assertThrows(S3Exception.class,
-        () -> s3Client.getObjectTorrent(b -> 
b.bucket(bucketName).key(keyName)));
-
-    assertEquals(501, exception.statusCode());
-    assertEquals(S3ErrorTable.NOT_IMPLEMENTED.getCode(), 
exception.awsErrorDetails().errorCode());
-    assertEquals(S3ErrorTable.NOT_IMPLEMENTED.getErrorMessage(), 
exception.awsErrorDetails().errorMessage());
+    assertNotImplemented(() -> s3Client.getObjectTorrent(b -> 
b.bucket(bucketName).key(keyName)));
 
     // object must be untouched
     HeadObjectResponse headObjectResponse = s3Client.headObject(b -> 
b.bucket(bucketName).key(keyName));
     assertEquals(content.length(), headObjectResponse.contentLength());
   }
 
+  @Test
+  public void testGetObjectAclNotImplemented() {
+    final String bucketName = getBucketName();
+    final String keyName = getKeyName();
+    s3Client.createBucket(b -> b.bucket(bucketName));
+    s3Client.putObject(b -> b.bucket(bucketName).key(keyName), 
RequestBody.fromString("bar"));
+
+    assertNotImplemented(() -> s3Client.getObjectAcl(b -> 
b.bucket(bucketName).key(keyName)));
+  }
+
+  @Test
+  public void testGetBucketAccelerateConfigurationNotImplemented() {
+    final String bucketName = getBucketName();
+    s3Client.createBucket(b -> b.bucket(bucketName));
+
+    assertNotImplemented(() -> s3Client.getBucketAccelerateConfiguration(b -> 
b.bucket(bucketName)));
+  }
+
+  @Test
+  public void testListObjectAnnotationsNotImplemented() {
+    final String bucketName = getBucketName();
+    final String keyName = getKeyName();
+    s3Client.createBucket(b -> b.bucket(bucketName));
+    s3Client.putObject(b -> b.bucket(bucketName).key(keyName), 
RequestBody.fromString("bar"));
+
+    assertNotImplemented(() -> s3Client.listObjectAnnotations(b -> 
b.bucket(bucketName).key(keyName)));
+  }
+
+  @Test
+  public void testDeleteBucketMetadataTableConfigurationNotImplemented() {
+    final String bucketName = getBucketName();
+    s3Client.createBucket(b -> b.bucket(bucketName));
+
+    assertNotImplemented(() -> 
s3Client.deleteBucketMetadataTableConfiguration(b -> b.bucket(bucketName)));
+
+    // the empty bucket must not be deleted
+    assertDoesNotThrow(() -> s3Client.headBucket(b -> b.bucket(bucketName)));
+  }
+
+  @Test
+  public void testDeleteObjectsWithVersionIdNotImplemented() {
+    final String bucketName = getBucketName();
+    final String keyName = getKeyName();
+    s3Client.createBucket(b -> b.bucket(bucketName));
+    s3Client.putObject(b -> b.bucket(bucketName).key(keyName), 
RequestBody.fromString("bar"));
+
+    assertNotImplemented(
+        () -> s3Client.deleteObjects(
+            b -> b.bucket(bucketName).delete(d -> 
d.objects(ObjectIdentifier.builder().key(keyName)
+                .versionId("nonexistent").build()))));
+
+    // the current object must not be deleted
+    assertDoesNotThrow(() -> s3Client.headObject(b -> 
b.bucket(bucketName).key(keyName)));
+  }
+
+  @Test
+  public void testPutObjectLegalHoldNotImplemented() {
+    final String bucketName = getBucketName();
+    final String keyName = getKeyName();
+    final String content = "bar";
+    s3Client.createBucket(b -> b.bucket(bucketName));
+    s3Client.putObject(b -> b.bucket(bucketName).key(keyName), 
RequestBody.fromString(content));
+
+    assertNotImplemented(
+        () -> s3Client.putObjectLegalHold(
+            b -> b.bucket(bucketName).key(keyName).legalHold(h -> 
h.status(ObjectLockLegalHoldStatus.ON))));
+
+    // the object must not be overwritten with the request body
+    assertEquals(content, s3Client.getObjectAsBytes(b -> 
b.bucket(bucketName).key(keyName)).asUtf8String());
+  }
+
   @Test
   public void testHeadObjectIfMatch() {
     final String bucketName = getBucketName();
@@ -3100,6 +3167,13 @@ private String getKeyName(String ignored) {
     return uniqueObjectName();
   }
 
+  private static void assertNotImplemented(Executable request) {
+    final S3Exception exception = assertThrows(S3Exception.class, request);
+    assertEquals(501, exception.statusCode());
+    assertEquals(S3ErrorTable.NOT_IMPLEMENTED.getCode(), 
exception.awsErrorDetails().errorCode());
+    assertEquals(S3ErrorTable.NOT_IMPLEMENTED.getErrorMessage(), 
exception.awsErrorDetails().errorMessage());
+  }
+
   private String multipartUpload(String bucketName, String key, File file, int 
partSize,
                                  Map<String, String> userMetadata, List<Tag> 
tags) throws Exception {
     String uploadId = initiateMultipartUpload(bucketName, key, userMetadata, 
tags);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
index f215cd6cb0f..0171d19f9a6 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
@@ -62,13 +62,17 @@ public enum S3GAction implements AuditAction {
   GET_BUCKET_TAGGING,
   PUT_BUCKET_TAGGING,
   DELETE_BUCKET_TAGGING,
+  GET_OBJECT_ACL,
   PUT_OBJECT_ACL,
 
   // STS endpoint
   ASSUME_ROLE,
   GET_CALLER_IDENTITY,
 
-  GET_OBJECT_ATTRIBUTES;
+  GET_OBJECT_ATTRIBUTES,
+
+  // bucket and object subresource operations that are not implemented
+  NOT_IMPLEMENTED;
   
   @Override
   public String getAction() {
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketAclHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketAclHandler.java
index bc9e9dca281..c68bb5915d6 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketAclHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketAclHandler.java
@@ -70,6 +70,11 @@ private boolean shouldHandle() {
     return queryParams().get(QueryParams.ACL) != null;
   }
 
+  @Override
+  S3GAction getAction() {
+    return shouldHandle() ? S3GAction.NOT_IMPLEMENTED : null;
+  }
+
   /**
    * Implement acl get.
    * <p>
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 497f4efc76b..298ee53e8a7 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
@@ -352,6 +352,11 @@ public MultiDeleteResponse multiDelete(
       throw newError(S3ErrorTable.MALFORMED_XML, bucketName);
     }
 
+    // Deleting a specific version is not implemented; ignoring VersionId 
would delete the current object instead.
+    if (request.getObjects() != null && 
request.getObjects().stream().anyMatch(o -> o.getVersionId() != null)) {
+      throw newError(S3ErrorTable.NOT_IMPLEMENTED, bucketName);
+    }
+
     final OzoneBucket bucket;
     try {
       bucket = getVolume().getBucket(bucketName);
@@ -456,6 +461,7 @@ protected void init() {
         .add(new ListMultipartUploadsHandler())
         .add(new BucketTaggingHandler())
         .add(new BucketLifecycleHandler())
+        .add(new BucketOperationNotImplementedHandler())
         .add(new BucketCrudHandler())
         .add(this)
         .build();
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketGetLocationHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketGetLocationHandler.java
index b01daf14e85..256a150fdbf 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketGetLocationHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketGetLocationHandler.java
@@ -44,4 +44,9 @@ Response handleGetRequest(S3RequestContext context, String 
bucketName)
     context.setAction(S3GAction.GET_BUCKET_LOCATION);
     throw newError(NOT_IMPLEMENTED, "GetBucketLocation");
   }
+
+  @Override
+  S3GAction getAction() {
+    return queryParams().get(QueryParams.LOCATION) == null ? null : 
S3GAction.NOT_IMPLEMENTED;
+  }
 }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketOperationHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketOperationHandler.java
index e557d28f16d..7b74583ae6b 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketOperationHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketOperationHandler.java
@@ -20,6 +20,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import javax.ws.rs.core.Response;
+import org.apache.hadoop.ozone.audit.S3GAction;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
 
 /**
@@ -32,6 +33,16 @@
  */
 abstract class BucketOperationHandler extends EndpointBase {
 
+  /**
+   * Returns the action for the current request if this handler is responsible 
for its subresource, null otherwise.
+   * A handler that does not support all HTTP methods of its subresource 
returns
+   * {@link S3GAction#NOT_IMPLEMENTED} for the unsupported ones, which the 
default implementations of
+   * {@code handlePutRequest}, {@code handleGetRequest} and {@code 
handleDeleteRequest} reject.
+   */
+  S3GAction getAction() {
+    return null;
+  }
+
   /**
    * Handle the bucket PUT operation if this handler is responsible for it.
    * The handler inspects the request (query parameters, headers, etc.) to 
determine
@@ -41,11 +52,11 @@ abstract class BucketOperationHandler extends EndpointBase {
    * @param body the request body stream
    * @return Response if this handler handles the request, null otherwise
    * @throws IOException if an I/O error occurs
-   * @throws OS3Exception if an S3-specific error occurs
+   * @throws OS3Exception if an S3-specific error occurs, e.g. NotImplemented 
for an unsupported subresource operation
    */
   Response handlePutRequest(S3RequestContext context, String bucketName, 
InputStream body)
       throws IOException, OS3Exception {
-    return null;
+    return rejectNotImplemented(context, getAction(), bucketName);
   }
 
   /**
@@ -57,16 +68,16 @@ Response handlePutRequest(S3RequestContext context, String 
bucketName, InputStre
    * @param bucketName the name of the bucket
    * @return Response if this handler handles the request, null otherwise
    * @throws IOException if an I/O error occurs
-   * @throws OS3Exception if an S3-specific error occurs
+   * @throws OS3Exception if an S3-specific error occurs, e.g. NotImplemented 
for an unsupported subresource operation
    */
   Response handleGetRequest(S3RequestContext context, String bucketName)
       throws IOException, OS3Exception {
-    return null;
+    return rejectNotImplemented(context, getAction(), bucketName);
   }
 
   Response handleDeleteRequest(S3RequestContext context, String bucketName)
       throws IOException, OS3Exception {
-    return null;
+    return rejectNotImplemented(context, getAction(), bucketName);
   }
 
   BucketOperationHandler copyDependenciesFrom(EndpointBase other) {
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketOperationNotImplementedHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketOperationNotImplementedHandler.java
new file mode 100644
index 00000000000..0ca3cd5b413
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketOperationNotImplementedHandler.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+package org.apache.hadoop.ozone.s3.endpoint;
+
+import com.google.common.collect.ImmutableSet;
+import java.io.InputStream;
+import java.util.Set;
+import javax.ws.rs.core.Response;
+import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
+
+/**
+ * Rejects operations on bucket subresources that no other handler recognizes.
+ * <p>
+ * Subresources that have a handler ({@link BucketAclHandler}, {@link 
BucketGetLocationHandler},
+ * {@link BucketLifecycleHandler}, {@link BucketTaggingHandler}, {@link 
ListMultipartUploadsHandler}) reject their own
+ * unsupported HTTP methods.
+ * <p>
+ * Must be added to the chain before {@link BucketCrudHandler}. Otherwise, the 
requests fall through to another
+ * operation of the same HTTP method: a GET returns a {@code ListBucketResult} 
body, a PUT creates the bucket and a
+ * DELETE deletes it.
+ * <p>
+ * {@code ?delete} is only valid for POST ({@code DeleteObjects}), which does 
not go through this chain.
+ */
+class BucketOperationNotImplementedHandler extends BucketOperationHandler {
+
+  private static final Set<String> SUBRESOURCES = ImmutableSet.of(
+      QueryParams.ABAC, QueryParams.ACCELERATE, QueryParams.ANALYTICS, 
QueryParams.CORS, QueryParams.DELETE,
+      QueryParams.ENCRYPTION, QueryParams.INTELLIGENT_TIERING, 
QueryParams.INVENTORY, QueryParams.LOGGING,
+      QueryParams.METADATA_ANNOTATION_TABLE, 
QueryParams.METADATA_CONFIGURATION, QueryParams.METADATA_INVENTORY_TABLE,
+      QueryParams.METADATA_JOURNAL_TABLE, QueryParams.METADATA_TABLE, 
QueryParams.METRICS, QueryParams.NOTIFICATION,
+      QueryParams.OBJECT_LOCK, QueryParams.OWNERSHIP_CONTROLS, 
QueryParams.POLICY, QueryParams.POLICY_STATUS,
+      QueryParams.PUBLIC_ACCESS_BLOCK, QueryParams.REPLICATION, 
QueryParams.REQUEST_PAYMENT, QueryParams.SESSION,
+      QueryParams.VERSIONING, QueryParams.VERSIONS, QueryParams.WEBSITE);
+
+  @Override
+  Response handleGetRequest(S3RequestContext context, String bucketName) {
+    return rejectNotImplemented(context, SUBRESOURCES, bucketName);
+  }
+
+  @Override
+  Response handlePutRequest(S3RequestContext context, String bucketName, 
InputStream body) {
+    return rejectNotImplemented(context, SUBRESOURCES, bucketName);
+  }
+
+  @Override
+  Response handleDeleteRequest(S3RequestContext context, String bucketName) {
+    return rejectNotImplemented(context, SUBRESOURCES, bucketName);
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index 2f89c4e4fdf..38aca6b6fe9 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -60,6 +60,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
+import jakarta.annotation.Nullable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
@@ -764,6 +765,30 @@ protected void validateObjectKeyUri(String keyPath) throws 
OS3Exception {
     }
   }
 
+  /**
+   * Rejects the request with {@code NotImplemented} if it has any of the 
{@code subresources}.
+   *
+   * @return null if the request has none of the {@code subresources}
+   */
+  Response rejectNotImplemented(S3RequestContext s3RequestContext, Set<String> 
subresources, String resource) {
+    final boolean requested = subresources.stream().anyMatch(subresource -> 
queryParams().get(subresource) != null);
+    return rejectNotImplemented(s3RequestContext, requested ? 
S3GAction.NOT_IMPLEMENTED : null, resource);
+  }
+
+  /**
+   * Rejects the request with {@code NotImplemented} unless {@code action} is 
null.
+   * The {@code action} is stored in the request context for audit logging.
+   *
+   * @param action the action determined by the handler, null if the handler 
is not responsible for the request
+   * @return null if {@code action} is null
+   */
+  Response rejectNotImplemented(S3RequestContext s3RequestContext, @Nullable 
S3GAction action, String resource) {
+    if (s3RequestContext.ignore(action)) {
+      return null;
+    }
+    throw newError(S3ErrorTable.NOT_IMPLEMENTED, resource);
+  }
+
   protected ReplicationConfig getReplicationConfig(OzoneBucket ozoneBucket) 
throws OS3Exception {
     String storageType = getHeaders().getHeaderString(STORAGE_CLASS_HEADER);
     String storageConfig = 
getHeaders().getHeaderString(CUSTOM_METADATA_HEADER_PREFIX + 
STORAGE_CONFIG_HEADER);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListMultipartUploadsHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListMultipartUploadsHandler.java
index f574417d79e..0542eb99aa0 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListMultipartUploadsHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListMultipartUploadsHandler.java
@@ -34,6 +34,11 @@
  */
 class ListMultipartUploadsHandler extends BucketOperationHandler {
 
+  @Override
+  S3GAction getAction() {
+    return queryParams().get(QueryParams.UPLOADS) == null ? null : 
S3GAction.NOT_IMPLEMENTED;
+  }
+
   @Override
   Response handleGetRequest(S3RequestContext context, String bucketName)
       throws IOException, OS3Exception {
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAclHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAclHandler.java
index 263c093970a..b203065934a 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAclHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAclHandler.java
@@ -31,6 +31,15 @@
 /** Not implemented yet. */
 class ObjectAclHandler extends ObjectOperationHandler {
 
+  @Override
+  Response handleGetRequest(ObjectRequestContext context, String keyName) 
throws IOException {
+    if (context.ignore(getAction())) {
+      return null;
+    }
+
+    throw newError(NOT_IMPLEMENTED, keyName);
+  }
+
   @Override
   Response handlePutRequest(ObjectRequestContext context, String keyName, 
InputStream body) throws IOException {
     if (context.ignore(getAction())) {
@@ -45,17 +54,19 @@ Response handlePutRequest(ObjectRequestContext context, 
String keyName, InputStr
     }
   }
 
-  @SuppressWarnings("SwitchStatementWithTooFewBranches")
+  @Override
   S3GAction getAction() {
     if (queryParams().get(S3Consts.QueryParams.ACL) == null) {
       return null;
     }
 
     switch (getContext().getMethod()) {
+    case HttpMethod.GET:
+      return S3GAction.GET_OBJECT_ACL;
     case HttpMethod.PUT:
       return S3GAction.PUT_OBJECT_ACL;
     default:
-      return null;
+      return S3GAction.NOT_IMPLEMENTED;
     }
   }
 }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
index 1a1319bc13a..171fabde56b 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
@@ -101,6 +101,11 @@ static boolean hasStoredAdditionalChecksum(OzoneKey key) {
     return false;
   }
 
+  @Override
+  S3GAction getAction() {
+    return queryParams().get(QueryParams.ATTRIBUTES) == null ? null : 
S3GAction.NOT_IMPLEMENTED;
+  }
+
   @Override
   Response handleGetRequest(ObjectRequestContext context, String keyPath)
       throws IOException, OS3Exception {
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
index cccbe387fe0..430300163c5 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
@@ -155,6 +155,7 @@ protected void init() {
         .add(new ObjectTaggingHandler())
         .add(new ObjectAttributesHandler())
         .add(new MultipartKeyHandler())
+        .add(new ObjectOperationNotImplementedHandler())
         .add(this)
         .build();
     handler = new AuditingObjectOperationHandler(chain);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectGetTorrentHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectGetTorrentHandler.java
index 312d323d803..395fbcb357d 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectGetTorrentHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectGetTorrentHandler.java
@@ -45,4 +45,9 @@ Response handleGetRequest(ObjectRequestContext context, 
String keyName)
     context.setAction(S3GAction.GET_OBJECT_TORRENT);
     throw newError(NOT_IMPLEMENTED, "GetObjectTorrent");
   }
+
+  @Override
+  S3GAction getAction() {
+    return queryParams().get(QueryParams.TORRENT) == null ? null : 
S3GAction.NOT_IMPLEMENTED;
+  }
 }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectOperationHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectOperationHandler.java
index 82ddda475c3..cbbe47f3c2a 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectOperationHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectOperationHandler.java
@@ -20,18 +20,29 @@
 import java.io.IOException;
 import java.io.InputStream;
 import javax.ws.rs.core.Response;
+import org.apache.hadoop.ozone.audit.S3GAction;
 import org.apache.hadoop.ozone.s3.endpoint.ObjectEndpoint.ObjectRequestContext;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
 
 /** Interface for handling object operations using chain of responsibility 
pattern. */
 abstract class ObjectOperationHandler extends EndpointBase {
 
-  Response handleDeleteRequest(ObjectRequestContext context, String keyName) 
throws IOException, OS3Exception {
+  /**
+   * Returns the action for the current request if this handler is responsible 
for its subresource, null otherwise.
+   * A handler that does not support all HTTP methods of its subresource 
returns
+   * {@link S3GAction#NOT_IMPLEMENTED} for the unsupported ones, which the 
default implementations of
+   * {@code handleDeleteRequest}, {@code handleGetRequest} and {@code 
handlePutRequest} reject.
+   */
+  S3GAction getAction() {
     return null;
   }
 
+  Response handleDeleteRequest(ObjectRequestContext context, String keyName) 
throws IOException, OS3Exception {
+    return rejectNotImplemented(context, getAction(), keyName);
+  }
+
   Response handleGetRequest(ObjectRequestContext context, String keyName) 
throws IOException, OS3Exception {
-    return null;
+    return rejectNotImplemented(context, getAction(), keyName);
   }
 
   Response handleHeadRequest(ObjectRequestContext context, String keyName) 
throws IOException, OS3Exception {
@@ -40,7 +51,7 @@ Response handleHeadRequest(ObjectRequestContext context, 
String keyName) throws
 
   Response handlePutRequest(ObjectRequestContext context, String keyName, 
InputStream body)
       throws IOException, OS3Exception {
-    return null;
+    return rejectNotImplemented(context, getAction(), keyName);
   }
 
   ObjectOperationHandler copyDependenciesFrom(EndpointBase other) {
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectOperationNotImplementedHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectOperationNotImplementedHandler.java
new file mode 100644
index 00000000000..afce9aca85f
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectOperationNotImplementedHandler.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package org.apache.hadoop.ozone.s3.endpoint;
+
+import com.google.common.collect.ImmutableSet;
+import java.io.InputStream;
+import java.util.Set;
+import javax.ws.rs.core.Response;
+import org.apache.hadoop.ozone.s3.endpoint.ObjectEndpoint.ObjectRequestContext;
+import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
+
+/**
+ * Rejects operations on object subresources that no other handler recognizes.
+ * <p>
+ * Subresources that have a handler ({@link ObjectAclHandler}, {@link 
ObjectAttributesHandler},
+ * {@link ObjectGetTorrentHandler}, {@link ObjectTaggingHandler}) reject their 
own unsupported HTTP methods.
+ * <p>
+ * Must be added to the chain before {@link ObjectEndpoint}. Otherwise, the 
requests fall through to another operation
+ * of the same HTTP method: a GET returns the object body, a PUT overwrites 
the object with the request body and a
+ * DELETE deletes it.
+ * <p>
+ * {@code ?uploads} is only valid for POST ({@code CreateMultipartUpload}), 
which does not go through this chain.
+ */
+class ObjectOperationNotImplementedHandler extends ObjectOperationHandler {
+
+  private static final Set<String> SUBRESOURCES = ImmutableSet.of(
+      QueryParams.ANNOTATION, QueryParams.ENCRYPTION, QueryParams.LEGAL_HOLD, 
QueryParams.RENAME_OBJECT,
+      QueryParams.RETENTION, QueryParams.UPLOADS);
+
+  // DeleteObject with versionId targets a specific version; ignoring it would 
delete the current object instead.
+  private static final Set<String> DELETE_SUBRESOURCES = 
ImmutableSet.<String>builder()
+      .addAll(SUBRESOURCES)
+      .add(QueryParams.VERSION_ID)
+      .build();
+
+  @Override
+  Response handleGetRequest(ObjectRequestContext context, String keyName) {
+    return rejectNotImplemented(context, SUBRESOURCES, keyName);
+  }
+
+  @Override
+  Response handlePutRequest(ObjectRequestContext context, String keyName, 
InputStream body) {
+    return rejectNotImplemented(context, SUBRESOURCES, keyName);
+  }
+
+  @Override
+  Response handleDeleteRequest(ObjectRequestContext context, String keyName) {
+    return rejectNotImplemented(context, DELETE_SUBRESOURCES, keyName);
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectTaggingHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectTaggingHandler.java
index c2afc0135a9..5c04df9a90d 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectTaggingHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectTaggingHandler.java
@@ -46,6 +46,7 @@ Response handlePutRequest(ObjectRequestContext context, 
String keyName, InputStr
     }
 
     try {
+      rejectVersionId(keyName);
       S3Tagging tagging;
       try {
         tagging = UNMARSHALLER.get().readFrom(body);
@@ -80,6 +81,7 @@ Response handleDeleteRequest(ObjectRequestContext context, 
String keyName)
       return null;
     }
     try {
+      rejectVersionId(keyName);
       getClientProtocol().deleteObjectTagging(context.getVolume().getName(), 
context.getBucketName(), keyName);
       
getMetrics().updateDeleteObjectTaggingSuccessStats(context.getStartNanos());
       return Response.noContent().build();
@@ -115,7 +117,15 @@ Response handleGetRequest(ObjectRequestContext context, 
String keyName)
     }
   }
 
-  private S3GAction getAction() {
+  // Tagging a specific version is not implemented; ignoring versionId would 
modify the tags of the current object.
+  private void rejectVersionId(String keyName) {
+    if (queryParams().get(S3Consts.QueryParams.VERSION_ID) != null) {
+      throw S3ErrorTable.newError(S3ErrorTable.NOT_IMPLEMENTED, keyName);
+    }
+  }
+
+  @Override
+  S3GAction getAction() {
     if (queryParams().get(S3Consts.QueryParams.TAGGING) == null) {
       return null;
     }
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 620d554aa5c..4f39645f62e 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
@@ -182,6 +182,41 @@ public static final class QueryParams {
     public static final String UPLOADS = "uploads";
     public static final String LIFECYCLE = "lifecycle";
     public static final String ATTRIBUTES = "attributes";
+    public static final String VERSION_ID = "versionId";
+
+    // Subresources of S3 APIs that are not implemented. They are only 
recognized so that the
+    // request can be rejected with NotImplemented instead of falling through 
to another operation.
+    // See BucketOperationNotImplementedHandler and 
ObjectOperationNotImplementedHandler.
+    public static final String ABAC = "abac";
+    public static final String ACCELERATE = "accelerate";
+    public static final String ANALYTICS = "analytics";
+    public static final String ANNOTATION = "annotation";
+    public static final String CORS = "cors";
+    public static final String ENCRYPTION = "encryption";
+    public static final String INTELLIGENT_TIERING = "intelligent-tiering";
+    public static final String INVENTORY = "inventory";
+    public static final String LEGAL_HOLD = "legal-hold";
+    public static final String LOGGING = "logging";
+    public static final String METADATA_ANNOTATION_TABLE = 
"metadataAnnotationTable";
+    public static final String METADATA_CONFIGURATION = 
"metadataConfiguration";
+    public static final String METADATA_INVENTORY_TABLE = 
"metadataInventoryTable";
+    public static final String METADATA_JOURNAL_TABLE = "metadataJournalTable";
+    public static final String METADATA_TABLE = "metadataTable";
+    public static final String METRICS = "metrics";
+    public static final String NOTIFICATION = "notification";
+    public static final String OBJECT_LOCK = "object-lock";
+    public static final String OWNERSHIP_CONTROLS = "ownershipControls";
+    public static final String POLICY = "policy";
+    public static final String POLICY_STATUS = "policyStatus";
+    public static final String PUBLIC_ACCESS_BLOCK = "publicAccessBlock";
+    public static final String RENAME_OBJECT = "renameObject";
+    public static final String REPLICATION = "replication";
+    public static final String REQUEST_PAYMENT = "requestPayment";
+    public static final String RETENTION = "retention";
+    public static final String SESSION = "session";
+    public static final String VERSIONING = "versioning";
+    public static final String VERSIONS = "versions";
+    public static final String WEBSITE = "website";
 
     private QueryParams() {
       // no instances
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
index 223b057b5bb..7615828d422 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
@@ -75,6 +75,8 @@ private S3GActionIamMapper() {
       return "PutObjectTagging";
     case DELETE_OBJECT_TAGGING:
       return "DeleteObjectTagging";
+    case GET_OBJECT_ACL:
+      return "GetObjectAcl";
     case PUT_OBJECT_ACL:
       return "PutObjectAcl";
     case COPY_OBJECT:
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketNotImplemented.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketNotImplemented.java
new file mode 100644
index 00000000000..c6c9b29a2e6
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketNotImplemented.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+package org.apache.hadoop.ozone.s3.endpoint;
+
+import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.util.stream.Stream;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneClientStub;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.s3.exception.S3ErrorTable;
+import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** Tests for bucket subresource operations that are not implemented. */
+public class TestBucketNotImplemented {
+
+  private static final String BUCKET_NAME = "b1";
+  private ObjectStore objectStore;
+  private BucketEndpoint bucketEndpoint;
+
+  @BeforeEach
+  public void setup() throws IOException {
+    final OzoneClient clientStub = new OzoneClientStub();
+    objectStore = clientStub.getObjectStore();
+    objectStore.createS3Bucket(BUCKET_NAME);
+
+    bucketEndpoint = EndpointBuilder.newBucketEndpointBuilder()
+        .setClient(clientStub)
+        .build();
+  }
+
+  /** Bucket subresources that are not implemented for any HTTP method. */
+  private static Stream<String> subresources() {
+    return Stream.of(QueryParams.ABAC, QueryParams.ACCELERATE, 
QueryParams.ANALYTICS, QueryParams.CORS,
+        QueryParams.DELETE, QueryParams.ENCRYPTION, 
QueryParams.INTELLIGENT_TIERING, QueryParams.INVENTORY,
+        QueryParams.LOCATION, QueryParams.LOGGING, 
QueryParams.METADATA_ANNOTATION_TABLE,
+        QueryParams.METADATA_CONFIGURATION, 
QueryParams.METADATA_INVENTORY_TABLE, QueryParams.METADATA_JOURNAL_TABLE,
+        QueryParams.METADATA_TABLE, QueryParams.METRICS, 
QueryParams.NOTIFICATION, QueryParams.OBJECT_LOCK,
+        QueryParams.OWNERSHIP_CONTROLS, QueryParams.POLICY, 
QueryParams.POLICY_STATUS, QueryParams.PUBLIC_ACCESS_BLOCK,
+        QueryParams.REPLICATION, QueryParams.REQUEST_PAYMENT, 
QueryParams.SESSION, QueryParams.VERSIONING,
+        QueryParams.VERSIONS, QueryParams.WEBSITE);
+  }
+
+  /** Same as {@link #subresources()}, plus uploads, for which only GET 
(ListMultipartUploads) is implemented. */
+  private static Stream<String> putSubresources() {
+    return Stream.concat(subresources(), Stream.of(QueryParams.UPLOADS));
+  }
+
+  /** Same as {@link #putSubresources()}, plus acl, for which only GET and PUT 
are implemented. */
+  private static Stream<String> deleteSubresources() {
+    return Stream.concat(putSubresources(), Stream.of(QueryParams.ACL));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subresources")
+  public void getIsNotImplemented(String subresource) {
+    bucketEndpoint.queryParamsForTest().set(subresource, "");
+
+    assertErrorResponse(S3ErrorTable.NOT_IMPLEMENTED, () -> 
bucketEndpoint.get(BUCKET_NAME));
+  }
+
+  @ParameterizedTest
+  @MethodSource("putSubresources")
+  public void putIsNotImplementedAndDoesNotCreateBucket(String subresource) {
+    final String newBucketName = "b2";
+    bucketEndpoint.queryParamsForTest().set(subresource, "");
+
+    assertErrorResponse(S3ErrorTable.NOT_IMPLEMENTED, () -> 
bucketEndpoint.put(newBucketName, null));
+    assertThrows(OMException.class, () -> 
objectStore.getS3Bucket(newBucketName));
+  }
+
+  @ParameterizedTest
+  @MethodSource("deleteSubresources")
+  public void deleteIsNotImplementedAndDoesNotDeleteBucket(String subresource) 
throws IOException {
+    bucketEndpoint.queryParamsForTest().set(subresource, "");
+
+    assertErrorResponse(S3ErrorTable.NOT_IMPLEMENTED, () -> 
bucketEndpoint.delete(BUCKET_NAME));
+    assertNotNull(objectStore.getS3Bucket(BUCKET_NAME));
+  }
+}
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 f4316152ce8..a9a2a1d278d 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
@@ -21,6 +21,7 @@
 import static java.util.Collections.singleton;
 import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
 import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_XML;
+import static 
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NOT_IMPLEMENTED;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
@@ -192,6 +193,29 @@ public void multiDeleteAuditsAllKeysWhenDeleteFails() 
throws Exception {
     assertEquals("[key1, key2, key3]", auditParams.get("failedDeletes"));
   }
 
+  @Test
+  public void multiDeleteWithVersionIdIsNotImplemented() throws Exception {
+    final OzoneClient client = new OzoneClientStub();
+    final OzoneBucket bucket = initTestData(client);
+    final BucketEndpoint rest = EndpointBuilder.newBucketEndpointBuilder()
+        .setClient(client)
+        .build();
+
+    final DeleteObject versioned = new DeleteObject("key2");
+    versioned.setVersionId("nonexistent");
+    final MultiDeleteRequest mdr = new MultiDeleteRequest();
+    mdr.getObjects().add(new DeleteObject("key1"));
+    mdr.getObjects().add(versioned);
+
+    assertErrorResponse(NOT_IMPLEMENTED, () -> rest.multiDelete("b1", "", 
mdr));
+
+    // the whole request is rejected, including the keys without VersionId
+    final Set<String> keysAtTheEnd = 
Sets.newHashSet(bucket.listKeys("")).stream()
+        .map(OzoneKey::getName)
+        .collect(Collectors.toSet());
+    assertEquals(Sets.newHashSet("key1", "key2", "key3"), keysAtTheEnd);
+  }
+
   private MultiDeleteRequest threeKeyRequest() {
     MultiDeleteRequest mdr = new MultiDeleteRequest();
     mdr.getObjects().add(new DeleteObject("key1"));
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectNotImplemented.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectNotImplemented.java
new file mode 100644
index 00000000000..f4270c7a699
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectNotImplemented.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+package org.apache.hadoop.ozone.s3.endpoint;
+
+import static 
org.apache.hadoop.ozone.client.OzoneClientTestUtils.assertKeyContent;
+import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
+import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertSucceeds;
+import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.delete;
+import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.get;
+import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.stream.Stream;
+import javax.ws.rs.core.HttpHeaders;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneClientStub;
+import org.apache.hadoop.ozone.s3.exception.S3ErrorTable;
+import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** Tests for object subresource operations that are not implemented. */
+public class TestObjectNotImplemented {
+
+  private static final String BUCKET_NAME = "b1";
+  private static final String KEY_NAME = "key1";
+  private static final String CONTENT = "content";
+  private OzoneBucket bucket;
+  private ObjectEndpoint objectEndpoint;
+
+  @BeforeEach
+  public void setup() throws IOException {
+    final OzoneClient clientStub = new OzoneClientStub();
+    clientStub.getObjectStore().createS3Bucket(BUCKET_NAME);
+    bucket = clientStub.getObjectStore().getS3Bucket(BUCKET_NAME);
+
+    final HttpHeaders headers = mock(HttpHeaders.class);
+    
when(headers.getHeaderString(X_AMZ_CONTENT_SHA256)).thenReturn("UNSIGNED-PAYLOAD");
+
+    objectEndpoint = EndpointBuilder.newObjectEndpointBuilder()
+        .setClient(clientStub)
+        .setHeaders(headers)
+        .build();
+    assertSucceeds(() -> put(objectEndpoint, BUCKET_NAME, KEY_NAME, CONTENT));
+  }
+
+  /** Object subresources that are not implemented for PUT and DELETE. */
+  private static Stream<String> subresources() {
+    return Stream.of(QueryParams.ACL, QueryParams.ANNOTATION, 
QueryParams.ATTRIBUTES, QueryParams.ENCRYPTION,
+        QueryParams.LEGAL_HOLD, QueryParams.RENAME_OBJECT, 
QueryParams.RETENTION, QueryParams.TORRENT,
+        QueryParams.UPLOADS);
+  }
+
+  /** Same as {@link #subresources()}, except GetObjectAttributes, which is 
implemented. */
+  private static Stream<String> getSubresources() {
+    return subresources().filter(subresource -> 
!QueryParams.ATTRIBUTES.equals(subresource));
+  }
+
+  @ParameterizedTest
+  @MethodSource("getSubresources")
+  public void getIsNotImplemented(String subresource) {
+    objectEndpoint.queryParamsForTest().set(subresource, "");
+
+    assertErrorResponse(S3ErrorTable.NOT_IMPLEMENTED, () -> 
get(objectEndpoint, BUCKET_NAME, KEY_NAME));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subresources")
+  public void putIsNotImplementedAndDoesNotOverwriteObject(String subresource) 
throws IOException {
+    objectEndpoint.queryParamsForTest().set(subresource, "");
+
+    assertErrorResponse(S3ErrorTable.NOT_IMPLEMENTED, () -> 
put(objectEndpoint, BUCKET_NAME, KEY_NAME, "other"));
+    assertKeyContent(bucket, KEY_NAME, CONTENT);
+  }
+
+  @ParameterizedTest
+  @MethodSource("subresources")
+  public void deleteIsNotImplementedAndDoesNotDeleteObject(String subresource) 
throws IOException {
+    objectEndpoint.queryParamsForTest().set(subresource, "");
+
+    assertErrorResponse(S3ErrorTable.NOT_IMPLEMENTED, () -> 
delete(objectEndpoint, BUCKET_NAME, KEY_NAME));
+    assertKeyContent(bucket, KEY_NAME, CONTENT);
+  }
+
+  @Test
+  public void deleteWithVersionIdIsNotImplementedAndDoesNotDeleteObject() 
throws IOException {
+    objectEndpoint.queryParamsForTest().set(QueryParams.VERSION_ID, 
"nonexistent");
+
+    assertErrorResponse(S3ErrorTable.NOT_IMPLEMENTED, () -> 
delete(objectEndpoint, BUCKET_NAME, KEY_NAME));
+    assertKeyContent(bucket, KEY_NAME, CONTENT);
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectTaggingDelete.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectTaggingDelete.java
index 792e193da44..a20b03569b1 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectTaggingDelete.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectTaggingDelete.java
@@ -36,6 +36,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
@@ -47,6 +48,7 @@
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
@@ -103,6 +105,16 @@ public void testDeleteTagging() throws IOException, 
OS3Exception {
         .getKey(KEY_WITH_TAG).getTags().isEmpty());
   }
 
+  @Test
+  public void testDeleteTaggingWithVersionIdIsNotImplemented() throws 
IOException {
+    rest.queryParamsForTest().set(QueryParams.VERSION_ID, "nonexistent");
+
+    assertErrorResponse(NOT_IMPLEMENTED, () -> deleteTagging(rest, 
BUCKET_NAME, KEY_WITH_TAG));
+    assertEquals(
+        ImmutableMap.of("tag1", "value1", "tag2", "value2"),
+        
client.getObjectStore().getS3Bucket(BUCKET_NAME).getKey(KEY_WITH_TAG).getTags());
+  }
+
   @Test
   public void testDeleteTaggingNoKeyFound() {
     assertErrorResponse(NO_SUCH_KEY, () -> deleteTagging(rest, BUCKET_NAME, 
"nonexistent"));
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectTaggingPut.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectTaggingPut.java
index b09e9bee1d4..f5777a6e9fc 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectTaggingPut.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectTaggingPut.java
@@ -116,6 +116,14 @@ public void testPutInvalidObjectTagging() {
     assertErrorResponse(MALFORMED_XML, () -> putTagging(objectEndpoint, 
BUCKET_NAME, KEY_NAME, tagValueNotSpecified()));
   }
 
+  @Test
+  public void testPutObjectTaggingWithVersionIdIsNotImplemented() throws 
Exception {
+    objectEndpoint.queryParamsForTest().set(S3Consts.QueryParams.VERSION_ID, 
"nonexistent");
+
+    assertErrorResponse(NOT_IMPLEMENTED, () -> putTagging(objectEndpoint, 
BUCKET_NAME, KEY_NAME, twoTags()));
+    
assertThat(clientStub.getObjectStore().getS3Bucket(BUCKET_NAME).getKey(KEY_NAME).getTags()).isEmpty();
+  }
+
   @Test
   public void testPutObjectTaggingNoKeyFound() {
     assertErrorResponse(NO_SUCH_KEY, () -> putTagging(objectEndpoint, 
BUCKET_NAME, "nonexistent", twoTags()));
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
index 82f0134cffe..b2d1625a185 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
@@ -50,6 +50,7 @@ public void mapsCoreObjectActions() {
     assertEquals("GetObjectTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_OBJECT_TAGGING));
     assertEquals("PutObjectTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_OBJECT_TAGGING));
     assertEquals("DeleteObjectTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_OBJECT_TAGGING));
+    assertEquals("GetObjectAcl", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_OBJECT_ACL));
     assertEquals("PutObjectAcl", 
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_OBJECT_ACL));
   }
 
@@ -64,6 +65,7 @@ public void nonIamActionsReturnNull() {
     assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.ASSUME_ROLE));
     
assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.GET_CALLER_IDENTITY));
     assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.GENERATE_SECRET));
+    assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.NOT_IMPLEMENTED));
     assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.REVOKE_SECRET));
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to