Skip to content

feat: better support of single table design #736

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 6 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -4,6 +4,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -40,6 +41,7 @@ public static DynamoDbEncryptionInterceptor CreateDynamoDbEncryptionInterceptor(
.build();
}

// return all attribute names that are keys in any index
private static Set<String> attributeNamesUsedInIndices(
final TableMetadata tableMetadata
) {
Expand All @@ -59,6 +61,7 @@ private static Set<String> attributeNamesUsedInIndices(
return allIndexAttributes;
}

// return attributes used in the primary table index
private static Set<String> attributeNamesUsedInPrimaryKey(
final TableMetadata tableMetadata
) {
Expand All @@ -69,62 +72,85 @@ private static Set<String> attributeNamesUsedInPrimaryKey(
return keyAttributes;
}

private static DynamoDbTableEncryptionConfig getTableConfig(
final DynamoDbEnhancedTableEncryptionConfig configWithSchema,
final String tableName
) {
Map<String, CryptoAction> actions = new HashMap<>();
private static void throwUsageError(String tableName, String attributeName, String usage, String usage2)
{
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Attribute %s of table %s is used as both %s and %s.",
attributeName, tableName, usage, usage2))
.build();

}

// Any given attribute MUST have one and only one Cryptographic Action.
// i.e: It can't be both SignOnly and DoNothing.
// validateAttributeUsage throws an error if
// the given attribute is marked with `usage` and another Cryptographic Action.
// For example, for a SignOnly, signOnly will be empty, and an error must be reported
// if the attribute exists in any of the other sets.
private static void validateAttributeUsage(
String tableName,
String attributeName,
String usage,
Optional<Set<String>> signOnly,
Optional<Set<String>> signAndInclude,
Optional<Set<String>> doNothing
)
{
if (signOnly.isPresent()) {
if (signOnly.get().contains(attributeName)) {
throwUsageError(tableName, attributeName, usage, "@DynamoDbEncryptionSignOnly");
}
}
if (signAndInclude.isPresent()) {
if (signAndInclude.get().contains(attributeName)) {
throwUsageError(tableName, attributeName, usage, "@DynamoDbEncryptionSignAndIncludeInEncryptionContext");
}
}
if (doNothing.isPresent()) {
if (doNothing.get().contains(attributeName)) {
throwUsageError(tableName, attributeName, usage, "@DynamoDbEncryptionDoNothing");
}
}
}

TableSchema<?> topTableSchema = configWithSchema.schemaOnEncrypt();
// return a map containing all top level attributes in the schema
// If an attribute is used in an index, it is SignOnly
// Else if an attribute is tagged with a single action, it gets that action
// Else if an attribute is tagged with a multiple actions, an error is thrown
// Else if an attribute is not tagged, it is to be encrypted
private static Map<String, CryptoAction> getActionsFromSchema(String tableName, TableSchema<?> topTableSchema)
{
Set<String> signOnlyAttributes = getSignOnlyAttributes(topTableSchema);
Set<String> signAndIncludeAttributes = getSignAndIncludeInEncryptionContextAttributes(topTableSchema);
Set<String> doNothingAttributes = getDoNothingAttributes(topTableSchema);
Set<String> keyAttributes = attributeNamesUsedInIndices(topTableSchema.tableMetadata());
Set<String> tableKeys = attributeNamesUsedInPrimaryKey(topTableSchema.tableMetadata());

if (!Collections.disjoint(keyAttributes, doNothingAttributes)) {
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Cannot use @DynamoDbEncryptionDoNothing on primary key attributes. Found on Table Name: %s",
tableName))
.build();
} else if (!Collections.disjoint(signOnlyAttributes, doNothingAttributes)) {
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Cannot use @DynamoDbEncryptionDoNothing and @DynamoDbEncryptionSignOnly on same attribute. Found on Table Name: %s",
tableName))
.build();
} else if (!Collections.disjoint(signOnlyAttributes, signAndIncludeAttributes)) {
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Cannot use @DynamoDbEncryptionSignAndIncludeInEncryptionContext and @DynamoDbEncryptionSignOnly on same attribute. Found on Table Name: %s",
tableName))
.build();
} else if (!Collections.disjoint(doNothingAttributes, signAndIncludeAttributes)) {
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Cannot use @DynamoDbEncryptionSignAndIncludeInEncryptionContext and @DynamoDbEncryptionDoNothing on same attribute. Found on Table Name: %s",
tableName))
.build();
}

List<String> attributeNames = topTableSchema.attributeNames();

Map<String, CryptoAction> actions = new HashMap<>();
StringBuilder path = new StringBuilder();
path.append(tableName).append(".");
for (String attributeName : attributeNames) {
if (tableKeys.contains(attributeName)) {
if (signAndIncludeAttributes.isEmpty()) {
if (signAndIncludeAttributes.isEmpty()) {
validateAttributeUsage(tableName, attributeName, "a primary key", Optional.empty(), Optional.of(signAndIncludeAttributes), Optional.of(doNothingAttributes));
actions.put(attributeName, CryptoAction.SIGN_ONLY);
} else {
} else {
validateAttributeUsage(tableName, attributeName, "a primary key", Optional.of(signOnlyAttributes), Optional.empty(), Optional.of(doNothingAttributes));
actions.put(attributeName, CryptoAction.SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT);
}
}
} else if (keyAttributes.contains(attributeName)) {
validateAttributeUsage(tableName, attributeName, "an index key", Optional.empty(), Optional.of(signAndIncludeAttributes), Optional.of(doNothingAttributes));
actions.put(attributeName, CryptoAction.SIGN_ONLY);
} else if (signOnlyAttributes.contains(attributeName)) {
validateAttributeUsage(tableName, attributeName, "@DynamoDbEncryptionSignOnly", Optional.empty(), Optional.of(signAndIncludeAttributes), Optional.of(doNothingAttributes));
actions.put(attributeName, CryptoAction.SIGN_ONLY);
} else if (signAndIncludeAttributes.contains(attributeName)) {
validateAttributeUsage(tableName, attributeName, "@DynamoDbEncryptionSignAndIncludeInEncryptionContext", Optional.of(signOnlyAttributes), Optional.empty(), Optional.of(doNothingAttributes));
actions.put(attributeName, CryptoAction.SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT);
} else if (doNothingAttributes.contains(attributeName)) {
validateAttributeUsage(tableName, attributeName, "@DynamoDbEncryptionDoNothing", Optional.of(signOnlyAttributes), Optional.of(signAndIncludeAttributes), Optional.empty());
actions.put(attributeName, CryptoAction.DO_NOTHING);
} else {
// non-key attributes are ENCRYPT_AND_SIGN unless otherwise annotated
Expand All @@ -134,12 +160,101 @@ private static DynamoDbTableEncryptionConfig getTableConfig(
// Detect Encryption Flags that are Ignored b/c they are in a Nested Class
scanForIgnoredEncryptionTags(topTableSchema, attributeName, path);
}
return actions;
}

// given action maps from multiple tables, merge them into one
// we throw an error if the one attribute is given two different actions
private static Map<String, CryptoAction> mergeActions(List<Map<String, CryptoAction>> actionList)
{
// most common case
if (actionList.size() == 1) {
return actionList.get(0);
}

// Gather set of all attributes
HashSet<String> attributes = new HashSet<>();
for (Map<String, CryptoAction> config : actionList) {
attributes.addAll(config.keySet());
}

// for each attribute, ensure that everyone agrees on its action
Map<String, CryptoAction> actions = new HashMap<>();
for (String attr : attributes) {
Optional<CryptoAction> action = Optional.empty();
for (Map<String, CryptoAction> config : actionList) {
CryptoAction act = config.get(attr);
if (act != null) {
if (action.isPresent()) {
if (!action.get().equals(act)) {
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Attribute %s set to %s in one table and %s in another.",
attr, action.get(), act))
.build();
}
} else {
action = Optional.of(act);
}
}
}
actions.put(attr, action.get());
}
return actions;
}

// return the partition key name
// throw an error if two schemas disagree
private static String getPartitionKeyName(List<TableSchema<?>> schemas)
{
String partitionName = schemas.get(0).tableMetadata().primaryPartitionKey();
for (TableSchema<?> schema : schemas) {
String part = schema.tableMetadata().primaryPartitionKey();
if (!partitionName.equals(part)) {
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Primary Key set to %s in one table and %s in another.",
partitionName, part))
.build();
}
}
return partitionName;
}

// return the sort key name
// throw an error if two schemas disagree
private static Optional<String> getSortKeyName(List<TableSchema<?>> schemas)
{
Optional<String> sortName = schemas.get(0).tableMetadata().primarySortKey();
for (TableSchema<?> schema : schemas) {
Optional<String> sort = schema.tableMetadata().primarySortKey();
if (!sortName.equals(sort)) {
throw DynamoDbEncryptionException.builder()
.message(String.format(
"Primary Key set to %s in one table and %s in another.",
sortName, sort))
.build();
}
}
return sortName;
}

// Convert enhanced client config to regular config
private static DynamoDbTableEncryptionConfig getTableConfig(
final DynamoDbEnhancedTableEncryptionConfig configWithSchema,
final String tableName
) {
List<Map<String, CryptoAction>> actionList = new ArrayList<>();
for (TableSchema<?> schema : configWithSchema.schemaOnEncrypt()) {
actionList.add(getActionsFromSchema(tableName, schema));
}
Map<String, CryptoAction> actions = mergeActions(actionList);

DynamoDbTableEncryptionConfig.Builder builder = DynamoDbTableEncryptionConfig.builder();
String partitionName = topTableSchema.tableMetadata().primaryPartitionKey();
String partitionName = getPartitionKeyName(configWithSchema.schemaOnEncrypt());
builder = builder.partitionKeyName(partitionName);

Optional<String> sortName = topTableSchema.tableMetadata().primarySortKey();
Optional<String> sortName = getSortKeyName(configWithSchema.schemaOnEncrypt());
if (sortName.isPresent()) {
builder = builder.sortKeyName(sortName.get());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient;

import java.util.List;
import java.util.ArrayList;
import java.util.Objects;

import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.LegacyOverride;
Expand All @@ -14,7 +15,7 @@

public class DynamoDbEnhancedTableEncryptionConfig {
private final String logicalTableName;
private final TableSchema<?> schemaOnEncrypt;
private final List<TableSchema<?>> schemaOnEncrypt;
private final List<String> allowedUnsignedAttributes;
private final String allowedUnsignedAttributePrefix;
private final Keyring keyring;
Expand All @@ -39,7 +40,7 @@ protected DynamoDbEnhancedTableEncryptionConfig(BuilderImpl builder) {

public String logicalTableName() { return this.logicalTableName; }

public TableSchema<?> schemaOnEncrypt() {
public List<TableSchema<?>> schemaOnEncrypt() {
return this.schemaOnEncrypt;
}

Expand Down Expand Up @@ -83,7 +84,7 @@ public interface Builder {
String logicalTableName();
Builder logicalTableName(String logicalTableName);
Builder schemaOnEncrypt(TableSchema<?> schemaOnEncrypt);
TableSchema<?> schemaOnEncrypt();
List<TableSchema<?>> schemaOnEncrypt();
Builder allowedUnsignedAttributes(List<String> allowedUnsignedAttributes);
List<String> allowedUnsignedAttributes();
Builder allowedUnsignedAttributePrefix(String allowedUnsignedAttributePrefix);
Expand All @@ -101,7 +102,7 @@ public interface Builder {

protected static class BuilderImpl implements Builder {
protected String logicalTableName;
protected TableSchema<?> schemaOnEncrypt;
protected List<TableSchema<?>> schemaOnEncrypt;
protected List<String> allowedUnsignedAttributes;
protected String allowedUnsignedAttributePrefix;
protected Keyring keyring;
Expand Down Expand Up @@ -132,11 +133,14 @@ public Builder logicalTableName(String logicalTableName) {
public String logicalTableName() { return this.logicalTableName; }

public Builder schemaOnEncrypt(TableSchema<?> schemaOnEncrypt) {
this.schemaOnEncrypt = schemaOnEncrypt;
if (Objects.isNull(this.schemaOnEncrypt())) {
this.schemaOnEncrypt = new ArrayList();
}
this.schemaOnEncrypt.add(schemaOnEncrypt);
return this;
}
Comment on lines 135 to 141
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯


public TableSchema<?> schemaOnEncrypt() {
public List<TableSchema<?>> schemaOnEncrypt() {
return this.schemaOnEncrypt;
}

Expand Down
Loading