4
4
import java .util .HashMap ;
5
5
import java .util .HashSet ;
6
6
import java .util .List ;
7
+ import java .util .ArrayList ;
7
8
import java .util .Map ;
8
9
import java .util .Objects ;
9
10
import java .util .Optional ;
@@ -40,6 +41,7 @@ public static DynamoDbEncryptionInterceptor CreateDynamoDbEncryptionInterceptor(
40
41
.build ();
41
42
}
42
43
44
+ // return all attribute names that are keys in any index
43
45
private static Set <String > attributeNamesUsedInIndices (
44
46
final TableMetadata tableMetadata
45
47
) {
@@ -59,6 +61,7 @@ private static Set<String> attributeNamesUsedInIndices(
59
61
return allIndexAttributes ;
60
62
}
61
63
64
+ // return attributes used in the primary table index
62
65
private static Set <String > attributeNamesUsedInPrimaryKey (
63
66
final TableMetadata tableMetadata
64
67
) {
@@ -69,62 +72,85 @@ private static Set<String> attributeNamesUsedInPrimaryKey(
69
72
return keyAttributes ;
70
73
}
71
74
72
- private static DynamoDbTableEncryptionConfig getTableConfig (
73
- final DynamoDbEnhancedTableEncryptionConfig configWithSchema ,
74
- final String tableName
75
- ) {
76
- Map <String , CryptoAction > actions = new HashMap <>();
75
+ private static void throwUsageError (String tableName , String attributeName , String usage , String usage2 )
76
+ {
77
+ throw DynamoDbEncryptionException .builder ()
78
+ .message (String .format (
79
+ "Attribute %s of table %s is used as both %s and %s." ,
80
+ attributeName , tableName , usage , usage2 ))
81
+ .build ();
82
+
83
+ }
84
+
85
+ // Any given attribute MUST have one and only one Cryptographic Action.
86
+ // i.e: It can't be both SignOnly and DoNothing.
87
+ // validateAttributeUsage throws an error if
88
+ // the given attribute is marked with `usage` and another Cryptographic Action.
89
+ // For example, for a SignOnly, signOnly will be empty, and an error must be reported
90
+ // if the attribute exists in any of the other sets.
91
+ private static void validateAttributeUsage (
92
+ String tableName ,
93
+ String attributeName ,
94
+ String usage ,
95
+ Optional <Set <String >> signOnly ,
96
+ Optional <Set <String >> signAndInclude ,
97
+ Optional <Set <String >> doNothing
98
+ )
99
+ {
100
+ if (signOnly .isPresent ()) {
101
+ if (signOnly .get ().contains (attributeName )) {
102
+ throwUsageError (tableName , attributeName , usage , "@DynamoDbEncryptionSignOnly" );
103
+ }
104
+ }
105
+ if (signAndInclude .isPresent ()) {
106
+ if (signAndInclude .get ().contains (attributeName )) {
107
+ throwUsageError (tableName , attributeName , usage , "@DynamoDbEncryptionSignAndIncludeInEncryptionContext" );
108
+ }
109
+ }
110
+ if (doNothing .isPresent ()) {
111
+ if (doNothing .get ().contains (attributeName )) {
112
+ throwUsageError (tableName , attributeName , usage , "@DynamoDbEncryptionDoNothing" );
113
+ }
114
+ }
115
+ }
77
116
78
- TableSchema <?> topTableSchema = configWithSchema .schemaOnEncrypt ();
117
+ // return a map containing all top level attributes in the schema
118
+ // If an attribute is used in an index, it is SignOnly
119
+ // Else if an attribute is tagged with a single action, it gets that action
120
+ // Else if an attribute is tagged with a multiple actions, an error is thrown
121
+ // Else if an attribute is not tagged, it is to be encrypted
122
+ private static Map <String , CryptoAction > getActionsFromSchema (String tableName , TableSchema <?> topTableSchema )
123
+ {
79
124
Set <String > signOnlyAttributes = getSignOnlyAttributes (topTableSchema );
80
125
Set <String > signAndIncludeAttributes = getSignAndIncludeInEncryptionContextAttributes (topTableSchema );
81
126
Set <String > doNothingAttributes = getDoNothingAttributes (topTableSchema );
82
127
Set <String > keyAttributes = attributeNamesUsedInIndices (topTableSchema .tableMetadata ());
83
128
Set <String > tableKeys = attributeNamesUsedInPrimaryKey (topTableSchema .tableMetadata ());
84
-
85
- if (!Collections .disjoint (keyAttributes , doNothingAttributes )) {
86
- throw DynamoDbEncryptionException .builder ()
87
- .message (String .format (
88
- "Cannot use @DynamoDbEncryptionDoNothing on primary key attributes. Found on Table Name: %s" ,
89
- tableName ))
90
- .build ();
91
- } else if (!Collections .disjoint (signOnlyAttributes , doNothingAttributes )) {
92
- throw DynamoDbEncryptionException .builder ()
93
- .message (String .format (
94
- "Cannot use @DynamoDbEncryptionDoNothing and @DynamoDbEncryptionSignOnly on same attribute. Found on Table Name: %s" ,
95
- tableName ))
96
- .build ();
97
- } else if (!Collections .disjoint (signOnlyAttributes , signAndIncludeAttributes )) {
98
- throw DynamoDbEncryptionException .builder ()
99
- .message (String .format (
100
- "Cannot use @DynamoDbEncryptionSignAndIncludeInEncryptionContext and @DynamoDbEncryptionSignOnly on same attribute. Found on Table Name: %s" ,
101
- tableName ))
102
- .build ();
103
- } else if (!Collections .disjoint (doNothingAttributes , signAndIncludeAttributes )) {
104
- throw DynamoDbEncryptionException .builder ()
105
- .message (String .format (
106
- "Cannot use @DynamoDbEncryptionSignAndIncludeInEncryptionContext and @DynamoDbEncryptionDoNothing on same attribute. Found on Table Name: %s" ,
107
- tableName ))
108
- .build ();
109
- }
110
-
111
129
List <String > attributeNames = topTableSchema .attributeNames ();
130
+
131
+ Map <String , CryptoAction > actions = new HashMap <>();
112
132
StringBuilder path = new StringBuilder ();
113
133
path .append (tableName ).append ("." );
114
134
for (String attributeName : attributeNames ) {
115
135
if (tableKeys .contains (attributeName )) {
116
- if (signAndIncludeAttributes .isEmpty ()) {
136
+ if (signAndIncludeAttributes .isEmpty ()) {
137
+ validateAttributeUsage (tableName , attributeName , "a primary key" , Optional .empty (), Optional .of (signAndIncludeAttributes ), Optional .of (doNothingAttributes ));
117
138
actions .put (attributeName , CryptoAction .SIGN_ONLY );
118
- } else {
139
+ } else {
140
+ validateAttributeUsage (tableName , attributeName , "a primary key" , Optional .of (signOnlyAttributes ), Optional .empty (), Optional .of (doNothingAttributes ));
119
141
actions .put (attributeName , CryptoAction .SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT );
120
- }
142
+ }
121
143
} else if (keyAttributes .contains (attributeName )) {
144
+ validateAttributeUsage (tableName , attributeName , "an index key" , Optional .empty (), Optional .of (signAndIncludeAttributes ), Optional .of (doNothingAttributes ));
122
145
actions .put (attributeName , CryptoAction .SIGN_ONLY );
123
146
} else if (signOnlyAttributes .contains (attributeName )) {
147
+ validateAttributeUsage (tableName , attributeName , "@DynamoDbEncryptionSignOnly" , Optional .empty (), Optional .of (signAndIncludeAttributes ), Optional .of (doNothingAttributes ));
124
148
actions .put (attributeName , CryptoAction .SIGN_ONLY );
125
149
} else if (signAndIncludeAttributes .contains (attributeName )) {
150
+ validateAttributeUsage (tableName , attributeName , "@DynamoDbEncryptionSignAndIncludeInEncryptionContext" , Optional .of (signOnlyAttributes ), Optional .empty (), Optional .of (doNothingAttributes ));
126
151
actions .put (attributeName , CryptoAction .SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXT );
127
152
} else if (doNothingAttributes .contains (attributeName )) {
153
+ validateAttributeUsage (tableName , attributeName , "@DynamoDbEncryptionDoNothing" , Optional .of (signOnlyAttributes ), Optional .of (signAndIncludeAttributes ), Optional .empty ());
128
154
actions .put (attributeName , CryptoAction .DO_NOTHING );
129
155
} else {
130
156
// non-key attributes are ENCRYPT_AND_SIGN unless otherwise annotated
@@ -134,12 +160,101 @@ private static DynamoDbTableEncryptionConfig getTableConfig(
134
160
// Detect Encryption Flags that are Ignored b/c they are in a Nested Class
135
161
scanForIgnoredEncryptionTags (topTableSchema , attributeName , path );
136
162
}
163
+ return actions ;
164
+ }
165
+
166
+ // given action maps from multiple tables, merge them into one
167
+ // we throw an error if the one attribute is given two different actions
168
+ private static Map <String , CryptoAction > mergeActions (List <Map <String , CryptoAction >> actionList )
169
+ {
170
+ // most common case
171
+ if (actionList .size () == 1 ) {
172
+ return actionList .get (0 );
173
+ }
174
+
175
+ // Gather set of all attributes
176
+ HashSet <String > attributes = new HashSet <>();
177
+ for (Map <String , CryptoAction > config : actionList ) {
178
+ attributes .addAll (config .keySet ());
179
+ }
180
+
181
+ // for each attribute, ensure that everyone agrees on its action
182
+ Map <String , CryptoAction > actions = new HashMap <>();
183
+ for (String attr : attributes ) {
184
+ Optional <CryptoAction > action = Optional .empty ();
185
+ for (Map <String , CryptoAction > config : actionList ) {
186
+ CryptoAction act = config .get (attr );
187
+ if (act != null ) {
188
+ if (action .isPresent ()) {
189
+ if (!action .get ().equals (act )) {
190
+ throw DynamoDbEncryptionException .builder ()
191
+ .message (String .format (
192
+ "Attribute %s set to %s in one table and %s in another." ,
193
+ attr , action .get (), act ))
194
+ .build ();
195
+ }
196
+ } else {
197
+ action = Optional .of (act );
198
+ }
199
+ }
200
+ }
201
+ actions .put (attr , action .get ());
202
+ }
203
+ return actions ;
204
+ }
205
+
206
+ // return the partition key name
207
+ // throw an error if two schemas disagree
208
+ private static String getPartitionKeyName (List <TableSchema <?>> schemas )
209
+ {
210
+ String partitionName = schemas .get (0 ).tableMetadata ().primaryPartitionKey ();
211
+ for (TableSchema <?> schema : schemas ) {
212
+ String part = schema .tableMetadata ().primaryPartitionKey ();
213
+ if (!partitionName .equals (part )) {
214
+ throw DynamoDbEncryptionException .builder ()
215
+ .message (String .format (
216
+ "Primary Key set to %s in one table and %s in another." ,
217
+ partitionName , part ))
218
+ .build ();
219
+ }
220
+ }
221
+ return partitionName ;
222
+ }
223
+
224
+ // return the sort key name
225
+ // throw an error if two schemas disagree
226
+ private static Optional <String > getSortKeyName (List <TableSchema <?>> schemas )
227
+ {
228
+ Optional <String > sortName = schemas .get (0 ).tableMetadata ().primarySortKey ();
229
+ for (TableSchema <?> schema : schemas ) {
230
+ Optional <String > sort = schema .tableMetadata ().primarySortKey ();
231
+ if (!sortName .equals (sort )) {
232
+ throw DynamoDbEncryptionException .builder ()
233
+ .message (String .format (
234
+ "Primary Key set to %s in one table and %s in another." ,
235
+ sortName , sort ))
236
+ .build ();
237
+ }
238
+ }
239
+ return sortName ;
240
+ }
241
+
242
+ // Convert enhanced client config to regular config
243
+ private static DynamoDbTableEncryptionConfig getTableConfig (
244
+ final DynamoDbEnhancedTableEncryptionConfig configWithSchema ,
245
+ final String tableName
246
+ ) {
247
+ List <Map <String , CryptoAction >> actionList = new ArrayList <>();
248
+ for (TableSchema <?> schema : configWithSchema .schemaOnEncrypt ()) {
249
+ actionList .add (getActionsFromSchema (tableName , schema ));
250
+ }
251
+ Map <String , CryptoAction > actions = mergeActions (actionList );
137
252
138
253
DynamoDbTableEncryptionConfig .Builder builder = DynamoDbTableEncryptionConfig .builder ();
139
- String partitionName = topTableSchema . tableMetadata (). primaryPartitionKey ( );
254
+ String partitionName = getPartitionKeyName ( configWithSchema . schemaOnEncrypt () );
140
255
builder = builder .partitionKeyName (partitionName );
141
256
142
- Optional <String > sortName = topTableSchema . tableMetadata (). primarySortKey ( );
257
+ Optional <String > sortName = getSortKeyName ( configWithSchema . schemaOnEncrypt () );
143
258
if (sortName .isPresent ()) {
144
259
builder = builder .sortKeyName (sortName .get ());
145
260
}
0 commit comments