diff --git a/CHANGELOG.md b/CHANGELOG.md index 9766eeb45..afdafc35d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,12 +163,14 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav * Features and fixes * ListObjectVersions API returns "isLatest=true" if versioning is not enabled. (fixes #2481) + * Tags are now verified for correctness. * Refactorings - * TBD + * README.md fixes, typos, wording, clarifications * Version updates (deliverable dependencies) * None * Version updates (build dependencies) * Bump kotlin.version from 2.1.21 to 2.2.0 + * Bump github/codeql-action from 3.29.0 to 3.29.1 * Bump com.puppycrawl.tools:checkstyle from 10.25.0 to 10.26.0 ## 4.5.0 diff --git a/README.md b/README.md index 76b7a2acb..a42b6238a 100755 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ * [Start using Docker compose](#start-using-docker-compose) * [Simple example](#simple-example) * [Expanded example](#expanded-example) - * [Start using self-signed SSL certificate](#start-using-self-signed-ssl-certificate) + * [Start using a self-signed SSL certificate](#start-using-a-self-signed-ssl-certificate) * [S3Mock Java](#s3mock-java) * [Start using the JUnit4 Rule](#start-using-the-junit4-rule) * [Start using the JUnit5 Extension](#start-using-the-junit5-extension) @@ -63,6 +63,7 @@ * [Security](#security) * [Contributing](#contributing) * [Licensing](#licensing) + * [Powered by](#powered-by) ## S3Mock @@ -221,7 +222,7 @@ S3Mock will accept presigned URLs, but it *ignores all parameters*. For instance, S3Mock does not verify the HTTP verb that the presigned uri was created with, and it does not validate whether the link is expired or not. -S3 SDKs can be used to create presigned URLs pointing to S3Mock if they're configured for path-style access. See the +S3 SDKs can be used to create presigned URLs pointing to S3Mock if they're configured for path-style access. See the "Usage..." section above for links to examples on how to use the SDK with presigned URLs. #### Self-signed SSL certificate @@ -327,9 +328,9 @@ The mock can be configured with the following environment variables: - Legacy name: `retainFilesOnExit` - Default: false - `debug`: set to `true` to - enable [Spring Boot's debug output](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.console-output). + enable [Spring Boot's debug output](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.console-output). - `trace`: set to `true` to - enable [Spring Boot's trace output](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.console-output). + enable [Spring Boot's trace output](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.console-output). ### S3Mock Docker @@ -339,7 +340,7 @@ The container is lightweight, built on top of the official [Linux Alpine image]( If needed, configure [memory](https://docs.docker.com/engine/reference/commandline/run/#specify-hard-limits-on-memory-available-to-containers--m---memory) -and [cpu](https://docs.docker.com/engine/reference/commandline/run/#options) limits for the S3Mock docker container. +and [cpu](https://docs.docker.com/engine/reference/commandline/run/#options) limits for the S3Mock Docker container. The JVM will automatically use half the available memory. @@ -348,7 +349,7 @@ The JVM will automatically use half the available memory. Starting on the command-line: ```shell - docker run -p 9090:9090 -p 9191:9191 -t adobe/s3mock +docker run -p 9090:9090 -p 9191:9191 -t adobe/s3mock ``` The port `9090` is for HTTP, port `9191` is for HTTPS. @@ -356,34 +357,30 @@ The port `9090` is for HTTP, port `9191` is for HTTPS. Example with configuration via environment variables: ```shell - docker run -p 9090:9090 -p 9191:9191 -e COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS=test -e debug=true -t adobe/s3mock +docker run -p 9090:9090 -p 9191:9191 -e COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS=test -e debug=true -t adobe/s3mock ``` #### Start using the Fabric8 Docker-Maven-Plugin Our [integration tests](integration-tests) are using the Amazon S3 Client to verify the server functionality against the S3Mock. During the Maven build, the Docker image is started using the [docker-maven-plugin](https://dmp.fabric8.io/) and -the corresponding ports are passed to the JUnit test through the `maven-failsafe-plugin`. See [ -`BucketIT`](integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt) as an example on how it's used -in the code. +the corresponding ports are passed to the JUnit test through the `maven-failsafe-plugin`. See [`BucketIT`](integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt) +as an example on how it's used in the code. -This way, one can easily switch between calling the S3Mock or the real S3 endpoint and this doesn't add any additional +This way, one can easily switch between calling the S3Mock or the real S3 endpoint, and this doesn't add any additional Java dependencies to the project. #### Start using Testcontainers -The [ -`S3MockContainer`](testsupport/testcontainers/src/main/java/com/adobe/testing/s3mock/testcontainers/S3MockContainer.java) +The [`S3MockContainer`](testsupport/testcontainers/src/main/java/com/adobe/testing/s3mock/testcontainers/S3MockContainer.java) is a `Testcontainer` implementation that comes pre-configured exposing HTTP and HTTPS ports. Environment variables can be set on startup. -The example [ -`S3MockContainerJupiterTest`](testsupport/testcontainers/src/test/java/com/adobe/testing/s3mock/testcontainers/S3MockContainerJupiterTest.java) -demonstrates the usage with JUnit 5. The example [ -`S3MockContainerManualTest`](testsupport/testcontainers/src/test/java/com/adobe/testing/s3mock/testcontainers/S3MockContainerManualTest.java) -demonstrates the usage with plain Java. +The example [`S3MockContainerJupiterTest`](testsupport/testcontainers/src/test/kotlin/com/adobe/testing/s3mock/testcontainers/S3MockContainerJupiterTest.kt) +demonstrates the usage with JUnit 5. The example [`S3MockContainerManualTest`](testsupport/testcontainers/src/test/kotlin/com/adobe/testing/s3mock/testcontainers/S3MockContainerManualTest.kt) +demonstrates the usage with plain Kotlin. Java will be similar. -Testcontainers provides integrations for JUnit 4, JUnit 5 and Spock. +Testcontainers provide integrations for JUnit 4, JUnit 5 and Spock. For more information, visit the [Testcontainers](https://www.testcontainers.org/) website. To use the [ @@ -391,7 +388,6 @@ To use the [ use the following Maven artifact in `test` scope: ```xml - com.adobe.testing s3mock-testcontainers @@ -430,7 +426,7 @@ docker compose down ##### Expanded example -Suppose we want to see what S3Mock is persisting, and look at the logs it generates in detail. +Suppose we want to see what S3Mock is persisting and look at the logs it generates in detail. A local directory is needed, let's call it `locals3root`. This directory must be mounted as a volume into the Docker container when it's started, and that mounted volume must then be configured as the `root` for S3Mock. Let's call the @@ -512,7 +508,7 @@ $ ls locals3root/my-test-bucket bucketMetadata.json ``` -#### Start using self-signed SSL certificate +#### Start using a self-signed SSL certificate S3Mock includes a self-signed SSL certificate: @@ -572,13 +568,13 @@ the `S3Mock` during a JUnit test, classpaths of the tested application and of th to unpredictable and undesired effects such as class conflicts or dependency version conflicts. This is especially problematic if the tested application itself is a Spring (Boot) application, as both applications will load configurations based on the availability of certain classes in the classpath, leading to unpredictable runtime -behaviour. +behavior. _This is the opposite of what software engineers are trying to achieve when thoroughly testing code in continuous integration..._ `S3Mock` dependencies are updated regularly, any update could break any number of projects. -**See also [issues labelled "dependency-problem"](https://github.com/adobe/S3Mock/issues?q=is%3Aissue+label%3Adependency-problem).** +**See also [issues labeled "dependency-problem"](https://github.com/adobe/S3Mock/issues?q=is%3Aissue+label%3Adependency-problem).** **See also [the Java section below](#Java)** @@ -605,11 +601,11 @@ The `S3MockExtension` can currently be used in two ways: 1. Declaratively using `@ExtendWith(S3MockExtension.class)` and by injecting a properly configured instance of `AmazonS3` client and/or the started `S3MockApplication` to the tests. See examples: [`S3MockExtensionDeclarativeTest`](testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk1/S3MockExtensionDeclarativeTest.java) (for SDKv1) - or [`S3MockExtensionDeclarativeTest`](testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk2/S3MockExtensionDeclarativeTest.java) (for SDKv2) + or [`S3MockExtensionDeclarativeTest`](testsupport/junit5/src/test/kotlin/com/adobe/testing/s3mock/junit5/sdk2/S3MockExtensionDeclarativeTest.kt) (for SDKv2) 2. Programmatically using `@RegisterExtension` and by creating and configuring the `S3MockExtension` using a _builder_. See examples: [`S3MockExtensionProgrammaticTest`](testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk1/S3MockExtensionProgrammaticTest.java) (for SDKv1) - or [`S3MockExtensionProgrammaticTest`](testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk2/S3MockExtensionProgrammaticTest.java) (for SDKv2) + or [`S3MockExtensionProgrammaticTest`](testsupport/junit5/src/test/kotlin/com/adobe/testing/s3mock/junit5/sdk2/S3MockExtensionProgrammaticTest.kt) (for SDKv2) To use the JUnit5 Extension, use the following Maven artifact in `test` scope: @@ -624,9 +620,9 @@ To use the JUnit5 Extension, use the following Maven artifact in `test` scope: #### Start using the TestNG Listener -The example [`S3MockListenerXMLConfigurationTest`](testsupport/testng/src/test/java/com/adobe/testing/s3mock/testng/S3MockListenerXmlConfigurationTest.java) -demonstrates the usage of the `S3MockListener`, which can be configured as shown in [`testng.xml`](testsupport/testng/src/test/resources/testng.xml). -The listener bootstraps the S3Mock application before TestNG execution starts and shuts down the application just before the execution terminates. +The example [`S3MockListenerXMLConfigurationTest`](testsupport/testng/src/test/kotlin/com/adobe/testing/s3mock/testng/S3MockListenerXmlConfigurationTest.kt) +demonstrates the usage of the `S3MockListener`, which can be configured as shown in [`testng.xml`](testsupport/testng/src/test/resources/testng.xml). +The listener bootstraps the S3Mock application before TestNG execution starts and shuts down the application just before the execution terminates. Please refer to [`IExecutionListener`](https://github.com/testng-team/testng/blob/master/testng-core-api/src/main/java/org/testng/IExecutionListener.java) in the TestNG API. @@ -666,7 +662,7 @@ If the environment variable `COM_ADOBE_TESTING_S3MOCK_STORE_RETAIN_FILES_ON_EXIT ### Root-Folder -S3Mock stores buckets and objects a root-folder. +S3Mock stores buckets and objects in a root-folder. This folder is expected to be empty when S3Mock starts. See also FYI above. @@ -825,8 +821,8 @@ Vulnerabilities may also be reported through the GitHub issue tracker. ## Security -S3Mock is not intended to be used in production environments. It is a mock server that is meant to be used in -development and testing environments only. It does not implement all security features of AWS S3, and should not be used +S3Mock is not intended to be used in production environments. It is a mock server meant to be used in +development and testing environments only. It does not implement all security features of AWS S3 and should not be used as a replacement for AWS S3 in production. It is implemented using [Spring Boot](https://github.com/spring-projects/spring-boot), which is a Java framework that is designed to be secure by default. @@ -838,3 +834,6 @@ Contributions are welcome! Read the [Contributing Guide](./.github/CONTRIBUTING. ## Licensing This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information. + +## Powered by +[![IntelliJ IDEA logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/IntelliJ_IDEA.svg)](https://jb.gg/OpenSourceSupport) diff --git a/server/src/main/java/com/adobe/testing/s3mock/ObjectController.java b/server/src/main/java/com/adobe/testing/s3mock/ObjectController.java index 9bc05e435..b21e89ab5 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/ObjectController.java +++ b/server/src/main/java/com/adobe/testing/s3mock/ObjectController.java @@ -554,6 +554,7 @@ public ResponseEntity putObjectTagging( var bucket = bucketService.verifyBucketExists(bucketName); var s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.key(), versionId); + objectService.verifyObjectTags(body.tagSet().tags()); objectService.setObjectTags(bucketName, key.key(), versionId, body.tagSet().tags()); return ResponseEntity .ok() @@ -673,8 +674,7 @@ public ResponseEntity getObjectRetention( @RequestParam(value = VERSION_ID, required = false) @Nullable String versionId) { var bucket = bucketService.verifyBucketExists(bucketName); bucketService.verifyBucketObjectLockEnabled(bucketName); - var s3ObjectMetadata = objectService.verifyObjectLockConfiguration(bucketName, key.key(), - versionId); + var s3ObjectMetadata = objectService.verifyObjectLockConfiguration(bucketName, key.key(), versionId); return ResponseEntity .ok() diff --git a/server/src/main/java/com/adobe/testing/s3mock/S3Exception.java b/server/src/main/java/com/adobe/testing/s3mock/S3Exception.java index 39374cccc..7e9927523 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/S3Exception.java +++ b/server/src/main/java/com/adobe/testing/s3mock/S3Exception.java @@ -43,6 +43,11 @@ public class S3Exception extends RuntimeException { "The list of parts was not in ascending order. The parts list must be specified in " + "order by part number."); + public static final S3Exception INVALID_TAG = + new S3Exception(BAD_REQUEST.value(), "InvalidTag", + "Your request contains tag input that is not valid. For example, your request might contain " + + "duplicate keys, keys or values that are too long, or system tags."); + public static S3Exception completeRequestMissingChecksum(String algorithm, Integer partNumber) { return new S3Exception(BAD_REQUEST.value(), BAD_REQUEST_CODE, "The upload was created using a " + algorithm + " checksum. " diff --git a/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java b/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java index 673172034..615891e13 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java +++ b/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java @@ -19,6 +19,7 @@ import static com.adobe.testing.s3mock.S3Exception.BAD_REQUEST_CONTENT; import static com.adobe.testing.s3mock.S3Exception.BAD_REQUEST_MD5; import static com.adobe.testing.s3mock.S3Exception.INVALID_REQUEST_RETAIN_DATE; +import static com.adobe.testing.s3mock.S3Exception.INVALID_TAG; import static com.adobe.testing.s3mock.S3Exception.NOT_FOUND_OBJECT_LOCK; import static com.adobe.testing.s3mock.S3Exception.NOT_MODIFIED; import static com.adobe.testing.s3mock.S3Exception.NO_SUCH_KEY; @@ -49,8 +50,10 @@ import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,6 +62,14 @@ public class ObjectService extends ServiceBase { static final String WILDCARD_ETAG = "\"*\""; static final String WILDCARD = "*"; private static final Logger LOG = LoggerFactory.getLogger(ObjectService.class); + private static final Pattern TAG_ALLOWED_CHARS = Pattern.compile("[\\w+ \\-=.:/@]*"); + private static final int MAX_ALLOWED_TAGS = 50; + private static final int MIN_ALLOWED_TAG_KEY_LENGTH = 1; + private static final int MAX_ALLOWED_TAG_KEY_LENGTH = 128; + private static final int MIN_ALLOWED_TAG_VALUE_LENGTH = 0; + private static final int MAX_ALLOWED_TAG_VALUE_LENGTH = 256; + private static final String DISALLOWED_TAG_KEY_PREFIX = "aws:"; + private final BucketStore bucketStore; private final ObjectStore objectStore; @@ -175,6 +186,48 @@ public void setObjectTags(String bucketName, String key, @Nullable String versio objectStore.storeObjectTags(bucketMetadata, uuid, versionId, tags); } + public void verifyObjectTags(List tags) { + if (tags.size() > MAX_ALLOWED_TAGS) { + throw INVALID_TAG; + } + verifyDuplicateTagKeys(tags); + for (var tag : tags) { + verifyTagKeyPrefix(tag.key()); + verifyTagLength(MIN_ALLOWED_TAG_KEY_LENGTH, MAX_ALLOWED_TAG_KEY_LENGTH, tag.key()); + verifyTagChars(tag.key()); + + verifyTagLength(MIN_ALLOWED_TAG_VALUE_LENGTH, MAX_ALLOWED_TAG_VALUE_LENGTH, tag.value()); + verifyTagChars(tag.value()); + } + } + + private void verifyDuplicateTagKeys(List tags) { + var tagKeys = new HashSet(); + for (var tag : tags) { + if (!tagKeys.add(tag.key())) { + throw INVALID_TAG; + } + } + } + + private void verifyTagKeyPrefix(String tagKey) { + if (tagKey.startsWith(DISALLOWED_TAG_KEY_PREFIX)) { + throw INVALID_TAG; + } + } + + private void verifyTagLength(int minLength, int maxLength, String tag) { + if (tag.length() < minLength || tag.length() > maxLength) { + throw INVALID_TAG; + } + } + + private void verifyTagChars(String tag) { + if (!TAG_ALLOWED_CHARS.matcher(tag).matches()) { + throw INVALID_TAG; + } + } + public void setLegalHold(String bucketName, String key, @Nullable String versionId, LegalHold legalHold) { var bucketMetadata = bucketStore.getBucketMetadata(bucketName); var uuid = bucketMetadata.getID(key); diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/ObjectStore.java b/server/src/main/java/com/adobe/testing/s3mock/store/ObjectStore.java index 5bb9e86fa..a50d6c1ed 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/ObjectStore.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/ObjectStore.java @@ -100,11 +100,11 @@ public S3ObjectMetadata storeS3ObjectMetadata( var existingVersions = getS3ObjectVersions(bucket, id); if (existingVersions != null) { versionId = existingVersions.createVersion(); - writeVersionsfile(bucket, id, existingVersions); + writeVersionsFile(bucket, id, existingVersions); } else { var newVersions = createS3ObjectVersions(bucket, id); versionId = newVersions.createVersion(); - writeVersionsfile(bucket, id, newVersions); + writeVersionsFile(bucket, id, newVersions); } } var dataFile = inputPathToFile(path, getDataFilePath(bucket, id, versionId)); @@ -279,7 +279,9 @@ public void storeRetention(BucketMetadata bucket, UUID id, @Nullable String vers public S3ObjectMetadata getS3ObjectMetadata(BucketMetadata bucket, UUID id, @Nullable String versionId) { if (bucket.isVersioningEnabled() && versionId == null) { var s3ObjectVersions = getS3ObjectVersions(bucket, id); - versionId = s3ObjectVersions.getLatestVersion(); + if (s3ObjectVersions != null) { + versionId = s3ObjectVersions.getLatestVersion(); + } } var metaPath = getMetaFilePath(bucket, id, versionId); @@ -321,7 +323,7 @@ public S3ObjectVersions createS3ObjectVersions(BucketMetadata bucket, UUID id) { } else { synchronized (lockStore.get(id)) { try { - writeVersionsfile(bucket, id, new S3ObjectVersions(id)); + writeVersionsFile(bucket, id, new S3ObjectVersions(id)); return objectMapper.readValue(metaPath.toFile(), S3ObjectVersions.class); } catch (java.io.IOException e) { throw new IllegalArgumentException("Could not read object versions-file " + id, e); @@ -458,22 +460,31 @@ public boolean deleteObject( } } + /** + * Deletes a specific version of an object, if found. + * If this is the last version of an object, it deletes the object. + * Returns true if the *LAST* version was deleted. + */ private boolean doDeleteVersion(BucketMetadata bucket, UUID id, String versionId) { synchronized (lockStore.get(id)) { try { var existingVersions = getS3ObjectVersions(bucket, id); + if (existingVersions == null) { + //no versions exist, nothing to delete. + return false; + } if (existingVersions.versions().size() <= 1) { //this is the last version of an object, delete object completely. return doDeleteObject(bucket, id); } else { //there is at least one version of an object left, delete only the version. existingVersions.deleteVersion(versionId); - writeVersionsfile(bucket, id, existingVersions); + writeVersionsFile(bucket, id, existingVersions); + return false; } } catch (Exception e) { throw new IllegalStateException("Could not delete object-version " + id, e); } - return false; } } @@ -502,7 +513,7 @@ private boolean insertDeleteMarker( var existingVersions = getS3ObjectVersions(bucket, id); if (existingVersions != null) { versionId = existingVersions.createVersion(); - writeVersionsfile(bucket, id, existingVersions); + writeVersionsFile(bucket, id, existingVersions); } writeMetafile(bucket, S3ObjectMetadata.deleteMarker(s3ObjectMetadata, versionId)); } catch (Exception e) { @@ -575,7 +586,7 @@ private Path getVersionFilePath(BucketMetadata bucket, UUID id) { return getObjectFolderPath(bucket, id).resolve(VERSIONS_FILE); } - private void writeVersionsfile(BucketMetadata bucket, UUID id, + private void writeVersionsFile(BucketMetadata bucket, UUID id, S3ObjectVersions s3ObjectVersions) { try { synchronized (lockStore.get(id)) { diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/S3ObjectMetadata.java b/server/src/main/java/com/adobe/testing/s3mock/store/S3ObjectMetadata.java index 93134cb4a..f03a2f333 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/S3ObjectMetadata.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/S3ObjectMetadata.java @@ -76,7 +76,7 @@ public record S3ObjectMetadata( checksumType = checksumType == null ? ChecksumType.FULL_OBJECT : checksumType; } - public static S3ObjectMetadata deleteMarker(S3ObjectMetadata metadata, String versionId) { + public static S3ObjectMetadata deleteMarker(S3ObjectMetadata metadata, @Nullable String versionId) { return new S3ObjectMetadata(metadata.id, metadata.key(), metadata.size(), diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/service/ObjectServiceTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/service/ObjectServiceTest.kt index 64ba6fde0..34ab54aab 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/service/ObjectServiceTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/service/ObjectServiceTest.kt @@ -17,11 +17,13 @@ package com.adobe.testing.s3mock.service import com.adobe.testing.s3mock.ChecksumTestUtil import com.adobe.testing.s3mock.S3Exception +import com.adobe.testing.s3mock.S3Exception.INVALID_TAG import com.adobe.testing.s3mock.dto.ChecksumAlgorithm import com.adobe.testing.s3mock.dto.Delete import com.adobe.testing.s3mock.dto.Mode import com.adobe.testing.s3mock.dto.Retention import com.adobe.testing.s3mock.dto.S3ObjectIdentifier +import com.adobe.testing.s3mock.dto.Tag import com.adobe.testing.s3mock.store.BucketMetadata import com.adobe.testing.s3mock.store.MultipartStore import com.adobe.testing.s3mock.util.AwsHttpHeaders @@ -308,6 +310,90 @@ internal class ObjectServiceTest : ServiceTestBase() { assertThat(tempFileAndChecksum.right).contains("Y8S4/uAGut7vjdFZQjLKZ7P28V9EPWb4BIoeniuM0mY=") } + @Test + fun `store tags succeeds`() { + val tags = listOf(Tag("key1", "value1"), Tag("key2", "value2")) + iut.verifyObjectTags(tags) + } + + @Test + fun `store tags succeeds with min key and value length`() { + val tags = listOf(Tag("1", ""), Tag("2", "")) + iut.verifyObjectTags(tags) + } + + @Test + fun `store tags succeeds with all allowed characters`() { + val tags = listOf(Tag("key1+-=._:/@ ", "value1"), Tag("key2", "value2")) + iut.verifyObjectTags(tags) + } + + @Test + fun `store tags fails with too many tags`() { + val tags = mutableListOf() + for (i in 0..60) { + tags.add(Tag("key$i", "value$i")) + } + assertThatThrownBy { + iut.verifyObjectTags(tags) + }.isInstanceOf(S3Exception::class.java) + .hasMessage(INVALID_TAG.message) + } + + @Test + fun `store tags fails with duplicate keys`() { + val tags = listOf(Tag("key1", "value1"), Tag("key1", "value2")) + assertThatThrownBy { + iut.verifyObjectTags(tags) + }.isInstanceOf(S3Exception::class.java) + .hasMessage(INVALID_TAG.message) + } + + @Test + fun `store tags fails with illegal characters`() { + val tags = listOf(Tag("key1%()", "value1")) + assertThatThrownBy { + iut.verifyObjectTags(tags) + }.isInstanceOf(S3Exception::class.java) + .hasMessage(INVALID_TAG.message) + } + + @Test + fun `store tags fails with key gt 127 characters`() { + val tags = listOf(Tag("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque pena", "value1")) + assertThatThrownBy { + iut.verifyObjectTags(tags) + }.isInstanceOf(S3Exception::class.java) + .hasMessage(INVALID_TAG.message) + } + + @Test + fun `store tags fails with value gt 255 characters`() { + val tags = listOf(Tag("key1", "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, s")) + assertThatThrownBy { + iut.verifyObjectTags(tags) + }.isInstanceOf(S3Exception::class.java) + .hasMessage(INVALID_TAG.message) + } + + @Test + fun `store tags fails with invalid key prefix`() { + val tags = listOf(Tag("aws:key1", "value1")) + assertThatThrownBy { + iut.verifyObjectTags(tags) + }.isInstanceOf(S3Exception::class.java) + .hasMessage(INVALID_TAG.message) + } + + @Test + fun `store tags fails with invalid key length`() { + val tags = listOf(Tag("", "value1")) + assertThatThrownBy { + iut.verifyObjectTags(tags) + }.isInstanceOf(S3Exception::class.java) + .hasMessage(INVALID_TAG.message) + } + @Throws(IOException::class) private fun toTempFile(path: Path, algorithm: software.amazon.awssdk.checksums.spi.ChecksumAlgorithm): Path { val (inputStream, _) = ChecksumTestUtil.prepareInputStream(path.toFile(), false, algorithm)