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

ChenSammi 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 1fb64c8e499 HDDS-16756. Reject unsupported actions and fields of 
Lifecycle configuration (#11461)
1fb64c8e499 is described below

commit 1fb64c8e499161d1452ae6948ffd461fc9d2c302
Author: KUAN-HAO HUANG <[email protected]>
AuthorDate: Wed Oct 14 07:53:36 2026 +0100

    HDDS-16756. Reject unsupported actions and fields of Lifecycle 
configuration (#11461)
---
 .../ozone/s3/endpoint/BucketLifecycleHandler.java  |   4 +-
 .../ozone/s3/endpoint/MessageUnmarshaller.java     |   6 +-
 ...utBucketLifecycleConfigurationUnmarshaller.java |  54 +++++
 .../endpoint/TestS3LifecycleConfigurationPut.java  | 256 +++++++++++++++++++++
 4 files changed, 318 insertions(+), 2 deletions(-)

diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
index a375988d660..214acfc5abf 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
@@ -25,6 +25,7 @@
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.ozone.audit.S3GAction;
 import org.apache.hadoop.ozone.client.OzoneBucket;
 import org.apache.hadoop.ozone.client.OzoneLifecycleConfiguration;
@@ -136,7 +137,8 @@ public Response 
putBucketLifecycleConfiguration(S3RequestContext context, String
       s3LifecycleConfiguration = new 
PutBucketLifecycleConfigurationUnmarshaller().readFrom(body);
       lcc = s3LifecycleConfiguration.toOmLifecycleConfiguration(ozoneBucket);
     } catch (WebApplicationException ex) {
-      throw S3ErrorTable.newError(S3ErrorTable.MALFORMED_XML, bucketName);
+      throw S3ErrorTable.newError(S3ErrorTable.MALFORMED_XML, bucketName, ex)
+          .withMessage(StringUtils.defaultIfEmpty(ex.getMessage(), 
S3ErrorTable.MALFORMED_XML.getErrorMessage()));
     } catch (OMException ex) {
       // Rule validation rejects client-supplied values with INVALID_REQUEST, 
which the shared
       // translation maps to InvalidRequest. AWS S3 uses InvalidArgument for a 
rejected lifecycle
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/MessageUnmarshaller.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/MessageUnmarshaller.java
index 90c1835b8d6..9866d937930 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/MessageUnmarshaller.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/MessageUnmarshaller.java
@@ -76,7 +76,7 @@ public T readFrom(
       UnmarshallerHandler unmarshallerHandler =
           context.createUnmarshaller().getUnmarshallerHandler();
       XmlNamespaceFilter filter =
-          new XmlNamespaceFilter(S3_XML_NAMESPACE);
+          createNamespaceFilter();
       filter.setContentHandler(unmarshallerHandler);
       filter.setParent(xmlReader);
       filter.parse(new InputSource(inputStream));
@@ -86,6 +86,10 @@ public T readFrom(
     }
   }
 
+  protected XmlNamespaceFilter createNamespaceFilter() {
+    return new XmlNamespaceFilter(S3_XML_NAMESPACE);
+  }
+
   /** Convenience method for programmatic invocation. */
   public T readFrom(InputStream inputStream) throws WebApplicationException {
     return readFrom(cls, cls, new Annotation[0], 
MediaType.APPLICATION_XML_TYPE, null, inputStream);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/PutBucketLifecycleConfigurationUnmarshaller.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/PutBucketLifecycleConfigurationUnmarshaller.java
index 36da3bde2de..6502b067169 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/PutBucketLifecycleConfigurationUnmarshaller.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/PutBucketLifecycleConfigurationUnmarshaller.java
@@ -17,14 +17,68 @@
 
 package org.apache.hadoop.ozone.s3.endpoint;
 
+import static org.apache.hadoop.ozone.s3.util.S3Consts.S3_XML_NAMESPACE;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
 /**
  * Custom unmarshaller to read Lifecycle configuration.
  */
 public class PutBucketLifecycleConfigurationUnmarshaller
     extends MessageUnmarshaller<S3LifecycleConfiguration> {
+  // Newly supported lifecycle actions and fields must be added to this map.
+  private static final ImmutableMap<String, ImmutableSet<String>> 
ALLOWED_CHILDREN =
+      ImmutableMap.<String, ImmutableSet<String>>builder()
+          .put("", ImmutableSet.of("LifecycleConfiguration"))
+          .put("LifecycleConfiguration", ImmutableSet.of("Rule"))
+          .put("Rule", ImmutableSet.of("ID", "Status", "Prefix", "Expiration", 
"AbortIncompleteMultipartUpload",
+              "Filter", "Transition"))
+          .put("Expiration", ImmutableSet.of("Days", "Date"))
+          .put("AbortIncompleteMultipartUpload", 
ImmutableSet.of("DaysAfterInitiation"))
+          .put("Filter", ImmutableSet.of("Prefix", "Tag", "And"))
+          .put("And", ImmutableSet.of("Prefix", "Tag"))
+          .put("Tag", ImmutableSet.of("Key", "Value"))
+          .put("Transition", ImmutableSet.of("Days", "Date", "StorageClass"))
+          .build();
 
   public PutBucketLifecycleConfigurationUnmarshaller() {
     super(S3LifecycleConfiguration.class);
   }
 
+  @Override
+  protected XmlNamespaceFilter createNamespaceFilter() {
+    return new LifecycleNamespaceFilter();
+  }
+
+  private static class LifecycleNamespaceFilter extends XmlNamespaceFilter {
+    private final Deque<String> parents = new ArrayDeque<>();
+
+    LifecycleNamespaceFilter() {
+      super(S3_XML_NAMESPACE);
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, 
Attributes attributes) throws SAXException {
+      // Match JAXB's name selection when the SAX parser is not 
namespace-aware.
+      String name = localName == null || localName.isEmpty() ? qName : 
localName;
+      String parent = parents.isEmpty() ? "" : parents.peek();
+      if (!ALLOWED_CHILDREN.getOrDefault(parent, 
ImmutableSet.of()).contains(name)) {
+        throw new SAXException("Unsupported or invalid lifecycle element: " + 
name);
+      }
+      parents.push(name);
+      super.startElement(uri, localName, qName, attributes);
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws 
SAXException {
+      super.endElement(uri, localName, qName);
+      parents.pop();
+    }
+  }
+
 }
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
index 3b735184d25..3e28752bcbc 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
@@ -46,12 +46,16 @@
 import static org.mockito.Mockito.when;
 
 import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
+import javax.xml.bind.JAXBContext;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.ozone.client.BucketArgs;
 import org.apache.hadoop.ozone.client.ObjectStore;
@@ -68,13 +72,17 @@
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
 import org.apache.hadoop.ozone.s3.exception.S3ErrorTable;
 import org.apache.hadoop.ozone.s3.util.S3Consts;
+import org.apache.hadoop.util.XMLUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
+import org.w3c.dom.Document;
 
 /**
  * Testing for PutBucketLifecycleConfiguration.
@@ -917,6 +925,254 @@ public void 
testPutLifecycleMatchingOwnerStillValidatesXml() throws Exception {
     verify(proxy, never()).setLifecycleConfiguration(any());
   }
 
+  @ParameterizedTest
+  @MethodSource("versioningActions")
+  public void testRejectVersioningActions(
+      String action, String status, boolean namespace, String expectedElement) 
throws Exception {
+    ClientProtocol proxy = mock(ClientProtocol.class);
+    BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers, 
BucketLayout.OBJECT_STORE);
+    String xml = "<LifecycleConfiguration" + (namespace ? " xmlns=\"" + 
S3Consts.S3_XML_NAMESPACE + "\"" : "")
+        + "><Rule><ID>versioning</ID><Prefix>prefix/</Prefix><Status>" + 
status + "</Status>"
+        + 
"<AbortIncompleteMultipartUpload><DaysAfterInitiation>1</DaysAfterInitiation>"
+        + "</AbortIncompleteMultipartUpload>" + action + 
"</Rule></LifecycleConfiguration>";
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML,
+        () -> endpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    assertThat(exception.getErrorMessage()).contains(expectedElement);
+    verify(proxy, never()).setLifecycleConfiguration(any());
+  }
+
+  private static Stream<Arguments> versioningActions() {
+    String transition = 
"<NoncurrentVersionTransition><NoncurrentDays>1</NoncurrentDays>"
+        + "<StorageClass>GLACIER</StorageClass></NoncurrentVersionTransition>";
+    return Stream.of(
+        
Arguments.of("<NoncurrentVersionExpiration><NoncurrentDays>1</NoncurrentDays></NoncurrentVersionExpiration>",
+            "NoncurrentVersionExpiration"),
+        Arguments.of("<NoncurrentVersionExpiration/>", 
"NoncurrentVersionExpiration"),
+        Arguments.of(
+            "<NoncurrentVersionExpiration 
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"; xsi:nil=\"true\"/>",
+            "NoncurrentVersionExpiration"),
+        Arguments.of("<NoncurrentVersionTransition/>", 
"NoncurrentVersionTransition"),
+        Arguments.of(
+            "<NoncurrentVersionTransition 
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"; xsi:nil=\"true\"/>",
+            "NoncurrentVersionTransition"),
+        Arguments.of(transition, "NoncurrentVersionTransition"),
+        Arguments.of(transition + transition, "NoncurrentVersionTransition"),
+        Arguments.of(
+            
"<Expiration><Days>30</Days><ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker></Expiration>",
+            "ExpiredObjectDeleteMarker"),
+        Arguments.of(
+            
"<Expiration><Days>30</Days><ExpiredObjectDeleteMarker>false</ExpiredObjectDeleteMarker></Expiration>",
+            "ExpiredObjectDeleteMarker"),
+        
Arguments.of("<Expiration><Days>30</Days><ExpiredObjectDeleteMarker/></Expiration>",
+            "ExpiredObjectDeleteMarker"),
+        Arguments.of("<Expiration><Days>30</Days><ExpiredObjectDeleteMarker "
+            + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"; 
xsi:nil=\"true\"/></Expiration>",
+            "ExpiredObjectDeleteMarker"),
+        
Arguments.of("<Expiration><ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker></Expiration>",
+            "ExpiredObjectDeleteMarker"))
+        .flatMap(action -> Stream.of("Enabled", "Disabled")
+            .flatMap(status -> Stream.of(true, false)
+                .map(namespace -> Arguments.of(action.get()[0], status, 
namespace, action.get()[1]))));
+  }
+
+  @Test
+  public void testRejectedVersioningConfigurationPreservesExistingRules() 
throws Exception {
+    bucketEndpoint.put("bucket1", onePrefix());
+    String xml = 
"<LifecycleConfiguration><Rule><ID>replacement</ID><Prefix>replacement/</Prefix>"
+        + "<Status>Enabled</Status>"
+        + 
"<Expiration><Days>1</Days></Expiration></Rule><Rule><ID>unsupported</ID><Prefix>prefix/</Prefix>"
+        + "<Status>Enabled</Status>"
+        + 
"<NoncurrentVersionExpiration><NoncurrentDays>1</NoncurrentDays></NoncurrentVersionExpiration>"
+        + "</Rule></LifecycleConfiguration>";
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML,
+        () -> bucketEndpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    
assertThat(exception.getErrorMessage()).contains("NoncurrentVersionExpiration");
+    try (Response response = bucketEndpoint.get("bucket1")) {
+      S3LifecycleConfiguration configuration = (S3LifecycleConfiguration) 
response.getEntity();
+      assertThat(configuration.getRules()).hasSize(1);
+      S3LifecycleConfiguration.Rule rule = configuration.getRules().get(0);
+      assertThat(rule.getId()).isEqualTo("remove logs after 30 days");
+      assertThat(rule.getPrefix()).isEqualTo("prefix/");
+      assertThat(rule.getExpiration().getDays()).isEqualTo(30);
+      StringWriter xmlOutput = new StringWriter();
+      
JAXBContext.newInstance(S3LifecycleConfiguration.class).createMarshaller().marshal(configuration,
 xmlOutput);
+      assertThat(xmlOutput.toString()).doesNotContain("NoncurrentVersion", 
"ExpiredObjectDeleteMarker");
+    }
+  }
+
+  @Test
+  public void testVersioningNamesInTextRemainSupported() throws Exception {
+    String xml = 
"<LifecycleConfiguration><Rule><ID>NoncurrentVersionExpiration</ID>"
+        + 
"<Prefix>NoncurrentVersionTransition/ExpiredObjectDeleteMarker/</Prefix><Status>Enabled</Status>"
+        + "<Expiration><Days>30</Days></Expiration>"
+        + "</Rule></LifecycleConfiguration>";
+    try (Response response = bucketEndpoint.put("bucket1",
+        new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))) {
+      assertThat(response.getStatus()).isEqualTo(HTTP_OK);
+    }
+  }
+
+  @Test
+  public void testVersioningRuleStillChecksStatusAndOwner() throws Exception {
+    ClientProtocol proxy = mock(ClientProtocol.class);
+    BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers, 
BucketLayout.OBJECT_STORE);
+    String xml = "<LifecycleConfiguration><Rule><Status>invalid</Status>"
+        + "<NoncurrentVersionExpiration/></Rule></LifecycleConfiguration>";
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML,
+        () -> endpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    
assertThat(exception.getErrorMessage()).contains("NoncurrentVersionExpiration");
+    
when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn("other-owner");
+    assertErrorResponse(ACCESS_DENIED,
+        () -> endpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    verify(proxy, never()).setLifecycleConfiguration(any());
+  }
+
+  @ParameterizedTest
+  @MethodSource("unsupportedLifecycleElements")
+  public void testRejectUnsupportedLifecycleElements(
+      String elements, boolean namespace, String expectedElement) throws 
Exception {
+    ClientProtocol proxy = mock(ClientProtocol.class);
+    BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers, 
BucketLayout.OBJECT_STORE);
+    String xml = "<LifecycleConfiguration" + (namespace ? " xmlns=\"" + 
S3Consts.S3_XML_NAMESPACE + "\"" : "")
+        + "><Rule><ID>unsupported</ID><Status>Enabled</Status>"
+        + elements + "</Rule></LifecycleConfiguration>";
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML,
+        () -> endpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    assertThat(exception.getErrorMessage()).contains(expectedElement);
+    verify(proxy, never()).setLifecycleConfiguration(any());
+  }
+
+  private static Stream<Arguments> unsupportedLifecycleElements() {
+    String prefix = "<Prefix>prefix/</Prefix>";
+    String expiration = "<Expiration><Days>30</Days></Expiration>";
+    return Stream.of(
+        Arguments.of(prefix + expiration + "<UnknownElement/>", 
"UnknownElement"),
+        Arguments.of(prefix + 
"<Expiration><Days>30</Days><UnknownElement/></Expiration>", "UnknownElement"),
+        Arguments.of(prefix + 
"<Expiration><Days>30<UnknownElement/></Days></Expiration>", "UnknownElement"),
+        Arguments.of(prefix + 
"<AbortIncompleteMultipartUpload><DaysAfterInitiation>1</DaysAfterInitiation>"
+            + "<UnknownElement/></AbortIncompleteMultipartUpload>", 
"UnknownElement"),
+        Arguments.of(
+            expiration + 
"<Filter><Prefix>prefix/</Prefix><ObjectSizeGreaterThan>1</ObjectSizeGreaterThan></Filter>",
+            "ObjectSizeGreaterThan"),
+        Arguments.of(
+            expiration + 
"<Filter><Prefix>prefix/</Prefix><ObjectSizeLessThan>1000</ObjectSizeLessThan></Filter>",
+            "ObjectSizeLessThan"),
+        Arguments.of(expiration + 
"<Filter><And><Prefix>prefix/</Prefix><Tag><Key>key</Key><Value>value</Value></Tag>"
+            + 
"<ObjectSizeGreaterThan>1</ObjectSizeGreaterThan></And></Filter>", 
"ObjectSizeGreaterThan"),
+        Arguments.of(expiration + 
"<Filter><Tag><Key>key</Key><Value>value</Value><UnknownElement/></Tag></Filter>",
+            "UnknownElement"),
+        Arguments.of(prefix + 
"<Expiration><Days>30</Days><ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker>"
+            + "</Expiration>" + expiration, "ExpiredObjectDeleteMarker"),
+        Arguments.of(prefix + 
"<Expiration><Days>30</Days><Tag><Key>key</Key><Value>value</Value></Tag></Expiration>",
+            "Tag"),
+        Arguments.of(
+            prefix + 
"<Transition><Days>30</Days><StorageClass>GLACIER</StorageClass><UnknownElement/></Transition>",
+            "UnknownElement"))
+        .flatMap(elements -> Stream.of(
+            Arguments.of(elements.get()[0], false, elements.get()[1]),
+            Arguments.of(elements.get()[0], true, elements.get()[1])));
+  }
+
+  @ParameterizedTest
+  @CsvSource({"<UnknownElement/>, UnknownElement", "<Prefix>prefix/</Prefix>, 
Prefix"})
+  public void testRejectUnsupportedRootElements(String element, String 
expectedElement) throws Exception {
+    ClientProtocol proxy = mock(ClientProtocol.class);
+    BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers, 
BucketLayout.OBJECT_STORE);
+    String xml = "<LifecycleConfiguration>" + element
+        + 
"<Rule><Prefix>prefix/</Prefix><Status>Enabled</Status><Expiration><Days>30</Days></Expiration>"
+        + "</Rule></LifecycleConfiguration>";
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML,
+        () -> endpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    assertThat(exception.getErrorMessage()).contains(expectedElement);
+    verify(proxy, never()).setLifecycleConfiguration(any());
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {
+      "<Rule xsi:nil=\"true\"><Prefix>prefix/</Prefix><Status>Enabled</Status>"
+          + "<Expiration><Days>30</Days></Expiration><UnknownElement/></Rule>",
+      "<Rule><Prefix>prefix/</Prefix><Status>Enabled</Status>"
+          + "<Expiration 
xsi:nil=\"true\"><Days>30</Days><UnknownElement/></Expiration></Rule>",
+      "<Rule><Prefix>prefix/</Prefix><Status>Enabled</Status>"
+          + "<Expiration 
xsi:type=\"xs:string\"><Days>30</Days><UnknownElement/></Expiration></Rule>"
+  })
+  public void testRejectUnsupportedElementsWithXmlTypeOverrides(String rule) 
throws Exception {
+    ClientProtocol proxy = mock(ClientProtocol.class);
+    BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers, 
BucketLayout.OBJECT_STORE);
+    String xml = "<LifecycleConfiguration 
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
+        + " xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>" + rule + 
"</LifecycleConfiguration>";
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML,
+        () -> endpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    assertThat(exception.getErrorMessage()).contains("UnknownElement");
+    verify(proxy, never()).setLifecycleConfiguration(any());
+  }
+
+  @ParameterizedTest
+  @CsvSource({"Rule, false", "Rule, true", "lifecycleConfiguration, false", 
"lifecycleConfiguration, true"})
+  public void testRejectUnexpectedLifecycleRoot(String root, boolean 
namespace) throws Exception {
+    ClientProtocol proxy = mock(ClientProtocol.class);
+    BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers, 
BucketLayout.OBJECT_STORE);
+    String xml = "<" + root + (namespace ? " xmlns=\"" + 
S3Consts.S3_XML_NAMESPACE + "\"" : "")
+        + "></" + root + ">";
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML,
+        () -> endpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    String message = "Unsupported or invalid lifecycle element: " + root;
+    assertThat(exception.getErrorMessage()).isEqualTo(message);
+    assertThat(exception.toXml()).contains("<Message>" + message + 
"</Message>");
+    verify(proxy, never()).setLifecycleConfiguration(any());
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  public void testSupportedLifecycleElementPaths(boolean namespace) throws 
Exception {
+    String xml = "<LifecycleConfiguration" + (namespace ? " xmlns=\"" + 
S3Consts.S3_XML_NAMESPACE + "\"" : "")
+        + 
"><Rule><ID>and</ID><Status>Enabled</Status><Expiration><Days>30</Days></Expiration>"
+        + 
"<Filter><And><Prefix>prefix/</Prefix><Tag><Key>key</Key><Value>value</Value></Tag></And></Filter></Rule>"
+        + 
"<Rule><ID>prefix</ID><Status>Disabled</Status><Expiration><Date>2044-01-19T00:00:00Z</Date></Expiration>"
+        + "<Filter><Prefix>prefix/</Prefix></Filter></Rule>"
+        + 
"<Rule><ID>tag</ID><Status>Enabled</Status><Expiration><Days>60</Days></Expiration>"
+        + 
"<Filter><Tag><Key>key</Key><Value>value</Value></Tag></Filter></Rule>"
+        + 
"<Rule><ID>abort</ID><Status>Enabled</Status><Prefix>prefix/</Prefix>"
+        + 
"<AbortIncompleteMultipartUpload><DaysAfterInitiation>1</DaysAfterInitiation>"
+        + "</AbortIncompleteMultipartUpload></Rule></LifecycleConfiguration>";
+    try (Response response = bucketEndpoint.put("bucket1",
+        new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))) {
+      assertEquals(HTTP_OK, response.getStatus());
+    }
+    try (Response response = bucketEndpoint.get("bucket1")) {
+      S3LifecycleConfiguration result = (S3LifecycleConfiguration) 
response.getEntity();
+      assertEquals(4, result.getRules().size());
+      assertEquals("value", 
result.getRules().get(0).getFilter().getAndOperator().getTags().get(0).getValue());
+      assertEquals("2044-01-19T00:00:00Z", 
result.getRules().get(1).getExpiration().getDate());
+      assertEquals("key", 
result.getRules().get(2).getFilter().getTag().getKey());
+      assertEquals(1, 
result.getRules().get(3).getAbortIncompleteMultipartUpload().getDaysAfterInitiation());
+    }
+  }
+
+  @Test
+  public void testLifecycleParserFailureWithoutMessage() throws Exception {
+    InputStream body = new InputStream() {
+      @Override
+      public int read() throws IOException {
+        throw new IOException();
+      }
+    };
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML, () -> 
bucketEndpoint.put("bucket1", body));
+    
assertThat(exception.getErrorMessage()).isEqualTo(MALFORMED_XML.getErrorMessage());
+    assertThat(exception.getResource()).isEqualTo("bucket1");
+  }
+
+  @Test
+  public void testMalformedLifecycleErrorMessageIsEscaped() throws Exception {
+    String xml = "<LifecycleConfiguration><Rule></LifecycleConfiguration>";
+    OS3Exception exception = assertErrorResponse(MALFORMED_XML,
+        () -> bucketEndpoint.put("bucket1", new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+    assertThat(exception.getErrorMessage()).contains("</Rule>");
+    Document error = 
XMLUtils.newSecureDocumentBuilderFactory().newDocumentBuilder()
+        .parse(new 
ByteArrayInputStream(exception.toXml().getBytes(StandardCharsets.UTF_8)));
+    
assertThat(error.getElementsByTagName("Message").item(0).getTextContent()).isEqualTo(exception.getErrorMessage());
+  }
+
   private BucketEndpoint newProtocolEndpoint(ClientProtocol proxy, HttpHeaders 
requestHeaders, BucketLayout layout)
       throws Exception {
     OzoneConfiguration conf = new OzoneConfiguration();


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

Reply via email to