Skip to content

Commit 81be9b6

Browse files
m
1 parent a2bd9bb commit 81be9b6

File tree

5 files changed

+79
-205
lines changed

5 files changed

+79
-205
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@ specification_compliance_report.html
1818
/.smithy.lsp.log
1919

2020
# logs
21-
*.log
21+
*.log
22+
23+
# Performance testing artifacts
24+
*.png
25+
*.dot
26+
*.prof

DynamoDbEncryption/runtimes/python/src/aws_database_encryption_sdk/encryptor/table.py

Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ def convert_conditions_to_client_expression(condition, expression_attribute_name
165165
"""
166166

167167
# from boto3.dynamodb.conditions import ConditionExpressionBuilder
168-
from aws_database_encryption_sdk.internal.condition_expression_builder import ConditionExpressionBuilder
168+
from aws_database_encryption_sdk.internal.condition_expression_builder import InternalDBESDKDynamoDBConditionExpressionBuilder
169169

170-
a = ConditionExpressionBuilder()
170+
a = InternalDBESDKDynamoDBConditionExpressionBuilder()
171171
out = a.build_expression(condition, expression_attribute_names, expression_attribute_values)
172172
print(f'our {out=}')
173173
return out
@@ -313,45 +313,42 @@ def process_condition(condition):
313313

314314
def convert_client_expression_to_conditions(expression):
315315
"""
316-
Converts a DynamoDB client-compatible expression into a Key or Attr condition.
316+
Crypto Tools internal method to convert a DynamoDB filter/key expression to boto3 Resource tokens.
317+
DO NOT USE FOR ANY OTHER PURPOSE.
318+
This is a basic implementation for simple expressions that will fail with complex expressions.
317319
318-
:param expression: A string of the DynamoDB client expression (e.g., "AttrName = :val").
319-
:param expression_values: A dictionary of attribute values (e.g., {":val": {"N": "0"}}).
320-
:param expression_names: A dictionary of attribute names, if placeholders are used (e.g., {"#attr": "AttrName"}).
321-
:return: A boto3.dynamodb.conditions object (Key, Attr, or a combination of them).
322-
"""
320+
To extend this to support one or a few complex expressions, consider extending the existing logic.
321+
To extend this to support all expressions, consider implementing and extending the code below:
323322
323+
```
324324
from aws_database_encryption_sdk.internaldafny.generated.DynamoDBFilterExpr import default__ as filter_expr
325325
import _dafny
326326
from smithy_dafny_standard_library.internaldafny.generated import Wrappers
327327
328-
329-
330328
dafny_expr_token = filter_expr.ParseExpr(
331329
_dafny.Seq(
332330
expression
333331
),
334332
)
335-
336-
337-
338-
# print(f"{dafny_expr_token.Elements=}")
339-
# print(f"{dafny_expr_token.Elements[0].__dict__=}")
340-
341-
# dafny_expr = filter_expr.ExtractAttributes(
342-
# _dafny.Seq(
343-
# expression
344-
# ),
345-
# Wrappers.Option_Some(_dafny.Map())
346-
# )
347-
348-
# print(f"{dafny_expr.Elements=}")
349-
# print(f"{dafny_expr.Elements[0].__dict__=}")
350-
351-
# def parse_dafny_tokens(dafny_tokens):
352-
# for
353-
354-
333+
```
334+
335+
This library's generated internal Dafny code has a DynamoDB string parser.
336+
This will parse a _dafny.Seq and produce Dafny tokens for the expression.
337+
Implementing this will involve
338+
1. Mapping Dafny tokens to boto3 Resource tokens.
339+
(e.g. class Token_Between -> boto3.dynamodb.conditions.Between)
340+
2. Converting Dafny token grammar to boto3 Resource token grammar.
341+
(e.g.
342+
Dafny: [Token_Between, Token_Open, Token_Attr, Token_And, Token_Attr, Token_Close]
343+
->
344+
boto3: [Between(Attr, Attr)]
345+
)
346+
347+
:param expression: A string of the DynamoDB client expression (e.g., "AttrName = :val").
348+
:param expression_values: A dictionary of attribute values (e.g., {":val": {"N": "0"}}).
349+
:param expression_names: A dictionary of attribute names, if placeholders are used (e.g., {"#attr": "AttrName"}).
350+
:return: A boto3.dynamodb.conditions object (Key, Attr, or a combination of them).
351+
"""
355352

356353
# Recursive parser for complex expressions
357354
def parse_expression(expr_tokens):
@@ -380,7 +377,7 @@ def parse_expression(expr_tokens):
380377
value = expr_tokens[3]
381378
return Attr(attr_name).contains(value)
382379

383-
# simple contains
380+
# simple begins_with
384381
elif "BEGINS_WITH" == expr_tokens[0].upper():
385382
attr_name = expr_tokens[2]
386383
if attr_name[-1] == ",":
@@ -390,14 +387,8 @@ def parse_expression(expr_tokens):
390387

391388
# Base case: Single comparison or condition
392389
if "AND" not in [t.upper() for t in expr_tokens] and "OR" not in [t.upper() for t in expr_tokens]:
393-
# # BETWEEN operator
394-
# if "BETWEEN" in [t.upper() for t in expr_tokens]:
395-
# attr_name = expr_tokens[0]
396-
# value1 = expr_tokens[2]
397-
# value2 = expr_tokens[4]
398-
# return Key(attr_name).between(value1, value2)
399-
400-
# Simple comparison
390+
391+
# simple comparison
401392
attr_name = expr_tokens[0]
402393
operator = expr_tokens[1].upper()
403394
value = expr_tokens[2]
@@ -415,12 +406,6 @@ def parse_expression(expr_tokens):
415406
return Key(attr_name).gte(value)
416407
elif operator in ("!=", "<>"):
417408
return Attr(attr_name).ne(value)
418-
elif operator == "CONTAINS":
419-
return Attr(attr_name).contains(value)
420-
elif operator == "BEGINS_WITH":
421-
return Key(attr_name).begins_with(value)
422-
elif operator == "BEGINS_WITH":
423-
return Key(attr_name).begins_with(value)
424409
else:
425410
raise ValueError(f"Unsupported operator: {operator}")
426411

Lines changed: 41 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,214 +1,98 @@
1-
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2-
#
3-
# Licensed under the Apache License, Version 2.0 (the "License"). You
4-
# may not use this file except in compliance with the License. A copy of
5-
# the License is located at
6-
#
7-
# https://aws.amazon.com/apache2.0/
8-
#
9-
# or in the "license" file accompanying this file. This file is
10-
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11-
# ANY KIND, either express or implied. See the License for the specific
12-
# language governing permissions and limitations under the License.
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
133
import re
144
from collections import namedtuple
155

166
from boto3.exceptions import (
177
DynamoDBNeedsConditionError,
18-
DynamoDBNeedsKeyConditionError,
19-
DynamoDBOperationNotSupportedError,
208
)
219
from boto3.dynamodb.conditions import (
2210
ConditionBase,
2311
BuiltConditionExpression,
2412
AttributeBase,
25-
Key,
26-
Contains,
27-
BeginsWith,
2813
)
29-
from aws_database_encryption_sdk.transform import dict_to_ddb
3014

31-
ATTR_NAME_REGEX = re.compile(r'[^.\[\]]+(?![^\[]*\])')
3215

33-
class ConditionExpressionBuilder:
34-
"""This class is used to build condition expressions with placeholders"""
16+
class InternalDBESDKDynamoDBConditionExpressionBuilder:
17+
"""
18+
This internal class is used to transform boto3 DyamoDB conditions from
19+
Python classes to DynamoDB ConditionExpressions
20+
for use by internal DBESDK DyanmoDB operation transformers.
3521
36-
def __init__(self):
37-
self._name_count = 0
38-
self._value_count = 0
39-
self._name_placeholder = 'n'
40-
self._value_placeholder = 'v'
41-
42-
def _get_name_placeholder(self):
43-
return '#' + self._name_placeholder + str(self._name_count)
44-
45-
def _get_value_placeholder(self):
46-
return ':' + self._value_placeholder + str(self._value_count)
47-
48-
def reset(self):
49-
"""Resets the placeholder name and values"""
50-
self._name_count = 0
51-
self._value_count = 0
22+
This class is a modified version of boto3's ConditionExpressionBuilder:
23+
https://github.com/boto/boto3/blob/c5c634b53be589d6e913e6dca51ad8d6480f58c7/boto3/dynamodb/conditions.py#L304
24+
The original class is intended to interface from boto3 Table resources to boto3 clients,
25+
and has logic to replace attribute names and values with placeholders.
26+
This class is intended to interface from boto3 Table resources to internal DBESDK DyanmoDB operation transformers,
27+
which does not expect inserted placeholders.
28+
The placeholder insertion logic has been removed from this class along with its supporting logic,
29+
but this class maintains boto3's recursive ConditionExpression traversal logic.
30+
"""
5231

5332
def build_expression(
5433
self,
5534
condition,
5635
expression_attribute_names,
5736
expression_attribute_values,
58-
is_key_condition=False
5937
):
60-
"""Builds the condition expression and the dictionary of placeholders.
38+
"""Builds the condition expression.
6139
6240
:type condition: ConditionBase
63-
:param condition: A condition to be built into a condition expression
64-
string with any necessary placeholders.
65-
66-
:type is_key_condition: Boolean
67-
:param is_key_condition: True if the expression is for a
68-
KeyConditionExpression. False otherwise.
41+
:param condition: A condition to be built into a condition expression string.
6942
7043
:rtype: (string, dict, dict)
71-
:returns: Will return a string representing the condition with
72-
placeholders inserted where necessary, a dictionary of
73-
placeholders for attribute names, and a dictionary of
74-
placeholders for attribute values. Here is a sample return value:
75-
76-
('#n0 = :v0', {'#n0': 'myattribute'}, {':v1': 'myvalue'})
44+
:returns: Will return a string representing the condition expression,
45+
the original dictionary of attribute names,
46+
and the original dictionary of attribute values.
7747
"""
7848
if not isinstance(condition, ConditionBase):
7949
raise DynamoDBNeedsConditionError(condition)
80-
attribute_name_placeholders = expression_attribute_names
81-
attribute_value_placeholders = expression_attribute_values
82-
condition_expression = self._build_expression(
83-
condition,
84-
attribute_name_placeholders,
85-
attribute_value_placeholders,
86-
is_key_condition=is_key_condition,
87-
)
50+
condition_expression = self._build_expression(condition)
8851
return BuiltConditionExpression(
8952
condition_expression=condition_expression,
90-
attribute_name_placeholders=attribute_name_placeholders,
91-
attribute_value_placeholders=attribute_value_placeholders,
53+
# BuiltConditionExpression uses "placeholders" nomenclature;
54+
# this is an artifact of the original boto3 ConditionExpressionBuilder.
55+
# This class neither creates placeholders, nor even uses the provided name/values dicts.
56+
# They are only here as required arguments for the BuiltConditionExpression class.
57+
attribute_name_placeholders=expression_attribute_names,
58+
attribute_value_placeholders=expression_attribute_values,
9259
)
9360

9461
def _build_expression(
9562
self,
9663
condition,
97-
attribute_name_placeholders,
98-
attribute_value_placeholders,
99-
is_key_condition,
10064
):
10165
expression_dict = condition.get_expression()
10266
replaced_values = []
10367
for value in expression_dict['values']:
104-
# Build the necessary placeholders for that value.
105-
# Placeholders are built for both attribute names and values.
106-
replaced_value = self._build_expression_component(
107-
value,
108-
attribute_name_placeholders,
109-
attribute_value_placeholders,
110-
condition,
111-
is_key_condition,
112-
)
68+
# Recurse for each value in the expression.
69+
replaced_value = self._build_expression_component(condition, value)
11370
replaced_values.append(replaced_value)
114-
# Fill out the expression using the operator and the
115-
# values that have been replaced with placeholders.
71+
# Fill out the expression using the operator and its recursive expressions.
11672
return expression_dict['format'].format(
11773
*replaced_values, operator=expression_dict['operator']
11874
)
11975

12076
def _build_expression_component(
12177
self,
122-
value,
123-
attribute_name_placeholders,
124-
attribute_value_placeholders,
12578
condition,
126-
is_key_condition,
79+
value,
12780
):
128-
has_grouped_values = condition.has_grouped_values
12981
# Continue to recurse if the value is a ConditionBase in order
13082
# to extract out all parts of the expression.
13183
if isinstance(value, ConditionBase):
13284
return self._build_expression(
13385
value,
134-
attribute_name_placeholders,
135-
attribute_value_placeholders,
136-
is_key_condition,
13786
)
13887
# If it is not a ConditionBase, we can recurse no further.
139-
# So we check if it is an attribute and add placeholders for
140-
# its name
88+
# If it's an attribute, insert its name.
14189
elif isinstance(value, AttributeBase):
142-
if is_key_condition and not isinstance(value, Key):
143-
raise DynamoDBNeedsKeyConditionError(
144-
f'Attribute object {value.name} is of type {type(value)}. '
145-
f'KeyConditionExpression only supports Attribute objects '
146-
f'of type Key'
147-
)
148-
# contains expressions can have attribute values where the rest of this class expects an attribute name
149-
if isinstance(condition, Contains) or isinstance(condition, BeginsWith):
150-
return value.name
151-
return self._build_name_placeholder(
152-
value, attribute_name_placeholders
153-
)
154-
# If it is anything else, we treat it as a value and thus placeholders
155-
# are needed for the value.
156-
else:
157-
if isinstance(condition, Contains) or isinstance(condition, BeginsWith):
158-
return value
159-
return self._build_value_placeholder(
160-
value, attribute_value_placeholders, has_grouped_values
161-
)
162-
163-
def _build_name_placeholder(self, value, attribute_name_placeholders):
164-
return value.name
165-
attribute_name = value.name
166-
# Figure out which parts of the attribute name that needs replacement.
167-
attribute_name_parts = ATTR_NAME_REGEX.findall(attribute_name)
168-
169-
# Add a temporary placeholder for each of these parts.
170-
placeholder_format = ATTR_NAME_REGEX.sub('%s', attribute_name)
171-
str_format_args = []
172-
for part in attribute_name_parts:
173-
if part[0] == "#":
174-
str_format_args.append(part)
175-
else:
176-
name_placeholder = self._get_name_placeholder()
177-
self._name_count += 1
178-
str_format_args.append(name_placeholder)
179-
# Add the placeholder and value to dictionary of name placeholders.
180-
attribute_name_placeholders[name_placeholder] = part
181-
# Replace the temporary placeholders with the designated placeholders.
182-
return placeholder_format % tuple(str_format_args)
183-
184-
def _build_value_placeholder(
185-
self, value, attribute_value_placeholders, has_grouped_values=False
186-
):
187-
return value
188-
# If the values are grouped, we need to add a placeholder for
189-
# each element inside of the actual value.
190-
if has_grouped_values:
191-
placeholder_list = []
192-
for v in value:
193-
if v[0] != ":":
194-
value_placeholder = self._get_value_placeholder()
195-
self._value_count += 1
196-
placeholder_list.append(value_placeholder)
197-
attribute_value_placeholders[value_placeholder] = list(dict_to_ddb({value_placeholder: v}).values())[0]
198-
else:
199-
placeholder_list.append(v)
200-
# Assuming the values are grouped by parenthesis.
201-
# IN is the currently the only one that uses this so it maybe
202-
# needed to be changed in future.
203-
return '(' + ', '.join(placeholder_list) + ')'
204-
# Otherwise, treat the value as a single value that needs only
205-
# one placeholder.
90+
return value.name
91+
# If it is anything else, we treat it as a value.
20692
else:
207-
if value[0] != ":":
208-
value_placeholder = self._get_value_placeholder()
209-
self._value_count += 1
210-
# Store DDB JSON
211-
attribute_value_placeholders[value_placeholder] = list(dict_to_ddb({value_placeholder: value}).values())[0]
212-
return value_placeholder
213-
else:
214-
return value
93+
if condition.has_grouped_values:
94+
# Assuming the values are grouped by parenthesis.
95+
# IN is the currently the only one that uses this so it maybe
96+
# needed to be changed in future.
97+
return '(' + ', '.join(value) + ')'
98+
return value
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
[{"Four":{"B":"AAD7KnQVcxeoznB8Uajn1/ci"},"Eight":{"B":"AQFyJkK+IlXwCh5HjKgcxD4IrU/5LrgoEHGlPHEW+FoOUy2itIvoz3sWHQ6P"},"aws_dbe_foot":{"B":"FP0wSCe43V/IQSF1QnjoKb7BZ+1mtMMgjw9WU8Mr85Cvci1ILlmE+0M9W1uTYlKaMGUCMQCOOxPNukDxJOrEmVTc+A8LS6VwgeuHs6j1G40av6Da6yhZp537pJ7szQDJT2Qwm2sCMALSYxpS0DelfE+EcIKHYC1u1IAP39jpm0Gpdg27xA9VVrNI3wMEGeNBpsHT9Sc0cw=="},"Five":{"B":"AARZvtbqCnxPCuSprYQAkvEc2w=="},"Nine":{"NS":["1.2","5.6","3.4"]},"aws_dbe_head":{"B":"AQGkiu+2MSydagmI8ev7R/TB4pQelSyJqkfeaj+NWNSDZgALZXNlZWVlc2VlZXMAAQAVYXdzLWNyeXB0by1wdWJsaWMta2V5AERBOUdxSmVKcFVGYkIwZFFNdTBia21va2hOYVFpZDZHNHE3d0JJVmkzREtuQWhHcUdpSHhiekw0eGNSWlE0dng0SEE9PQEAEWF3cy1rbXMtaGllcmFyY2h5ACRiZDM4NDJmZi0zMDc2LTQwOTItOTkxOC00Mzk1NzMwMDUwYjgAjGp4FJg4vlOWI56XAVh1dBUD15kNku5p2hi9vUfyuKaoIS1Dsjp4FNBopOWUgzQUmnEtudRJ1sITyMTE5hvM+C563lmLhlElIry0Gjvpzhij7bVCcp+GHKy3mX/2DDjRtTsoSUj12cHjX6L0AGByYna0mi+r/Oaqplsnqwwa9GAOVGDfbYwQknuDGQVDFV0MPRjq+xQSaTy5dk3kTKE4nsBju/2I6fmr0WWICdo="},"One":{"B":"AAEDqol3IBdhozgzrIwL21DGO2KhfNNOYPlB"},"Two":{"B":"AAKoVoKAsb1K/szCoYnh9C8ymAc="},"Three":{"B":"//8T1YauYR83OvCff6vkgz4vp+oqREBHgdgZGSHfjmjQWAKT"},"Ten":{"B":"Af+Pt42+cJvN2mV57lSYA1aY0WeaaLswO3k="},"Six":{"L":[{"S":"one"},{"S":"two"},{"S":"three"},{"L":[{"N":"1"},{"N":"2"},{"N":"3"},{"N":"4"}]}]},"Seven":{"B":"AgD+307HeS+GON5z56zJY/d5CAkn28Jb8Y6aYKK3tCkfpIQGMH1CKqLMeuQx5s+EhZtQxrxkcxSs5hi/TrLORaeqnE9sQ9tYStzY7mUVAm3CC7XdEI9XQl42RdXnuPm6PV6Tyui5lYz4Wl7OrmGouCfDbb3Rxg=="},"RecNum":{"N":"1"}}]
1+
[{"Seven":{"B":"AgDuPox5uciWu4KcxjOWH70WuPi1LmfrjDbRvB4JBltWNlQ+ZMwZNFFuygTKKFJcvJKzf8xkYwmyL4KB9t66k0t9/b4oIueKwWt4S3cJp7n0j9J7Ex151mzVORORHfZccrp1VE2wqekmx6wNGoKWbpAfsNKdog=="},"Nine":{"NS":["1.2","5.6","3.4"]},"Two":{"B":"AALTo//JqYFGzpXZjLi9/boI/Iw="},"aws_dbe_foot":{"B":"Ky6G1rZTgPqEm5G+KvQ7ASphPT9qDkeX0v3joJ8XNXDwoIqJ86PAO/NbxMzIiiAgMGUCMQDIJFHG/axnN9AvP6PKDWOabV6DAyToFo+iEMy0eCzGG9J72CdC4EfFP1cRl1785xgCMEsNFyTZTaxa/sjiVhIvmZV5Uhupk2/isaJen2RAbWW/ZETmVitchMQaxmTYogCsiQ=="},"Eight":{"B":"AQHUy33vIXcZCyGkrTRCUiwpMUaKaAPu0TGtTJ5mxTNyovy+emH3Df2r53N3"},"Five":{"B":"AASj4Votzqpoab3+8JBwzAnq4Q=="},"Ten":{"B":"Af8O9PgVy7IDD3EHbOSFVrnJNW+ynXqVUrE="},"Four":{"B":"AAC7GjM4xcx8rJnuNmF6L5lJ"},"RecNum":{"N":"1"},"aws_dbe_head":{"B":"AQEOAkZTb1Zn17gyi9iADAhIIUEhb210InRhHgFqKd4tXwALZXNlZWVlc2VlZXMAAQAVYXdzLWNyeXB0by1wdWJsaWMta2V5AERBNXpsSmJxRlE5NExMTlJIbWVJSjNJZGtlVTZrQlg0S3BWRS9JcHg4UDdVK01aMTMyTlA1ZGdkSWFEN1p3djVJRFE9PQEAEWF3cy1rbXMtaGllcmFyY2h5ACRiZDM4NDJmZi0zMDc2LTQwOTItOTkxOC00Mzk1NzMwMDUwYjgAjI8A2kl2IAJaI9dcE9W4vXkEmhrs/WDnLQEmZSw02vW7F6TdRFPPYNZb9Ks63Qlxpmtpv/LDjoRTKs3BhqbuOlSHnrTzvbJbeVK2lJDpzhij7bVCcp+GHKy3mX/2WsVWKfS8KjN11RkGbggimnxHSPt9mqOCNN35646sNkQRlSH8eWaFsFSuFcKhtk0jewuo5Q7QaLshZdZ1o1cBFWblFvEDjz9CFrEgwFS/u5c="},"One":{"B":"AAFBaG04EadjwvMrd3MnQG2d0zWAZyN52OrV"},"Six":{"L":[{"S":"one"},{"S":"two"},{"S":"three"},{"L":[{"N":"1"},{"N":"2"},{"N":"3"},{"N":"4"}]}]},"Three":{"B":"//+HOWAVE8dd3spet/L8LQIKOyxbaA3pxGyV5yzBS+RXzEhg"}}]

TestVectors/runtimes/python/decrypt.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)