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 18ba9f34934f..8a61793bd99e 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 @@ -52,6 +52,7 @@ import org.apache.hadoop.ozone.audit.S3GAction; import org.apache.hadoop.ozone.client.OzoneBucket; import org.apache.hadoop.ozone.client.OzoneKey; +import org.apache.hadoop.ozone.client.OzoneMultipartUpload; import org.apache.hadoop.ozone.client.OzoneMultipartUploadList; import org.apache.hadoop.ozone.om.exceptions.OMException; import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes; @@ -126,6 +127,19 @@ public Response get( OzoneBucket bucket = null; try { + final String aclMarker = queryParams().get(QueryParams.ACL); + if (aclMarker != null) { + s3GAction = S3GAction.GET_ACL; + S3BucketAcl result = getAcl(bucketName); + getMetrics().updateGetAclSuccessStats(startNanos); + auditReadSuccess(s3GAction); + return Response.ok(result, MediaType.APPLICATION_XML_TYPE).build(); + } + + if (prefix == null) { + prefix = ""; + } + final String uploads = queryParams().get(QueryParams.UPLOADS); if (uploads != null) { s3GAction = S3GAction.LIST_MULTIPART_UPLOAD; @@ -136,10 +150,6 @@ public Response get( maxKeys = validateMaxKeys(maxKeys); - if (prefix == null) { - prefix = ""; - } - // Assign marker to startAfter. for the compatibility of aws api v1 if (startAfter == null && marker != null) { startAfter = marker; @@ -317,17 +327,15 @@ public Response put( public Response listMultipartUploads( String bucketName, String prefix, + String delimiter, + String encodingType, String keyMarker, String uploadIdMarker, int maxUploads) throws OS3Exception, IOException { - if (maxUploads < 1) { - throw newError(S3ErrorTable.INVALID_ARGUMENT, "max-uploads", - new Exception("max-uploads must be positive")); - } else { - maxUploads = Math.min(maxUploads, 1000); - } + int sanitizedMaxUploads = sanitizeMaxUploads(maxUploads); + validateEncodingType(encodingType); long startNanos = Time.monotonicNowNanos(); S3GAction s3GAction = S3GAction.LIST_MULTIPART_UPLOAD; @@ -337,26 +345,17 @@ public Response listMultipartUploads( try { S3Owner.verifyBucketOwnerCondition(getHeaders(), bucketName, bucket.getOwner()); OzoneMultipartUploadList ozoneMultipartUploadList = - bucket.listMultipartUploads(prefix, keyMarker, uploadIdMarker, maxUploads); - - ListMultipartUploadsResult result = new ListMultipartUploadsResult(); - result.setBucket(bucketName); - result.setKeyMarker(keyMarker); - result.setUploadIdMarker(uploadIdMarker); - result.setNextKeyMarker(ozoneMultipartUploadList.getNextKeyMarker()); - result.setPrefix(prefix); - result.setNextUploadIdMarker(ozoneMultipartUploadList.getNextUploadIdMarker()); - result.setMaxUploads(maxUploads); - result.setTruncated(ozoneMultipartUploadList.isTruncated()); + bucket.listMultipartUploads(prefix, keyMarker, uploadIdMarker, + sanitizedMaxUploads); + + ListMultipartUploadsResult result = + buildMultipartUploadsResult(bucket, prefix, delimiter, encodingType, + keyMarker, uploadIdMarker, sanitizedMaxUploads, + ozoneMultipartUploadList); + + AUDIT.logReadSuccess(buildAuditMessageForSuccess(s3GAction, + getAuditParameters())); - ozoneMultipartUploadList.getUploads().forEach(upload -> result.addUpload( - new ListMultipartUploadsResult.Upload( - upload.getKeyName(), - upload.getUploadId(), - upload.getCreationTime(), - S3StorageType.fromReplicationConfig(upload.getReplicationConfig()) - ))); - auditReadSuccess(s3GAction); getMetrics().updateListMultipartUploadsSuccessStats(startNanos); return Response.ok(result).build(); } catch (OMException exception) { @@ -372,6 +371,139 @@ public Response listMultipartUploads( } } + private int sanitizeMaxUploads(int maxUploads) throws OS3Exception { + if (maxUploads < 1) { + throw newError(S3ErrorTable.INVALID_ARGUMENT, "max-uploads", + new Exception("max-uploads must be positive")); + } + return Math.min(maxUploads, 1000); + } + + private void validateEncodingType(String encodingType) throws OS3Exception { + if (encodingType != null && !encodingType.equals(ENCODING_TYPE)) { + throw S3ErrorTable.newError(S3ErrorTable.INVALID_ARGUMENT, encodingType); + } + } + + private ListMultipartUploadsResult buildMultipartUploadsResult( + OzoneBucket bucket, + String prefix, + String delimiter, + String encodingType, + String keyMarker, + String uploadIdMarker, + int maxUploads, + OzoneMultipartUploadList ozoneMultipartUploadList) { + + ListMultipartUploadsResult result = new ListMultipartUploadsResult(); + result.setBucket(bucket.getName()); + result.setKeyMarker(EncodingTypeObject.createNullable(keyMarker, encodingType)); + result.setUploadIdMarker(uploadIdMarker); + result.setNextKeyMarker(EncodingTypeObject.createNullable( + ozoneMultipartUploadList.getNextKeyMarker(), encodingType)); + result.setPrefix(EncodingTypeObject.createNullable(prefix, encodingType)); + result.setDelimiter(EncodingTypeObject.createNullable(delimiter, encodingType)); + result.setEncodingType(encodingType); + result.setNextUploadIdMarker(ozoneMultipartUploadList.getNextUploadIdMarker()); + result.setMaxUploads(maxUploads); + result.setTruncated(ozoneMultipartUploadList.isTruncated()); + + final String normalizedPrefix = prefix == null ? "" : prefix; + String prevDir = null; + String lastProcessedKey = null; + String lastProcessedUploadId = null; + int responseItemCount = 0; + + List pendingUploads = + ozoneMultipartUploadList.getUploads(); + int processedUploads = 0; + for (OzoneMultipartUpload upload : pendingUploads) { + String keyName = upload.getKeyName(); + + if (bucket.getBucketLayout().isFileSystemOptimized() + && StringUtils.isNotEmpty(normalizedPrefix) + && !keyName.startsWith(normalizedPrefix)) { + continue; + } + if (keyName.length() < normalizedPrefix.length()) { + continue; + } + + String relativeKeyName = keyName.substring(normalizedPrefix.length()); + String currentDirName = null; + boolean isDirectoryPlaceholder = false; + if (StringUtils.isNotBlank(delimiter)) { + int depth = StringUtils.countMatches(relativeKeyName, delimiter); + if (depth > 0) { + int delimiterIndex = relativeKeyName.indexOf(delimiter); + currentDirName = relativeKeyName.substring(0, delimiterIndex); + } else if (relativeKeyName.endsWith(delimiter)) { + currentDirName = relativeKeyName.substring( + 0, relativeKeyName.length() - delimiter.length()); + isDirectoryPlaceholder = true; + } + } + + if (responseItemCount >= maxUploads) { + if (StringUtils.isNotBlank(delimiter) + && currentDirName != null + && currentDirName.equals(prevDir)) { + lastProcessedKey = keyName; + lastProcessedUploadId = upload.getUploadId(); + continue; + } + break; + } + + boolean addedAsPrefix = false; + + if (StringUtils.isNotBlank(delimiter)) { + if (currentDirName != null && !currentDirName.equals(prevDir)) { + result.addCommonPrefix(EncodingTypeObject.createNullable( + normalizedPrefix + currentDirName + delimiter, encodingType)); + prevDir = currentDirName; + responseItemCount++; + addedAsPrefix = true; + } else if (isDirectoryPlaceholder) { + result.addCommonPrefix(EncodingTypeObject.createNullable( + normalizedPrefix + relativeKeyName, encodingType)); + responseItemCount++; + addedAsPrefix = true; + } else if (currentDirName != null) { + addedAsPrefix = true; + } + } + + if (!addedAsPrefix) { + result.addUpload(new ListMultipartUploadsResult.Upload( + EncodingTypeObject.createNullable(upload.getKeyName(), encodingType), + upload.getUploadId(), + upload.getCreationTime(), + S3StorageType.fromReplicationConfig(upload.getReplicationConfig()) + )); + responseItemCount++; + } + + lastProcessedKey = keyName; + lastProcessedUploadId = upload.getUploadId(); + processedUploads++; + } + + boolean hasMoreUploads = + processedUploads < pendingUploads.size() + || ozoneMultipartUploadList.isTruncated(); + + if (responseItemCount >= maxUploads && lastProcessedKey != null + && hasMoreUploads) { + result.setNextKeyMarker(EncodingTypeObject.createNullable(lastProcessedKey, encodingType)); + result.setNextUploadIdMarker(lastProcessedUploadId); + result.setTruncated(true); + } else { + result.setTruncated(ozoneMultipartUploadList.isTruncated()); + } + return result; + } + /** * Rest endpoint to check the existence of a bucket. *

diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListMultipartUploadsResult.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListMultipartUploadsResult.java index 98801a520e96..aadecf10b356 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListMultipartUploadsResult.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ListMultipartUploadsResult.java @@ -25,7 +25,10 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import org.apache.hadoop.ozone.s3.commontypes.CommonPrefix; +import org.apache.hadoop.ozone.s3.commontypes.EncodingTypeObject; import org.apache.hadoop.ozone.s3.commontypes.IsoDateAdapter; +import org.apache.hadoop.ozone.s3.commontypes.ObjectKeyNameAdapter; import org.apache.hadoop.ozone.s3.util.S3Consts; import org.apache.hadoop.ozone.s3.util.S3StorageType; @@ -40,17 +43,27 @@ public class ListMultipartUploadsResult { @XmlElement(name = "Bucket") private String bucket; + @XmlJavaTypeAdapter(ObjectKeyNameAdapter.class) @XmlElement(name = "KeyMarker") - private String keyMarker; + private EncodingTypeObject keyMarker; @XmlElement(name = "UploadIdMarker") private String uploadIdMarker; + @XmlJavaTypeAdapter(ObjectKeyNameAdapter.class) @XmlElement(name = "NextKeyMarker") - private String nextKeyMarker; + private EncodingTypeObject nextKeyMarker; + @XmlJavaTypeAdapter(ObjectKeyNameAdapter.class) @XmlElement(name = "Prefix") - private String prefix; + private EncodingTypeObject prefix; + + @XmlJavaTypeAdapter(ObjectKeyNameAdapter.class) + @XmlElement(name = "Delimiter") + private EncodingTypeObject delimiter; + + @XmlElement(name = "EncodingType") + private String encodingType; @XmlElement(name = "NextUploadIdMarker") private String nextUploadIdMarker; @@ -64,6 +77,9 @@ public class ListMultipartUploadsResult { @XmlElement(name = "Upload") private List uploads = new ArrayList<>(); + @XmlElement(name = "CommonPrefixes") + private List commonPrefixes = new ArrayList<>(); + public String getBucket() { return bucket; } @@ -72,11 +88,11 @@ public void setBucket(String bucket) { this.bucket = bucket; } - public String getKeyMarker() { + public EncodingTypeObject getKeyMarker() { return keyMarker; } - public void setKeyMarker(String keyMarker) { + public void setKeyMarker(EncodingTypeObject keyMarker) { this.keyMarker = keyMarker; } @@ -88,22 +104,38 @@ public void setUploadIdMarker(String uploadIdMarker) { this.uploadIdMarker = uploadIdMarker; } - public String getNextKeyMarker() { + public EncodingTypeObject getNextKeyMarker() { return nextKeyMarker; } - public void setNextKeyMarker(String nextKeyMarker) { + public void setNextKeyMarker(EncodingTypeObject nextKeyMarker) { this.nextKeyMarker = nextKeyMarker; } - public String getPrefix() { + public EncodingTypeObject getPrefix() { return prefix; } - public void setPrefix(String prefix) { + public void setPrefix(EncodingTypeObject prefix) { this.prefix = prefix; } + public EncodingTypeObject getDelimiter() { + return delimiter; + } + + public void setDelimiter(EncodingTypeObject delimiter) { + this.delimiter = delimiter; + } + + public String getEncodingType() { + return encodingType; + } + + public void setEncodingType(String encodingType) { + this.encodingType = encodingType; + } + public String getNextUploadIdMarker() { return nextUploadIdMarker; } @@ -141,6 +173,18 @@ public void addUpload(Upload upload) { this.uploads.add(upload); } + public List getCommonPrefixes() { + return commonPrefixes; + } + + public void setCommonPrefixes(List commonPrefixes) { + this.commonPrefixes = commonPrefixes; + } + + public void addCommonPrefix(EncodingTypeObject relativeKeyName) { + commonPrefixes.add(new CommonPrefix(relativeKeyName)); + } + /** * Upload information. */ @@ -148,8 +192,9 @@ public void addUpload(Upload upload) { @XmlRootElement(name = "Upload") public static class Upload { + @XmlJavaTypeAdapter(ObjectKeyNameAdapter.class) @XmlElement(name = "Key") - private String key; + private EncodingTypeObject key; @XmlElement(name = "UploadId") private String uploadId; @@ -170,13 +215,13 @@ public static class Upload { public Upload() { } - public Upload(String key, String uploadId, Instant initiated) { + public Upload(EncodingTypeObject key, String uploadId, Instant initiated) { this.key = key; this.uploadId = uploadId; this.initiated = initiated; } - public Upload(String key, String uploadId, Instant initiated, + public Upload(EncodingTypeObject key, String uploadId, Instant initiated, S3StorageType storageClass) { this.key = key; this.uploadId = uploadId; @@ -184,11 +229,11 @@ public Upload(String key, String uploadId, Instant initiated, this.storageClass = storageClass.toString(); } - public String getKey() { + public EncodingTypeObject getKey() { return key; } - public void setKey(String key) { + public void setKey(EncodingTypeObject key) { this.key = key; } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java index 8037f65fda1c..81b65d557c3f 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java @@ -27,12 +27,14 @@ import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.SortedMap; import java.util.TreeMap; import java.util.UUID; import java.util.stream.Collectors; @@ -583,7 +585,55 @@ public OzoneMultipartUploadPartListParts listParts(String key, @Override public List getAcls() throws IOException { - return (List)aclList.clone(); + return new ArrayList<>(aclList); + } + + @Override + public OzoneMultipartUploadList listMultipartUploads(String prefix, + String keyMarker, String uploadIdMarker, int maxUploads) + throws IOException { + String normalizedPrefix = prefix == null ? "" : prefix; + SortedMap sortedUploads = + new TreeMap<>(keyToMultipartUpload); + List uploads = new ArrayList<>(); + for (Map.Entry entry : sortedUploads.entrySet()) { + String keyName = entry.getKey(); + MultipartInfoStub uploadInfo = entry.getValue(); + + if (!keyName.startsWith(normalizedPrefix)) { + continue; + } + + if (StringUtils.isNotEmpty(keyMarker)) { + int cmp = keyName.compareTo(keyMarker); + if (cmp < 0) { + continue; + } + if (cmp == 0) { + if (StringUtils.isNotEmpty(uploadIdMarker)) { + if (uploadInfo.getUploadId().compareTo(uploadIdMarker) <= 0) { + continue; + } + } else { + continue; + } + } + } + + uploads.add(new OzoneMultipartUpload( + getVolumeName(), + getName(), + keyName, + uploadInfo.getUploadId(), + uploadInfo.getCreationTime(), + getReplicationConfig())); + } + + return new OzoneMultipartUploadList( + uploads, + null, + null, + false); } @Override @@ -820,12 +870,14 @@ private static class MultipartInfoStub { private final String uploadId; private final Map metadata; private final Map tags; + private final Instant creationTime; MultipartInfoStub(String uploadId, Map metadata, Map tags) { this.uploadId = uploadId; this.metadata = metadata; this.tags = tags; + this.creationTime = Instant.now(); } public String getUploadId() { @@ -839,6 +891,10 @@ public Map getMetadata() { public Map getTags() { return tags; } + + public Instant getCreationTime() { + return creationTime; + } } } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketList.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketList.java index c62a7e8da1c8..e912efca2f17 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketList.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketList.java @@ -552,4 +552,134 @@ private OzoneClient createClientWithKeys(String... keys) throws IOException { } return client; } + + private OzoneClient createClientWithMultipartUploads(String... keys) + throws IOException { + OzoneClient client = new OzoneClientStub(); + client.getObjectStore().createS3Bucket("b1"); + OzoneBucket bucket = client.getObjectStore().getS3Bucket("b1"); + for (String key : keys) { + bucket.initiateMultipartUpload(key); + } + return client; + } + + @Test + public void listMultipartUploadsWithDelimiter() throws OS3Exception, IOException { + BucketEndpoint getBucket = new BucketEndpoint(); + + OzoneClient ozoneClient = createClientWithMultipartUploads( + "dir1/file1", + "dir1/file2", + "dir2/file3", + "file4" + ); + + getBucket.setClient(ozoneClient); + getBucket.setRequestIdentifier(new RequestIdentifier()); + + ListMultipartUploadsResult result = + (ListMultipartUploadsResult) getBucket.get("b1", "/", null, null, 100, + "", null, null, "uploads", null, null, null, 1000).getEntity(); + + // With delimiter="/", should have 2 CommonPrefixes (dir1/, dir2/) and 1 Upload (file4) + assertEquals(2, result.getCommonPrefixes().size()); + assertEquals("dir1/", result.getCommonPrefixes().get(0).getPrefix().getName()); + assertEquals("dir2/", result.getCommonPrefixes().get(1).getPrefix().getName()); + assertEquals(1, result.getUploads().size()); + assertEquals("file4", result.getUploads().get(0).getKey().getName()); + } + + @Test + public void listMultipartUploadsWithDelimiterAndPrefix() throws OS3Exception, IOException { + BucketEndpoint getBucket = new BucketEndpoint(); + + OzoneClient ozoneClient = createClientWithMultipartUploads( + "test/dir1/file1", + "test/dir1/file2", + "test/dir2/file3", + "test/file4" + ); + + getBucket.setClient(ozoneClient); + getBucket.setRequestIdentifier(new RequestIdentifier()); + + ListMultipartUploadsResult result = + (ListMultipartUploadsResult) getBucket.get("b1", "/", null, null, 100, + "test/", null, null, "uploads", null, null, null, 1000).getEntity(); + + // With prefix="test/" and delimiter="/", should have 2 CommonPrefixes and 1 Upload + assertEquals(2, result.getCommonPrefixes().size()); + assertEquals("test/dir1/", result.getCommonPrefixes().get(0).getPrefix().getName()); + assertEquals("test/dir2/", result.getCommonPrefixes().get(1).getPrefix().getName()); + assertEquals(1, result.getUploads().size()); + assertEquals("test/file4", result.getUploads().get(0).getKey().getName()); + } + + @Test + public void listMultipartUploadsWithDelimiterAndPagination() + throws OS3Exception, IOException { + BucketEndpoint getBucket = new BucketEndpoint(); + + OzoneClient ozoneClient = createClientWithMultipartUploads( + "test/dir1/file1", + "test/dir1/file2", + "test/dir1/file3", + "test/dir2/file4", + "test/dir2/file5", + "test/dir2/file6", + "test/dir3/file7", + "test/file8" + ); + + getBucket.setClient(ozoneClient); + getBucket.setRequestIdentifier(new RequestIdentifier()); + + int maxUploads = 2; + + // First page: should get 2 CommonPrefixes (dir1/, dir2/) + ListMultipartUploadsResult result = + (ListMultipartUploadsResult) getBucket.get("b1", "/", null, null, 100, + "test/", null, null, "uploads", null, null, null, maxUploads).getEntity(); + + assertEquals(2, result.getCommonPrefixes().size()); + assertEquals("test/dir1/", result.getCommonPrefixes().get(0).getPrefix().getName()); + assertEquals("test/dir2/", result.getCommonPrefixes().get(1).getPrefix().getName()); + assertEquals(0, result.getUploads().size()); + assertTrue(result.isTruncated()); + assertNotNull(result.getNextKeyMarker()); + + // Second page: should get 1 CommonPrefix (dir3/) and 1 Upload (file8) + result = (ListMultipartUploadsResult) getBucket.get("b1", "/", null, null, 100, + "test/", null, null, "uploads", null, + result.getNextKeyMarker().getName(), result.getNextUploadIdMarker(), maxUploads).getEntity(); + assertEquals(1, result.getCommonPrefixes().size()); + assertEquals("test/dir3/", result.getCommonPrefixes().get(0).getPrefix().getName()); + assertEquals(1, result.getUploads().size()); + assertEquals("test/file8", result.getUploads().get(0).getKey().getName()); + assertFalse(result.isTruncated()); + } + + @Test + public void listMultipartUploadsWithoutDelimiter() throws OS3Exception, IOException { + BucketEndpoint getBucket = new BucketEndpoint(); + + OzoneClient ozoneClient = createClientWithMultipartUploads( + "dir1/file1", + "dir1/file2", + "dir2/file3", + "file4" + ); + + getBucket.setClient(ozoneClient); + getBucket.setRequestIdentifier(new RequestIdentifier()); + + // Without delimiter, all uploads should be returned as individual Upload entries + ListMultipartUploadsResult result = + (ListMultipartUploadsResult) getBucket.get("b1", null, null, null, 100, + "", null, null, "uploads", null, null, null, 1000).getEntity(); + + assertEquals(0, result.getCommonPrefixes().size()); + assertEquals(4, result.getUploads().size()); + } } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestPermissionCheck.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestPermissionCheck.java index 2783c1f2d807..1f3ac6bd5e5c 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestPermissionCheck.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestPermissionCheck.java @@ -159,7 +159,7 @@ public void testListMultiUpload() throws IOException { .setClient(client) .build(); OS3Exception e = assertThrows(OS3Exception.class, () -> - bucketEndpoint.listMultipartUploads("bucketName", "prefix", "", "", 10)); + bucketEndpoint.listMultipartUploads("bucketName", "prefix", null, null, "", "", 10)); assertEquals(HTTP_FORBIDDEN, e.getHttpCode()); }