Skip to content

feat(EnhancedClient): Detect and Error on Ignored DynamoDB Encryption Tags #248

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

Merged
merged 19 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bb62eba
test(EnhancedClient): Nested Objects via Annotations
texastony Jul 12, 2023
ced9024
test(EnhancedClient): Valid Nested Annotations
texastony Jul 13, 2023
b3299bd
test(EnhancedClient): Valid & Invalid ConvertedBy
texastony Jul 13, 2023
3600f61
test(EnhancedClient): Remove Converter Provider
texastony Jul 13, 2023
ce903f5
test(EnhancedClient): Refactor InvalidAnnotatedNestedBean
texastony Jul 13, 2023
b053724
test(EnhancedClient): Java Doc InvalidAnnotatedNestedBean
texastony Jul 13, 2023
b9f4adc
test(EnhancedClient): Conflicting Behavior
texastony Jul 14, 2023
0758027
feat(EnhancedClient): Detect Conflicting Configurations
texastony Jul 14, 2023
26f9369
feat(EnhancedClient): Remove commented out work in progress
texastony Jul 14, 2023
2a8bcd4
Merge branch 'main' into tony-test-nested-via-annotations
texastony Jul 14, 2023
1e50fad
refactor(EnhancedClient): scanForIgnoredEncryptionTagsShallow
texastony Jul 14, 2023
2d26691
refactor(Tests-EnhancedClient): Inline comments for IGNORED
texastony Jul 14, 2023
8e40186
refactor(Tests-EnhancedClient): Remove un-supported configurations th…
texastony Jul 15, 2023
883df88
Merge branch 'main' of github.com:aws/aws-database-encryption-sdk-dyn…
texastony Jul 17, 2023
160ac9a
test(EnhancedClient): Skip & Document Failing Config test
texastony Jul 17, 2023
90ac3d7
docs(EnhancedClient): Document Scan for Ignored Tags
texastony Jul 17, 2023
5287e66
Merge branch 'main' into tony-test-nested-via-annotations
texastony Jul 17, 2023
c11face
docs(EnhancedClient): reference GHI #259 for skipped test
texastony Jul 17, 2023
f2756fb
Merge branch 'tony-test-nested-via-annotations' of github.com:aws/aws…
texastony Jul 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata;
import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;

import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbEncryptionException;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig;

import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction;
import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;

import static software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DoNothingTag.CUSTOM_DDB_ENCRYPTION_DO_NOTHING_PREFIX;
import static software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.SignOnlyTag.CUSTOM_DDB_ENCRYPTION_SIGN_ONLY_PREFIX;
Expand Down Expand Up @@ -82,6 +85,9 @@ private static DynamoDbTableEncryptionConfig getTableConfig(DynamoDbEnhancedTabl
// non-key attributes are ENCRYPT_AND_SIGN unless otherwise annotated
actions.put(attributeName, CryptoAction.ENCRYPT_AND_SIGN);
}

// Detect Encryption Flags that are Ignored b/c they are in a Nested Class
scanForIgnoredEncryptionTagsShallow(configWithSchema, attributeName);
}

DynamoDbTableEncryptionConfig.Builder builder = DynamoDbTableEncryptionConfig.builder();
Expand Down Expand Up @@ -115,4 +121,48 @@ private static DynamoDbTableEncryptionConfig getTableConfig(DynamoDbEnhancedTabl
.legacyOverride(configWithSchema.legacyOverride())
.build();
}

/**
* Detects DynamoDB Encryption Tags in Nested Enhanced Types.<p>
* This method ONLY parses ONE Layer of nesting.<p>
* It does NOT traverse further nested Enhanced Types.<p>
* DynamoDB Encryption Tags in Nested Classes are IGNORED by the
* Database Encryption SDK for DynamoDB.<p>
* As such, Detection of a nested DynamoDB Encryption Tag on a Nested Type
* triggers a Runtime Exception that MUST NOT BE ignored.<p>
* CAVEAT: Encryption Tags on fields of Nested Classes that are
* Flattened onto the top record are Respected.<p>
* The behavior of Flatten pushes the Attributes onto the top level record,
* making the "flattened sub-fields" equivalent to any other DynamoDB Attribute.<p>
* However, there still exists a possibility for IGNORED Encryption Tags,
* as any Encryption Tag on the field that will be "flattened" is ignored.<p>
* This method DOES NOT detect these "ignored-by-flattened" tags.
*/
private static void scanForIgnoredEncryptionTagsShallow(
final DynamoDbEnhancedTableEncryptionConfig configWithSchema,
final String attributeName
) {
AttributeConverter attributeConverter = configWithSchema.schemaOnEncrypt().converterForAttribute(attributeName);
if (
Objects.nonNull(attributeConverter) &&
Objects.nonNull(attributeConverter.type()) &&
attributeConverter.type().tableSchema().isPresent()
) {
Object maybeTableSchema = attributeConverter.type().tableSchema().get();
if (maybeTableSchema instanceof TableSchema) {
TableSchema subTableSchema = (TableSchema) maybeTableSchema;
if (
subTableSchema.tableMetadata().customMetadata().containsKey(CUSTOM_DDB_ENCRYPTION_SIGN_ONLY_PREFIX) ||
subTableSchema.tableMetadata().customMetadata().containsKey(CUSTOM_DDB_ENCRYPTION_DO_NOTHING_PREFIX)
) {
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Detected a DynamoDbEncryption Tag/Configuration on a nested attribute of %s. " +
"This is NOT Supported at this time!",
attributeName))
.build();
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient;

import java.util.HashMap;
import java.util.Map;

import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

/**
* This is a valid use of DynamoDbEncryption annotations attributes with
* DynamoDbConvertedBy.<p>
* The DynamoDbEncryption annotations are placed on elements that are converted
* to Maps.<p>
* In this case, only {@code nestedEncrypted} will be written to the DynamoDB Table as a
* binary. {@code nestedSigned} and {@code nestedIgnored} are recorded as DynamoDB Maps.
*/
@DynamoDbBean
public class AnnotatedConvertedBy {
private String partitionKey;
private int sortKey;
private ConvertedByNestedBean nestedEncrypted;
private ConvertedByNestedBean nestedSigned;
private ConvertedByNestedBean nestedIgnored;

@DynamoDbPartitionKey
@DynamoDbAttribute(value = "partition_key")
public String getPartitionKey() {
return this.partitionKey;
}
public void setPartitionKey(String partitionKey) {
this.partitionKey = partitionKey;
}

@DynamoDbSortKey
@DynamoDbAttribute(value = "sort_key")
public int getSortKey() {
return this.sortKey;
}
public void setSortKey(int sortKey) {
this.sortKey = sortKey;
}

@DynamoDbConvertedBy(ConvertedByNestedBean.NestedBeanConverter.class)
@DynamoDbAttribute("nestedEncrypted")
public ConvertedByNestedBean getNestedEncrypted() {
return this.nestedEncrypted;
}
public void setNestedEncrypted(ConvertedByNestedBean nested) {
this.nestedEncrypted = nested;
}

@DynamoDbConvertedBy(ConvertedByNestedBean.NestedBeanConverter.class)
@DynamoDbEncryptionSignOnly
@DynamoDbAttribute("nestedSigned")
public ConvertedByNestedBean getNestedSigned() {
return this.nestedSigned;
}
public void setNestedSigned(ConvertedByNestedBean nested) {
this.nestedSigned = nested;
}

@DynamoDbConvertedBy(ConvertedByNestedBean.NestedBeanConverter.class)
@DynamoDbEncryptionDoNothing
@DynamoDbAttribute("nestedIgnored")
public ConvertedByNestedBean getNestedIgnored() {
return this.nestedIgnored;
}
public void setNestedIgnored(ConvertedByNestedBean nested) {
this.nestedIgnored = nested;
}

public static class ConvertedByNestedBean {
private String id;
private String firstName;
private String lastName;

// A Default Empty constructor is needed by the Enhanced Client
public ConvertedByNestedBean() {};

public ConvertedByNestedBean(String id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}

public String getId() { return this.id; }
public void setId(String id) { this.id = id; }

public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }

public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
ConvertedByNestedBean other = (ConvertedByNestedBean) obj;
if (id == null) {
if (other.getId() != null) return false;
} else if (!id.equals(other.getId())) return false;
if (firstName == null) {
if (other.getFirstName() != null) return false;
} else if (!firstName.equals(other.getFirstName())) return false;
if (lastName == null) {
if (other.getLastName() != null) return false;
} else if (!lastName.equals(other.getLastName())) return false;
return true;
}

public static final class NestedBeanConverter implements AttributeConverter<ConvertedByNestedBean> {
@Override
public AttributeValue transformFrom(ConvertedByNestedBean ConvertedByNestedBean) {
Map<String, AttributeValue> map = new HashMap<>(3);
map.put("id", AttributeValue.fromS(ConvertedByNestedBean.getId()));
map.put("firstName", AttributeValue.fromS(ConvertedByNestedBean.getFirstName()));
map.put("lastName", AttributeValue.fromS(ConvertedByNestedBean.getLastName()));
return AttributeValue.fromM(map);
}

@Override
public ConvertedByNestedBean transformTo(AttributeValue attributeValue) {
Map<String, AttributeValue> map = attributeValue.m();
return new ConvertedByNestedBean(
map.get("id").s(),
map.get("firstName").s(),
map.get("lastName").s());
}

@Override
public EnhancedType<ConvertedByNestedBean> type() {
return EnhancedType.of(ConvertedByNestedBean.class);
}

@Override
public AttributeValueType attributeValueType() {
return AttributeValueType.M;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient;

import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;

/**
* This is a valid use of DynamoDbEncryption annotations on nested attributes.
* Currently, the ONLY WAY to properly specify DynamoDbEncryption actions
* on nested attributes is via the @DynamoDbFlatten attribute.<p>
*
* Note that you MUST NOT use a DynamoDbEncryption annotation on the
* Flattened Attribute itself.<p>
* Only on it's sub-fields.<p>
*/
@DynamoDbBean
public class AnnotatedFlattenedBean {
private String partitionKey;
private int sortKey;
private FlattenedNestedBean nestedBeanClass;

@DynamoDbPartitionKey
@DynamoDbAttribute(value = "partition_key")
public String getPartitionKey() {
return this.partitionKey;
}

public void setPartitionKey(String partitionKey) {
this.partitionKey = partitionKey;
}

@DynamoDbSortKey
@DynamoDbAttribute(value = "sort_key")
public int getSortKey() {
return this.sortKey;
}

public void setSortKey(int sortKey) {
this.sortKey = sortKey;
}

// Any @DynamoDbEncryption annotation here would be IGNORED
// Instead, the Annotations MUST BE placed on the Getter Methods of
// FlattenedNestedBean.
@DynamoDbFlatten
public FlattenedNestedBean getNestedBeanClass() {
return this.nestedBeanClass;
}

public void setNestedBeanClass(FlattenedNestedBean nestedBeanClass) {
this.nestedBeanClass = nestedBeanClass;
}

@DynamoDbBean
public static class FlattenedNestedBean {
private String id;
private String firstName;
private String lastName;

public FlattenedNestedBean() {};

public FlattenedNestedBean(String id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}

@DynamoDbAttribute("id")
public String getId() { return this.id; }
public void setId(String id) { this.id = id; }

@DynamoDbEncryptionSignOnly
@DynamoDbAttribute("firstName")
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }

@DynamoDbEncryptionDoNothing
@DynamoDbAttribute("lastName")
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
FlattenedNestedBean other = (FlattenedNestedBean) obj;
if (id == null) {
if (other.getId() != null) return false;
} else if (!id.equals(other.getId())) return false;
if (firstName == null) {
if (other.getFirstName() != null) return false;
} else if (!firstName.equals(other.getFirstName())) return false;
if (lastName == null) {
if (other.getLastName() != null) return false;
} else if (!lastName.equals(other.getLastName())) return false;
return true;
}
}
}
Loading