Skip to content

Commit bfd16ae

Browse files
committed
test(enhancedClient): Verify EC's null behavior does not interfere
1 parent 186d507 commit bfd16ae

File tree

3 files changed

+204
-0
lines changed

3 files changed

+204
-0
lines changed

DynamoDbEncryption/runtimes/java/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ dependencies {
105105
testImplementation("junit:junit:4.13.2")
106106
// https://mvnrepository.com/artifact/edu.umd.cs.mtc/multithreadedtc
107107
testImplementation("edu.umd.cs.mtc:multithreadedtc:1.01")
108+
// https://mvnrepository.com/artifact/org.projectlombok/lombok
109+
testImplementation("org.projectlombok:lombok:1.18.28")
110+
testAnnotationProcessor("org.projectlombok:lombok:1.18.28")
108111
}
109112

110113
publishing {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient;
2+
3+
import org.testng.annotations.Test;
4+
5+
import java.time.Clock;
6+
import java.time.Instant;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
import java.util.Objects;
10+
11+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
12+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
13+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
14+
import software.amazon.awssdk.enhanced.dynamodb.Expression;
15+
import software.amazon.awssdk.enhanced.dynamodb.Key;
16+
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
17+
import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension;
18+
import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.InstantAsStringAttributeConverter;
19+
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest;
20+
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
21+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
22+
23+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
24+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.datamodels.SimpleViaLombok;
25+
import software.amazon.cryptography.materialproviders.IKeyring;
26+
27+
import static java.lang.Math.abs;
28+
import static software.amazon.cryptography.dbencryptionsdk.dynamodb.TestUtils.TEST_TABLE_NAME;
29+
import static software.amazon.cryptography.dbencryptionsdk.dynamodb.TestUtils.createKmsKeyring;
30+
31+
/**
32+
* From the DynamoDB Enhanced Client's Developer Guide:
33+
* > When you use the putItem API,
34+
* > the enhanced client does not include null-valued attributes of
35+
* > a mapped data object in the request to DynamoDB.
36+
* This test asserts the Enhanced Client's Null behavior does not cause an issue.
37+
* It SHOULD NOT.
38+
* The DB-ESDK for DynamoDB forces CLOBBER or PUT behavior.
39+
* Even though the Enhanced Client does not include the null values
40+
* in the request to DynamoDB,
41+
* since the request is made with a full WRITE,
42+
* the pre-existing non-null values will be dropped.
43+
*/
44+
public class DynamoDbEncryptionEnhancedClientNullIntegrationTest {
45+
final static String PARTITION = "TestEnhancedClientPutNull";
46+
final static int SORT = 20230716;
47+
final static String COULD_NOT_READ_MSG = String.format(
48+
"Testing Null Write with Enhanced Client Required a particular Read that could not complete. " +
49+
"Recommend Deleting the record via the console and retrying. " +
50+
"Record's Key is Partition: %s, Sort: %s", PARTITION, SORT);
51+
public static final int TOLERANCE = 1000;
52+
53+
@Test
54+
public void TestEnhancedClientPutNull() {
55+
final String UNSIGNED_PREFIX = ":";
56+
final IKeyring kmsKeyring = createKmsKeyring();
57+
final TableSchema<SimpleViaLombok> schemaOnEncrypt =
58+
TableSchema.fromImmutableClass(SimpleViaLombok.class);
59+
final InstantAsStringAttributeConverter instantConverter =
60+
InstantAsStringAttributeConverter.create();
61+
final Key key = Key.builder()
62+
.partitionValue(PARTITION).sortValue(SORT)
63+
.build();
64+
final Clock clock = Clock.systemUTC();
65+
66+
DynamoDbClient ddb = initDDBWithEncryption(UNSIGNED_PREFIX, kmsKeyring, schemaOnEncrypt);
67+
DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
68+
.extensions(AutoGeneratedTimestampRecordExtension.create())
69+
.dynamoDbClient(ddb)
70+
.build();
71+
DynamoDbTable<SimpleViaLombok> table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt);
72+
73+
final SimpleViaLombok.SimpleViaLombokBuilder itemBuilder = SimpleViaLombok.builder();
74+
itemBuilder.partitionKey(PARTITION).sortKey(SORT);
75+
itemBuilder.attribute1("In the Jungle");
76+
itemBuilder.attribute2("The mighty Jungle");
77+
itemBuilder.attribute3("The Lion Sleeps Tonight");
78+
SimpleViaLombok item = itemBuilder.build();
79+
Instant firstWriteTime = clock.instant();
80+
table.putItem(item);
81+
82+
// Get the item back from the table
83+
SimpleViaLombok firstWriteDecrypted = getItemWithTimeStamp(
84+
firstWriteTime, table, key, TOLERANCE);
85+
86+
// We are going to Modify the Values to be Null,
87+
// Write that to DDB with Encryption
88+
final SimpleViaLombok.SimpleViaLombokBuilder nullBuilder = SimpleViaLombok.builder();
89+
nullBuilder.partitionKey(PARTITION).sortKey(SORT);
90+
nullBuilder.attribute1(null).attribute2(null).attribute3(null);
91+
SimpleViaLombok nullItem = nullBuilder.build();
92+
final Instant secondWriteTime = clock.instant();
93+
// We need to be sure that we are replacing This Test Runs' Write,
94+
// so we will use a condition expression
95+
table.putItem(
96+
(PutItemEnhancedRequest.Builder<SimpleViaLombok> requestBuilder) ->
97+
requestBuilder.conditionExpression(
98+
Expression.builder().expression("#a = :b")
99+
.putExpressionName("#a", ":lastModifiedTimeStamp")
100+
.putExpressionValue(":b",
101+
instantConverter.transformFrom(firstWriteDecrypted.getLastModifiedTimeStamp()))
102+
.build())
103+
.item(nullItem)
104+
);
105+
106+
SimpleViaLombok secondWriteDecrypted = getItemWithTimeStamp(secondWriteTime, table, key, TOLERANCE);
107+
assert Objects.isNull(secondWriteDecrypted.getAttribute1());
108+
assert Objects.isNull(secondWriteDecrypted.getAttribute2());
109+
assert Objects.isNull(secondWriteDecrypted.getAttribute3());
110+
}
111+
112+
private static SimpleViaLombok getItemWithTimeStamp(
113+
final Instant writeTime,
114+
final DynamoDbTable<SimpleViaLombok> table,
115+
final Key key,
116+
final int tolerance
117+
) {
118+
int retries = 3;
119+
final GetItemEnhancedRequest request =
120+
GetItemEnhancedRequest.builder()
121+
.consistentRead(true).key(key).build();
122+
try {
123+
while (retries != 0) {
124+
SimpleViaLombok read = table.getItem(request);
125+
Instant readTime = read.getLastModifiedTimeStamp();
126+
long diff = writeTime.toEpochMilli() - readTime.toEpochMilli();
127+
if (abs(diff) < tolerance) return read;
128+
// We did not get the record we want, sleep for 1 Seconds
129+
Thread.sleep(TOLERANCE);
130+
retries = retries - 1;
131+
}
132+
} catch (InterruptedException ex) {
133+
throw new RuntimeException(COULD_NOT_READ_MSG, ex);
134+
}
135+
throw new RuntimeException(COULD_NOT_READ_MSG);
136+
}
137+
138+
private static <T> DynamoDbClient initDDBWithEncryption(
139+
final String unsignedPrefix,
140+
final IKeyring kmsKeyring,
141+
TableSchema<T> schemaOnEncrypt
142+
) {
143+
Map<String, DynamoDbEnhancedTableEncryptionConfig> tableConfigs = new HashMap<>();
144+
tableConfigs.put(TEST_TABLE_NAME,
145+
DynamoDbEnhancedTableEncryptionConfig.builder()
146+
.logicalTableName(TEST_TABLE_NAME)
147+
.keyring(kmsKeyring)
148+
.allowedUnsignedAttributePrefix(unsignedPrefix)
149+
.schemaOnEncrypt(schemaOnEncrypt)
150+
.build());
151+
DynamoDbEncryptionInterceptor interceptor =
152+
DynamoDbEnhancedClientEncryption.CreateDynamoDbEncryptionInterceptor(
153+
CreateDynamoDbEncryptionInterceptorInput.builder()
154+
.tableEncryptionConfigs(tableConfigs)
155+
.build()
156+
);
157+
return DynamoDbClient.builder()
158+
.overrideConfiguration(
159+
ClientOverrideConfiguration.builder()
160+
.addExecutionInterceptor(interceptor)
161+
.build())
162+
.build();
163+
}
164+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.datamodels;
2+
3+
import java.time.Instant;
4+
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.Value;
8+
9+
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute;
10+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
11+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
12+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
13+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
14+
15+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DynamoDbEncryptionDoNothing;
16+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DynamoDbEncryptionSignOnly;
17+
18+
@Value
19+
@Builder
20+
@DynamoDbImmutable(builder = SimpleViaLombok.SimpleViaLombokBuilder.class)
21+
public class SimpleViaLombok {
22+
@Getter(onMethod_= {@DynamoDbPartitionKey, @DynamoDbAttribute(value = "partition_key")})
23+
String partitionKey;
24+
@Getter(onMethod_= {@DynamoDbSortKey, @DynamoDbAttribute(value = "sort_key")})
25+
int sortKey;
26+
String attribute1;
27+
@Getter(onMethod_=@DynamoDbEncryptionSignOnly)
28+
String attribute2;
29+
@Getter(onMethod_= {@DynamoDbEncryptionDoNothing, @DynamoDbAttribute(value = ":attribute3")})
30+
String attribute3;
31+
@Getter(onMethod_= {
32+
@DynamoDbEncryptionDoNothing,
33+
@DynamoDbAttribute(value = ":lastModifiedTimeStamp"),
34+
@DynamoDbAutoGeneratedTimestampAttribute
35+
})
36+
Instant lastModifiedTimeStamp;
37+
}

0 commit comments

Comments
 (0)