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 5e200e1350a..3209e8bb9ee 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 @@ -21,8 +21,12 @@ import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.calculateDigest; import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.createFile; import static org.apache.hadoop.ozone.s3.util.S3Consts.CUSTOM_METADATA_HEADER_PREFIX; +import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_KEY_LENGTH_LIMIT; +import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_NUM_LIMIT; +import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_VALUE_LENGTH_LIMIT; import static org.apache.hadoop.ozone.s3.util.S3Utils.stripQuotes; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -41,6 +45,7 @@ import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; import com.amazonaws.services.s3.model.CreateBucketRequest; import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.amazonaws.services.s3.model.GetBucketAclRequest; import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.Grantee; import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; @@ -94,10 +99,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import javax.xml.bind.DatatypeConverter; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; @@ -105,7 +112,9 @@ import org.apache.hadoop.hdds.client.ReplicationConfig; import org.apache.hadoop.hdds.client.ReplicationFactor; import org.apache.hadoop.hdds.client.ReplicationType; +import org.apache.hadoop.hdds.protocol.StorageType; import org.apache.hadoop.ozone.MiniOzoneCluster; +import org.apache.hadoop.ozone.client.BucketArgs; import org.apache.hadoop.ozone.client.ObjectStore; import org.apache.hadoop.ozone.client.OzoneBucket; import org.apache.hadoop.ozone.client.OzoneClient; @@ -129,6 +138,8 @@ import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; /** @@ -535,6 +546,192 @@ public void testGetObjectWithoutETag() throws Exception { } } + @Test + public void testPutAndGetObjectTagging() throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final String content = "test content"; + s3Client.createBucket(bucketName); + s3Client.putObject(bucketName, keyName, content); + + Map tags = new HashMap<>(); + tags.put("env", "test"); + tags.put("project", "ozone"); + + try (OzoneClient ozoneClient = cluster.newClient()) { + ObjectStore store = ozoneClient.getObjectStore(); + OzoneVolume volume = store.getS3Volume(); + OzoneBucket bucket = volume.getBucket(bucketName); + bucket.putObjectTagging(keyName, tags); + + Map retrievedTags = bucket.getObjectTagging(keyName); + assertEquals(tags.size(), retrievedTags.size()); + assertEquals("test", retrievedTags.get("env")); + assertEquals("ozone", retrievedTags.get("project")); + } + } + + @Test + public void testDeleteObjectTagging() throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final String content = "test content"; + s3Client.createBucket(bucketName); + s3Client.putObject(bucketName, keyName, content); + + Map tags = new HashMap<>(); + tags.put("temp", "data"); + + try (OzoneClient ozoneClient = cluster.newClient()) { + ObjectStore store = ozoneClient.getObjectStore(); + OzoneVolume volume = store.getS3Volume(); + OzoneBucket bucket = volume.getBucket(bucketName); + bucket.putObjectTagging(keyName, tags); + + Map beforeDelete = bucket.getObjectTagging(keyName); + assertEquals(1, beforeDelete.size()); + + bucket.deleteObjectTagging(keyName); + + Map afterDelete = bucket.getObjectTagging(keyName); + assertTrue(afterDelete.isEmpty()); + } + } + + @Test + public void testPutObjectTaggingExceedsLimit() throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(bucketName); + s3Client.putObject(bucketName, keyName, "content"); + + Map tags = new HashMap<>(); + for (int i = 1; i <= TAG_NUM_LIMIT + 1; i++) { + tags.put("key" + i, "value" + i); + } + + try (OzoneClient ozoneClient = cluster.newClient()) { + ObjectStore store = ozoneClient.getObjectStore(); + OzoneVolume volume = store.getS3Volume(); + OzoneBucket bucket = volume.getBucket(bucketName); + assertThrows(Exception.class, () -> bucket.putObjectTagging(keyName, tags)); + } + } + + @Test + public void testPutObjectTaggingReplacesExistingTags() throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(bucketName); + s3Client.putObject(bucketName, keyName, "content"); + + Map initialTags = new HashMap<>(); + initialTags.put("tag1", "value1"); + initialTags.put("tag2", "value2"); + + Map replacementTags = new HashMap<>(); + replacementTags.put("tag3", "value3"); + + try (OzoneClient ozoneClient = cluster.newClient()) { + ObjectStore store = ozoneClient.getObjectStore(); + OzoneVolume volume = store.getS3Volume(); + OzoneBucket bucket = volume.getBucket(bucketName); + bucket.putObjectTagging(keyName, initialTags); + bucket.putObjectTagging(keyName, replacementTags); + + Map retrievedTags = bucket.getObjectTagging(keyName); + assertEquals(1, retrievedTags.size()); + assertEquals("value3", retrievedTags.get("tag3")); + assertFalse(retrievedTags.containsKey("tag1")); + assertFalse(retrievedTags.containsKey("tag2")); + } + } + + private static String repeatChar(char c, int count) { + StringBuilder sb = new StringBuilder(count); + for (int i = 0; i < count; i++) { + sb.append(c); + } + return sb.toString(); + } + + private static Stream invalidTagConstraintsProvider() { + return Stream.of( + Arguments.of( + repeatChar('a', TAG_KEY_LENGTH_LIMIT + 1), + "value" + ), + Arguments.of( + "valid-key", + repeatChar('b', TAG_VALUE_LENGTH_LIMIT + 1) + ), + Arguments.of( + "t$ag@#invalid", + "value" + ), + Arguments.of( + "aws:test", + "value" + ) + ); + } + + @ParameterizedTest + @MethodSource("invalidTagConstraintsProvider") + public void testPutObjectTaggingInvalidConstraints(String tagKey, String tagValue) + throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(bucketName); + s3Client.putObject(bucketName, keyName, "content"); + + Map invalidTags = new HashMap<>(); + invalidTags.put(tagKey, tagValue); + + try (OzoneClient ozoneClient = cluster.newClient()) { + ObjectStore store = ozoneClient.getObjectStore(); + OzoneVolume volume = store.getS3Volume(); + OzoneBucket bucket = volume.getBucket(bucketName); + Exception exception = assertThrows(Exception.class, + () -> bucket.putObjectTagging(keyName, invalidTags)); + assertTrue(exception.getMessage().contains("Invalid") || + exception.getMessage().contains("exceed") || + exception.getMessage().contains("aws:")); + } + } + + @Test + public void testPutAndGetObjectTaggingOnNonExistentObject() throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(bucketName); + + Map tags = new HashMap<>(); + tags.put("env", "test"); + + try (OzoneClient ozoneClient = cluster.newClient()) { + ObjectStore store = ozoneClient.getObjectStore(); + OzoneVolume volume = store.getS3Volume(); + OzoneBucket bucket = volume.getBucket(bucketName); + assertThrows(Exception.class, () -> bucket.putObjectTagging(keyName, tags)); + assertThrows(Exception.class, () -> bucket.getObjectTagging(keyName)); + } + } + + @Test + public void testDeleteObjectTaggingOnNonExistentObject() throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(bucketName); + + try (OzoneClient ozoneClient = cluster.newClient()) { + ObjectStore store = ozoneClient.getObjectStore(); + OzoneVolume volume = store.getS3Volume(); + OzoneBucket bucket = volume.getBucket(bucketName); + assertThrows(Exception.class, () -> bucket.deleteObjectTagging(keyName)); + } + } + @Test public void testListObjectsMany() throws Exception { testListObjectsMany(false); @@ -1042,7 +1239,7 @@ public void testQuotaExceeded() throws IOException { RandomStringUtils.secure().nextAlphanumeric(1024))); assertEquals(ErrorType.Client, ase.getErrorType()); - assertEquals(403, ase.getStatusCode()); + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, ase.getStatusCode()); assertEquals("QuotaExceeded", ase.getErrorCode()); } @@ -1358,6 +1555,86 @@ public void testPresignedUrlDelete() throws IOException { } } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class LinkBucketTests { + private String nonS3VolumeName; + private String linkBucketName; + private String sourceBucketName; + private String danglingSourceBucketName; + private String danglingLinkBucketName; + private OzoneVolume nonS3Volume; + private OzoneVolume s3Volume; + + @BeforeAll + public void setup() throws Exception { + nonS3VolumeName = randomName("link-vol"); + linkBucketName = randomName("link-bucket"); + sourceBucketName = randomName("source"); + danglingSourceBucketName = randomName("link-source"); + danglingLinkBucketName = randomName("link-bucket-dangling"); + try (OzoneClient ozoneClient = cluster.newClient()) { + ObjectStore store = ozoneClient.getObjectStore(); + store.createVolume(nonS3VolumeName); + nonS3Volume = store.getVolume(nonS3VolumeName); + s3Volume = store.getS3Volume(); + } + } + + @Test + public void setBucketVerificationOnLinkBucket() throws Exception { + nonS3Volume.createBucket(sourceBucketName); + BucketArgs.Builder bb = new BucketArgs.Builder() + .setStorageType(StorageType.DEFAULT) + .setVersioning(false) + .setSourceVolume(nonS3VolumeName) + .setSourceBucket(sourceBucketName); + s3Volume.createBucket(linkBucketName, bb.build()); + + GetBucketAclRequest wrongRequest = new GetBucketAclRequest(linkBucketName) + .withExpectedBucketOwner("wrong-owner"); + AmazonServiceException wrongOwner = assertThrows(AmazonServiceException.class, + () -> s3Client.getBucketAcl(wrongRequest)); + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, wrongOwner.getStatusCode()); + assertEquals("Access Denied", wrongOwner.getErrorCode()); + + Owner owner = s3Client.getBucketAcl(linkBucketName).getOwner(); + GetBucketAclRequest correctRequest = new GetBucketAclRequest(linkBucketName) + .withExpectedBucketOwner(owner.getDisplayName()); + assertDoesNotThrow(() -> s3Client.getBucketAcl(correctRequest)); + } + + @Test + public void testDanglingBucket() throws Exception { + nonS3Volume.createBucket(danglingSourceBucketName); + BucketArgs.Builder bb = new BucketArgs.Builder() + .setStorageType(StorageType.DEFAULT) + .setVersioning(false) + .setSourceVolume(nonS3VolumeName) + .setSourceBucket(danglingSourceBucketName); + s3Volume.createBucket(danglingLinkBucketName, bb.build()); + + nonS3Volume.deleteBucket(danglingSourceBucketName); + + GetBucketAclRequest wrongRequest = new GetBucketAclRequest(danglingLinkBucketName) + .withExpectedBucketOwner("wrong-owner"); + AmazonServiceException wrongOwner = assertThrows(AmazonServiceException.class, + () -> s3Client.getBucketAcl(wrongRequest)); + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, wrongOwner.getStatusCode()); + assertEquals("Access Denied", wrongOwner.getErrorCode()); + + Owner owner = s3Client.getBucketAcl(danglingLinkBucketName).getOwner(); + GetBucketAclRequest correctRequest = new GetBucketAclRequest(danglingLinkBucketName) + .withExpectedBucketOwner(owner.getDisplayName()); + assertDoesNotThrow(() -> s3Client.getBucketAcl(correctRequest)); + } + + private String randomName(String prefix) { + return (prefix + "-" + RandomStringUtils.secure().nextAlphanumeric(8)) + .toLowerCase(Locale.ROOT); + } + } + /** * Tests the functionality to create a snapshot of an Ozone bucket and then read files * from the snapshot directory using the S3 SDK. 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 09026dcb918..15034d19bda 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 @@ -17,9 +17,14 @@ package org.apache.hadoop.ozone.s3.awssdk.v2; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static org.apache.hadoop.ozone.OzoneConsts.MB; import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.calculateDigest; import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.createFile; +import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_KEY_LENGTH_LIMIT; +import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_NUM_LIMIT; +import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_VALUE_LENGTH_LIMIT; import static org.apache.hadoop.ozone.s3.util.S3Utils.stripQuotes; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -47,8 +52,10 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.xml.bind.DatatypeConverter; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; @@ -57,7 +64,6 @@ import org.apache.hadoop.hdds.client.ReplicationType; import org.apache.hadoop.hdds.protocol.StorageType; import org.apache.hadoop.ozone.MiniOzoneCluster; -import org.apache.hadoop.ozone.OzoneConsts; import org.apache.hadoop.ozone.client.BucketArgs; import org.apache.hadoop.ozone.client.ObjectStore; import org.apache.hadoop.ozone.client.OzoneBucket; @@ -79,6 +85,9 @@ 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; +import org.junit.jupiter.params.provider.MethodSource; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.sync.RequestBody; @@ -109,6 +118,7 @@ import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest; +import software.amazon.awssdk.services.s3.model.GetObjectTaggingResponse; import software.amazon.awssdk.services.s3.model.HeadBucketRequest; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.HeadObjectResponse; @@ -370,6 +380,215 @@ public void testCopyObject() { assertEquals("\"37b51d194a7513e45b56f6524f2d51f2\"", copyObjectResponse.copyObjectResult().eTag()); } + @Test + public void testPutAndGetObjectTagging() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final String content = "test content"; + s3Client.createBucket(b -> b.bucket(bucketName)); + + s3Client.putObject(b -> b + .bucket(bucketName) + .key(keyName), + RequestBody.fromString(content)); + + List tags = Arrays.asList( + Tag.builder().key("env").value("test").build(), + Tag.builder().key("project").value("ozone").build() + ); + + s3Client.putObjectTagging(b -> b + .bucket(bucketName) + .key(keyName) + .tagging(Tagging.builder().tagSet(tags).build())); + + GetObjectTaggingResponse getResponse = s3Client.getObjectTagging(b -> b + .bucket(bucketName) + .key(keyName)); + + assertEquals(tags.size(), getResponse.tagSet().size()); + Map tagMap = getResponse.tagSet().stream() + .collect(Collectors.toMap(Tag::key, Tag::value)); + assertEquals("test", tagMap.get("env")); + assertEquals("ozone", tagMap.get("project")); + } + + @Test + public void testDeleteObjectTagging() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + final String content = "test content"; + s3Client.createBucket(b -> b.bucket(bucketName)); + + s3Client.putObject(b -> b + .bucket(bucketName) + .key(keyName), + RequestBody.fromString(content)); + + List tags = Arrays.asList( + Tag.builder().key("temp").value("data").build() + ); + + s3Client.putObjectTagging(b -> b + .bucket(bucketName) + .key(keyName) + .tagging(Tagging.builder().tagSet(tags).build())); + + GetObjectTaggingResponse beforeDelete = s3Client.getObjectTagging(b -> b + .bucket(bucketName) + .key(keyName)); + assertEquals(1, beforeDelete.tagSet().size()); + + s3Client.deleteObjectTagging(b -> b + .bucket(bucketName) + .key(keyName)); + + GetObjectTaggingResponse afterDelete = s3Client.getObjectTagging(b -> b + .bucket(bucketName) + .key(keyName)); + assertTrue(afterDelete.tagSet().isEmpty()); + } + + @Test + public void testPutObjectTaggingExceedsLimit() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + s3Client.putObject(b -> b.bucket(bucketName).key(keyName), + RequestBody.fromString("content")); + + List tags = new ArrayList<>(); + for (int i = 1; i <= TAG_NUM_LIMIT + 1; i++) { + tags.add(Tag.builder().key("key" + i).value("value" + i).build()); + } + + S3Exception exception = assertThrows(S3Exception.class, () -> + s3Client.putObjectTagging(b -> b + .bucket(bucketName) + .key(keyName) + .tagging(Tagging.builder().tagSet(tags).build()))); + assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, exception.statusCode()); + } + + @Test + public void testPutObjectTaggingReplacesExistingTags() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + s3Client.putObject(b -> b.bucket(bucketName).key(keyName), + RequestBody.fromString("content")); + + List initialTags = Arrays.asList( + Tag.builder().key("tag1").value("value1").build(), + Tag.builder().key("tag2").value("value2").build() + ); + s3Client.putObjectTagging(b -> b + .bucket(bucketName) + .key(keyName) + .tagging(Tagging.builder().tagSet(initialTags).build())); + + List replacementTags = Arrays.asList( + Tag.builder().key("tag3").value("value3").build() + ); + s3Client.putObjectTagging(b -> b + .bucket(bucketName) + .key(keyName) + .tagging(Tagging.builder().tagSet(replacementTags).build())); + + GetObjectTaggingResponse response = s3Client.getObjectTagging(b -> b + .bucket(bucketName) + .key(keyName)); + assertEquals(1, response.tagSet().size()); + Map tagMap = response.tagSet().stream() + .collect(Collectors.toMap(Tag::key, Tag::value)); + assertEquals("value3", tagMap.get("tag3")); + assertFalse(tagMap.containsKey("tag1")); + assertFalse(tagMap.containsKey("tag2")); + } + + private static String repeatChar(char c, int count) { + StringBuilder sb = new StringBuilder(count); + for (int i = 0; i < count; i++) { + sb.append(c); + } + return sb.toString(); + } + + private static Stream invalidTagConstraintsProvider() { + return Stream.of( + Arguments.of( + Arrays.asList(Tag.builder().key(repeatChar('a', TAG_KEY_LENGTH_LIMIT + 1)).value("value").build()), + HTTP_BAD_REQUEST + ), + Arguments.of( + Arrays.asList(Tag.builder().key("valid-key").value(repeatChar('b', TAG_VALUE_LENGTH_LIMIT + 1)).build()), + HTTP_BAD_REQUEST + ), + Arguments.of( + Arrays.asList(Tag.builder().key("t$ag@#invalid").value("value").build()), + HTTP_BAD_REQUEST + ), + Arguments.of( + Arrays.asList(Tag.builder().key("aws:test").value("value").build()), + HTTP_BAD_REQUEST + ) + ); + } + + @ParameterizedTest + @MethodSource("invalidTagConstraintsProvider") + public void testPutObjectTaggingInvalidConstraints(List invalidTags, int expectedStatusCode) { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + s3Client.putObject(b -> b.bucket(bucketName).key(keyName), + RequestBody.fromString("content")); + + S3Exception exception = assertThrows(S3Exception.class, () -> + s3Client.putObjectTagging(b -> b + .bucket(bucketName) + .key(keyName) + .tagging(Tagging.builder().tagSet(invalidTags).build()))); + assertEquals(expectedStatusCode, exception.statusCode()); + } + + @Test + public void testPutAndGetObjectTaggingOnNonExistentObject() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + + List tags = Arrays.asList( + Tag.builder().key("env").value("test").build() + ); + + NoSuchKeyException exception = assertThrows(NoSuchKeyException.class, () -> + s3Client.putObjectTagging(b -> b + .bucket(bucketName) + .key(keyName) + .tagging(Tagging.builder().tagSet(tags).build()))); + assertEquals(HttpURLConnection.HTTP_NOT_FOUND, exception.statusCode()); + + exception = assertThrows(NoSuchKeyException.class, () -> + s3Client.getObjectTagging(b -> b + .bucket(bucketName) + .key(keyName))); + assertEquals(HttpURLConnection.HTTP_NOT_FOUND, exception.statusCode()); + } + + @Test + public void testDeleteObjectTaggingOnNonExistentObject() { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + s3Client.createBucket(b -> b.bucket(bucketName)); + + NoSuchKeyException exception = assertThrows(NoSuchKeyException.class, () -> + s3Client.deleteObjectTagging(b -> b + .bucket(bucketName) + .key(keyName))); + assertEquals(HttpURLConnection.HTTP_NOT_FOUND, exception.statusCode()); + } + @Test public void testLowLevelMultipartUpload(@TempDir Path tempDir) throws Exception { final String bucketName = getBucketName(); @@ -1258,7 +1477,7 @@ public void testHeadBucket() { .expectedBucketOwner(WRONG_OWNER) .build(); S3Exception exception = assertThrows(S3Exception.class, () -> s3Client.headBucket(wrongRequest)); - assertEquals(403, exception.statusCode()); + assertEquals(HTTP_FORBIDDEN, exception.statusCode()); } @Test @@ -1531,7 +1750,7 @@ public void testHeadKey() { .expectedBucketOwner(WRONG_OWNER) .build(); S3Exception exception = assertThrows(S3Exception.class, () -> s3Client.headObject(wrongRequest)); - assertEquals(403, exception.statusCode()); + assertEquals(HTTP_FORBIDDEN, exception.statusCode()); } @Test @@ -1658,29 +1877,35 @@ public void testCompleteMultipartUpload() { @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) class LinkBucketTests { - private static final String NON_S3_VOLUME_NAME = "link-bucket-volume"; + private String nonS3VolumeName; + private String linkBucketName; + private String danglingSourceBucketName; + private String danglingLinkBucketName; private OzoneVolume nonS3Volume; private OzoneVolume s3Volume; @BeforeAll public void setup() throws Exception { try (OzoneClient ozoneClient = cluster.newClient()) { - ozoneClient.getObjectStore().createVolume(NON_S3_VOLUME_NAME); - nonS3Volume = ozoneClient.getObjectStore().getVolume(NON_S3_VOLUME_NAME); + nonS3VolumeName = randomName("link-vol"); + linkBucketName = randomName("link-bucket"); + danglingSourceBucketName = randomName("link-source"); + danglingLinkBucketName = randomName("link-bucket-dangling"); + ozoneClient.getObjectStore().createVolume(nonS3VolumeName); + nonS3Volume = ozoneClient.getObjectStore().getVolume(nonS3VolumeName); s3Volume = ozoneClient.getObjectStore().getS3Volume(); } } @Test public void setBucketVerificationOnLinkBucket() throws Exception { - // create link bucket - String linkBucketName = "link-bucket"; - nonS3Volume.createBucket(OzoneConsts.BUCKET); + String sourceBucketName = randomName("source"); + nonS3Volume.createBucket(sourceBucketName); BucketArgs.Builder bb = new BucketArgs.Builder() .setStorageType(StorageType.DEFAULT) .setVersioning(false) - .setSourceVolume(NON_S3_VOLUME_NAME) - .setSourceBucket(OzoneConsts.BUCKET); + .setSourceVolume(nonS3VolumeName) + .setSourceBucket(sourceBucketName); s3Volume.createBucket(linkBucketName, bb.build()); GetBucketAclRequest wrongRequest = GetBucketAclRequest.builder() @@ -1703,36 +1928,39 @@ public void setBucketVerificationOnLinkBucket() throws Exception { @Test public void testDanglingBucket() throws Exception { - String sourceBucket = "source-bucket"; - String linkBucket = "link-bucket-dangling"; - nonS3Volume.createBucket(sourceBucket); + nonS3Volume.createBucket(danglingSourceBucketName); BucketArgs.Builder bb = new BucketArgs.Builder() .setStorageType(StorageType.DEFAULT) .setVersioning(false) - .setSourceVolume(NON_S3_VOLUME_NAME) - .setSourceBucket(sourceBucket); - s3Volume.createBucket(linkBucket, bb.build()); + .setSourceVolume(nonS3VolumeName) + .setSourceBucket(danglingSourceBucketName); + s3Volume.createBucket(danglingLinkBucketName, bb.build()); // remove source bucket to make dangling bucket - nonS3Volume.deleteBucket(sourceBucket); + nonS3Volume.deleteBucket(danglingSourceBucketName); GetBucketAclRequest wrongRequest = GetBucketAclRequest.builder() - .bucket(linkBucket) + .bucket(danglingLinkBucketName) .expectedBucketOwner(WRONG_OWNER) .build(); verifyBucketOwnershipVerificationAccessDenied(() -> s3Client.getBucketAcl(wrongRequest)); String owner = s3Client.getBucketAcl(GetBucketAclRequest.builder() - .bucket(linkBucket) + .bucket(danglingLinkBucketName) .build()).owner().displayName(); GetBucketAclRequest correctRequest = GetBucketAclRequest.builder() - .bucket(linkBucket) + .bucket(danglingLinkBucketName) .expectedBucketOwner(owner) .build(); verifyPassBucketOwnershipVerification(() -> s3Client.getBucketAcl(correctRequest)); } + + private String randomName(String prefix) { + return (prefix + "-" + RandomStringUtils.secure().nextAlphanumeric(8)) + .toLowerCase(Locale.ROOT); + } } private void verifyPassBucketOwnershipVerification(Executable function) { @@ -1741,7 +1969,7 @@ private void verifyPassBucketOwnershipVerification(Executable function) { private void verifyBucketOwnershipVerificationAccessDenied(Executable function) { S3Exception exception = assertThrows(S3Exception.class, function); - assertEquals(403, exception.statusCode()); + assertEquals(HTTP_FORBIDDEN, exception.statusCode()); assertEquals("Access Denied", exception.awsErrorDetails().errorCode()); } }