diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketPut.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketPut.java index 96eea22d9ebd..e3e163ea0b36 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketPut.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketPut.java @@ -17,19 +17,38 @@ package org.apache.hadoop.ozone.s3.endpoint; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_CONFLICT; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_OK; +import static org.apache.hadoop.ozone.OzoneAcl.AclScope.ACCESS; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.BUCKET_ALREADY_EXISTS; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_ARGUMENT; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_REQUEST; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_HEADER; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_BUCKET; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; +import org.apache.hadoop.ozone.OzoneAcl; import org.apache.hadoop.ozone.OzoneConsts; +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.OS3Exception; +import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,6 +59,25 @@ public class TestBucketPut { private String bucketName = OzoneConsts.BUCKET; private BucketEndpoint bucketEndpoint; + private HttpHeaders mockHeaders; + private static final String VALID_ACL_XML = + "" + + " " + + " owner-id" + + " owner-name" + + " " + + " " + + " " + + " " + + " owner-id" + + " owner-name" + + " " + + " FULL_CONTROL" + + " " + + " " + + ""; + private static final String WHITESPACE_ONLY = " "; @BeforeEach public void setup() throws Exception { @@ -51,6 +89,36 @@ public void setup() throws Exception { bucketEndpoint = EndpointBuilder.newBucketEndpointBuilder() .setClient(clientStub) .build(); + + mockHeaders = mock(HttpHeaders.class); + bucketEndpoint.setHeaders(mockHeaders); + } + + @Test + public void testAclWithMissingHeaders() throws Exception { + bucketEndpoint.getClient().getObjectStore().createS3Bucket(bucketName); + + InputStream body = new ByteArrayInputStream(VALID_ACL_XML.getBytes(StandardCharsets.UTF_8)); + + Response resp = bucketEndpoint.putAcl(bucketName, body); + assertEquals(HTTP_OK, resp.getStatus()); + } + + @Test + public void testAclWithMissingHeadersAndNoBody() throws Exception { + bucketEndpoint.getClient().getObjectStore().createS3Bucket(bucketName); + + WebApplicationException wae = assertThrows(WebApplicationException.class, + () -> bucketEndpoint.putAcl(bucketName, null)); + + Throwable cause = wae.getCause(); + assertNotNull(cause); + assertTrue(cause instanceof OS3Exception); + + OS3Exception os3 = (OS3Exception) cause; + + assertEquals(INVALID_REQUEST.getCode(), os3.getCode()); + assertEquals(HTTP_BAD_REQUEST, os3.getHttpCode()); } @Test @@ -66,14 +134,106 @@ public void testBucketFailWithAuthHeaderMissing() throws Exception { @Test public void testBucketPut() throws Exception { Response response = bucketEndpoint.put(bucketName, null); - assertEquals(200, response.getStatus()); + assertEquals(HTTP_OK, response.getStatus()); assertNotNull(response.getLocation()); // Create-bucket on an existing bucket fails OS3Exception e = assertThrows(OS3Exception.class, () -> bucketEndpoint.put( bucketName, null)); - assertEquals(HTTP_CONFLICT, e.getHttpCode()); assertEquals(BUCKET_ALREADY_EXISTS.getCode(), e.getCode()); + assertEquals(HTTP_CONFLICT, e.getHttpCode()); + } + + @Test + public void testPutAclOnNonExistingBucket() throws Exception { + OS3Exception e = assertThrows(OS3Exception.class, + () -> bucketEndpoint.putAcl(bucketName, null)); + assertEquals(NO_SUCH_BUCKET.getCode(), e.getCode()); + assertEquals(HTTP_NOT_FOUND, e.getHttpCode()); + } + + @Test + public void testPutAclWithGrantFullControlHeader() throws Exception { + bucketEndpoint.getClient().getObjectStore().createS3Bucket(bucketName); + + when(mockHeaders.getHeaderString(S3Acl.GRANT_FULL_CONTROL)) + .thenReturn("id=\"owner-id\""); + + Response resp = bucketEndpoint.putAcl(bucketName, null); + + assertEquals(HTTP_OK, resp.getStatus()); + } + + @Test + public void testPutAclWithInvalidXmlBody() throws Exception { + bucketEndpoint.getClient().getObjectStore().createS3Bucket(bucketName); + + InputStream body = new ByteArrayInputStream( + "not-xml".getBytes(StandardCharsets.UTF_8)); + + WebApplicationException wae = assertThrows(WebApplicationException.class, + () -> bucketEndpoint.putAcl(bucketName, body)); + + Throwable cause = wae.getCause(); + assertNotNull(cause); + assertTrue(cause instanceof OS3Exception); + OS3Exception os3 = (OS3Exception) cause; + + assertEquals(INVALID_REQUEST.getCode(), os3.getCode()); + assertEquals(HTTP_BAD_REQUEST, os3.getHttpCode()); + } + + @Test + public void testPutAclWithMalformedGrantHeader() throws Exception { + bucketEndpoint.getClient().getObjectStore().createS3Bucket(bucketName); + + when(mockHeaders.getHeaderString(S3Acl.GRANT_FULL_CONTROL)) + .thenReturn("id\"owner-id\""); + + OS3Exception os3 = assertThrows(OS3Exception.class, + () -> bucketEndpoint.putAcl(bucketName, null)); + + assertEquals(INVALID_ARGUMENT.getCode(), os3.getCode()); + assertEquals(HTTP_BAD_REQUEST, os3.getHttpCode()); + } + + @Test + public void testPutAclWithBothHeadersAndBody() throws Exception { + bucketEndpoint.getClient().getObjectStore().createS3Bucket(bucketName); + + // Header: READ + when(mockHeaders.getHeaderString(S3Acl.GRANT_READ)) + .thenReturn("id=owner-id"); + + // Body: FULL_CONTROL + InputStream body = new ByteArrayInputStream( + VALID_ACL_XML.getBytes(StandardCharsets.UTF_8)); + + Response resp = bucketEndpoint.putAcl(bucketName, body); + assertEquals(HTTP_OK, resp.getStatus()); + + OzoneBucket bucket = bucketEndpoint.getClient() + .getObjectStore() + .getS3Bucket(bucketName); + + List acls = bucket.getAcls(); + assertFalse(acls.isEmpty()); + + OzoneAcl ownerAcl = acls.stream() + .filter(acl -> "owner-id".equals(acl.getName()) + && acl.getAclScope() == ACCESS) + .findFirst() + .orElseThrow(() -> new AssertionError("owner-id ACL not found")); + + assertEquals("owner-id", ownerAcl.getName()); + + List permissions = ownerAcl.getAclList(); + + assertTrue(permissions.contains(IAccessAuthorizer.ACLType.READ), + "Expected READ permission from header"); + + assertFalse(permissions.contains(IAccessAuthorizer.ACLType.ALL), + "FULL_CONTROL/ALL from body should not be applied when header is present"); } @Test @@ -85,4 +245,30 @@ public void testBucketFailWithInvalidHeader() throws Exception { assertEquals(MALFORMED_HEADER.getCode(), ex.getCode()); } } + + @Test + public void testPutAclWithEmptyGrantHeaderValue() throws Exception { + bucketEndpoint.getClient().getObjectStore().createS3Bucket(bucketName); + + when(mockHeaders.getHeaderString(S3Acl.GRANT_FULL_CONTROL)) + .thenReturn(""); // empty + + Response resp = bucketEndpoint.putAcl(bucketName, null); + + assertEquals(HTTP_OK, resp.getStatus()); + } + + @Test + public void testPutAclWithWhitespaceGrantHeaderValue() throws Exception { + bucketEndpoint.getClient().getObjectStore().createS3Bucket(bucketName); + + when(mockHeaders.getHeaderString(S3Acl.GRANT_FULL_CONTROL)) + .thenReturn(WHITESPACE_ONLY); // whitespace only + + OS3Exception ex = assertThrows(OS3Exception.class, + () -> bucketEndpoint.putAcl(bucketName, null)); + + assertEquals(INVALID_ARGUMENT.getCode(), ex.getCode()); + assertEquals(HTTP_BAD_REQUEST, ex.getHttpCode()); + } }