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]