From 0b1c25638333bf84737863fb4614eb2308a3eb00 Mon Sep 17 00:00:00 2001 From: Yujie Lyu Date: Mon, 18 May 2026 14:13:30 -0700 Subject: [PATCH] Add awsGeneratedTags field to S3Bucket in s3-event-notifications Amazon S3 event notifications delivered to SNS, SQS, and Lambda now include an `awsGeneratedTags` map on the `bucket` object when system tags are enabled on the source bucket. The Java SDK model did not round-trip this field, so consumers had to fall back to raw JSON parsing to read the tags. This change adds `Map awsGeneratedTags` to `S3Bucket` and threads it through the reader and writer: * `S3Bucket`: adds `awsGeneratedTags` field with a getter, a new four-argument constructor, and updates `equals`/`hashCode`/ `toString`. The pre-existing three-argument constructor is preserved so existing callers are source- and behavior-compatible. Tags are defensively copied on construction and returned as an unmodifiable view. * `DefaultS3EventNotificationReader`: parses `awsGeneratedTags` as `Map`. Absent field yields `null`. Null value entries are preserved. Non-string values throw `IllegalArgumentException` with a helpful message including the offending key and value. * `DefaultS3EventNotificationWriter`: emits `awsGeneratedTags` only when the map is non-null and non-empty, matching S3's server-side `@JsonInclude(NON_EMPTY)` behavior so round-tripped output is indistinguishable from what S3 emits. Also bumps `eventVersion` fixtures in the module test files from `2.4` to `2.5` to reflect the schema version emitted alongside this launch. The change is fully additive: * Older SDKs remain compatible with new payloads (unknown fields are ignored during parse). * Newer SDKs remain compatible with old payloads (`getAwsGeneratedTags()` returns `null` when the field is absent). --- .../feature-S3EventNotification-6d2df07.json | 6 + .../DefaultS3EventNotificationReader.java | 25 +- .../DefaultS3EventNotificationWriter.java | 12 + .../eventnotifications/s3/model/S3Bucket.java | 38 ++- .../s3/internal/EqualsAndHashCodeTest.java | 3 +- .../s3/model/S3BucketTest.java | 82 ++++++ .../model/S3EventNotificationReaderTest.java | 271 +++++++++++++++++- .../model/S3EventNotificationWriterTest.java | 202 ++++++++++++- 8 files changed, 610 insertions(+), 29 deletions(-) create mode 100644 .changes/next-release/feature-S3EventNotification-6d2df07.json create mode 100644 services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3BucketTest.java diff --git a/.changes/next-release/feature-S3EventNotification-6d2df07.json b/.changes/next-release/feature-S3EventNotification-6d2df07.json new file mode 100644 index 000000000000..f90287809b4e --- /dev/null +++ b/.changes/next-release/feature-S3EventNotification-6d2df07.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "S3 Event Notification", + "contributor": "", + "description": "Added `awsGeneratedTags` field to `S3Bucket` in the S3 Event Notifications module. Amazon S3 emits AWS-generated system tags on the `bucket` portion of event notifications when system tags are enabled on the source bucket. SDK consumers on the SNS/SQS/Lambda delivery paths can now access these tags via `S3Bucket#getAwsGeneratedTags()`." +} diff --git a/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationReader.java b/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationReader.java index 5675d4e73a92..e6a7a3bab04b 100644 --- a/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationReader.java +++ b/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationReader.java @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -242,7 +243,29 @@ private S3Bucket readBucket(JsonNode jsonNode) { String name = expectStringOrNull(bucketNode, "name"); UserIdentity ownerIdentity = readOwnerIdentity(bucketNode.get("ownerIdentity")); String arn = expectStringOrNull(bucketNode, "arn"); - return new S3Bucket(name, ownerIdentity, arn); + Map awsGeneratedTags = readStringMapOrNull(bucketNode.get("awsGeneratedTags"), "awsGeneratedTags"); + return new S3Bucket(name, ownerIdentity, arn, awsGeneratedTags); + } + + private Map readStringMapOrNull(JsonNode jsonNode, String name) { + Map mapNode = expectObjectOrNull(jsonNode, name); + if (mapNode == null) { + return null; + } + Map result = new LinkedHashMap<>(); + mapNode.forEach((tagKey, tagValueNode) -> + result.put(tagKey, readTagStringValueOrNull(name, tagKey, tagValueNode))); + return result; + } + + private String readTagStringValueOrNull(String mapName, String tagKey, JsonNode tagValue) { + if (isNull(tagValue)) { + return null; + } + Validate.isTrue(tagValue.isString(), + "The value of tag '%s' in '%s' was not a string, but was: %s", + tagKey, mapName, tagValue); + return tagValue.asString(); } private UserIdentity readOwnerIdentity(JsonNode jsonNode) { diff --git a/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationWriter.java b/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationWriter.java index 9e55f4d4c197..67559d2ee4f5 100644 --- a/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationWriter.java +++ b/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationWriter.java @@ -19,6 +19,7 @@ import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Map; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.eventnotifications.s3.model.GlacierEventData; import software.amazon.awssdk.eventnotifications.s3.model.IntelligentTieringEventData; @@ -227,6 +228,17 @@ private void writeS3Bucket(JsonWriter writer, S3Bucket bucket) { writer.writeEndObject(); } writeStringField(writer, "arn", bucket.getArn()); + Map awsGeneratedTags = bucket.getAwsGeneratedTags(); + if (awsGeneratedTags != null && !awsGeneratedTags.isEmpty()) { + writeStringMap(writer, "awsGeneratedTags", awsGeneratedTags); + } + writer.writeEndObject(); + } + + private void writeStringMap(JsonWriter writer, String field, Map map) { + writer.writeFieldName(field); + writer.writeStartObject(); + map.forEach((k, v) -> writeStringField(writer, k, v)); writer.writeEndObject(); } diff --git a/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/model/S3Bucket.java b/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/model/S3Bucket.java index 3cfe0f695d73..c0e71a8831d3 100644 --- a/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/model/S3Bucket.java +++ b/services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/model/S3Bucket.java @@ -16,6 +16,9 @@ package software.amazon.awssdk.eventnotifications.s3.model; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Objects; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.utils.ToString; @@ -29,11 +32,19 @@ public class S3Bucket { private final String name; private final UserIdentity ownerIdentity; private final String arn; + private final Map awsGeneratedTags; public S3Bucket(String name, UserIdentity ownerIdentity, String arn) { + this(name, ownerIdentity, arn, null); + } + + public S3Bucket(String name, UserIdentity ownerIdentity, String arn, Map awsGeneratedTags) { this.name = name; this.ownerIdentity = ownerIdentity; this.arn = arn; + this.awsGeneratedTags = awsGeneratedTags == null + ? null + : Collections.unmodifiableMap(new LinkedHashMap<>(awsGeneratedTags)); } /** @@ -57,6 +68,14 @@ public String getArn() { return arn; } + /** + * @return The AWS-generated system tags for the bucket, or {@code null} if not present in the event. The returned map + * is unmodifiable. + */ + public Map getAwsGeneratedTags() { + return awsGeneratedTags; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -74,7 +93,10 @@ public boolean equals(Object o) { if (!Objects.equals(ownerIdentity, s3Bucket.ownerIdentity)) { return false; } - return Objects.equals(arn, s3Bucket.arn); + if (!Objects.equals(arn, s3Bucket.arn)) { + return false; + } + return Objects.equals(awsGeneratedTags, s3Bucket.awsGeneratedTags); } @Override @@ -82,15 +104,19 @@ public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (ownerIdentity != null ? ownerIdentity.hashCode() : 0); result = 31 * result + (arn != null ? arn.hashCode() : 0); + result = 31 * result + (awsGeneratedTags != null ? awsGeneratedTags.hashCode() : 0); return result; } @Override public String toString() { - return ToString.builder("S3Bucket") - .add("name", name) - .add("ownerIdentity", ownerIdentity) - .add("arn", arn) - .build(); + ToString builder = ToString.builder("S3Bucket") + .add("name", name) + .add("ownerIdentity", ownerIdentity) + .add("arn", arn); + if (awsGeneratedTags != null) { + builder.add("awsGeneratedTags", awsGeneratedTags); + } + return builder.build(); } } diff --git a/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/internal/EqualsAndHashCodeTest.java b/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/internal/EqualsAndHashCodeTest.java index 118f2ff38050..b7b7c65bb1bf 100644 --- a/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/internal/EqualsAndHashCodeTest.java +++ b/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/internal/EqualsAndHashCodeTest.java @@ -15,9 +15,8 @@ package software.amazon.awssdk.eventnotifications.s3.internal; -import nl.jqno.equalsverifier.Warning; - import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; import org.junit.jupiter.api.Test; class EqualsAndHashCodeTest { diff --git a/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3BucketTest.java b/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3BucketTest.java new file mode 100644 index 000000000000..8e77bae6757a --- /dev/null +++ b/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3BucketTest.java @@ -0,0 +1,82 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.eventnotifications.s3.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class S3BucketTest { + + @Test + void awsGeneratedTags_nullInput_returnsNull() { + S3Bucket bucket = new S3Bucket("name", new UserIdentity("p"), "arn", null); + assertThat(bucket.getAwsGeneratedTags()).isNull(); + } + + @Test + void threeArgConstructor_leavesAwsGeneratedTagsNull() { + S3Bucket bucket = new S3Bucket("name", new UserIdentity("p"), "arn"); + assertThat(bucket.getAwsGeneratedTags()).isNull(); + } + + @Test + void toString_omitsAwsGeneratedTags_whenNull() { + S3Bucket bucket = new S3Bucket("mybucket", new UserIdentity("p"), "arn:aws:s3:::mybucket"); + assertThat(bucket.toString()).doesNotContain("awsGeneratedTags"); + } + + @Test + void toString_includesAwsGeneratedTags_whenPresent() { + Map tags = new LinkedHashMap<>(); + tags.put("aws:resource:owner", "123456789012"); + tags.put("aws:resource:region", "us-west-2"); + + S3Bucket bucket = new S3Bucket("mybucket", new UserIdentity("p"), "arn:aws:s3:::mybucket", tags); + assertThat(bucket.toString()) + .contains("awsGeneratedTags={aws:resource:owner=123456789012, aws:resource:region=us-west-2}"); + } + + @Test + void awsGeneratedTags_isDefensivelyCopied() { + Map tags = new LinkedHashMap<>(); + tags.put("aws:resource:owner", "123456789012"); + + S3Bucket bucket = new S3Bucket("mybucket", new UserIdentity("p"), "arn:aws:s3:::mybucket", tags); + + // Mutating the caller's map after construction must not affect the bucket. + tags.put("aws:resource:region", "us-west-2"); + tags.remove("aws:resource:owner"); + + assertThat(bucket.getAwsGeneratedTags()) + .containsExactly(new java.util.AbstractMap.SimpleEntry<>("aws:resource:owner", "123456789012")); + } + + @Test + void getAwsGeneratedTags_isUnmodifiable() { + Map tags = new LinkedHashMap<>(); + tags.put("aws:resource:owner", "123456789012"); + + S3Bucket bucket = new S3Bucket("mybucket", new UserIdentity("p"), "arn:aws:s3:::mybucket", tags); + + Map returned = bucket.getAwsGeneratedTags(); + assertThatThrownBy(() -> returned.put("aws:resource:region", "us-west-2")) + .isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotificationReaderTest.java b/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotificationReaderTest.java index fe1c199e07a8..1a0f30efd8ee 100644 --- a/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotificationReaderTest.java +++ b/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotificationReaderTest.java @@ -22,6 +22,8 @@ import java.time.Instant; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import org.junit.jupiter.api.Test; import software.amazon.awssdk.eventnotifications.s3.model.GlacierEventData; import software.amazon.awssdk.eventnotifications.s3.model.IntelligentTieringEventData; @@ -49,7 +51,7 @@ void fromJson_containsNullValues_shouldSucceed() { "ObjectCreated:Put", "aws:s3", "1970-01-01T00:00:00.000Z", - "2.4", + "2.5", null, new ResponseElements( null, null), @@ -73,7 +75,7 @@ void fromJson_containsNullValues_shouldSucceed() { String json = eventNotification.toJsonPretty(); assertThat(json).isEqualTo("{\n" + " \"Records\" : [ {\n" - + " \"eventVersion\" : \"2.4\",\n" + + " \"eventVersion\" : \"2.5\",\n" + " \"eventSource\" : \"aws:s3\",\n" + " \"awsRegion\" : \"us-west-2\",\n" + " \"eventTime\" : \"1970-01-01T00:00:00Z\",\n" @@ -115,7 +117,7 @@ void givenEventWithoutOptionalFields_whenReadingJson_expectOnlyRequiredFields() String eventJson = "{ " + " \"Records\":[ " + " { " - + " \"eventVersion\":\"2.4\"," + + " \"eventVersion\":\"2.5\"," + " \"eventSource\":\"aws:s3\"," + " \"awsRegion\":\"us-west-2\"," + " \"eventTime\":\"1970-01-01T00:00:00.000Z\"," @@ -163,7 +165,7 @@ void givenEventWithoutOptionalFields_whenReadingJson_expectOnlyRequiredFields() "ObjectCreated:Put", "aws:s3", "1970-01-01T00:00:00.000Z", - "2.4", + "2.5", new RequestParameters("127.0.0.1"), new ResponseElements( "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD", "C3D13FE58DE4C810"), @@ -192,7 +194,7 @@ void givenEventWithoutOptionalFields_whenReadingJson_expectOnlyRequiredFields() assertThat(rec.getAwsRegion()).isEqualTo("us-west-2"); assertThat(rec.getEventName()).isEqualTo("ObjectCreated:Put"); assertThat(rec.getEventTime()).isEqualTo(Instant.parse("1970-01-01T00:00:00.000Z")); - assertThat(rec.getEventVersion()).isEqualTo("2.4"); + assertThat(rec.getEventVersion()).isEqualTo("2.5"); UserIdentity userIdentity = rec.getUserIdentity(); assertThat(userIdentity).isNotNull(); @@ -238,7 +240,7 @@ void givenEventContainingOptionalFields_whenReadingJson_expectAllFields() { String eventJson = "{\n" + " \"Records\":[\n" + " {\n" - + " \"eventVersion\":\"2.4\",\n" + + " \"eventVersion\":\"2.5\",\n" + " \"eventSource\":\"aws:s3\",\n" + " \"awsRegion\":\"us-west-2\",\n" + " \"eventTime\":\"1970-01-01T00:00:00.000Z\",\n" @@ -313,7 +315,7 @@ void givenEventContainingOptionalFields_whenReadingJson_expectAllFields() { "ObjectCreated:Put", "aws:s3", "1970-01-01T00:00:00.000Z", - "2.4", + "2.5", new RequestParameters("127.0.0.1"), new ResponseElements( "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD", "C3D13FE58DE4C810"), @@ -419,7 +421,7 @@ void emptyRecordList_shouldContainEmptyRecordList() { void missingField_shouldBeNull() { String json = "{\n" + " \"Records\" : [ {\n" - + " \"eventVersion\" : \"2.4\",\n" + + " \"eventVersion\" : \"2.5\",\n" + " \"eventSource\" : \"aws:s3\",\n" + " \"awsRegion\" : \"us-west-2\",\n" + " \"eventTime\" : \"1970-01-01T01:01:01.001Z\",\n" @@ -463,7 +465,7 @@ void missingField_shouldBeNull() { void eventTimeIsNullWhenEventNamePresent_shouldSucceed() { String json = "{\n" + " \"Records\" : [ {\n" - + " \"eventVersion\" : \"2.4\",\n" + + " \"eventVersion\" : \"2.5\",\n" + " \"eventSource\" : \"aws:s3\",\n" + " \"awsRegion\" : \"us-west-2\",\n" // missing eventTime @@ -525,7 +527,7 @@ void fromJsonInputStream_handlesCorrectly() { String eventJson = "{ " + " \"Records\":[ " + " { " - + " \"eventVersion\":\"2.4\"," + + " \"eventVersion\":\"2.5\"," + " \"eventSource\":\"aws:s3\"," + " \"awsRegion\":\"us-west-2\"," + " \"eventTime\":\"1970-01-01T00:00:00.000Z\"," @@ -575,7 +577,7 @@ void fromJsonInputStream_handlesCorrectly() { "ObjectCreated:Put", "aws:s3", "1970-01-01T00:00:00.000Z", - "2.4", + "2.5", new RequestParameters("127.0.0.1"), new ResponseElements( "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD", "C3D13FE58DE4C810"), @@ -608,7 +610,7 @@ void fromJsonInputStream_handlesCorrectly() { assertThat(rec.getAwsRegion()).isEqualTo("us-west-2"); assertThat(rec.getEventName()).isEqualTo("ObjectCreated:Put"); assertThat(rec.getEventTime()).isEqualTo(Instant.parse("1970-01-01T00:00:00.000Z")); - assertThat(rec.getEventVersion()).isEqualTo("2.4"); + assertThat(rec.getEventVersion()).isEqualTo("2.5"); UserIdentity userIdentity = rec.getUserIdentity(); assertThat(userIdentity).isNotNull(); @@ -655,4 +657,249 @@ void fromJsonInputStream_handlesCorrectly() { assertThat(rec.getReplicationEventData()).isNull(); } + + @Test + void awsGeneratedTags_whenPresent_isParsed() { + String json = "{\n" + + " \"Records\" : [ {\n" + + " \"eventVersion\" : \"2.5\",\n" + + " \"eventSource\" : \"aws:s3\",\n" + + " \"awsRegion\" : \"us-west-2\",\n" + + " \"eventTime\" : \"1970-01-01T00:00:00.000Z\",\n" + + " \"eventName\" : \"ObjectCreated:Put\",\n" + + " \"s3\" : {\n" + + " \"s3SchemaVersion\" : \"1.0\",\n" + + " \"configurationId\" : \"testConfigRule\",\n" + + " \"bucket\" : {\n" + + " \"name\" : \"mybucket\",\n" + + " \"ownerIdentity\" : {\n" + + " \"principalId\" : \"A3NL1KOZZKExample\"\n" + + " },\n" + + " \"arn\" : \"arn:aws:s3:::mybucket\",\n" + + " \"awsGeneratedTags\" : {\n" + + " \"aws:resource:owner\" : \"123456789012\",\n" + + " \"aws:resource:region\" : \"us-west-2\"\n" + + " }\n" + + " },\n" + + " \"object\" : {\n" + + " \"key\" : \"HappyFace.jpg\",\n" + + " \"size\" : 1024,\n" + + " \"eTag\" : \"d41d8cd98f00b204e9800998ecf8427e\",\n" + + " \"versionId\" : \"096fKKXTRTtl3on89fVO.nfljtsv6qko\",\n" + + " \"sequencer\" : \"0055AED6DCD90281E5\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + "}"; + + S3EventNotification event = S3EventNotification.fromJson(json); + S3Bucket bucket = event.getRecords().get(0).getS3().getBucket(); + + Map expectedTags = new LinkedHashMap<>(); + expectedTags.put("aws:resource:owner", "123456789012"); + expectedTags.put("aws:resource:region", "us-west-2"); + assertThat(bucket.getAwsGeneratedTags()).containsExactlyEntriesOf(expectedTags); + } + + @Test + void awsGeneratedTags_whenAbsent_isNull() { + String json = "{\n" + + " \"Records\" : [ {\n" + + " \"eventVersion\" : \"2.5\",\n" + + " \"eventSource\" : \"aws:s3\",\n" + + " \"awsRegion\" : \"us-west-2\",\n" + + " \"eventTime\" : \"1970-01-01T00:00:00.000Z\",\n" + + " \"eventName\" : \"ObjectCreated:Put\",\n" + + " \"s3\" : {\n" + + " \"s3SchemaVersion\" : \"1.0\",\n" + + " \"configurationId\" : \"testConfigRule\",\n" + + " \"bucket\" : {\n" + + " \"name\" : \"mybucket\",\n" + + " \"ownerIdentity\" : {\n" + + " \"principalId\" : \"A3NL1KOZZKExample\"\n" + + " },\n" + + " \"arn\" : \"arn:aws:s3:::mybucket\"\n" + + " },\n" + + " \"object\" : {\n" + + " \"key\" : \"HappyFace.jpg\",\n" + + " \"size\" : 1024,\n" + + " \"eTag\" : \"d41d8cd98f00b204e9800998ecf8427e\",\n" + + " \"versionId\" : \"096fKKXTRTtl3on89fVO.nfljtsv6qko\",\n" + + " \"sequencer\" : \"0055AED6DCD90281E5\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + "}"; + + S3EventNotification event = S3EventNotification.fromJson(json); + assertThat(event.getRecords().get(0).getS3().getBucket().getAwsGeneratedTags()).isNull(); + } + + @Test + void awsGeneratedTags_whenEmpty_isEmptyMap() { + String json = "{\n" + + " \"Records\" : [ {\n" + + " \"eventVersion\" : \"2.5\",\n" + + " \"eventSource\" : \"aws:s3\",\n" + + " \"awsRegion\" : \"us-west-2\",\n" + + " \"eventTime\" : \"1970-01-01T00:00:00.000Z\",\n" + + " \"eventName\" : \"ObjectCreated:Put\",\n" + + " \"s3\" : {\n" + + " \"s3SchemaVersion\" : \"1.0\",\n" + + " \"configurationId\" : \"testConfigRule\",\n" + + " \"bucket\" : {\n" + + " \"name\" : \"mybucket\",\n" + + " \"ownerIdentity\" : {\n" + + " \"principalId\" : \"A3NL1KOZZKExample\"\n" + + " },\n" + + " \"arn\" : \"arn:aws:s3:::mybucket\",\n" + + " \"awsGeneratedTags\" : {}\n" + + " },\n" + + " \"object\" : {\n" + + " \"key\" : \"HappyFace.jpg\",\n" + + " \"size\" : 1024,\n" + + " \"eTag\" : \"d41d8cd98f00b204e9800998ecf8427e\",\n" + + " \"versionId\" : \"096fKKXTRTtl3on89fVO.nfljtsv6qko\",\n" + + " \"sequencer\" : \"0055AED6DCD90281E5\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + "}"; + + S3EventNotification event = S3EventNotification.fromJson(json); + assertThat(event.getRecords().get(0).getS3().getBucket().getAwsGeneratedTags()).isEmpty(); + } + + @Test + void awsGeneratedTags_withEmptyStringValue_isPreservedAsEmptyString() { + String json = "{\n" + + " \"Records\" : [ {\n" + + " \"eventVersion\" : \"2.5\",\n" + + " \"eventSource\" : \"aws:s3\",\n" + + " \"awsRegion\" : \"us-west-2\",\n" + + " \"eventTime\" : \"1970-01-01T00:00:00.000Z\",\n" + + " \"eventName\" : \"ObjectCreated:Put\",\n" + + " \"s3\" : {\n" + + " \"s3SchemaVersion\" : \"1.0\",\n" + + " \"configurationId\" : \"testConfigRule\",\n" + + " \"bucket\" : {\n" + + " \"name\" : \"mybucket\",\n" + + " \"ownerIdentity\" : { \"principalId\" : \"A3NL1KOZZKExample\" },\n" + + " \"arn\" : \"arn:aws:s3:::mybucket\",\n" + + " \"awsGeneratedTags\" : {\n" + + " \"aws:resource:owner\" : \"\"\n" + + " }\n" + + " },\n" + + " \"object\" : {\n" + + " \"key\" : \"HappyFace.jpg\",\n" + + " \"size\" : 1024,\n" + + " \"eTag\" : \"d41d8cd98f00b204e9800998ecf8427e\",\n" + + " \"versionId\" : \"096fKKXTRTtl3on89fVO.nfljtsv6qko\",\n" + + " \"sequencer\" : \"0055AED6DCD90281E5\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + "}"; + + S3EventNotification event = S3EventNotification.fromJson(json); + Map tags = event.getRecords().get(0).getS3().getBucket().getAwsGeneratedTags(); + assertThat(tags).containsEntry("aws:resource:owner", ""); + } + + @Test + void awsGeneratedTags_withNullValue_isPreservedAsNull() { + String json = "{\n" + + " \"Records\" : [ {\n" + + " \"eventVersion\" : \"2.5\",\n" + + " \"eventSource\" : \"aws:s3\",\n" + + " \"awsRegion\" : \"us-west-2\",\n" + + " \"eventTime\" : \"1970-01-01T00:00:00.000Z\",\n" + + " \"eventName\" : \"ObjectCreated:Put\",\n" + + " \"s3\" : {\n" + + " \"s3SchemaVersion\" : \"1.0\",\n" + + " \"configurationId\" : \"testConfigRule\",\n" + + " \"bucket\" : {\n" + + " \"name\" : \"mybucket\",\n" + + " \"ownerIdentity\" : { \"principalId\" : \"A3NL1KOZZKExample\" },\n" + + " \"arn\" : \"arn:aws:s3:::mybucket\",\n" + + " \"awsGeneratedTags\" : {\n" + + " \"aws:resource:owner\" : null,\n" + + " \"aws:resource:region\" : \"us-west-2\"\n" + + " }\n" + + " },\n" + + " \"object\" : {\n" + + " \"key\" : \"HappyFace.jpg\",\n" + + " \"size\" : 1024,\n" + + " \"eTag\" : \"d41d8cd98f00b204e9800998ecf8427e\",\n" + + " \"versionId\" : \"096fKKXTRTtl3on89fVO.nfljtsv6qko\",\n" + + " \"sequencer\" : \"0055AED6DCD90281E5\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + "}"; + + S3EventNotification event = S3EventNotification.fromJson(json); + Map tags = event.getRecords().get(0).getS3().getBucket().getAwsGeneratedTags(); + assertThat(tags).containsEntry("aws:resource:owner", null); + assertThat(tags).containsEntry("aws:resource:region", "us-west-2"); + } + + @Test + void awsGeneratedTags_withNonStringValue_throws() { + String json = "{\n" + + " \"Records\" : [ {\n" + + " \"s3\" : {\n" + + " \"bucket\" : {\n" + + " \"name\" : \"mybucket\",\n" + + " \"awsGeneratedTags\" : {\n" + + " \"aws:resource:owner\" : 123\n" + + " }\n" + + " }\n" + + " }\n" + + " } ]\n" + + "}"; + + assertThatThrownBy(() -> S3EventNotification.fromJson(json)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The value of tag 'aws:resource:owner' in 'awsGeneratedTags' was not a string, but was: 123"); + } + + @Test + void awsGeneratedTags_withBooleanValue_throws() { + String json = "{\n" + + " \"Records\" : [ {\n" + + " \"s3\" : {\n" + + " \"bucket\" : {\n" + + " \"name\" : \"mybucket\",\n" + + " \"awsGeneratedTags\" : {\n" + + " \"aws:resource:owner\" : true\n" + + " }\n" + + " }\n" + + " }\n" + + " } ]\n" + + "}"; + + assertThatThrownBy(() -> S3EventNotification.fromJson(json)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The value of tag 'aws:resource:owner' in 'awsGeneratedTags' was not a string, but was: true"); + } + + @Test + void awsGeneratedTags_notAnObject_throws() { + String json = "{\n" + + " \"Records\" : [ {\n" + + " \"s3\" : {\n" + + " \"bucket\" : {\n" + + " \"name\" : \"mybucket\",\n" + + " \"awsGeneratedTags\" : \"not-an-object\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + "}"; + + assertThatThrownBy(() -> S3EventNotification.fromJson(json)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("expected 'awsGeneratedTags' to be an object, but was not."); + } + } diff --git a/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotificationWriterTest.java b/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotificationWriterTest.java index a9b9beca7734..248536bdc1f8 100644 --- a/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotificationWriterTest.java +++ b/services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotificationWriterTest.java @@ -19,6 +19,8 @@ import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import org.junit.jupiter.api.Test; import software.amazon.awssdk.eventnotifications.s3.model.GlacierEventData; import software.amazon.awssdk.eventnotifications.s3.model.IntelligentTieringEventData; @@ -46,7 +48,7 @@ void testPrettyPrint_requiredFieldOnly() { "ObjectCreated:Get", "aws:s3", "1970-01-01T01:01:01.001Z", - "2.4", + "2.5", new RequestParameters("127.1.2.3"), new ResponseElements( "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANxid2", "C3D13FE58DE4CRID"), @@ -70,7 +72,7 @@ void testPrettyPrint_requiredFieldOnly() { String expected = "{\n" + " \"Records\" : [ {\n" - + " \"eventVersion\" : \"2.4\",\n" + + " \"eventVersion\" : \"2.5\",\n" + " \"eventSource\" : \"aws:s3\",\n" + " \"awsRegion\" : \"us-west-2\",\n" + " \"eventTime\" : \"1970-01-01T01:01:01.001Z\",\n" @@ -110,7 +112,7 @@ void testPrettyPrint_requiredFieldOnly() { @Test void testToJson_requiredFielsdOnly() { - String expected = "{\"Records\":[{\"eventVersion\":\"2.4\",\"eventSource\":\"aws:s3\",\"awsRegion\":\"us-west-2\"," + String expected = "{\"Records\":[{\"eventVersion\":\"2.5\",\"eventSource\":\"aws:s3\",\"awsRegion\":\"us-west-2\"," + "\"eventTime\":\"1970-01-01T01:01:01.001Z\",\"eventName\":\"ObjectCreated:Get\"," + "\"userIdentity\":{\"principalId\"" + ":\"AIDAJDPLRKLG7UEXAMUID\"},\"requestParameters\":{\"sourceIPAddress\":\"127.1.2.3\"}," @@ -128,7 +130,7 @@ void testToJson_requiredFielsdOnly() { "ObjectCreated:Get", "aws:s3", "1970-01-01T01:01:01.001Z", - "2.4", + "2.5", new RequestParameters("127.1.2.3"), new ResponseElements( "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANxid2", "C3D13FE58DE4CRID"), @@ -156,7 +158,7 @@ void testToJson_requiredFielsdOnly() { void testPrettyPrint_allFields() { String expected = "{\n" + " \"Records\" : [ {\n" - + " \"eventVersion\" : \"2.4\",\n" + + " \"eventVersion\" : \"2.5\",\n" + " \"eventSource\" : \"aws:s3\",\n" + " \"awsRegion\" : \"us-west-2\",\n" + " \"eventTime\" : \"1970-01-01T01:01:01.001Z\",\n" @@ -226,7 +228,7 @@ void testPrettyPrint_allFields() { "ObjectCreated:Put", "aws:s3", "1970-01-01T01:01:01.001Z", - "2.4", + "2.5", new RequestParameters("127.0.0.1"), new ResponseElements( "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD", "C3D13FE58DE4C810"), @@ -267,7 +269,7 @@ void testPrettyPrint_allFields() { @Test void testToJson_allFields() { - String expected = "{\"Records\":[{\"eventVersion\":\"2.4\",\"eventSource\":\"aws:s3\",\"awsRegion\":" + String expected = "{\"Records\":[{\"eventVersion\":\"2.5\",\"eventSource\":\"aws:s3\",\"awsRegion\":" + "\"us-west-2\",\"eventTime\":\"1970-01-01T01:01:01.001Z\",\"eventName\":\"ObjectCreated:Get\"," + "\"userIdentity\":{\"principalId\":\"PRINCIPALIDTEST\"},\"requestParameters\":{\"sourceIPAddress\":" + "\"127.1.2.3\"},\"responseElements\":{\"x-amz-request-id\":\"C3D13FE58DE4CRID\",\"x-amz-id-2\":" @@ -292,7 +294,7 @@ void testToJson_allFields() { "ObjectCreated:Get", "aws:s3", "1970-01-01T01:01:01.001Z", - "2.4", + "2.5", new RequestParameters("127.1.2.3"), new ResponseElements( "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANxid2", "C3D13FE58DE4CRID"), @@ -388,4 +390,188 @@ void nullableNumberFieldsHandledCorrectly() { assertThat(event.toJson()).isEqualTo(expected); } + + @Test + void awsGeneratedTags_whenSet_isWritten() { + Map tags = new LinkedHashMap<>(); + tags.put("aws:resource:owner", "123456789012"); + tags.put("aws:resource:region", "us-west-2"); + + S3EventNotification event = new S3EventNotification(Collections.singletonList( + new S3EventNotificationRecord( + "us-west-2", + "ObjectCreated:Put", + "aws:s3", + "1970-01-01T00:00:00.000Z", + "2.5", + null, + null, + new S3( + "testConfigRule", + new S3Bucket( + "mybucket", + new UserIdentity("A3NL1KOZZKExample"), + "arn:aws:s3:::mybucket", + tags), + new S3Object( + "HappyFace.jpg", + 1024L, + "d41d8cd98f00b204e9800998ecf8427e", + "096fKKXTRTtl3on89fVO.nfljtsv6qko", + "0055AED6DCD90281E5"), + "1.0"), + new UserIdentity("AIDAJDPLRKLG7UEXAMPLE")) + )); + + String json = event.toJson(); + assertThat(json).contains("\"arn\":\"arn:aws:s3:::mybucket\"," + + "\"awsGeneratedTags\":{" + + "\"aws:resource:owner\":\"123456789012\"," + + "\"aws:resource:region\":\"us-west-2\"" + + "}"); + + // Round trip + assertThat(S3EventNotification.fromJson(json)).isEqualTo(event); + } + + @Test + void awsGeneratedTags_whenNull_isOmitted() { + S3EventNotification event = new S3EventNotification(Collections.singletonList( + new S3EventNotificationRecord( + "us-west-2", + "ObjectCreated:Put", + "aws:s3", + "1970-01-01T00:00:00.000Z", + "2.5", + null, + null, + new S3( + "testConfigRule", + new S3Bucket( + "mybucket", + new UserIdentity("A3NL1KOZZKExample"), + "arn:aws:s3:::mybucket"), + new S3Object( + "HappyFace.jpg", + 1024L, + "d41d8cd98f00b204e9800998ecf8427e", + "096fKKXTRTtl3on89fVO.nfljtsv6qko", + "0055AED6DCD90281E5"), + "1.0"), + new UserIdentity("AIDAJDPLRKLG7UEXAMPLE")) + )); + + assertThat(event.toJson()).doesNotContain("awsGeneratedTags"); + } + + @Test + void awsGeneratedTags_withNullValue_isWritten() { + Map tags = new LinkedHashMap<>(); + tags.put("aws:resource:owner", null); + tags.put("aws:resource:region", "us-west-2"); + + S3EventNotification event = new S3EventNotification(Collections.singletonList( + new S3EventNotificationRecord( + "us-west-2", + "ObjectCreated:Put", + "aws:s3", + "1970-01-01T00:00:00.000Z", + "2.5", + null, + null, + new S3( + "testConfigRule", + new S3Bucket( + "mybucket", + new UserIdentity("A3NL1KOZZKExample"), + "arn:aws:s3:::mybucket", + tags), + new S3Object( + "HappyFace.jpg", + 1024L, + "d41d8cd98f00b204e9800998ecf8427e", + "096fKKXTRTtl3on89fVO.nfljtsv6qko", + "0055AED6DCD90281E5"), + "1.0"), + new UserIdentity("AIDAJDPLRKLG7UEXAMPLE")) + )); + + String json = event.toJson(); + assertThat(json).contains("\"awsGeneratedTags\":{" + + "\"aws:resource:owner\":null," + + "\"aws:resource:region\":\"us-west-2\"" + + "}"); + + // Round trip + assertThat(S3EventNotification.fromJson(json)).isEqualTo(event); + } + + @Test + void awsGeneratedTags_withEmptyStringValue_isWritten() { + Map tags = new LinkedHashMap<>(); + tags.put("aws:resource:owner", ""); + + S3EventNotification event = new S3EventNotification(Collections.singletonList( + new S3EventNotificationRecord( + "us-west-2", + "ObjectCreated:Put", + "aws:s3", + "1970-01-01T00:00:00.000Z", + "2.5", + null, + null, + new S3( + "testConfigRule", + new S3Bucket( + "mybucket", + new UserIdentity("A3NL1KOZZKExample"), + "arn:aws:s3:::mybucket", + tags), + new S3Object( + "HappyFace.jpg", + 1024L, + "d41d8cd98f00b204e9800998ecf8427e", + "096fKKXTRTtl3on89fVO.nfljtsv6qko", + "0055AED6DCD90281E5"), + "1.0"), + new UserIdentity("AIDAJDPLRKLG7UEXAMPLE")) + )); + + String json = event.toJson(); + assertThat(json).contains("\"awsGeneratedTags\":{\"aws:resource:owner\":\"\"}"); + + // Round trip + assertThat(S3EventNotification.fromJson(json)).isEqualTo(event); + } + + @Test + void awsGeneratedTags_whenEmpty_isOmitted() { + S3EventNotification event = new S3EventNotification(Collections.singletonList( + new S3EventNotificationRecord( + "us-west-2", + "ObjectCreated:Put", + "aws:s3", + "1970-01-01T00:00:00.000Z", + "2.5", + null, + null, + new S3( + "testConfigRule", + new S3Bucket( + "mybucket", + new UserIdentity("A3NL1KOZZKExample"), + "arn:aws:s3:::mybucket", + Collections.emptyMap()), + new S3Object( + "HappyFace.jpg", + 1024L, + "d41d8cd98f00b204e9800998ecf8427e", + "096fKKXTRTtl3on89fVO.nfljtsv6qko", + "0055AED6DCD90281E5"), + "1.0"), + new UserIdentity("AIDAJDPLRKLG7UEXAMPLE")) + )); + + assertThat(event.toJson()).doesNotContain("awsGeneratedTags"); + } }