Skip to content

Commit be80451

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

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-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,167 @@
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 prevents Signed Attributes
39+
* from being used in an update expression.
40+
* As such, all writes with a Signed Attribute are only permitted
41+
* with a full write.
42+
* Even though the Enhanced Client does not include the null values
43+
* in the request to DynamoDB,
44+
* since the request is made with a full WRITE,
45+
* the pre-existing NOW null values will be dropped.
46+
*/
47+
public class DynamoDbEncryptionEnhancedClientNullIntegrationTest {
48+
final static String PARTITION = "TestEnhancedClientPutNull";
49+
final static int SORT = 20230716;
50+
final static String COULD_NOT_READ_MSG = String.format(
51+
"Testing Null Write with Enhanced Client Required a particular Read that could not complete. " +
52+
"Recommend Deleting the record via the console and retrying. " +
53+
"Record's Key is Partition: %s, Sort: %s", PARTITION, SORT);
54+
public static final int TOLERANCE = 1000;
55+
56+
@Test
57+
public void TestEnhancedClientPutNull() {
58+
final String UNSIGNED_PREFIX = ":";
59+
final IKeyring kmsKeyring = createKmsKeyring();
60+
final TableSchema<SimpleViaLombok> schemaOnEncrypt =
61+
TableSchema.fromImmutableClass(SimpleViaLombok.class);
62+
final InstantAsStringAttributeConverter instantConverter =
63+
InstantAsStringAttributeConverter.create();
64+
final Key key = Key.builder()
65+
.partitionValue(PARTITION).sortValue(SORT)
66+
.build();
67+
final Clock clock = Clock.systemUTC();
68+
69+
DynamoDbClient ddb = initDDBWithEncryption(UNSIGNED_PREFIX, kmsKeyring, schemaOnEncrypt);
70+
DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
71+
.extensions(AutoGeneratedTimestampRecordExtension.create())
72+
.dynamoDbClient(ddb)
73+
.build();
74+
DynamoDbTable<SimpleViaLombok> table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt);
75+
76+
final SimpleViaLombok.SimpleViaLombokBuilder itemBuilder = SimpleViaLombok.builder();
77+
itemBuilder.partitionKey(PARTITION).sortKey(SORT);
78+
itemBuilder.attribute1("In the Jungle");
79+
itemBuilder.attribute2("The mighty Jungle");
80+
itemBuilder.attribute3("The Lion Sleeps Tonight");
81+
SimpleViaLombok item = itemBuilder.build();
82+
Instant firstWriteTime = clock.instant();
83+
table.putItem(item);
84+
85+
// Get the item back from the table
86+
SimpleViaLombok firstWriteDecrypted = getItemWithTimeStamp(
87+
firstWriteTime, table, key, TOLERANCE);
88+
89+
// We are going to Modify the Values to be Null,
90+
// Write that to DDB with Encryption
91+
final SimpleViaLombok.SimpleViaLombokBuilder nullBuilder = SimpleViaLombok.builder();
92+
nullBuilder.partitionKey(PARTITION).sortKey(SORT);
93+
nullBuilder.attribute1(null).attribute2(null).attribute3(null);
94+
SimpleViaLombok nullItem = nullBuilder.build();
95+
final Instant secondWriteTime = clock.instant();
96+
// We need to be sure that we are replacing This Test Runs' Write,
97+
// so we will use a condition expression
98+
table.putItem(
99+
(PutItemEnhancedRequest.Builder<SimpleViaLombok> requestBuilder) ->
100+
requestBuilder.conditionExpression(
101+
Expression.builder().expression("#a = :b")
102+
.putExpressionName("#a", ":lastModifiedTimeStamp")
103+
.putExpressionValue(":b",
104+
instantConverter.transformFrom(firstWriteDecrypted.getLastModifiedTimeStamp()))
105+
.build())
106+
.item(nullItem)
107+
);
108+
109+
SimpleViaLombok secondWriteDecrypted = getItemWithTimeStamp(secondWriteTime, table, key, TOLERANCE);
110+
assert Objects.isNull(secondWriteDecrypted.getAttribute1());
111+
assert Objects.isNull(secondWriteDecrypted.getAttribute2());
112+
assert Objects.isNull(secondWriteDecrypted.getAttribute3());
113+
}
114+
115+
private static SimpleViaLombok getItemWithTimeStamp(
116+
final Instant writeTime,
117+
final DynamoDbTable<SimpleViaLombok> table,
118+
final Key key,
119+
final int tolerance
120+
) {
121+
int retries = 3;
122+
final GetItemEnhancedRequest request =
123+
GetItemEnhancedRequest.builder()
124+
.consistentRead(true).key(key).build();
125+
try {
126+
while (retries != 0) {
127+
SimpleViaLombok read = table.getItem(request);
128+
Instant readTime = read.getLastModifiedTimeStamp();
129+
long diff = writeTime.toEpochMilli() - readTime.toEpochMilli();
130+
if (abs(diff) < tolerance) return read;
131+
// We did not get the record we want, sleep for 1 Seconds
132+
Thread.sleep(TOLERANCE);
133+
retries = retries - 1;
134+
}
135+
} catch (InterruptedException ex) {
136+
throw new RuntimeException(COULD_NOT_READ_MSG, ex);
137+
}
138+
throw new RuntimeException(COULD_NOT_READ_MSG);
139+
}
140+
141+
private static <T> DynamoDbClient initDDBWithEncryption(
142+
final String unsignedPrefix,
143+
final IKeyring kmsKeyring,
144+
TableSchema<T> schemaOnEncrypt
145+
) {
146+
Map<String, DynamoDbEnhancedTableEncryptionConfig> tableConfigs = new HashMap<>();
147+
tableConfigs.put(TEST_TABLE_NAME,
148+
DynamoDbEnhancedTableEncryptionConfig.builder()
149+
.logicalTableName(TEST_TABLE_NAME)
150+
.keyring(kmsKeyring)
151+
.allowedUnsignedAttributePrefix(unsignedPrefix)
152+
.schemaOnEncrypt(schemaOnEncrypt)
153+
.build());
154+
DynamoDbEncryptionInterceptor interceptor =
155+
DynamoDbEnhancedClientEncryption.CreateDynamoDbEncryptionInterceptor(
156+
CreateDynamoDbEncryptionInterceptorInput.builder()
157+
.tableEncryptionConfigs(tableConfigs)
158+
.build()
159+
);
160+
return DynamoDbClient.builder()
161+
.overrideConfiguration(
162+
ClientOverrideConfiguration.builder()
163+
.addExecutionInterceptor(interceptor)
164+
.build())
165+
.build();
166+
}
167+
}
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)