Skip to content

Commit 28e03f3

Browse files
author
Lucas McDonald
committed
sync
1 parent 9aa4a48 commit 28e03f3

File tree

4 files changed

+129
-32
lines changed

4 files changed

+129
-32
lines changed

DynamoDbEncryption/runtimes/python/src/aws_dbesdk_dynamodb/encrypted/table.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
DynamoDbEncryptionTransforms,
2020
)
2121
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import (
22+
DeleteItemInputTransformInput,
23+
DeleteItemOutputTransformInput,
2224
GetItemInputTransformInput,
2325
GetItemOutputTransformInput,
2426
PutItemInputTransformInput,
@@ -27,6 +29,8 @@
2729
QueryOutputTransformInput,
2830
ScanInputTransformInput,
2931
ScanOutputTransformInput,
32+
UpdateItemInputTransformInput,
33+
UpdateItemOutputTransformInput,
3034
)
3135

3236

@@ -47,8 +51,10 @@ class EncryptedTable(EncryptedBotoInterface):
4751
* ``get_item``
4852
* ``query``
4953
* ``scan``
54+
* ``delete_item``
5055
51-
The ``update_item`` operation is not currently supported. Calling this operation will raise ``NotImplementedError``.
56+
Any calls to ``update_item`` can only update unsigned attributes. If an attribute to be updated is marked as signed,
57+
this operation will raise a ``DynamoDbEncryptionTransformsException``.
5258
5359
Calling ``batch_writer()`` will return a ``BatchWriter`` that transparently encrypts batch write requests.
5460
@@ -197,18 +203,69 @@ def scan(self, **kwargs) -> dict[str, Any]:
197203
table_method=self._table.scan,
198204
)
199205

206+
def delete_item(self, **kwargs) -> dict[str, Any]:
207+
"""
208+
Delete an item from the table.
209+
210+
The input and output syntaxes match those for the boto3 DynamoDB table ``delete_item`` API:
211+
212+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/delete_item.html
213+
214+
Args:
215+
**kwargs: Keyword arguments to pass to the operation. This matches the boto3 Table ``delete_item`` request
216+
syntax.
217+
218+
Returns:
219+
dict: The response from DynamoDB. This matches the boto3 Table ``delete_item`` response syntax.
220+
Any values in ``"Attributes"`` will be decrypted locally after being read from DynamoDB.
221+
222+
"""
223+
return self._table_operation_logic(
224+
operation_input=kwargs,
225+
input_encryption_transform_method=self._transformer.delete_item_input_transform,
226+
input_encryption_transform_shape=DeleteItemInputTransformInput,
227+
input_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.delete_item_request,
228+
input_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.delete_item_request,
229+
output_encryption_transform_method=self._transformer.delete_item_output_transform,
230+
output_encryption_transform_shape=DeleteItemOutputTransformInput,
231+
output_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.delete_item_response,
232+
output_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.delete_item_response,
233+
table_method=self._table.delete_item,
234+
)
235+
200236
def update_item(self, **kwargs):
201237
"""
202-
Not implemented. Raises ``NotImplementedError``.
238+
Update an unsigned attribute in the table.
239+
240+
If the attribute is signed, this operation will raise DynamoDbEncryptionTransformsException.
241+
242+
The input and output syntaxes match those for the boto3 DynamoDB table ``update_item`` API:
243+
244+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/update_item.html
203245
204246
Args:
205-
**kwargs: Any arguments passed to this method
247+
**kwargs: Keyword arguments to pass to the operation. This matches the boto3 Table ``update_item`` request
248+
syntax.
249+
250+
Returns:
251+
dict: The response from DynamoDB. This matches the boto3 Table ``update_item`` response syntax.
206252
207253
Raises:
208-
NotImplementedError: This operation is not yet implemented
254+
DynamoDbEncryptionTransformsException: If an attribute specified in the ``UpdateExpression`` is signed.
209255
210256
"""
211-
raise NotImplementedError('"update_item" is not yet implemented')
257+
return self._table_operation_logic(
258+
operation_input=kwargs,
259+
input_encryption_transform_method=self._transformer.update_item_input_transform,
260+
input_encryption_transform_shape=UpdateItemInputTransformInput,
261+
input_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.update_item_request,
262+
input_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.update_item_request,
263+
output_encryption_transform_method=self._transformer.update_item_output_transform,
264+
output_encryption_transform_shape=UpdateItemOutputTransformInput,
265+
output_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.update_item_response,
266+
output_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.update_item_response,
267+
table_method=self._table.update_item,
268+
)
212269

213270
def batch_writer(self, overwrite_by_pkeys: list[str] | None = None) -> BatchWriter:
214271
"""

DynamoDbEncryption/runtimes/python/test/integ/encrypted/test_table.py

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22
import pytest
33

44
from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable
5+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import (
6+
DynamoDbEncryptionTransformsException,
7+
)
58

69
from ...constants import (
710
INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME,
811
INTEG_TEST_DEFAULT_TABLE_CONFIGS,
912
)
1013
from ...items import complex_item_dict, simple_item_dict
1114
from ...requests import (
15+
basic_delete_item_request_dict,
1216
basic_get_item_request_dict,
1317
basic_put_item_request_dict,
1418
basic_query_request_dict,
1519
basic_scan_request_dict,
20+
basic_update_item_request_dict_signed_attribute,
21+
basic_update_item_request_dict_unsigned_attribute,
1622
)
1723

1824

@@ -61,21 +67,37 @@ def test_item(request):
6167
return request.param
6268

6369

64-
def test_GIVEN_item_WHEN_basic_put_and_basic_get_THEN_round_trip_passes(table, test_item):
65-
"""Test put_item and get_item operations."""
66-
# Given: Simple and complex items in appropriate format for client
70+
def test_GIVEN_item_WHEN_basic_put_AND_basic_get_AND_basic_delete_THEN_round_trip_passes(table, test_item):
71+
"""Test put_item, get_item, and delete_item operations."""
72+
# Given: Valid put_item request
6773
put_item_request_dict = basic_put_item_request_dict(test_item)
68-
69-
# When: Putting and getting item
74+
# When: put_item
7075
put_response = table.put_item(**put_item_request_dict)
76+
# Then: put_item succeeds
7177
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
7278

79+
# Given: Valid get_item request for the same item
7380
get_item_request_dict = basic_get_item_request_dict(test_item)
81+
# When: get_item
7482
get_response = table.get_item(**get_item_request_dict)
7583
# Then: Simple item is encrypted and decrypted correctly
7684
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
7785
assert get_response["Item"] == put_item_request_dict["Item"]
7886

87+
# Given: Valid delete_item request for the same item
88+
delete_item_request_dict = basic_delete_item_request_dict(test_item)
89+
# When: delete_item
90+
delete_response = table.delete_item(**delete_item_request_dict)
91+
# Then: delete_item succeeds
92+
assert delete_response["ResponseMetadata"]["HTTPStatusCode"] == 200
93+
94+
# Given: Valid get_item request for the same item
95+
get_item_request_dict = basic_get_item_request_dict(test_item)
96+
# When: get_item
97+
get_response = table.get_item(**get_item_request_dict)
98+
# Then: get_item is empty (i.e. the item was deleted)
99+
assert "Item" not in get_response
100+
79101

80102
def test_GIVEN_items_WHEN_batch_write_and_get_THEN_round_trip_passes(
81103
table,
@@ -154,7 +176,6 @@ def scan_request(encrypted, test_item):
154176

155177

156178
def test_GIVEN_valid_put_and_scan_requests_WHEN_put_and_scan_THEN_round_trip_passes(table, test_item, scan_request):
157-
"""Test put_item and scan operations."""
158179
# Given: Simple and complex items in appropriate format for client
159180
put_item_request_dict = basic_put_item_request_dict(test_item)
160181
table.put_item(**put_item_request_dict)
@@ -166,24 +187,44 @@ def test_GIVEN_valid_put_and_scan_requests_WHEN_put_and_scan_THEN_round_trip_pas
166187
assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
167188

168189

169-
def test_WHEN_update_item_THEN_raises_not_implemented_error():
170-
# Given: Encrypted client and update item parameters
190+
def test_GIVEN_update_for_unsigned_attribute_WHEN_update_item_THEN_passes(table, test_item):
191+
# Given: some item is already in the table
192+
put_response = table.put_item(**basic_put_item_request_dict(test_item))
193+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
194+
195+
# Given: Valid update item request for unsigned attribute
196+
update_item_request = basic_update_item_request_dict_unsigned_attribute(test_item)
197+
171198
# When: Calling update_item
172-
with pytest.raises(NotImplementedError):
173-
encrypted_table().update_item(
174-
TableName=INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME,
175-
Key={"partition_key": "test-key", "sort_key": 1},
176-
UpdateExpression="SET attribute1 = :val",
177-
ExpressionAttributeValues={":val": {"S": "new value"}},
178-
)
179-
# Then: NotImplementedError is raised
199+
update_response = table.update_item(**update_item_request)
200+
# Then: update_item succeeds
201+
assert update_response["ResponseMetadata"]["HTTPStatusCode"] == 200
202+
203+
204+
def test_GIVEN_update_for_signed_attribute_WHEN_update_item_THEN_raises_DynamoDbEncryptionTransformsException(
205+
table, test_item, encrypted
206+
):
207+
if not encrypted:
208+
pytest.skip("Skipping negative test for plaintext client")
209+
210+
# Given: some item is already in the table
211+
put_response = table.put_item(**basic_put_item_request_dict(test_item))
212+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
213+
214+
# Given: Valid update item request for signed attribute
215+
update_item_request = basic_update_item_request_dict_signed_attribute(test_item)
216+
217+
# Then: raises DynamoDbEncryptionTransformsException
218+
with pytest.raises(DynamoDbEncryptionTransformsException):
219+
# When: Calling update_item
220+
table.update_item(**update_item_request)
180221

181222

182223
def test_WHEN_call_passthrough_method_THEN_correct_response_is_returned():
183224
"""Test that calling a passthrough method returns the correct response."""
184-
# Given: Encrypted client
185-
# When: Calling some passthrough method that does not explicitly exist on EncryptedClient,
186-
# but exists on the underlying boto3 client
187-
response = encrypted_table().meta.client.list_backups()
188-
# Then: Correct response is returned, i.e. EncryptedClient forwards the call to the underlying boto3 client
189-
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
225+
# Given: Encrypted or plaintext table
226+
# When: Calling some passthrough method that does not explicitly exist on EncryptedTable,
227+
# but exists on the underlying boto3 table
228+
response = encrypted_table().table_name
229+
# Then: Correct response is returned, i.e. EncryptedTable forwards the call to the underlying boto3 table
230+
assert response == INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME

Examples/runtimes/python/DynamoDBEncryption/src/basic_put_get_example/with_encrypted_table.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def encrypted_table_put_get_example(
110110
table_configs[dynamodb_table_name] = table_config
111111
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
112112

113-
# 5. Create the EncryptedClient
113+
# 5. Create the EncryptedTable
114114
encrypted_table = EncryptedTable(
115115
table=boto3.resource("dynamodb").Table(dynamodb_table_name),
116116
encryption_config=tables_config,
@@ -128,7 +128,6 @@ def encrypted_table_put_get_example(
128128
}
129129

130130
put_item_request = {
131-
"TableName": dynamodb_table_name,
132131
"Item": item_to_encrypt,
133132
}
134133

@@ -142,7 +141,7 @@ def encrypted_table_put_get_example(
142141
# back the original item.
143142
key_to_get = {"partition_key": "BasicPutGetExample", "sort_key": 0}
144143

145-
get_item_request = {"TableName": dynamodb_table_name, "Key": key_to_get}
144+
get_item_request = {"Key": key_to_get}
146145

147146
get_item_response = encrypted_table.get_item(**get_item_request)
148147

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
"""Test suite for the EncryptedClient example."""
3+
"""Test suite for the EncryptedTable example."""
44
import pytest
55

66
from ...src.basic_put_get_example.with_encrypted_table import encrypted_table_put_get_example
@@ -9,7 +9,7 @@
99

1010

1111
def test_encrypted_table_put_get_example():
12-
"""Test function for encrypt and decrypt using the EncryptedClient example."""
12+
"""Test function for encrypt and decrypt using the EncryptedTable example."""
1313
test_kms_key_id = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"
1414
test_dynamodb_table_name = "DynamoDbEncryptionInterceptorTestTable"
1515
encrypted_table_put_get_example(test_kms_key_id, test_dynamodb_table_name)

0 commit comments

Comments
 (0)