Skip to content

Commit ab611e1

Browse files
author
Lucas McDonald
committed
m
1 parent b6c74c1 commit ab611e1

File tree

2 files changed

+126
-27
lines changed
  • DynamoDbEncryption/runtimes/python

2 files changed

+126
-27
lines changed

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

Lines changed: 63 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,11 @@ class EncryptedTable(EncryptedBotoInterface):
4751
* ``get_item``
4852
* ``query``
4953
* ``scan``
54+
* ``delete_item``
55+
* ``update_item``
5056
51-
The ``update_item`` operation is not currently supported. Calling this operation will raise ``NotImplementedError``.
57+
Any calls to ``update_item`` can only update unsigned attributes. If an attribute to be updated is marked as signed,
58+
this operation will raise a ``DynamoDbEncryptionTransformsException``.
5259
5360
Calling ``batch_writer()`` will return a ``BatchWriter`` that transparently encrypts batch write requests.
5461
@@ -197,18 +204,69 @@ def scan(self, **kwargs) -> dict[str, Any]:
197204
table_method=self._table.scan,
198205
)
199206

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

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

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

0 commit comments

Comments
 (0)