Skip to content

Commit 9bdac03

Browse files
authored
Merge pull request #2473 from adobe/verify-object-tagging
Verify object tagging
2 parents 339bc49 + a94ac89 commit 9bdac03

File tree

8 files changed

+200
-44
lines changed

8 files changed

+200
-44
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,14 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
163163

164164
* Features and fixes
165165
* ListObjectVersions API returns "isLatest=true" if versioning is not enabled. (fixes #2481)
166+
* Tags are now verified for correctness.
166167
* Refactorings
167-
* TBD
168+
* README.md fixes, typos, wording, clarifications
168169
* Version updates (deliverable dependencies)
169170
* None
170171
* Version updates (build dependencies)
171172
* Bump kotlin.version from 2.1.21 to 2.2.0
173+
* Bump github/codeql-action from 3.29.0 to 3.29.1
172174
* Bump com.puppycrawl.tools:checkstyle from 10.25.0 to 10.26.0
173175

174176
## 4.5.0

README.md

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* [Start using Docker compose](#start-using-docker-compose)
3636
* [Simple example](#simple-example)
3737
* [Expanded example](#expanded-example)
38-
* [Start using self-signed SSL certificate](#start-using-self-signed-ssl-certificate)
38+
* [Start using a self-signed SSL certificate](#start-using-a-self-signed-ssl-certificate)
3939
* [S3Mock Java](#s3mock-java)
4040
* [Start using the JUnit4 Rule](#start-using-the-junit4-rule)
4141
* [Start using the JUnit5 Extension](#start-using-the-junit5-extension)
@@ -63,6 +63,7 @@
6363
* [Security](#security)
6464
* [Contributing](#contributing)
6565
* [Licensing](#licensing)
66+
* [Powered by](#powered-by)
6667
<!-- TOC -->
6768

6869
## S3Mock
@@ -221,7 +222,7 @@ S3Mock will accept presigned URLs, but it *ignores all parameters*.
221222
For instance, S3Mock does not verify the HTTP verb that the presigned uri was created with, and it does not validate
222223
whether the link is expired or not.
223224

224-
S3 SDKs can be used to create presigned URLs pointing to S3Mock if they're configured for path-style access. See the
225+
S3 SDKs can be used to create presigned URLs pointing to S3Mock if they're configured for path-style access. See the
225226
"Usage..." section above for links to examples on how to use the SDK with presigned URLs.
226227

227228
#### Self-signed SSL certificate
@@ -327,9 +328,9 @@ The mock can be configured with the following environment variables:
327328
- Legacy name: `retainFilesOnExit`
328329
- Default: false
329330
- `debug`: set to `true` to
330-
enable [Spring Boot's debug output](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.console-output).
331+
enable [Spring Boot's debug output](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.console-output).
331332
- `trace`: set to `true` to
332-
enable [Spring Boot's trace output](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.console-output).
333+
enable [Spring Boot's trace output](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.console-output).
333334

334335
### S3Mock Docker
335336

@@ -339,7 +340,7 @@ The container is lightweight, built on top of the official [Linux Alpine image](
339340

340341
If needed,
341342
configure [memory](https://docs.docker.com/engine/reference/commandline/run/#specify-hard-limits-on-memory-available-to-containers--m---memory)
342-
and [cpu](https://docs.docker.com/engine/reference/commandline/run/#options) limits for the S3Mock docker container.
343+
and [cpu](https://docs.docker.com/engine/reference/commandline/run/#options) limits for the S3Mock Docker container.
343344

344345
The JVM will automatically use half the available memory.
345346

@@ -348,50 +349,45 @@ The JVM will automatically use half the available memory.
348349
Starting on the command-line:
349350

350351
```shell
351-
docker run -p 9090:9090 -p 9191:9191 -t adobe/s3mock
352+
docker run -p 9090:9090 -p 9191:9191 -t adobe/s3mock
352353
```
353354

354355
The port `9090` is for HTTP, port `9191` is for HTTPS.
355356

356357
Example with configuration via environment variables:
357358

358359
```shell
359-
docker run -p 9090:9090 -p 9191:9191 -e COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS=test -e debug=true -t adobe/s3mock
360+
docker run -p 9090:9090 -p 9191:9191 -e COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS=test -e debug=true -t adobe/s3mock
360361
```
361362

362363
#### Start using the Fabric8 Docker-Maven-Plugin
363364

364365
Our [integration tests](integration-tests) are using the Amazon S3 Client to verify the server functionality against the
365366
S3Mock. During the Maven build, the Docker image is started using the [docker-maven-plugin](https://dmp.fabric8.io/) and
366-
the corresponding ports are passed to the JUnit test through the `maven-failsafe-plugin`. See [
367-
`BucketIT`](integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt) as an example on how it's used
368-
in the code.
367+
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)
368+
as an example on how it's used in the code.
369369

370-
This way, one can easily switch between calling the S3Mock or the real S3 endpoint and this doesn't add any additional
370+
This way, one can easily switch between calling the S3Mock or the real S3 endpoint, and this doesn't add any additional
371371
Java dependencies to the project.
372372

373373
#### Start using Testcontainers
374374

375-
The [
376-
`S3MockContainer`](testsupport/testcontainers/src/main/java/com/adobe/testing/s3mock/testcontainers/S3MockContainer.java)
375+
The [`S3MockContainer`](testsupport/testcontainers/src/main/java/com/adobe/testing/s3mock/testcontainers/S3MockContainer.java)
377376
is a `Testcontainer` implementation that comes pre-configured exposing HTTP and HTTPS ports. Environment variables can
378377
be set on startup.
379378

380-
The example [
381-
`S3MockContainerJupiterTest`](testsupport/testcontainers/src/test/java/com/adobe/testing/s3mock/testcontainers/S3MockContainerJupiterTest.java)
382-
demonstrates the usage with JUnit 5. The example [
383-
`S3MockContainerManualTest`](testsupport/testcontainers/src/test/java/com/adobe/testing/s3mock/testcontainers/S3MockContainerManualTest.java)
384-
demonstrates the usage with plain Java.
379+
The example [`S3MockContainerJupiterTest`](testsupport/testcontainers/src/test/kotlin/com/adobe/testing/s3mock/testcontainers/S3MockContainerJupiterTest.kt)
380+
demonstrates the usage with JUnit 5. The example [`S3MockContainerManualTest`](testsupport/testcontainers/src/test/kotlin/com/adobe/testing/s3mock/testcontainers/S3MockContainerManualTest.kt)
381+
demonstrates the usage with plain Kotlin. Java will be similar.
385382

386-
Testcontainers provides integrations for JUnit 4, JUnit 5 and Spock.
383+
Testcontainers provide integrations for JUnit 4, JUnit 5 and Spock.
387384
For more information, visit the [Testcontainers](https://www.testcontainers.org/) website.
388385

389386
To use the [
390387
`S3MockContainer`](testsupport/testcontainers/src/main/java/com/adobe/testing/s3mock/testcontainers/S3MockContainer.java),
391388
use the following Maven artifact in `test` scope:
392389

393390
```xml
394-
395391
<dependency>
396392
<groupId>com.adobe.testing</groupId>
397393
<artifactId>s3mock-testcontainers</artifactId>
@@ -430,7 +426,7 @@ docker compose down
430426

431427
##### Expanded example
432428

433-
Suppose we want to see what S3Mock is persisting, and look at the logs it generates in detail.
429+
Suppose we want to see what S3Mock is persisting and look at the logs it generates in detail.
434430

435431
A local directory is needed, let's call it `locals3root`. This directory must be mounted as a volume into the Docker
436432
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
512508
bucketMetadata.json
513509
```
514510

515-
#### Start using self-signed SSL certificate
511+
#### Start using a self-signed SSL certificate
516512

517513
S3Mock includes a self-signed SSL certificate:
518514

@@ -572,13 +568,13 @@ the `S3Mock` during a JUnit test, classpaths of the tested application and of th
572568
to unpredictable and undesired effects such as class conflicts or dependency version conflicts.
573569
This is especially problematic if the tested application itself is a Spring (Boot) application, as both applications
574570
will load configurations based on the availability of certain classes in the classpath, leading to unpredictable runtime
575-
behaviour.
571+
behavior.
576572

577573
_This is the opposite of what software engineers are trying to achieve when thoroughly testing code in continuous
578574
integration..._
579575

580576
`S3Mock` dependencies are updated regularly, any update could break any number of projects.
581-
**See also [issues labelled "dependency-problem"](https://github.com/adobe/S3Mock/issues?q=is%3Aissue+label%3Adependency-problem).**
577+
**See also [issues labeled "dependency-problem"](https://github.com/adobe/S3Mock/issues?q=is%3Aissue+label%3Adependency-problem).**
582578

583579
**See also [the Java section below](#Java)**
584580

@@ -605,11 +601,11 @@ The `S3MockExtension` can currently be used in two ways:
605601
1. Declaratively using `@ExtendWith(S3MockExtension.class)` and by injecting a properly configured instance of
606602
`AmazonS3` client and/or the started `S3MockApplication` to the tests.
607603
See examples: [`S3MockExtensionDeclarativeTest`](testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk1/S3MockExtensionDeclarativeTest.java) (for SDKv1)
608-
or [`S3MockExtensionDeclarativeTest`](testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk2/S3MockExtensionDeclarativeTest.java) (for SDKv2)
604+
or [`S3MockExtensionDeclarativeTest`](testsupport/junit5/src/test/kotlin/com/adobe/testing/s3mock/junit5/sdk2/S3MockExtensionDeclarativeTest.kt) (for SDKv2)
609605

610606
2. Programmatically using `@RegisterExtension` and by creating and configuring the `S3MockExtension` using a _builder_.
611607
See examples: [`S3MockExtensionProgrammaticTest`](testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk1/S3MockExtensionProgrammaticTest.java) (for SDKv1)
612-
or [`S3MockExtensionProgrammaticTest`](testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk2/S3MockExtensionProgrammaticTest.java) (for SDKv2)
608+
or [`S3MockExtensionProgrammaticTest`](testsupport/junit5/src/test/kotlin/com/adobe/testing/s3mock/junit5/sdk2/S3MockExtensionProgrammaticTest.kt) (for SDKv2)
613609

614610
To use the JUnit5 Extension, use the following Maven artifact in `test` scope:
615611

@@ -624,9 +620,9 @@ To use the JUnit5 Extension, use the following Maven artifact in `test` scope:
624620

625621
#### Start using the TestNG Listener
626622

627-
The example [`S3MockListenerXMLConfigurationTest`](testsupport/testng/src/test/java/com/adobe/testing/s3mock/testng/S3MockListenerXmlConfigurationTest.java)
628-
demonstrates the usage of the `S3MockListener`, which can be configured as shown in [`testng.xml`](testsupport/testng/src/test/resources/testng.xml).
629-
The listener bootstraps the S3Mock application before TestNG execution starts and shuts down the application just before the execution terminates.
623+
The example [`S3MockListenerXMLConfigurationTest`](testsupport/testng/src/test/kotlin/com/adobe/testing/s3mock/testng/S3MockListenerXmlConfigurationTest.kt)
624+
demonstrates the usage of the `S3MockListener`, which can be configured as shown in [`testng.xml`](testsupport/testng/src/test/resources/testng.xml).
625+
The listener bootstraps the S3Mock application before TestNG execution starts and shuts down the application just before the execution terminates.
630626
Please refer to [`IExecutionListener`](https://github.com/testng-team/testng/blob/master/testng-core-api/src/main/java/org/testng/IExecutionListener.java)
631627
in the TestNG API.
632628

@@ -666,7 +662,7 @@ If the environment variable `COM_ADOBE_TESTING_S3MOCK_STORE_RETAIN_FILES_ON_EXIT
666662

667663
### Root-Folder
668664

669-
S3Mock stores buckets and objects a root-folder.
665+
S3Mock stores buckets and objects in a root-folder.
670666

671667
This folder is expected to be empty when S3Mock starts. See also FYI above.
672668

@@ -825,8 +821,8 @@ Vulnerabilities may also be reported through the GitHub issue tracker.
825821

826822
## Security
827823

828-
S3Mock is not intended to be used in production environments. It is a mock server that is meant to be used in
829-
development and testing environments only. It does not implement all security features of AWS S3, and should not be used
824+
S3Mock is not intended to be used in production environments. It is a mock server meant to be used in
825+
development and testing environments only. It does not implement all security features of AWS S3 and should not be used
830826
as a replacement for AWS S3 in production.
831827
It is implemented using [Spring Boot](https://github.com/spring-projects/spring-boot), which is a Java framework that is
832828
designed to be secure by default.
@@ -838,3 +834,6 @@ Contributions are welcome! Read the [Contributing Guide](./.github/CONTRIBUTING.
838834
## Licensing
839835

840836
This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information.
837+
838+
## Powered by
839+
[![IntelliJ IDEA logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/IntelliJ_IDEA.svg)](https://jb.gg/OpenSourceSupport)

server/src/main/java/com/adobe/testing/s3mock/ObjectController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ public ResponseEntity<Void> putObjectTagging(
554554
var bucket = bucketService.verifyBucketExists(bucketName);
555555

556556
var s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.key(), versionId);
557+
objectService.verifyObjectTags(body.tagSet().tags());
557558
objectService.setObjectTags(bucketName, key.key(), versionId, body.tagSet().tags());
558559
return ResponseEntity
559560
.ok()
@@ -673,8 +674,7 @@ public ResponseEntity<Retention> getObjectRetention(
673674
@RequestParam(value = VERSION_ID, required = false) @Nullable String versionId) {
674675
var bucket = bucketService.verifyBucketExists(bucketName);
675676
bucketService.verifyBucketObjectLockEnabled(bucketName);
676-
var s3ObjectMetadata = objectService.verifyObjectLockConfiguration(bucketName, key.key(),
677-
versionId);
677+
var s3ObjectMetadata = objectService.verifyObjectLockConfiguration(bucketName, key.key(), versionId);
678678

679679
return ResponseEntity
680680
.ok()

server/src/main/java/com/adobe/testing/s3mock/S3Exception.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public class S3Exception extends RuntimeException {
4343
"The list of parts was not in ascending order. The parts list must be specified in "
4444
+ "order by part number.");
4545

46+
public static final S3Exception INVALID_TAG =
47+
new S3Exception(BAD_REQUEST.value(), "InvalidTag",
48+
"Your request contains tag input that is not valid. For example, your request might contain "
49+
+ "duplicate keys, keys or values that are too long, or system tags.");
50+
4651
public static S3Exception completeRequestMissingChecksum(String algorithm, Integer partNumber) {
4752
return new S3Exception(BAD_REQUEST.value(), BAD_REQUEST_CODE,
4853
"The upload was created using a " + algorithm + " checksum. "

server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.adobe.testing.s3mock.S3Exception.BAD_REQUEST_CONTENT;
2020
import static com.adobe.testing.s3mock.S3Exception.BAD_REQUEST_MD5;
2121
import static com.adobe.testing.s3mock.S3Exception.INVALID_REQUEST_RETAIN_DATE;
22+
import static com.adobe.testing.s3mock.S3Exception.INVALID_TAG;
2223
import static com.adobe.testing.s3mock.S3Exception.NOT_FOUND_OBJECT_LOCK;
2324
import static com.adobe.testing.s3mock.S3Exception.NOT_MODIFIED;
2425
import static com.adobe.testing.s3mock.S3Exception.NO_SUCH_KEY;
@@ -49,8 +50,10 @@
4950
import java.nio.file.Path;
5051
import java.time.Instant;
5152
import java.util.ArrayList;
53+
import java.util.HashSet;
5254
import java.util.List;
5355
import java.util.Map;
56+
import java.util.regex.Pattern;
5457
import org.jspecify.annotations.Nullable;
5558
import org.slf4j.Logger;
5659
import org.slf4j.LoggerFactory;
@@ -59,6 +62,14 @@ public class ObjectService extends ServiceBase {
5962
static final String WILDCARD_ETAG = "\"*\"";
6063
static final String WILDCARD = "*";
6164
private static final Logger LOG = LoggerFactory.getLogger(ObjectService.class);
65+
private static final Pattern TAG_ALLOWED_CHARS = Pattern.compile("[\\w+ \\-=.:/@]*");
66+
private static final int MAX_ALLOWED_TAGS = 50;
67+
private static final int MIN_ALLOWED_TAG_KEY_LENGTH = 1;
68+
private static final int MAX_ALLOWED_TAG_KEY_LENGTH = 128;
69+
private static final int MIN_ALLOWED_TAG_VALUE_LENGTH = 0;
70+
private static final int MAX_ALLOWED_TAG_VALUE_LENGTH = 256;
71+
private static final String DISALLOWED_TAG_KEY_PREFIX = "aws:";
72+
6273
private final BucketStore bucketStore;
6374
private final ObjectStore objectStore;
6475

@@ -175,6 +186,48 @@ public void setObjectTags(String bucketName, String key, @Nullable String versio
175186
objectStore.storeObjectTags(bucketMetadata, uuid, versionId, tags);
176187
}
177188

189+
public void verifyObjectTags(List<Tag> tags) {
190+
if (tags.size() > MAX_ALLOWED_TAGS) {
191+
throw INVALID_TAG;
192+
}
193+
verifyDuplicateTagKeys(tags);
194+
for (var tag : tags) {
195+
verifyTagKeyPrefix(tag.key());
196+
verifyTagLength(MIN_ALLOWED_TAG_KEY_LENGTH, MAX_ALLOWED_TAG_KEY_LENGTH, tag.key());
197+
verifyTagChars(tag.key());
198+
199+
verifyTagLength(MIN_ALLOWED_TAG_VALUE_LENGTH, MAX_ALLOWED_TAG_VALUE_LENGTH, tag.value());
200+
verifyTagChars(tag.value());
201+
}
202+
}
203+
204+
private void verifyDuplicateTagKeys(List<Tag> tags) {
205+
var tagKeys = new HashSet<String>();
206+
for (var tag : tags) {
207+
if (!tagKeys.add(tag.key())) {
208+
throw INVALID_TAG;
209+
}
210+
}
211+
}
212+
213+
private void verifyTagKeyPrefix(String tagKey) {
214+
if (tagKey.startsWith(DISALLOWED_TAG_KEY_PREFIX)) {
215+
throw INVALID_TAG;
216+
}
217+
}
218+
219+
private void verifyTagLength(int minLength, int maxLength, String tag) {
220+
if (tag.length() < minLength || tag.length() > maxLength) {
221+
throw INVALID_TAG;
222+
}
223+
}
224+
225+
private void verifyTagChars(String tag) {
226+
if (!TAG_ALLOWED_CHARS.matcher(tag).matches()) {
227+
throw INVALID_TAG;
228+
}
229+
}
230+
178231
public void setLegalHold(String bucketName, String key, @Nullable String versionId, LegalHold legalHold) {
179232
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
180233
var uuid = bucketMetadata.getID(key);

0 commit comments

Comments
 (0)