Skip to content

Commit fc5f30c

Browse files
feat: Python EncryptedResource, EncryptedTablesCollectionManager, and tests (#1904)
1 parent 41c4d92 commit fc5f30c

File tree

12 files changed

+1137
-0
lines changed

12 files changed

+1137
-0
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""High-level helper classes to provide encrypting wrappers for boto3 DynamoDB resources."""
4+
from collections.abc import Callable, Generator
5+
from copy import deepcopy
6+
from typing import Any
7+
8+
from boto3.resources.base import ServiceResource
9+
from boto3.resources.collection import CollectionManager
10+
11+
from aws_dbesdk_dynamodb.encrypted.boto3_interface import EncryptedBotoInterface
12+
from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable
13+
from aws_dbesdk_dynamodb.internal.client_to_resource import ClientShapeToResourceShapeConverter
14+
from aws_dbesdk_dynamodb.internal.resource_to_client import ResourceShapeToClientShapeConverter
15+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.models import (
16+
DynamoDbTablesEncryptionConfig,
17+
)
18+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import (
19+
DynamoDbEncryptionTransforms,
20+
)
21+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import (
22+
BatchGetItemInputTransformInput,
23+
BatchGetItemOutputTransformInput,
24+
BatchWriteItemInputTransformInput,
25+
BatchWriteItemOutputTransformInput,
26+
)
27+
28+
29+
class EncryptedTablesCollectionManager(EncryptedBotoInterface):
30+
"""
31+
Collection manager that yields EncryptedTable objects.
32+
33+
The API matches boto3's tables collection manager interface:
34+
35+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/tables.html
36+
37+
All operations on this class will yield ``EncryptedTable`` objects.
38+
"""
39+
40+
def __init__(
41+
self,
42+
*,
43+
collection: CollectionManager,
44+
encryption_config: DynamoDbTablesEncryptionConfig,
45+
):
46+
"""
47+
Create an ``EncryptedTablesCollectionManager`` object.
48+
49+
Args:
50+
collection (CollectionManager): Pre-configured boto3 DynamoDB table collection manager
51+
encryption_config (DynamoDbTablesEncryptionConfig): Initialized DynamoDbTablesEncryptionConfig
52+
53+
"""
54+
self._collection = collection
55+
self._encryption_config = encryption_config
56+
57+
def all(self) -> Generator[EncryptedTable, None, None]:
58+
"""
59+
Create an iterable of all EncryptedTable resources in the collection.
60+
61+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/tables.html#DynamoDB.ServiceResource.all
62+
63+
Returns:
64+
Generator[EncryptedTable, None, None]: An iterable of EncryptedTable objects
65+
66+
"""
67+
yield from self._transform_table(self._collection.all)
68+
69+
def filter(self, **kwargs) -> Generator[EncryptedTable, None, None]:
70+
"""
71+
Create an iterable of all EncryptedTable resources in the collection filtered by kwargs passed to method.
72+
73+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/tables.html#filter
74+
75+
Returns:
76+
Generator[EncryptedTable, None, None]: An iterable of EncryptedTable objects
77+
78+
"""
79+
yield from self._transform_table(self._collection.filter, **kwargs)
80+
81+
def limit(self, **kwargs) -> Generator[EncryptedTable, None, None]:
82+
"""
83+
Create an iterable of all EncryptedTable resources in the collection filtered by kwargs passed to method.
84+
85+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/tables.html#limit
86+
87+
Returns:
88+
Generator[EncryptedTable, None, None]: An iterable of EncryptedTable objects
89+
90+
"""
91+
yield from self._transform_table(self._collection.limit, **kwargs)
92+
93+
def page_size(self, **kwargs) -> Generator[EncryptedTable, None, None]:
94+
"""
95+
Create an iterable of all EncryptedTable resources in the collection.
96+
97+
This limits the number of items returned by each service call by the specified amount.
98+
99+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/tables.html#page_size
100+
101+
Returns:
102+
Generator[EncryptedTable, None, None]: An iterable of EncryptedTable objects
103+
104+
"""
105+
yield from self._transform_table(self._collection.page_size, **kwargs)
106+
107+
def _transform_table(
108+
self,
109+
method: Callable,
110+
**kwargs,
111+
) -> Generator[EncryptedTable, None, None]:
112+
for table in method(**kwargs):
113+
yield EncryptedTable(table=table, encryption_config=self._encryption_config)
114+
115+
@property
116+
def _boto_client_attr_name(self) -> str:
117+
"""
118+
Name of the attribute containing the underlying boto3 client.
119+
120+
Returns:
121+
str: '_collection'
122+
123+
"""
124+
return "_collection"
125+
126+
127+
class EncryptedResource(EncryptedBotoInterface):
128+
"""
129+
Wrapper for a boto3 DynamoDB resource.
130+
131+
This class implements the complete boto3 DynamoDB resource API, allowing it to serve as a
132+
drop-in replacement that transparently handles encryption and decryption of items.
133+
134+
The API matches the standard boto3 DynamoDB resource interface:
135+
136+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/index.html
137+
138+
This class will encrypt/decrypt items for the following operations:
139+
140+
* ``batch_get_item``
141+
* ``batch_write_item``
142+
143+
Calling ``Table()`` will return an ``EncryptedTable`` object.
144+
145+
Any other operations on this class will defer to the underlying boto3 DynamoDB resource's implementation
146+
and will not be encrypted/decrypted.
147+
148+
"""
149+
150+
def __init__(
151+
self,
152+
*,
153+
resource: ServiceResource,
154+
encryption_config: DynamoDbTablesEncryptionConfig,
155+
):
156+
"""
157+
Create an ``EncryptedResource`` object.
158+
159+
Args:
160+
resource (ServiceResource): Initialized boto3 DynamoDB resource
161+
encryption_config (DynamoDbTablesEncryptionConfig): Initialized DynamoDbTablesEncryptionConfig
162+
163+
"""
164+
self._resource = resource
165+
self._encryption_config = encryption_config
166+
self._transformer = DynamoDbEncryptionTransforms(config=encryption_config)
167+
self._client_shape_to_resource_shape_converter = ClientShapeToResourceShapeConverter()
168+
self._resource_shape_to_client_shape_converter = ResourceShapeToClientShapeConverter()
169+
self.tables = EncryptedTablesCollectionManager(
170+
collection=self._resource.tables, encryption_config=self._encryption_config
171+
)
172+
173+
def Table(self, name):
174+
"""
175+
Create an ``EncryptedTable`` resource.
176+
177+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/Table.html
178+
179+
Args:
180+
name (str): The EncryptedTable's name identifier. This must be set.
181+
182+
Returns:
183+
EncryptedTable: An ``EncryptedTable`` resource
184+
185+
"""
186+
return EncryptedTable(table=self._resource.Table(name), encryption_config=self._encryption_config)
187+
188+
def batch_get_item(self, **kwargs):
189+
"""
190+
Get multiple items from one or more tables. Decrypts any returned items.
191+
192+
The input and output syntaxes match those for the boto3 DynamoDB resource ``batch_get_item`` API:
193+
194+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/batch_get_item.html
195+
196+
Args:
197+
**kwargs: Keyword arguments to pass to the operation. These match the boto3 resource ``batch_get_item``
198+
request syntax.
199+
200+
Returns:
201+
dict: The response from DynamoDB. This matches the boto3 resource ``batch_get_item`` response syntax.
202+
The ``"Responses"`` field will be decrypted locally after being read from DynamoDB.
203+
204+
"""
205+
return self._resource_operation_logic(
206+
operation_input=kwargs,
207+
input_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.batch_get_item_request,
208+
input_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.batch_get_item_request,
209+
input_encryption_transform_method=self._transformer.batch_get_item_input_transform,
210+
input_encryption_transform_shape=BatchGetItemInputTransformInput,
211+
output_encryption_transform_method=self._transformer.batch_get_item_output_transform,
212+
output_encryption_transform_shape=BatchGetItemOutputTransformInput,
213+
output_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.batch_get_item_response,
214+
output_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.batch_get_item_response,
215+
resource_method=self._resource.batch_get_item,
216+
)
217+
218+
def batch_write_item(self, **kwargs):
219+
"""
220+
Put or delete multiple items in one or more tables.
221+
222+
For put operations, encrypts items before writing.
223+
224+
The input and output syntaxes match those for the boto3 DynamoDB resource ``batch_write_item`` API:
225+
226+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/service-resource/batch_write_item.html
227+
228+
Args:
229+
**kwargs: Keyword arguments to pass to the operation. These match the boto3 resource
230+
``batch_write_item`` request syntax. Any ``"PutRequest"`` values in the ``"RequestItems"``
231+
argument will be encrypted locally before being written to DynamoDB.
232+
233+
Returns:
234+
dict: The response from DynamoDB. This matches the boto3 resource ``batch_write_item`` response syntax.
235+
236+
"""
237+
return self._resource_operation_logic(
238+
operation_input=kwargs,
239+
input_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.batch_write_item_request,
240+
input_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.batch_write_item_request,
241+
input_encryption_transform_method=self._transformer.batch_write_item_input_transform,
242+
input_encryption_transform_shape=BatchWriteItemInputTransformInput,
243+
output_encryption_transform_method=self._transformer.batch_write_item_output_transform,
244+
output_encryption_transform_shape=BatchWriteItemOutputTransformInput,
245+
output_resource_to_client_shape_transform_method=self._resource_shape_to_client_shape_converter.batch_write_item_response,
246+
output_client_to_resource_shape_transform_method=self._client_shape_to_resource_shape_converter.batch_write_item_response,
247+
resource_method=self._resource.batch_write_item,
248+
)
249+
250+
def _resource_operation_logic(
251+
self,
252+
*,
253+
operation_input: dict[str, Any],
254+
input_resource_to_client_shape_transform_method: Callable,
255+
input_client_to_resource_shape_transform_method: Callable,
256+
input_encryption_transform_method: Callable,
257+
input_encryption_transform_shape: Any,
258+
output_encryption_transform_method: Callable,
259+
output_encryption_transform_shape: Any,
260+
output_resource_to_client_shape_transform_method: Callable,
261+
output_client_to_resource_shape_transform_method: Callable,
262+
resource_method: Callable,
263+
):
264+
operation_input = deepcopy(operation_input)
265+
# Table inputs are formatted as Python dictionary JSON, but encryption transformers expect DynamoDB JSON.
266+
# `input_resource_to_client_shape_transform_method` formats the supplied Python dictionary as DynamoDB JSON.
267+
input_transform_input = input_resource_to_client_shape_transform_method(operation_input)
268+
269+
# Apply encryption transformation to the user-supplied input
270+
input_transform_output = input_encryption_transform_method(
271+
input_encryption_transform_shape(sdk_input=input_transform_input)
272+
).transformed_input
273+
274+
# The encryption transformation result is formatted in DynamoDB JSON,
275+
# but the underlying boto3 table expects Python dictionary JSON.
276+
# `input_client_to_resource_shape_transform_method` formats the transformation as Python dictionary JSON.
277+
sdk_input = input_client_to_resource_shape_transform_method(input_transform_output)
278+
279+
# Call boto3 Table method with Python-dictionary-JSON-formatted, encryption-transformed input,
280+
# and receive Python-dictionary-JSON-formatted boto3 output.
281+
sdk_output = resource_method(**sdk_input)
282+
283+
# Format Python dictionary JSON-formatted SDK output as DynamoDB JSON for encryption transformer
284+
output_transform_input = output_resource_to_client_shape_transform_method(sdk_output)
285+
286+
# Apply encryption transformer to boto3 output
287+
output_transform_output = output_encryption_transform_method(
288+
output_encryption_transform_shape(
289+
original_input=input_transform_input,
290+
sdk_output=output_transform_input,
291+
)
292+
).transformed_output
293+
294+
# Format DynamoDB JSON-formatted encryption transformation result as Python dictionary JSON
295+
dbesdk_response = output_client_to_resource_shape_transform_method(output_transform_output)
296+
# Copy any missing fields from the SDK output to the response
297+
# (e.g. `ConsumedCapacity`)
298+
dbesdk_response = self._copy_sdk_response_to_dbesdk_response(sdk_output, dbesdk_response)
299+
300+
# Clean up the expression builder for the next operation
301+
self._resource_shape_to_client_shape_converter.expression_builder.reset()
302+
303+
return dbesdk_response
304+
305+
@property
306+
def _boto_client_attr_name(self) -> str:
307+
"""
308+
Name of the attribute containing the underlying boto3 client.
309+
310+
Returns:
311+
str: '_resource'
312+
313+
"""
314+
return "_resource"

0 commit comments

Comments
 (0)