Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a0d0bce

Browse files
authoredApr 28, 2018
Merge pull request #20 from mattsb42-aws/de-duplication
Helper client code consolidation
2 parents bb4b2d1 + 34eaec6 commit a0d0bce

File tree

5 files changed

+333
-328
lines changed

5 files changed

+333
-328
lines changed
 

‎src/dynamodb_encryption_sdk/encrypted/__init__.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,3 @@ def copy(self):
8787
encryption_context=copy.copy(self.encryption_context),
8888
attribute_actions=self.attribute_actions
8989
)
90-
91-
92-
def validate_get_arguments(kwargs):
93-
# type: (Dict[Text, Any]) -> None
94-
"""Verify that attribute filtering parameters are not found in the request.
95-
96-
:raises InvalidArgumentError: if banned parameters are found
97-
"""
98-
for arg in ('AttributesToGet', 'ProjectionExpression'):
99-
if arg in kwargs:
100-
raise InvalidArgumentError('"{}" is not supported for this operation'.format(arg))
101-
102-
if kwargs.get('Select', None) in ('SPECIFIC_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES'):
103-
raise InvalidArgumentError('Scan "Select" value of "{}" is not supported'.format(kwargs['Select']))

‎src/dynamodb_encryption_sdk/encrypted/client.py

Lines changed: 57 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,26 @@
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
1313
"""High-level helper class to provide a familiar interface to encrypted tables."""
14+
from functools import partial
15+
1416
import attr
1517
import botocore.client
1618

17-
from dynamodb_encryption_sdk.internal.utils import TableInfoCache
19+
from dynamodb_encryption_sdk.internal.utils import (
20+
crypto_config_from_cache, crypto_config_from_kwargs,
21+
decrypt_batch_get_item, decrypt_get_item, decrypt_multi_get,
22+
encrypt_batch_write_item, encrypt_put_item, TableInfoCache
23+
)
1824
from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider
19-
from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext
20-
from . import CryptoConfig, validate_get_arguments
25+
from dynamodb_encryption_sdk.structures import AttributeActions
2126
from .item import decrypt_dynamodb_item, encrypt_dynamodb_item
2227

2328
__all__ = ('EncryptedClient',)
2429

2530

2631
@attr.s
2732
class EncryptedClient(object):
33+
# pylint: disable=too-few-public-methods,too-many-instance-attributes
2834
"""High-level helper class to provide a familiar interface to encrypted tables.
2935
3036
.. note::
@@ -57,11 +63,57 @@ class EncryptedClient(object):
5763
)
5864

5965
def __attrs_post_init__(self):
60-
"""Set up the table info cache."""
61-
self._table_info_cache = TableInfoCache(
66+
"""Set up the table info cache and translation methods."""
67+
self._table_info_cache = TableInfoCache( # attrs confuses pylint: disable=attribute-defined-outside-init
6268
client=self._client,
6369
auto_refresh_table_indexes=self._auto_refresh_table_indexes
6470
)
71+
self._table_crypto_config = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
72+
crypto_config_from_cache,
73+
self._materials_provider,
74+
self._attribute_actions,
75+
self._table_info_cache
76+
)
77+
self._item_crypto_config = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
78+
crypto_config_from_kwargs,
79+
self._table_crypto_config
80+
)
81+
self.get_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
82+
decrypt_get_item,
83+
decrypt_dynamodb_item,
84+
self._item_crypto_config,
85+
self._client.get_item
86+
)
87+
self.put_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
88+
encrypt_put_item,
89+
encrypt_dynamodb_item,
90+
self._item_crypto_config,
91+
self._client.put_item
92+
)
93+
self.query = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
94+
decrypt_multi_get,
95+
decrypt_dynamodb_item,
96+
self._item_crypto_config,
97+
self._client.query
98+
)
99+
self.scan = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
100+
decrypt_multi_get,
101+
decrypt_dynamodb_item,
102+
self._item_crypto_config,
103+
self._client.scan
104+
)
105+
self.batch_get_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
106+
decrypt_batch_get_item,
107+
decrypt_dynamodb_item,
108+
self._table_crypto_config,
109+
self._client.batch_get_item
110+
)
111+
self.batch_write_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
112+
encrypt_batch_write_item,
113+
encrypt_dynamodb_item,
114+
self._table_crypto_config,
115+
self._client.batch_write_item
116+
)
65117

66118
def __getattr__(self, name):
67119
"""Catch any method/attribute lookups that are not defined in this class and try
@@ -73,133 +125,6 @@ def __getattr__(self, name):
73125
"""
74126
return getattr(self._client, name)
75127

76-
def _crypto_config(self, table_name, **kwargs):
77-
"""Pull all encryption-specific parameters from the request and use them to build a crypto config.
78-
79-
:returns: crypto config and updated kwargs
80-
:rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict
81-
"""
82-
crypto_config = kwargs.pop('crypto_config', None)
83-
84-
if crypto_config is not None:
85-
return crypto_config, kwargs
86-
87-
table_info = self._table_info_cache.table_info(table_name)
88-
89-
attribute_actions = self._attribute_actions.copy()
90-
attribute_actions.set_index_keys(*table_info.protected_index_keys())
91-
92-
crypto_config = CryptoConfig(
93-
materials_provider=self._materials_provider,
94-
encryption_context=EncryptionContext(**table_info.encryption_context_values),
95-
attribute_actions=attribute_actions
96-
)
97-
return crypto_config, kwargs
98-
99128
def update_item(self, **kwargs):
100129
"""Update item is not yet supported."""
101130
raise NotImplementedError('"update_item" is not yet implemented')
102-
103-
def get_item(self, **kwargs):
104-
"""Transparently decrypt an item after getting it from the table.
105-
106-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.get_item
107-
"""
108-
validate_get_arguments(kwargs)
109-
crypto_config, ddb_kwargs = self._crypto_config(kwargs['TableName'], **kwargs)
110-
response = self._client.get_item(**ddb_kwargs)
111-
if 'Item' in response:
112-
response['Item'] = decrypt_dynamodb_item(
113-
item=response['Item'],
114-
crypto_config=crypto_config
115-
)
116-
return response
117-
118-
def put_item(self, **kwargs):
119-
"""Transparently encrypt an item before putting it to the table.
120-
121-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.put_item
122-
"""
123-
crypto_config, ddb_kwargs = self._crypto_config(kwargs['TableName'], **kwargs)
124-
ddb_kwargs['Item'] = encrypt_dynamodb_item(
125-
item=ddb_kwargs['Item'],
126-
crypto_config=crypto_config
127-
)
128-
return self._client.put_item(**ddb_kwargs)
129-
130-
def _encrypted_multi_get_single_table(self, method, **kwargs):
131-
"""Transparently decrypt multiple items after getting them from the table.
132-
133-
:param method: Method from underlying DynamoDB client object to use
134-
:type method: callable
135-
"""
136-
validate_get_arguments(kwargs)
137-
crypto_config, ddb_kwargs = self._crypto_config(kwargs['TableName'], **kwargs)
138-
response = method(**ddb_kwargs)
139-
for pos in range(len(response['Items'])):
140-
response['Items'][pos] = decrypt_dynamodb_item(
141-
item=response['Items'][pos],
142-
crypto_config=crypto_config
143-
)
144-
return response
145-
146-
def query(self, **kwargs):
147-
"""Transparently decrypt multiple items after getting them from a query request to the table.
148-
149-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.query
150-
"""
151-
return self._encrypted_multi_get_single_table(self._client.query, **kwargs)
152-
153-
def scan(self, **kwargs):
154-
"""Transparently decrypt multiple items after getting them from a scan request to the table.
155-
156-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.scan
157-
"""
158-
return self._encrypted_multi_get_single_table(self._client.scan, **kwargs)
159-
160-
def batch_get_item(self, **kwargs):
161-
"""Transparently decrypt multiple items after getting them from a batch get item request.
162-
163-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.batch_get_item
164-
"""
165-
for _table_name, table_kwargs in kwargs['RequestItems'].items():
166-
validate_get_arguments(table_kwargs)
167-
168-
request_crypto_config = kwargs.pop('crypto_config', None)
169-
170-
response = self._client.batch_get_item(**kwargs)
171-
for table_name, items in response['Responses'].items():
172-
if request_crypto_config is not None:
173-
crypto_config = request_crypto_config
174-
else:
175-
crypto_config = self._crypto_config(table_name)[0]
176-
177-
for pos in range(len(items)):
178-
items[pos] = decrypt_dynamodb_item(
179-
item=items[pos],
180-
crypto_config=crypto_config
181-
)
182-
return response
183-
184-
def batch_write_item(self, **kwargs):
185-
"""Transparently encrypt multiple items before writing them with a batch write item request.
186-
187-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.batch_write_item
188-
"""
189-
request_crypto_config = kwargs.pop('crypto_config', None)
190-
191-
for table_name, items in kwargs['RequestItems'].items():
192-
if request_crypto_config is not None:
193-
crypto_config = request_crypto_config
194-
else:
195-
crypto_config = self._crypto_config(table_name)[0]
196-
197-
for pos in range(len(items)):
198-
for request_type, item in items[pos].items():
199-
# We don't encrypt primary indexes, so we can ignore DeleteItem requests
200-
if request_type == 'PutRequest':
201-
items[pos][request_type]['Item'] = encrypt_dynamodb_item(
202-
item=item['Item'],
203-
crypto_config=crypto_config
204-
)
205-
return self._client.batch_write_item(**kwargs)

‎src/dynamodb_encryption_sdk/encrypted/resource.py

Lines changed: 49 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
1313
"""High-level helper class to provide a familiar interface to encrypted tables."""
14+
from functools import partial
15+
1416
import attr
1517
from boto3.resources.base import ServiceResource
1618
from boto3.resources.collection import CollectionManager
1719

18-
from dynamodb_encryption_sdk.internal.utils import TableInfoCache
20+
from dynamodb_encryption_sdk.internal.utils import (
21+
crypto_config_from_cache, decrypt_batch_get_item, encrypt_batch_write_item, TableInfoCache
22+
)
1923
from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider
20-
from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext
21-
from . import CryptoConfig, validate_get_arguments
24+
from dynamodb_encryption_sdk.structures import AttributeActions
2225
from .item import decrypt_python_item, encrypt_python_item
2326
from .table import EncryptedTable
2427

@@ -27,6 +30,7 @@
2730

2831
@attr.s
2932
class EncryptedTablesCollectionManager(object):
33+
# pylint: disable=too-few-public-methods
3034
"""Tables collection manager that provides EncryptedTable objects.
3135
3236
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.tables
@@ -46,6 +50,25 @@ class EncryptedTablesCollectionManager(object):
4650
_attribute_actions = attr.ib(validator=attr.validators.instance_of(AttributeActions))
4751
_table_info_cache = attr.ib(validator=attr.validators.instance_of(TableInfoCache))
4852

53+
def __attrs_post_init__(self):
54+
"""Set up the translation methods."""
55+
self.all = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
56+
self._transform_table,
57+
self._collection.all
58+
)
59+
self.filter = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
60+
self._transform_table,
61+
self._collection.filter
62+
)
63+
self.limit = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
64+
self._transform_table,
65+
self._collection.limit
66+
)
67+
self.page_size = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
68+
self._transform_table,
69+
self._collection.page_size
70+
)
71+
4972
def __getattr__(self, name):
5073
"""Catch any method/attribute lookups that are not defined in this class and try
5174
to find them on the provided collection object.
@@ -71,38 +94,10 @@ def _transform_table(self, method, **kwargs):
7194
attribute_actions=self._attribute_actions
7295
)
7396

74-
def all(self):
75-
"""Creates an iterable of all EncryptedTable resources in the collection.
76-
77-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.all
78-
"""
79-
return self._transform_table(self._collection.all)
80-
81-
def filter(self, **kwargs):
82-
"""Creates an iterable of all EncryptedTable resources in the collection filtered by kwargs passed to method.
83-
84-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.filter
85-
"""
86-
return self._transform_table(self._collection.filter, **kwargs)
87-
88-
def limit(self, **kwargs):
89-
"""Creates an iterable up to a specified amount of EncryptedTable resources in the collection.
90-
91-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.limit
92-
"""
93-
return self._transform_table(self._collection.limit, **kwargs)
94-
95-
def page_size(self, **kwargs):
96-
"""Creates an iterable of all EncryptedTable resources in the collection, but limits
97-
the number of items returned by each service call by the specified amount.
98-
99-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.page_size
100-
"""
101-
return self._transform_table(self._collection.page_size, **kwargs)
102-
10397

10498
@attr.s
10599
class EncryptedResource(object):
100+
# pylint: disable=too-few-public-methods
106101
"""High-level helper class to provide a familiar interface to encrypted tables.
107102
108103
.. note::
@@ -138,17 +133,35 @@ class EncryptedResource(object):
138133
)
139134

140135
def __attrs_post_init__(self):
141-
"""Set up the table info cache and the encrypted tables collection manager."""
142-
self._table_info_cache = TableInfoCache(
136+
"""Set up the table info cache, encrypted tables collection manager, and translation methods."""
137+
self._table_info_cache = TableInfoCache( # attrs confuses pylint: disable=attribute-defined-outside-init
143138
client=self._resource.meta.client,
144139
auto_refresh_table_indexes=self._auto_refresh_table_indexes
145140
)
146-
self.tables = EncryptedTablesCollectionManager(
141+
self._crypto_config = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
142+
crypto_config_from_cache,
143+
self._materials_provider,
144+
self._attribute_actions,
145+
self._table_info_cache
146+
)
147+
self.tables = EncryptedTablesCollectionManager( # attrs confuses pylint: disable=attribute-defined-outside-init
147148
collection=self._resource.tables,
148149
materials_provider=self._materials_provider,
149150
attribute_actions=self._attribute_actions,
150151
table_info_cache=self._table_info_cache
151152
)
153+
self.batch_get_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
154+
decrypt_batch_get_item,
155+
decrypt_python_item,
156+
self._crypto_config,
157+
self._resource.batch_get_item
158+
)
159+
self.batch_write_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
160+
encrypt_batch_write_item,
161+
encrypt_python_item,
162+
self._crypto_config,
163+
self._resource.batch_write_item
164+
)
152165

153166
def __getattr__(self, name):
154167
"""Catch any method/attribute lookups that are not defined in this class and try
@@ -160,72 +173,8 @@ def __getattr__(self, name):
160173
"""
161174
return getattr(self._resource, name)
162175

163-
def _crypto_config(self, table_name):
164-
"""Pull all encryption-specific parameters from the request and use them to build a crypto config.
165-
166-
:returns: crypto config
167-
:rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig
168-
"""
169-
table_info = self._table_info_cache.table_info(table_name)
170-
171-
attribute_actions = self._attribute_actions.copy()
172-
attribute_actions.set_index_keys(*table_info.protected_index_keys())
173-
174-
crypto_config = CryptoConfig(
175-
materials_provider=self._materials_provider,
176-
encryption_context=EncryptionContext(**table_info.encryption_context_values),
177-
attribute_actions=attribute_actions
178-
)
179-
return crypto_config
180-
181-
def batch_get_item(self, **kwargs):
182-
"""Transparently decrypt multiple items after getting them from a batch get item request.
183-
184-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.batch_get_item
185-
"""
186-
request_crypto_config = kwargs.pop('crypto_config', None)
187-
188-
for _table_name, table_kwargs in kwargs['RequestItems'].items():
189-
validate_get_arguments(table_kwargs)
190-
191-
response = self._resource.batch_get_item(**kwargs)
192-
for table_name, items in response['Responses'].items():
193-
if request_crypto_config is not None:
194-
crypto_config = request_crypto_config
195-
else:
196-
crypto_config = self._crypto_config(table_name)
197-
198-
for pos in range(len(items)):
199-
items[pos] = decrypt_python_item(
200-
item=items[pos],
201-
crypto_config=crypto_config
202-
)
203-
return response
204-
205-
def batch_write_item(self, **kwargs):
206-
"""Transparently encrypt multiple items before writing them with a batch write item request.
207-
208-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.batch_write_item
209-
"""
210-
request_crypto_config = kwargs.pop('crypto_config', None)
211-
212-
for table_name, items in kwargs['RequestItems'].items():
213-
if request_crypto_config is not None:
214-
crypto_config = request_crypto_config
215-
else:
216-
crypto_config = self._crypto_config(table_name)
217-
218-
for pos in range(len(items)):
219-
for request_type, item in items[pos].items():
220-
# We don't encrypt primary indexes, so we can ignore DeleteItem requests
221-
if request_type == 'PutRequest':
222-
items[pos][request_type]['Item'] = encrypt_python_item(
223-
item=item['Item'],
224-
crypto_config=crypto_config
225-
)
226-
return self._resource.batch_write_item(**kwargs)
227-
228176
def Table(self, name, **kwargs):
177+
# naming chosen to align with boto3 resource name, so pylint: disable=invalid-name
229178
"""Creates an EncryptedTable resource.
230179
231180
If any of the optional configuration values are not provided, the corresponding values

‎src/dynamodb_encryption_sdk/encrypted/table.py

Lines changed: 43 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,25 @@
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
1313
"""High-level helper class to provide a familiar interface to encrypted tables."""
14+
from functools import partial
15+
1416
import attr
1517
from boto3.resources.base import ServiceResource
1618

19+
from dynamodb_encryption_sdk.internal.utils import (
20+
crypto_config_from_kwargs, crypto_config_from_table_info,
21+
decrypt_get_item, decrypt_multi_get, encrypt_put_item
22+
)
1723
from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider
18-
from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext, TableInfo
19-
from . import CryptoConfig, validate_get_arguments
24+
from dynamodb_encryption_sdk.structures import AttributeActions, TableInfo
2025
from .item import decrypt_python_item, encrypt_python_item
2126

2227
__all__ = ('EncryptedTable',)
2328

2429

2530
@attr.s
2631
class EncryptedTable(object):
32+
# pylint: disable=too-few-public-methods
2733
"""High-level helper class to provide a familiar interface to encrypted tables.
2834
2935
.. note::
@@ -69,7 +75,7 @@ class EncryptedTable(object):
6975
)
7076

7177
def __attrs_post_init__(self):
72-
"""Prepare table info is it was not set."""
78+
"""Prepare table info is it was not set and set up translation methods."""
7379
if self._table_info is None:
7480
self._table_info = TableInfo(name=self._table.name)
7581

@@ -80,6 +86,40 @@ def __attrs_post_init__(self):
8086
self._attribute_actions = self._attribute_actions.copy()
8187
self._attribute_actions.set_index_keys(*self._table_info.protected_index_keys())
8288

89+
self._crypto_config = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
90+
crypto_config_from_kwargs,
91+
partial(
92+
crypto_config_from_table_info,
93+
self._materials_provider,
94+
self._attribute_actions,
95+
self._table_info
96+
)
97+
)
98+
self.get_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
99+
decrypt_get_item,
100+
decrypt_python_item,
101+
self._crypto_config,
102+
self._table.get_item
103+
)
104+
self.put_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
105+
encrypt_put_item,
106+
encrypt_python_item,
107+
self._crypto_config,
108+
self._table.put_item
109+
)
110+
self.query = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
111+
decrypt_multi_get,
112+
decrypt_python_item,
113+
self._crypto_config,
114+
self._table.query
115+
)
116+
self.scan = partial( # attrs confuses pylint: disable=attribute-defined-outside-init
117+
decrypt_multi_get,
118+
decrypt_python_item,
119+
self._crypto_config,
120+
self._table.scan
121+
)
122+
83123
def __getattr__(self, name):
84124
"""Catch any method/attribute lookups that are not defined in this class and try
85125
to find them on the provided bridge object.
@@ -93,78 +133,3 @@ def __getattr__(self, name):
93133
def update_item(self, **kwargs):
94134
"""Update item is not yet supported."""
95135
raise NotImplementedError('"update_item" is not yet implemented')
96-
97-
def _crypto_config(self, **kwargs):
98-
"""Pull all encryption-specific parameters from the request and use them to build a crypto config.
99-
100-
:returns: crypto config and updated kwargs
101-
:rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict
102-
"""
103-
crypto_config = kwargs.pop('crypto_config', None)
104-
105-
if crypto_config is not None:
106-
return crypto_config, kwargs
107-
108-
crypto_config = CryptoConfig(
109-
materials_provider=self._materials_provider,
110-
encryption_context=EncryptionContext(**self._table_info.encryption_context_values),
111-
attribute_actions=self._attribute_actions
112-
)
113-
return crypto_config, kwargs
114-
115-
def get_item(self, **kwargs):
116-
"""Transparently decrypt an item after getting it from the table.
117-
118-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Table.get_item
119-
"""
120-
validate_get_arguments(kwargs)
121-
crypto_config, ddb_kwargs = self._crypto_config(**kwargs)
122-
response = self._table.get_item(**ddb_kwargs)
123-
if 'Item' in response:
124-
response['Item'] = decrypt_python_item(
125-
item=response['Item'],
126-
crypto_config=crypto_config
127-
)
128-
return response
129-
130-
def put_item(self, **kwargs):
131-
"""Transparently encrypt an item before putting it to the table.
132-
133-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Table.put_item
134-
"""
135-
crypto_config, ddb_kwargs = self._crypto_config(**kwargs)
136-
ddb_kwargs['Item'] = encrypt_python_item(
137-
item=ddb_kwargs['Item'],
138-
crypto_config=crypto_config
139-
)
140-
return self._table.put_item(**ddb_kwargs)
141-
142-
def _encrypted_multi_get(self, method, **kwargs):
143-
"""Transparently decrypt multiple items after getting them from the table.
144-
145-
:param method: Method from underlying DynamoDB table object to use
146-
:type method: callable
147-
"""
148-
validate_get_arguments(kwargs)
149-
crypto_config, ddb_kwargs = self._crypto_config(**kwargs)
150-
response = method(**ddb_kwargs)
151-
for pos in range(len(response['Items'])):
152-
response['Items'][pos] = decrypt_python_item(
153-
item=response['Items'][pos],
154-
crypto_config=crypto_config
155-
)
156-
return response
157-
158-
def query(self, **kwargs):
159-
"""Transparently decrypt multiple items after getting them from a query request to the table.
160-
161-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Table.query
162-
"""
163-
return self._encrypted_multi_get(self._table.query, **kwargs)
164-
165-
def scan(self, **kwargs):
166-
"""Transparently decrypt multiple items after getting them from a scan request to the table.
167-
168-
https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Table.scan
169-
"""
170-
return self._encrypted_multi_get(self._table.scan, **kwargs)

‎src/dynamodb_encryption_sdk/internal/utils.py

Lines changed: 184 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,23 @@
1414
import attr
1515
import botocore.client
1616

17+
from dynamodb_encryption_sdk.encrypted import CryptoConfig
18+
from dynamodb_encryption_sdk.exceptions import InvalidArgumentError
1719
from dynamodb_encryption_sdk.internal.str_ops import to_bytes
18-
from dynamodb_encryption_sdk.structures import TableInfo
20+
from dynamodb_encryption_sdk.structures import EncryptionContext, TableInfo
1921

2022
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
21-
from typing import Dict, Text # noqa pylint: disable=unused-import
23+
from typing import Callable, Dict, Text # noqa pylint: disable=unused-import
2224
except ImportError: # pragma: no cover
2325
# We only actually need these imports when running the mypy checks
2426
pass
2527

26-
__all__ = ('sorted_key_map', 'TableInfoCache')
28+
__all__ = (
29+
'sorted_key_map', 'TableInfoCache',
30+
'crypto_config_from_kwargs', 'crypto_config_from_table_info', 'crypto_config_from_cache',
31+
'decrypt_get_item', 'decrypt_multi_get', 'decrypt_batch_get_item',
32+
'encrypt_put_item', 'encrypt_batch_write_item'
33+
)
2734

2835

2936
def sorted_key_map(item, transform=to_bytes):
@@ -44,6 +51,7 @@ def sorted_key_map(item, transform=to_bytes):
4451

4552
@attr.s
4653
class TableInfoCache(object):
54+
# pylint: disable=too-few-public-methods
4755
"""Very simple cache of TableInfo objects, providing configuration information about DynamoDB tables.
4856
4957
:param client: Boto3 DynamoDB client
@@ -57,7 +65,7 @@ class TableInfoCache(object):
5765

5866
def __attrs_post_init__(self):
5967
"""Set up the empty cache."""
60-
self._all_tables_info = {} # type: Dict[Text, TableInfo]
68+
self._all_tables_info = {} # type: Dict[Text, TableInfo] # pylint: disable=attribute-defined-outside-init
6169

6270
def table_info(self, table_name):
6371
"""Collect a TableInfo object for the specified table, creating and adding it to
@@ -75,3 +83,175 @@ def table_info(self, table_name):
7583
_table_info.refresh_indexed_attributes(self._client)
7684
self._all_tables_info[table_name] = _table_info
7785
return _table_info
86+
87+
88+
def validate_get_arguments(kwargs):
89+
# type: (Dict[Text, Any]) -> None
90+
"""Verify that attribute filtering parameters are not found in the request.
91+
92+
:raises InvalidArgumentError: if banned parameters are found
93+
"""
94+
for arg in ('AttributesToGet', 'ProjectionExpression'):
95+
if arg in kwargs:
96+
raise InvalidArgumentError('"{}" is not supported for this operation'.format(arg))
97+
98+
if kwargs.get('Select', None) in ('SPECIFIC_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES'):
99+
raise InvalidArgumentError('Scan "Select" value of "{}" is not supported'.format(kwargs['Select']))
100+
101+
102+
def crypto_config_from_kwargs(fallback, **kwargs):
103+
"""Pull all encryption-specific parameters from the request and use them to build a crypto config.
104+
105+
:returns: crypto config and updated kwargs
106+
:rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict
107+
"""
108+
try:
109+
crypto_config = kwargs.pop('crypto_config')
110+
except KeyError:
111+
try:
112+
fallback_kwargs = {'table_name': kwargs['TableName']}
113+
except KeyError:
114+
fallback_kwargs = {}
115+
crypto_config = fallback(**fallback_kwargs)
116+
return crypto_config, kwargs
117+
118+
119+
def crypto_config_from_table_info(materials_provider, attribute_actions, table_info):
120+
"""Build a crypto config from the provided values and table info.
121+
122+
:returns: crypto config and updated kwargs
123+
:rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict
124+
"""
125+
return CryptoConfig(
126+
materials_provider=materials_provider,
127+
encryption_context=EncryptionContext(**table_info.encryption_context_values),
128+
attribute_actions=attribute_actions
129+
)
130+
131+
132+
def crypto_config_from_cache(materials_provider, attribute_actions, table_info_cache, table_name):
133+
"""Build a crypto config from the provided values, loading the table info from the provided cache.
134+
135+
:returns: crypto config and updated kwargs
136+
:rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict
137+
"""
138+
table_info = table_info_cache.table_info(table_name)
139+
140+
attribute_actions = attribute_actions.copy()
141+
attribute_actions.set_index_keys(*table_info.protected_index_keys())
142+
143+
return crypto_config_from_table_info(materials_provider, attribute_actions, table_info)
144+
145+
146+
def decrypt_multi_get(decrypt_method, crypto_config_method, read_method, **kwargs):
147+
# type: (Callable, Callable, Callable, **Any) -> Dict
148+
# TODO: narrow this down
149+
"""Transparently decrypt multiple items after getting them from the table.
150+
151+
:param callable decrypt_method: Method to use to decrypt items
152+
:param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig``
153+
:param callable read_method: Method that reads from the table
154+
"""
155+
validate_get_arguments(kwargs)
156+
crypto_config, ddb_kwargs = crypto_config_method(**kwargs)
157+
response = read_method(**ddb_kwargs)
158+
for pos in range(len(response['Items'])):
159+
response['Items'][pos] = decrypt_method(
160+
item=response['Items'][pos],
161+
crypto_config=crypto_config
162+
)
163+
return response
164+
165+
166+
def decrypt_get_item(decrypt_method, crypto_config_method, read_method, **kwargs):
167+
# type: (Callable, Callable, Callable, **Any) -> Dict
168+
# TODO: narrow this down
169+
"""Transparently decrypt an item after getting it from the table.
170+
171+
:param callable decrypt_method: Method to use to decrypt item
172+
:param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig``
173+
:param callable read_method: Method that reads from the table
174+
"""
175+
validate_get_arguments(kwargs)
176+
crypto_config, ddb_kwargs = crypto_config_method(**kwargs)
177+
response = read_method(**ddb_kwargs)
178+
if 'Item' in response:
179+
response['Item'] = decrypt_method(
180+
item=response['Item'],
181+
crypto_config=crypto_config
182+
)
183+
return response
184+
185+
186+
def decrypt_batch_get_item(decrypt_method, crypto_config_method, read_method, **kwargs):
187+
# type: (Callable, Callable, Callable, **Any) -> Dict
188+
# TODO: narrow this down
189+
"""Transparently decrypt multiple items after getting them in a batch request.
190+
191+
:param callable decrypt_method: Method to use to decrypt items
192+
:param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig``
193+
:param callable read_method: Method that reads from the table
194+
"""
195+
request_crypto_config = kwargs.pop('crypto_config', None)
196+
197+
for _table_name, table_kwargs in kwargs['RequestItems'].items():
198+
validate_get_arguments(table_kwargs)
199+
200+
response = read_method(**kwargs)
201+
for table_name, items in response['Responses'].items():
202+
if request_crypto_config is not None:
203+
crypto_config = request_crypto_config
204+
else:
205+
crypto_config = crypto_config_method(table_name=table_name)
206+
207+
for pos, value in enumerate(items):
208+
items[pos] = decrypt_method(
209+
item=value,
210+
crypto_config=crypto_config
211+
)
212+
return response
213+
214+
215+
def encrypt_put_item(encrypt_method, crypto_config_method, write_method, **kwargs):
216+
# type: (Callable, Callable, Callable, **Any) -> Dict
217+
# TODO: narrow this down
218+
"""Transparently encrypt an item before putting it to the table.
219+
220+
:param callable encrypt_method: Method to use to encrypt items
221+
:param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig``
222+
:param callable read_method: Method that reads from the table
223+
"""
224+
crypto_config, ddb_kwargs = crypto_config_method(**kwargs)
225+
ddb_kwargs['Item'] = encrypt_method(
226+
item=ddb_kwargs['Item'],
227+
crypto_config=crypto_config
228+
)
229+
return write_method(**ddb_kwargs)
230+
231+
232+
def encrypt_batch_write_item(encrypt_method, crypto_config_method, write_method, **kwargs):
233+
# type: (Callable, Callable, Callable, **Any) -> Dict
234+
# TODO: narrow this down
235+
"""Transparently encrypt multiple items before putting them in a batch request.
236+
237+
:param callable encrypt_method: Method to use to encrypt items
238+
:param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig``
239+
:param callable read_method: Method that reads from the table
240+
"""
241+
request_crypto_config = kwargs.pop('crypto_config', None)
242+
243+
for table_name, items in kwargs['RequestItems'].items():
244+
if request_crypto_config is not None:
245+
crypto_config = request_crypto_config
246+
else:
247+
crypto_config = crypto_config_method(table_name=table_name)
248+
249+
for pos, value in enumerate(items):
250+
for request_type, item in value.items():
251+
# We don't encrypt primary indexes, so we can ignore DeleteItem requests
252+
if request_type == 'PutRequest':
253+
items[pos][request_type]['Item'] = encrypt_method(
254+
item=item['Item'],
255+
crypto_config=crypto_config
256+
)
257+
return write_method(**kwargs)

0 commit comments

Comments
 (0)
Please sign in to comment.