Skip to content

Commit 2d3b6ab

Browse files
author
Lucas McDonald
committed
sync
1 parent 0e4fd02 commit 2d3b6ab

File tree

11 files changed

+975
-0
lines changed

11 files changed

+975
-0
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""High-level helper class to provide an encrypting wrapper for boto3 DynamoDB tables."""
4+
from collections.abc import Callable
5+
from typing import Any
6+
7+
from boto3.dynamodb.table import BatchWriter
8+
from boto3.resources.base import ServiceResource
9+
10+
from aws_dbesdk_dynamodb.encrypted.boto3_interface import EncryptedBotoInterface
11+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
12+
from aws_dbesdk_dynamodb.internal.client_to_resource import ClientShapeToResourceShapeConverter
13+
from aws_dbesdk_dynamodb.internal.resource_to_client import ResourceShapeToClientShapeConverter
14+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.models import (
15+
DynamoDbTablesEncryptionConfig,
16+
)
17+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import (
18+
DynamoDbEncryptionTransforms,
19+
)
20+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import (
21+
GetItemInputTransformInput,
22+
GetItemOutputTransformInput,
23+
PutItemInputTransformInput,
24+
PutItemOutputTransformInput,
25+
QueryInputTransformInput,
26+
QueryOutputTransformInput,
27+
ScanInputTransformInput,
28+
ScanOutputTransformInput,
29+
)
30+
31+
32+
class EncryptedTable(EncryptedBotoInterface):
33+
"""
34+
Wrapper for a boto3 DynamoDB table that transparently encrypts/decrypts items.
35+
36+
This class implements the complete boto3 DynamoDB table API, allowing it to serve as a
37+
drop-in replacement that transparently handles encryption and decryption of items.
38+
39+
The API matches the standard boto3 DynamoDB table interface:
40+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table.html
41+
42+
This class will encrypt/decrypt items for the following operations:
43+
* put_item
44+
* get_item
45+
* query
46+
* scan
47+
48+
Calling batch_writer() will return a BatchWriter that transparently encrypts batch write requests.
49+
50+
Any other operations on this class will defer to the underlying boto3 DynamoDB Table's implementation
51+
and will not be encrypted/decrypted.
52+
53+
Note: The update_item operation is not currently supported. Calling this operation will raise NotImplementedError.
54+
"""
55+
56+
def __init__(
57+
self,
58+
*,
59+
table: ServiceResource,
60+
encryption_config: DynamoDbTablesEncryptionConfig,
61+
):
62+
"""
63+
Create an EncryptedTable object.
64+
65+
Args:
66+
table (ServiceResource): Initialized boto3 DynamoDB table
67+
encryption_config (DynamoDbTablesEncryptionConfig): Initialized DynamoDbTablesEncryptionConfig
68+
69+
"""
70+
self._table = table
71+
self._encryption_config = encryption_config
72+
self._transformer = DynamoDbEncryptionTransforms(config=encryption_config)
73+
self._client_shape_to_resource_shape_converter = ClientShapeToResourceShapeConverter()
74+
self._resource_shape_to_client_shape_converter = ResourceShapeToClientShapeConverter(
75+
table_name=self._table.table_name
76+
)
77+
78+
def put_item(self, **kwargs) -> dict[str, Any]:
79+
"""
80+
Put a single item to the table. Encrypts the item before writing to DynamoDB.
81+
82+
The parameters and return value match the boto3 DynamoDB table put_item API:
83+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/put_item.html
84+
85+
Args:
86+
**kwargs: Keyword arguments to pass to the operation. These match the boto3 put_item API parameters.
87+
The "Item" field will be encrypted locally before being written to DynamoDB.
88+
89+
Returns:
90+
dict: The response from DynamoDB. This matches the boto3 put_item API response.
91+
92+
"""
93+
return self._table_operation_logic(
94+
operation_input=kwargs,
95+
input_encryption_transform_method=self._transformer.put_item_input_transform,
96+
input_encryption_transform_shape=PutItemInputTransformInput,
97+
input_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.put_item_request,
98+
input_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.put_item_request,
99+
output_encryption_transform_method=self._transformer.put_item_output_transform,
100+
output_encryption_transform_shape=PutItemOutputTransformInput,
101+
output_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.put_item_response,
102+
output_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.put_item_response,
103+
table_method=self._table.put_item,
104+
)
105+
106+
def get_item(self, **kwargs) -> dict[str, Any]:
107+
"""
108+
Get a single item from the table. Decrypts the item after reading from DynamoDB.
109+
110+
The parameters and return value match the boto3 DynamoDB table get_item API:
111+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/get_item.html
112+
113+
Args:
114+
**kwargs: Keyword arguments to pass to the operation. These match the boto3 get_item API parameters.
115+
116+
Returns:
117+
dict: The response from DynamoDB. This matches the boto3 get_item API response.
118+
The "Item" field will be decrypted locally after being read from DynamoDB.
119+
120+
"""
121+
return self._table_operation_logic(
122+
operation_input=kwargs,
123+
input_encryption_transform_method=self._transformer.get_item_input_transform,
124+
input_encryption_transform_shape=GetItemInputTransformInput,
125+
input_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.get_item_request,
126+
input_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.get_item_request,
127+
output_encryption_transform_method=self._transformer.get_item_output_transform,
128+
output_encryption_transform_shape=GetItemOutputTransformInput,
129+
output_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.get_item_response,
130+
output_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.get_item_response,
131+
table_method=self._table.get_item,
132+
)
133+
134+
def query(self, **kwargs) -> dict[str, Any]:
135+
"""
136+
Query items from the table or index. Decrypts any returned items.
137+
138+
The parameters and return value match the boto3 DynamoDB table query API:
139+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/query.html
140+
141+
Args:
142+
**kwargs: Keyword arguments to pass to the operation. These match the boto3 query API parameters.
143+
144+
Returns:
145+
dict: The response from DynamoDB. This matches the boto3 query API response.
146+
The "Items" field will be decrypted locally after being read from DynamoDB.
147+
148+
"""
149+
return self._table_operation_logic(
150+
operation_input=kwargs,
151+
input_encryption_transform_method=self._transformer.query_input_transform,
152+
input_encryption_transform_shape=QueryInputTransformInput,
153+
input_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.query_request,
154+
input_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.query_request,
155+
output_encryption_transform_method=self._transformer.query_output_transform,
156+
output_encryption_transform_shape=QueryOutputTransformInput,
157+
output_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.query_response,
158+
output_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.query_response,
159+
table_method=self._table.query,
160+
)
161+
162+
def scan(self, **kwargs) -> dict[str, Any]:
163+
"""
164+
Scan the entire table or index. Decrypts any returned items.
165+
166+
The parameters and return value match the boto3 DynamoDB table scan API:
167+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/scan.html
168+
169+
Args:
170+
**kwargs: Keyword arguments to pass to the operation. These match the boto3 scan API parameters.
171+
172+
Returns:
173+
dict: The response from DynamoDB. This matches the boto3 scan API response.
174+
The "Items" field will be decrypted locally after being read from DynamoDB.
175+
176+
"""
177+
return self._table_operation_logic(
178+
operation_input=kwargs,
179+
input_encryption_transform_method=self._transformer.scan_input_transform,
180+
input_encryption_transform_shape=ScanInputTransformInput,
181+
input_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.scan_request,
182+
input_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.scan_request,
183+
output_encryption_transform_method=self._transformer.scan_output_transform,
184+
output_encryption_transform_shape=ScanOutputTransformInput,
185+
output_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.scan_response,
186+
output_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.scan_response,
187+
table_method=self._table.scan,
188+
)
189+
190+
def update_item(self, **kwargs):
191+
"""
192+
Not implemented. Raises NotImplementedError.
193+
194+
Args:
195+
**kwargs: Any arguments passed to this method
196+
197+
Raises:
198+
NotImplementedError: This operation is not yet implemented
199+
200+
"""
201+
raise NotImplementedError('"update_item" is not yet implemented')
202+
203+
def batch_writer(self, overwrite_by_pkeys: list[str] | None = None) -> BatchWriter:
204+
"""
205+
Create a batch writer object that will transparently encrypt requests to DynamoDB.
206+
207+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/batch_writer.html
208+
209+
Args:
210+
overwrite_by_pkeys: De-duplicate request items in buffer if match new request
211+
item on specified primary keys. i.e ``["partition_key1", "sort_key2", "sort_key3"]``
212+
213+
Returns:
214+
BatchWriter: A batch writer that will transparently encrypt requests
215+
216+
"""
217+
encrypted_client = EncryptedClient(
218+
client=self._table.meta.client,
219+
encryption_config=self._encryption_config,
220+
# The boto3 client comes from the underlying table, which is a ServiceResource.
221+
# ServiceResource clients expect standard dictionaries, not DynamoDB JSON.
222+
expect_standard_dictionaries=True,
223+
)
224+
return BatchWriter(table_name=self._table.name, client=encrypted_client, overwrite_by_pkeys=overwrite_by_pkeys)
225+
226+
def _table_operation_logic(
227+
self,
228+
*,
229+
operation_input: dict[str, Any],
230+
input_encryption_transform_method: Callable,
231+
input_encryption_transform_shape: Any,
232+
input_resource_to_client_shape_transform_method: Callable,
233+
input_client_to_resource_shape_transform_method: Callable,
234+
output_encryption_transform_method: Callable,
235+
output_encryption_transform_shape: Any,
236+
output_resource_to_client_shape_transform_method: Callable,
237+
output_client_to_resource_shape_transform_method: Any,
238+
table_method: Callable,
239+
) -> dict[str, Any]:
240+
"""
241+
Interface between user-supplied input, encryption/decryption transformers, and boto3 Tables.
242+
243+
Args:
244+
operation_input: User-supplied input to the operation
245+
input_encryption_transform_method: The method to transform the input for encryption/decryption
246+
input_encryption_transform_shape: The shape to supply to the input encryption/decryption transform
247+
input_resource_to_client_shape_transform_method: Method to transform resource-formatted input shape
248+
to client-formattted input shape
249+
input_client_to_resource_shape_transform_method: Method to transform client-formatted input shape
250+
to resource-formattted input shape
251+
output_encryption_transform_method: The method to transform the output for encryption/decryption
252+
output_encryption_transform_shape: The shape to supply to the output encryption/decryption transform
253+
output_resource_to_client_shape_transform_method: Method to transform resource-formatted output shape
254+
to client-formattted output shape
255+
output_client_to_resource_shape_transform_method: Method to transform client-formatted output shape
256+
to resource-formattted output shape
257+
table_method: The underlying table method to call
258+
259+
Returns:
260+
dict: The transformed response from DynamoDB
261+
262+
"""
263+
# EncryptedTable inputs are formatted as standard dictionaries, but DBESDK transformations expect DynamoDB JSON.
264+
# Convert from standard dictionaries to DynamoDB JSON.
265+
input_transform_input = input_resource_to_client_shape_transform_method(operation_input)
266+
267+
# Apply DBESDK transformation to the input
268+
input_transform_output = input_encryption_transform_method(
269+
input_encryption_transform_shape(sdk_input=input_transform_input)
270+
).transformed_input
271+
272+
# The encryption transformation result is formatted in DynamoDB JSON,
273+
# but the underlying boto3 table expects standard dictionaries.
274+
# Convert from DynamoDB JSON to standard dictionaries.
275+
sdk_input = input_client_to_resource_shape_transform_method(input_transform_output)
276+
277+
sdk_output = table_method(**sdk_input)
278+
279+
# Table outputs are formatted as standard dictionaries, but DBESDK transformations expect DynamoDB JSON.
280+
# Convert from standard dictionaries to DynamoDB JSON.
281+
output_transform_input = output_resource_to_client_shape_transform_method(sdk_output)
282+
283+
# Apply DBESDK transformation to boto3 output
284+
output_transform_output = output_encryption_transform_method(
285+
output_encryption_transform_shape(
286+
original_input=input_transform_input,
287+
sdk_output=output_transform_input,
288+
)
289+
).transformed_output
290+
291+
# EncryptedTable outputs are formatted as standard dictionaries,
292+
# but DBESDK transformations provide DynamoDB JSON.
293+
# Convert from DynamoDB JSON to standard dictionaries.
294+
dbesdk_response = output_client_to_resource_shape_transform_method(output_transform_output)
295+
296+
# Copy any missing fields from the SDK output to the response (e.g. `ConsumedCapacity`)
297+
dbesdk_response = self._copy_sdk_response_to_dbesdk_response(sdk_output, dbesdk_response)
298+
299+
return dbesdk_response
300+
301+
@property
302+
def _boto_client_attr_name(self) -> str:
303+
"""
304+
Name of the attribute containing the underlying boto3 client.
305+
306+
Returns:
307+
str: '_table'
308+
309+
"""
310+
return "_table"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Integration tests for encrypted interfaces.
2+
3+
These integration tests verify that encrypted boto3 interfaces behave as drop-in replacements for plaintext boto3 interfaces.
4+
5+
Each test runs with both a plaintext client and an encrypted client, using the same request parameters and expecting the same response.
6+
7+
This validates that encrypted clients expect the same input shapes as plaintext clients
8+
and encrypted clients return the same output shapes as plaintext clients.
9+
10+
This guarantees that users can substitute encrypted interfaces without modifying their application logic.

0 commit comments

Comments
 (0)