Skip to content

test(enhancedClient): Verify EC's null behavior does not interfere #255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient;

import org.testng.annotations.Test;

import java.time.Clock;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
import software.amazon.awssdk.enhanced.dynamodb.Expression;
import software.amazon.awssdk.enhanced.dynamodb.Key;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension;
import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.InstantAsStringAttributeConverter;
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.validdatamodels.SimpleViaLombok;
import software.amazon.cryptography.materialproviders.IKeyring;

import static java.lang.Math.abs;
import static software.amazon.cryptography.dbencryptionsdk.dynamodb.TestUtils.TEST_TABLE_NAME;
import static software.amazon.cryptography.dbencryptionsdk.dynamodb.TestUtils.createKmsKeyring;

/**
* From the DynamoDB Enhanced Client's Developer Guide:
* > When you use the putItem API,
* > the enhanced client does not include null-valued attributes of
* > a mapped data object in the request to DynamoDB.
* This test asserts the Enhanced Client's Null behavior does not cause an issue.
* It SHOULD NOT.
* The DB-ESDK for DynamoDB prevents Signed Attributes
* from being used in an update expression.
* As such, all writes with a Signed Attribute are only permitted
* with a full write.
* Even though the Enhanced Client does not include the null values
* in the request to DynamoDB,
* since the request is made with a full WRITE,
* the pre-existing NOW null values will be dropped.
*/
public class DynamoDbEncryptionEnhancedClientNullIntegrationTest {
final static String PARTITION = "TestEnhancedClientPutNull";
final static int SORT = 20230716;
final static String COULD_NOT_READ_MSG = String.format(
"Testing Null Write with Enhanced Client Required a particular Read that could not complete. " +
"Recommend Deleting the record via the console and retrying. " +
"Record's Key is Partition: %s, Sort: %s", PARTITION, SORT);
public static final int TOLERANCE = 1000;

@Test
public void TestEnhancedClientPutNull() {
final String UNSIGNED_PREFIX = ":";
final IKeyring kmsKeyring = createKmsKeyring();
final TableSchema<SimpleViaLombok> schemaOnEncrypt =
TableSchema.fromImmutableClass(SimpleViaLombok.class);
final InstantAsStringAttributeConverter instantConverter =
InstantAsStringAttributeConverter.create();
final Key key = Key.builder()
.partitionValue(PARTITION).sortValue(SORT)
.build();
final Clock clock = Clock.systemUTC();

DynamoDbClient ddb = initDDBWithEncryption(UNSIGNED_PREFIX, kmsKeyring, schemaOnEncrypt);
DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
.extensions(AutoGeneratedTimestampRecordExtension.create())
.dynamoDbClient(ddb)
.build();
DynamoDbTable<SimpleViaLombok> table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt);

final SimpleViaLombok.SimpleViaLombokBuilder itemBuilder = SimpleViaLombok.builder();
itemBuilder.partitionKey(PARTITION).sortKey(SORT);
itemBuilder.attribute1("In the Jungle");
itemBuilder.attribute2("The mighty Jungle");
itemBuilder.attribute3("The Lion Sleeps Tonight");
SimpleViaLombok item = itemBuilder.build();
Instant firstWriteTime = clock.instant();
table.putItem(item);

// Get the item back from the table
SimpleViaLombok firstWriteDecrypted = getItemWithTimeStamp(
firstWriteTime, table, key, TOLERANCE);

// We are going to Modify the Values to be Null,
// Write that to DDB with Encryption
final SimpleViaLombok.SimpleViaLombokBuilder nullBuilder = SimpleViaLombok.builder();
nullBuilder.partitionKey(PARTITION).sortKey(SORT);
nullBuilder.attribute1(null).attribute2(null).attribute3(null);
SimpleViaLombok nullItem = nullBuilder.build();
final Instant secondWriteTime = clock.instant();
// We need to be sure that we are replacing This Test Runs' Write,
// so we will use a condition expression
table.putItem(
(PutItemEnhancedRequest.Builder<SimpleViaLombok> requestBuilder) ->
requestBuilder.conditionExpression(
Expression.builder().expression("#a = :b")
.putExpressionName("#a", ":lastModifiedTimeStamp")
.putExpressionValue(":b",
instantConverter.transformFrom(firstWriteDecrypted.getLastModifiedTimeStamp()))
.build())
.item(nullItem)
);

SimpleViaLombok secondWriteDecrypted = getItemWithTimeStamp(secondWriteTime, table, key, TOLERANCE);
assert Objects.isNull(secondWriteDecrypted.getAttribute1());
assert Objects.isNull(secondWriteDecrypted.getAttribute2());
assert Objects.isNull(secondWriteDecrypted.getAttribute3());
}

private static SimpleViaLombok getItemWithTimeStamp(
final Instant writeTime,
final DynamoDbTable<SimpleViaLombok> table,
final Key key,
final int tolerance
) {
int retries = 3;
final GetItemEnhancedRequest request =
GetItemEnhancedRequest.builder()
.consistentRead(true).key(key).build();
try {
while (retries != 0) {
SimpleViaLombok read = table.getItem(request);
Instant readTime = read.getLastModifiedTimeStamp();
long diff = writeTime.toEpochMilli() - readTime.toEpochMilli();
if (abs(diff) < tolerance) return read;
// We did not get the record we want, sleep for 1 Seconds
Thread.sleep(TOLERANCE);
retries = retries - 1;
}
} catch (InterruptedException ex) {
throw new RuntimeException(COULD_NOT_READ_MSG, ex);
}
throw new RuntimeException(COULD_NOT_READ_MSG);
}

private static <T> DynamoDbClient initDDBWithEncryption(
final String unsignedPrefix,
final IKeyring kmsKeyring,
TableSchema<T> schemaOnEncrypt
) {
Map<String, DynamoDbEnhancedTableEncryptionConfig> tableConfigs = new HashMap<>();
tableConfigs.put(TEST_TABLE_NAME,
DynamoDbEnhancedTableEncryptionConfig.builder()
.logicalTableName(TEST_TABLE_NAME)
.keyring(kmsKeyring)
.allowedUnsignedAttributePrefix(unsignedPrefix)
.schemaOnEncrypt(schemaOnEncrypt)
.build());
DynamoDbEncryptionInterceptor interceptor =
DynamoDbEnhancedClientEncryption.CreateDynamoDbEncryptionInterceptor(
CreateDynamoDbEncryptionInterceptorInput.builder()
.tableEncryptionConfigs(tableConfigs)
.build()
);
return DynamoDbClient.builder()
.overrideConfiguration(
ClientOverrideConfiguration.builder()
.addExecutionInterceptor(interceptor)
.build())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.validdatamodels;

import java.time.Instant;

import lombok.Builder;
import lombok.Getter;
import lombok.Value;

import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;

import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DynamoDbEncryptionDoNothing;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DynamoDbEncryptionSignOnly;

@Value
@Builder
@DynamoDbImmutable(builder = SimpleViaLombok.SimpleViaLombokBuilder.class)
public class SimpleViaLombok {
@Getter(onMethod_= {@DynamoDbPartitionKey, @DynamoDbAttribute(value = "partition_key")})
String partitionKey;
@Getter(onMethod_= {@DynamoDbSortKey, @DynamoDbAttribute(value = "sort_key")})
int sortKey;
String attribute1;
@Getter(onMethod_=@DynamoDbEncryptionSignOnly)
String attribute2;
@Getter(onMethod_= {@DynamoDbEncryptionDoNothing, @DynamoDbAttribute(value = ":attribute3")})
String attribute3;
@Getter(onMethod_= {
@DynamoDbEncryptionDoNothing,
@DynamoDbAttribute(value = ":lastModifiedTimeStamp"),
@DynamoDbAutoGeneratedTimestampAttribute
})
Instant lastModifiedTimeStamp;
}