|
| 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" |
0 commit comments