From 4e4b8d41706abb747ee5f2e6e3ae8d8657ed85f5 Mon Sep 17 00:00:00 2001 From: Arne Franken Date: Thu, 30 May 2024 18:40:59 +0200 Subject: [PATCH] Support Bucket Versioning Config --- README.md | 4 +- .../adobe/testing/s3mock/its/BucketV2IT.kt | 52 +++++++++++++++ .../testing/s3mock/BucketController.java | 65 ++++++++++++++++++- .../testing/s3mock/service/BucketService.java | 16 +++++ .../testing/s3mock/store/BucketMetadata.java | 25 ++++++- .../testing/s3mock/store/BucketStore.java | 9 +++ .../s3mock/util/AwsHttpParameters.java | 19 +++--- .../s3mock/service/MultipartServiceTest.kt | 2 +- .../testing/s3mock/service/ServiceTestBase.kt | 1 + .../s3mock/store/StoreConfigurationTest.kt | 2 +- .../testing/s3mock/store/StoreTestBase.kt | 1 + 11 files changed, 179 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8d2fa8c67..cee8d0764 100755 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Of these [operations of the Amazon S3 API](https://docs.aws.amazon.com/AmazonS3/ | [GetBucketReplication](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketReplication.html) | :x: | | | [GetBucketRequestPayment](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketRequestPayment.html) | :x: | | | [GetBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketTagging.html) | :x: | | -| [GetBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketVersioning.html) | :x: | | +| [GetBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketVersioning.html) | :white_check_mark: | | | [GetBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketWebsite.html) | :x: | | | [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) | :white_check_mark: | | | [GetObjectAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAcl.html) | :white_check_mark: | | @@ -144,7 +144,7 @@ Of these [operations of the Amazon S3 API](https://docs.aws.amazon.com/AmazonS3/ | [PutBucketReplication](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html) | :x: | | | [PutBucketRequestPayment](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketRequestPayment.html) | :x: | | | [PutBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketTagging.html) | :x: | | -| [PutBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketVersioning.html) | :x: | | +| [PutBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketVersioning.html) | :white_check_mark: | | | [PutBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketWebsite.html) | :x: | | | [PutObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) | :white_check_mark: | | | [PutObjectAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html) | :white_check_mark: | | diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt index 446731a09..0fed4e0dd 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt @@ -26,18 +26,24 @@ import software.amazon.awssdk.awscore.exception.AwsServiceException import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.AbortIncompleteMultipartUpload import software.amazon.awssdk.services.s3.model.BucketLifecycleConfiguration +import software.amazon.awssdk.services.s3.model.BucketVersioningStatus import software.amazon.awssdk.services.s3.model.CreateBucketRequest import software.amazon.awssdk.services.s3.model.DeleteBucketLifecycleRequest import software.amazon.awssdk.services.s3.model.DeleteBucketRequest import software.amazon.awssdk.services.s3.model.ExpirationStatus import software.amazon.awssdk.services.s3.model.GetBucketLifecycleConfigurationRequest import software.amazon.awssdk.services.s3.model.GetBucketLocationRequest +import software.amazon.awssdk.services.s3.model.GetBucketVersioningRequest import software.amazon.awssdk.services.s3.model.HeadBucketRequest import software.amazon.awssdk.services.s3.model.LifecycleExpiration import software.amazon.awssdk.services.s3.model.LifecycleRule import software.amazon.awssdk.services.s3.model.LifecycleRuleFilter +import software.amazon.awssdk.services.s3.model.MFADelete +import software.amazon.awssdk.services.s3.model.MFADeleteStatus import software.amazon.awssdk.services.s3.model.NoSuchBucketException import software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationRequest +import software.amazon.awssdk.services.s3.model.PutBucketVersioningRequest +import software.amazon.awssdk.services.s3.model.VersioningConfiguration import java.util.concurrent.TimeUnit /** @@ -79,6 +85,52 @@ internal class BucketV2IT : S3TestBase() { assertThat(bucketLocation.locationConstraint().toString()).isEqualTo("eu-west-1") } + @Test + @S3VerifiedSuccess(year = 2024) + fun getDefaultBucketVersioning(testInfo: TestInfo) { + val bucketName = givenBucketV2(testInfo) + + s3ClientV2.getBucketVersioning( + GetBucketVersioningRequest + .builder() + .bucket(bucketName) + .build() + ).also { + assertThat(it.status()).isNull() + assertThat(it.mfaDelete()).isNull() + } + } + + @Test + @S3VerifiedFailure(year = 2024, reason = "No real Mfa value") + fun putAndGetBucketVersioning(testInfo: TestInfo) { + val bucketName = givenBucketV2(testInfo) + s3ClientV2.putBucketVersioning( + PutBucketVersioningRequest + .builder() + .bucket(bucketName) + .mfa("fakeMfaValue") + .versioningConfiguration( + VersioningConfiguration + .builder() + .status(BucketVersioningStatus.ENABLED) + .mfaDelete(MFADelete.ENABLED) + .build() + ) + .build() + ) + + s3ClientV2.getBucketVersioning( + GetBucketVersioningRequest + .builder() + .bucket(bucketName) + .build() + ).also { + assertThat(it.status()).isEqualTo(BucketVersioningStatus.ENABLED) + assertThat(it.mfaDelete()).isEqualTo(MFADeleteStatus.ENABLED) + } + } + @Test @S3VerifiedSuccess(year = 2024) fun duplicateBucketCreation(testInfo: TestInfo) { diff --git a/server/src/main/java/com/adobe/testing/s3mock/BucketController.java b/server/src/main/java/com/adobe/testing/s3mock/BucketController.java index 8d826647e..9348abb29 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/BucketController.java +++ b/server/src/main/java/com/adobe/testing/s3mock/BucketController.java @@ -30,9 +30,11 @@ import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_LOCATION; import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_OBJECT_LOCK; import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_UPLOADS; +import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_VERSIONING; import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_VERSIONS; import static com.adobe.testing.s3mock.util.AwsHttpParameters.OBJECT_LOCK; import static com.adobe.testing.s3mock.util.AwsHttpParameters.START_AFTER; +import static com.adobe.testing.s3mock.util.AwsHttpParameters.VERSIONING; import static com.adobe.testing.s3mock.util.AwsHttpParameters.VERSIONS; import static com.adobe.testing.s3mock.util.AwsHttpParameters.VERSION_ID_MARKER; import static org.springframework.http.MediaType.APPLICATION_XML_VALUE; @@ -44,6 +46,7 @@ import com.adobe.testing.s3mock.dto.ListVersionsResult; import com.adobe.testing.s3mock.dto.LocationConstraint; import com.adobe.testing.s3mock.dto.ObjectLockConfiguration; +import com.adobe.testing.s3mock.dto.VersioningConfiguration; import com.adobe.testing.s3mock.service.BucketService; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -116,7 +119,8 @@ public ResponseEntity listBuckets() { }, params = { NOT_OBJECT_LOCK, - NOT_LIFECYCLE + NOT_LIFECYCLE, + NOT_VERSIONING } ) public ResponseEntity createBucket(@PathVariable final String bucketName, @@ -180,6 +184,62 @@ public ResponseEntity deleteBucket(@PathVariable String bucketName) { return ResponseEntity.noContent().build(); } + /** + * Get VersioningConfiguration of a bucket. + * API Reference + * + * @param bucketName name of the Bucket. + * + * @return 200, VersioningConfiguration + */ + @GetMapping( + value = { + //AWS SDK V2 pattern + "/{bucketName:.+}", + //AWS SDK V1 pattern + "/{bucketName:.+}/" + }, + params = { + VERSIONING, + NOT_LIST_TYPE + }, + produces = APPLICATION_XML_VALUE + ) + public ResponseEntity getVersioningConfiguration( + @PathVariable String bucketName) { + bucketService.verifyBucketExists(bucketName); + var configuration = bucketService.getVersioningConfiguration(bucketName); + return ResponseEntity.ok(configuration); + } + + /** + * Put VersioningConfiguration of a bucket. + * API Reference + * + * @param bucketName name of the Bucket. + * + * @return 200 + */ + @PutMapping( + value = { + //AWS SDK V2 pattern + "/{bucketName:.+}", + //AWS SDK V1 pattern + "/{bucketName:.+}/" + }, + params = { + VERSIONING + }, + consumes = APPLICATION_XML_VALUE + ) + public ResponseEntity putVersioningConfiguration( + @PathVariable String bucketName, + @RequestBody VersioningConfiguration configuration) { + bucketService.verifyBucketExists(bucketName); + bucketService.setVersioningConfiguration(bucketName, configuration); + return ResponseEntity.ok().build(); + } + /** * Get ObjectLockConfiguration of a bucket. * API Reference @@ -362,7 +422,8 @@ public ResponseEntity getBucketLocation( NOT_LIST_TYPE, NOT_LIFECYCLE, NOT_LOCATION, - NOT_VERSIONS + NOT_VERSIONS, + NOT_VERSIONING }, produces = APPLICATION_XML_VALUE ) diff --git a/server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java b/server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java index d8169ea88..00711b041 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java +++ b/server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java @@ -40,6 +40,7 @@ import com.adobe.testing.s3mock.dto.ObjectVersion; import com.adobe.testing.s3mock.dto.Prefix; import com.adobe.testing.s3mock.dto.S3Object; +import com.adobe.testing.s3mock.dto.VersioningConfiguration; import com.adobe.testing.s3mock.store.BucketStore; import com.adobe.testing.s3mock.store.ObjectStore; import java.util.ArrayList; @@ -113,6 +114,21 @@ public boolean deleteBucket(String bucketName) { return bucketStore.deleteBucket(bucketName); } + public void setVersioningConfiguration(String bucketName, VersioningConfiguration configuration) { + var bucketMetadata = bucketStore.getBucketMetadata(bucketName); + bucketStore.storeVersioningConfiguration(bucketMetadata, configuration); + } + + public VersioningConfiguration getVersioningConfiguration(String bucketName) { + var bucketMetadata = bucketStore.getBucketMetadata(bucketName); + var configuration = bucketMetadata.versioningConfiguration(); + if (configuration != null) { + return configuration; + } else { + throw NOT_FOUND_BUCKET_OBJECT_LOCK; + } + } + public void setObjectLockConfiguration(String bucketName, ObjectLockConfiguration configuration) { var bucketMetadata = bucketStore.getBucketMetadata(bucketName); bucketStore.storeObjectLockConfiguration(bucketMetadata, configuration); diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java b/server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java index b0b984bc6..e0c271d2a 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java @@ -18,6 +18,7 @@ import com.adobe.testing.s3mock.dto.BucketLifecycleConfiguration; import com.adobe.testing.s3mock.dto.ObjectLockConfiguration; +import com.adobe.testing.s3mock.dto.VersioningConfiguration; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -30,6 +31,7 @@ public record BucketMetadata( String name, String creationDate, + VersioningConfiguration versioningConfiguration, ObjectLockConfiguration objectLockConfiguration, BucketLifecycleConfiguration bucketLifecycleConfiguration, ObjectOwnership objectOwnership, @@ -38,12 +40,14 @@ public record BucketMetadata( ) { public BucketMetadata(String name, String creationDate, + VersioningConfiguration versioningConfiguration, ObjectLockConfiguration objectLockConfiguration, BucketLifecycleConfiguration bucketLifecycleConfiguration, ObjectOwnership objectOwnership, Path path) { this(name, creationDate, + versioningConfiguration, objectLockConfiguration, bucketLifecycleConfiguration, objectOwnership, @@ -51,9 +55,23 @@ public BucketMetadata(String name, String creationDate, new HashMap<>()); } + public BucketMetadata withVersioningConfiguration( + VersioningConfiguration versioningConfiguration) { + return new BucketMetadata(name(), + creationDate(), + versioningConfiguration, + objectLockConfiguration(), + bucketLifecycleConfiguration(), + objectOwnership(), + path()); + } + public BucketMetadata withObjectLockConfiguration( ObjectLockConfiguration objectLockConfiguration) { - return new BucketMetadata(name(), creationDate(), objectLockConfiguration, + return new BucketMetadata(name(), + creationDate(), + versioningConfiguration(), + objectLockConfiguration, bucketLifecycleConfiguration(), objectOwnership(), path()); @@ -61,7 +79,10 @@ public BucketMetadata withObjectLockConfiguration( public BucketMetadata withBucketLifecycleConfiguration( BucketLifecycleConfiguration bucketLifecycleConfiguration) { - return new BucketMetadata(name(), creationDate(), objectLockConfiguration(), + return new BucketMetadata(name(), + creationDate(), + versioningConfiguration(), + objectLockConfiguration(), bucketLifecycleConfiguration, objectOwnership(), path()); diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java b/server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java index fe75784a4..6a3ee72f4 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java @@ -19,6 +19,7 @@ import com.adobe.testing.s3mock.dto.BucketLifecycleConfiguration; import com.adobe.testing.s3mock.dto.ObjectLockConfiguration; import com.adobe.testing.s3mock.dto.ObjectLockEnabled; +import com.adobe.testing.s3mock.dto.VersioningConfiguration; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; @@ -193,6 +194,7 @@ public BucketMetadata createBucket(String bucketName, var newBucketMetadata = new BucketMetadata( bucketName, s3ObjectDateFormat.format(LocalDateTime.now()), + new VersioningConfiguration(null, null, null), objectLockEnabled ? new ObjectLockConfiguration(ObjectLockEnabled.ENABLED, null) : null, null, @@ -232,6 +234,13 @@ public void storeObjectLockConfiguration(BucketMetadata metadata, } } + public void storeVersioningConfiguration(BucketMetadata metadata, + VersioningConfiguration configuration) { + synchronized (lockStore.get(metadata.name())) { + writeToDisk(metadata.withVersioningConfiguration(configuration)); + } + } + public void storeBucketLifecycleConfiguration(BucketMetadata metadata, BucketLifecycleConfiguration configuration) { synchronized (lockStore.get(metadata.name())) { diff --git a/server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpParameters.java b/server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpParameters.java index 609a908c7..b21f482b0 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpParameters.java +++ b/server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpParameters.java @@ -21,30 +21,30 @@ */ public class AwsHttpParameters { - private static final String NOT = "!"; - - public static final String ACL = "acl"; - public static final String NOT_ACL = NOT + ACL; public static final String CONTINUATION_TOKEN = "continuation-token"; public static final String DELETE = "delete"; public static final String ENCODING_TYPE = "encoding-type"; public static final String KEY_MARKER = "key-marker"; public static final String VERSION_ID_MARKER = "version-id-marker"; public static final String LIST_TYPE_V2 = "list-type=2"; - public static final String VERSIONS = "versions"; - public static final String NOT_VERSIONS = "!versions"; public static final String NOT_LIST_TYPE = "!list-type"; public static final String MAX_KEYS = "max-keys"; public static final String PART_NUMBER = "partNumber"; public static final String START_AFTER = "start-after"; + public static final String VERSION_ID = "versionId"; + + private static final String NOT = "!"; + + public static final String ACL = "acl"; + public static final String NOT_ACL = NOT + ACL; + public static final String VERSIONS = "versions"; + public static final String NOT_VERSIONS = NOT + VERSIONS; public static final String TAGGING = "tagging"; public static final String NOT_TAGGING = NOT + TAGGING; public static final String UPLOADS = "uploads"; public static final String NOT_UPLOADS = NOT + UPLOADS; - public static final String UPLOAD_ID = "uploadId"; public static final String NOT_UPLOAD_ID = NOT + UPLOAD_ID; - public static final String LEGAL_HOLD = "legal-hold"; public static final String NOT_LEGAL_HOLD = NOT + LEGAL_HOLD; public static final String OBJECT_LOCK = "object-lock"; @@ -57,8 +57,9 @@ public class AwsHttpParameters { public static final String NOT_ATTRIBUTES = NOT + ATTRIBUTES; public static final String LOCATION = "location"; public static final String NOT_LOCATION = NOT + LOCATION; + public static final String VERSIONING = "versioning"; + public static final String NOT_VERSIONING = NOT + VERSIONING; - public static final String VERSION_ID = "versionId"; private AwsHttpParameters() { // private constructor for utility classes diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt index 8147432b2..812415c19 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt @@ -158,7 +158,7 @@ internal class MultipartServiceTest : ServiceTestBase() { val uploadId = "uploadId" val bucketName = "bucketName" whenever(bucketStore.getBucketMetadata(bucketName)) - .thenReturn(BucketMetadata(null, null, null, null, null, null)) + .thenReturn(BucketMetadata(null, null, null, null, null, null, null)) whenever( multipartStore.getMultipartUpload( ArgumentMatchers.any( diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt index 2e4cdd565..84b17ab5e 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt @@ -130,6 +130,7 @@ internal abstract class ServiceTestBase { Date().toString(), null, null, + null, BUCKET_OWNER_ENFORCED, Files.createTempDirectory(bucketName) ) diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreConfigurationTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreConfigurationTest.kt index bd938a9f5..0c966338a 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreConfigurationTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreConfigurationTest.kt @@ -61,7 +61,7 @@ internal class StoreConfigurationTest { val bucketMetadata = BucketMetadata( existingBucketName, Instant.now().toString(), - null, null, ObjectOwnership.BUCKET_OWNER_ENFORCED, existingBucket + null, null, null, ObjectOwnership.BUCKET_OWNER_ENFORCED, existingBucket ) val metaFile = Paths.get(existingBucket.toString(), BUCKET_META_FILE) OBJECT_MAPPER.writeValue(metaFile.toFile(), bucketMetadata) diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreTestBase.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreTestBase.kt index 1e32619a4..9df530933 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreTestBase.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreTestBase.kt @@ -37,6 +37,7 @@ internal abstract class StoreTestBase { Date().toString(), null, null, + null, ObjectOwnership.BUCKET_OWNER_ENFORCED, Paths.get(rootFolder.toString(), bucketName), mapOf()