diff --git a/src/dynamodb_encryption_sdk/encrypted/__init__.py b/src/dynamodb_encryption_sdk/encrypted/__init__.py index c9b24e7d..56ca57bc 100644 --- a/src/dynamodb_encryption_sdk/encrypted/__init__.py +++ b/src/dynamodb_encryption_sdk/encrypted/__init__.py @@ -87,17 +87,3 @@ def copy(self): encryption_context=copy.copy(self.encryption_context), attribute_actions=self.attribute_actions ) - - -def validate_get_arguments(kwargs): - # type: (Dict[Text, Any]) -> None - """Verify that attribute filtering parameters are not found in the request. - - :raises InvalidArgumentError: if banned parameters are found - """ - for arg in ('AttributesToGet', 'ProjectionExpression'): - if arg in kwargs: - raise InvalidArgumentError('"{}" is not supported for this operation'.format(arg)) - - if kwargs.get('Select', None) in ('SPECIFIC_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES'): - raise InvalidArgumentError('Scan "Select" value of "{}" is not supported'.format(kwargs['Select'])) diff --git a/src/dynamodb_encryption_sdk/encrypted/client.py b/src/dynamodb_encryption_sdk/encrypted/client.py index 4c61674e..c1218a59 100644 --- a/src/dynamodb_encryption_sdk/encrypted/client.py +++ b/src/dynamodb_encryption_sdk/encrypted/client.py @@ -11,13 +11,18 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """High-level helper class to provide a familiar interface to encrypted tables.""" +from functools import partial + import attr import botocore.client -from dynamodb_encryption_sdk.internal.utils import TableInfoCache +from dynamodb_encryption_sdk.internal.utils import ( + crypto_config_from_cache, crypto_config_from_kwargs, + decrypt_batch_get_item, decrypt_get_item, decrypt_multi_get, + encrypt_batch_write_item, encrypt_put_item, TableInfoCache +) from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider -from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext -from . import CryptoConfig, validate_get_arguments +from dynamodb_encryption_sdk.structures import AttributeActions from .item import decrypt_dynamodb_item, encrypt_dynamodb_item __all__ = ('EncryptedClient',) @@ -25,6 +30,7 @@ @attr.s class EncryptedClient(object): + # pylint: disable=too-few-public-methods,too-many-instance-attributes """High-level helper class to provide a familiar interface to encrypted tables. .. note:: @@ -57,11 +63,57 @@ class EncryptedClient(object): ) def __attrs_post_init__(self): - """Set up the table info cache.""" - self._table_info_cache = TableInfoCache( + """Set up the table info cache and translation methods.""" + self._table_info_cache = TableInfoCache( # attrs confuses pylint: disable=attribute-defined-outside-init client=self._client, auto_refresh_table_indexes=self._auto_refresh_table_indexes ) + self._table_crypto_config = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + crypto_config_from_cache, + self._materials_provider, + self._attribute_actions, + self._table_info_cache + ) + self._item_crypto_config = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + crypto_config_from_kwargs, + self._table_crypto_config + ) + self.get_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + decrypt_get_item, + decrypt_dynamodb_item, + self._item_crypto_config, + self._client.get_item + ) + self.put_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + encrypt_put_item, + encrypt_dynamodb_item, + self._item_crypto_config, + self._client.put_item + ) + self.query = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + decrypt_multi_get, + decrypt_dynamodb_item, + self._item_crypto_config, + self._client.query + ) + self.scan = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + decrypt_multi_get, + decrypt_dynamodb_item, + self._item_crypto_config, + self._client.scan + ) + self.batch_get_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + decrypt_batch_get_item, + decrypt_dynamodb_item, + self._table_crypto_config, + self._client.batch_get_item + ) + self.batch_write_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + encrypt_batch_write_item, + encrypt_dynamodb_item, + self._table_crypto_config, + self._client.batch_write_item + ) def __getattr__(self, name): """Catch any method/attribute lookups that are not defined in this class and try @@ -73,133 +125,6 @@ def __getattr__(self, name): """ return getattr(self._client, name) - def _crypto_config(self, table_name, **kwargs): - """Pull all encryption-specific parameters from the request and use them to build a crypto config. - - :returns: crypto config and updated kwargs - :rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict - """ - crypto_config = kwargs.pop('crypto_config', None) - - if crypto_config is not None: - return crypto_config, kwargs - - table_info = self._table_info_cache.table_info(table_name) - - attribute_actions = self._attribute_actions.copy() - attribute_actions.set_index_keys(*table_info.protected_index_keys()) - - crypto_config = CryptoConfig( - materials_provider=self._materials_provider, - encryption_context=EncryptionContext(**table_info.encryption_context_values), - attribute_actions=attribute_actions - ) - return crypto_config, kwargs - def update_item(self, **kwargs): """Update item is not yet supported.""" raise NotImplementedError('"update_item" is not yet implemented') - - def get_item(self, **kwargs): - """Transparently decrypt an item after getting it from the table. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.get_item - """ - validate_get_arguments(kwargs) - crypto_config, ddb_kwargs = self._crypto_config(kwargs['TableName'], **kwargs) - response = self._client.get_item(**ddb_kwargs) - if 'Item' in response: - response['Item'] = decrypt_dynamodb_item( - item=response['Item'], - crypto_config=crypto_config - ) - return response - - def put_item(self, **kwargs): - """Transparently encrypt an item before putting it to the table. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.put_item - """ - crypto_config, ddb_kwargs = self._crypto_config(kwargs['TableName'], **kwargs) - ddb_kwargs['Item'] = encrypt_dynamodb_item( - item=ddb_kwargs['Item'], - crypto_config=crypto_config - ) - return self._client.put_item(**ddb_kwargs) - - def _encrypted_multi_get_single_table(self, method, **kwargs): - """Transparently decrypt multiple items after getting them from the table. - - :param method: Method from underlying DynamoDB client object to use - :type method: callable - """ - validate_get_arguments(kwargs) - crypto_config, ddb_kwargs = self._crypto_config(kwargs['TableName'], **kwargs) - response = method(**ddb_kwargs) - for pos in range(len(response['Items'])): - response['Items'][pos] = decrypt_dynamodb_item( - item=response['Items'][pos], - crypto_config=crypto_config - ) - return response - - def query(self, **kwargs): - """Transparently decrypt multiple items after getting them from a query request to the table. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.query - """ - return self._encrypted_multi_get_single_table(self._client.query, **kwargs) - - def scan(self, **kwargs): - """Transparently decrypt multiple items after getting them from a scan request to the table. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.scan - """ - return self._encrypted_multi_get_single_table(self._client.scan, **kwargs) - - def batch_get_item(self, **kwargs): - """Transparently decrypt multiple items after getting them from a batch get item request. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.batch_get_item - """ - for _table_name, table_kwargs in kwargs['RequestItems'].items(): - validate_get_arguments(table_kwargs) - - request_crypto_config = kwargs.pop('crypto_config', None) - - response = self._client.batch_get_item(**kwargs) - for table_name, items in response['Responses'].items(): - if request_crypto_config is not None: - crypto_config = request_crypto_config - else: - crypto_config = self._crypto_config(table_name)[0] - - for pos in range(len(items)): - items[pos] = decrypt_dynamodb_item( - item=items[pos], - crypto_config=crypto_config - ) - return response - - def batch_write_item(self, **kwargs): - """Transparently encrypt multiple items before writing them with a batch write item request. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.batch_write_item - """ - request_crypto_config = kwargs.pop('crypto_config', None) - - for table_name, items in kwargs['RequestItems'].items(): - if request_crypto_config is not None: - crypto_config = request_crypto_config - else: - crypto_config = self._crypto_config(table_name)[0] - - for pos in range(len(items)): - for request_type, item in items[pos].items(): - # We don't encrypt primary indexes, so we can ignore DeleteItem requests - if request_type == 'PutRequest': - items[pos][request_type]['Item'] = encrypt_dynamodb_item( - item=item['Item'], - crypto_config=crypto_config - ) - return self._client.batch_write_item(**kwargs) diff --git a/src/dynamodb_encryption_sdk/encrypted/resource.py b/src/dynamodb_encryption_sdk/encrypted/resource.py index 3e49eb78..7b5d17f6 100644 --- a/src/dynamodb_encryption_sdk/encrypted/resource.py +++ b/src/dynamodb_encryption_sdk/encrypted/resource.py @@ -11,14 +11,17 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """High-level helper class to provide a familiar interface to encrypted tables.""" +from functools import partial + import attr from boto3.resources.base import ServiceResource from boto3.resources.collection import CollectionManager -from dynamodb_encryption_sdk.internal.utils import TableInfoCache +from dynamodb_encryption_sdk.internal.utils import ( + crypto_config_from_cache, decrypt_batch_get_item, encrypt_batch_write_item, TableInfoCache +) from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider -from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext -from . import CryptoConfig, validate_get_arguments +from dynamodb_encryption_sdk.structures import AttributeActions from .item import decrypt_python_item, encrypt_python_item from .table import EncryptedTable @@ -27,6 +30,7 @@ @attr.s class EncryptedTablesCollectionManager(object): + # pylint: disable=too-few-public-methods """Tables collection manager that provides EncryptedTable objects. https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.tables @@ -46,6 +50,25 @@ class EncryptedTablesCollectionManager(object): _attribute_actions = attr.ib(validator=attr.validators.instance_of(AttributeActions)) _table_info_cache = attr.ib(validator=attr.validators.instance_of(TableInfoCache)) + def __attrs_post_init__(self): + """Set up the translation methods.""" + self.all = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + self._transform_table, + self._collection.all + ) + self.filter = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + self._transform_table, + self._collection.filter + ) + self.limit = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + self._transform_table, + self._collection.limit + ) + self.page_size = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + self._transform_table, + self._collection.page_size + ) + def __getattr__(self, name): """Catch any method/attribute lookups that are not defined in this class and try to find them on the provided collection object. @@ -71,38 +94,10 @@ def _transform_table(self, method, **kwargs): attribute_actions=self._attribute_actions ) - def all(self): - """Creates an iterable of all EncryptedTable resources in the collection. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.all - """ - return self._transform_table(self._collection.all) - - def filter(self, **kwargs): - """Creates an iterable of all EncryptedTable resources in the collection filtered by kwargs passed to method. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.filter - """ - return self._transform_table(self._collection.filter, **kwargs) - - def limit(self, **kwargs): - """Creates an iterable up to a specified amount of EncryptedTable resources in the collection. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.limit - """ - return self._transform_table(self._collection.limit, **kwargs) - - def page_size(self, **kwargs): - """Creates an iterable of all EncryptedTable resources in the collection, but limits - the number of items returned by each service call by the specified amount. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.page_size - """ - return self._transform_table(self._collection.page_size, **kwargs) - @attr.s class EncryptedResource(object): + # pylint: disable=too-few-public-methods """High-level helper class to provide a familiar interface to encrypted tables. .. note:: @@ -138,17 +133,35 @@ class EncryptedResource(object): ) def __attrs_post_init__(self): - """Set up the table info cache and the encrypted tables collection manager.""" - self._table_info_cache = TableInfoCache( + """Set up the table info cache, encrypted tables collection manager, and translation methods.""" + self._table_info_cache = TableInfoCache( # attrs confuses pylint: disable=attribute-defined-outside-init client=self._resource.meta.client, auto_refresh_table_indexes=self._auto_refresh_table_indexes ) - self.tables = EncryptedTablesCollectionManager( + self._crypto_config = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + crypto_config_from_cache, + self._materials_provider, + self._attribute_actions, + self._table_info_cache + ) + self.tables = EncryptedTablesCollectionManager( # attrs confuses pylint: disable=attribute-defined-outside-init collection=self._resource.tables, materials_provider=self._materials_provider, attribute_actions=self._attribute_actions, table_info_cache=self._table_info_cache ) + self.batch_get_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + decrypt_batch_get_item, + decrypt_python_item, + self._crypto_config, + self._resource.batch_get_item + ) + self.batch_write_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + encrypt_batch_write_item, + encrypt_python_item, + self._crypto_config, + self._resource.batch_write_item + ) def __getattr__(self, name): """Catch any method/attribute lookups that are not defined in this class and try @@ -160,72 +173,8 @@ def __getattr__(self, name): """ return getattr(self._resource, name) - def _crypto_config(self, table_name): - """Pull all encryption-specific parameters from the request and use them to build a crypto config. - - :returns: crypto config - :rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig - """ - table_info = self._table_info_cache.table_info(table_name) - - attribute_actions = self._attribute_actions.copy() - attribute_actions.set_index_keys(*table_info.protected_index_keys()) - - crypto_config = CryptoConfig( - materials_provider=self._materials_provider, - encryption_context=EncryptionContext(**table_info.encryption_context_values), - attribute_actions=attribute_actions - ) - return crypto_config - - def batch_get_item(self, **kwargs): - """Transparently decrypt multiple items after getting them from a batch get item request. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.batch_get_item - """ - request_crypto_config = kwargs.pop('crypto_config', None) - - for _table_name, table_kwargs in kwargs['RequestItems'].items(): - validate_get_arguments(table_kwargs) - - response = self._resource.batch_get_item(**kwargs) - for table_name, items in response['Responses'].items(): - if request_crypto_config is not None: - crypto_config = request_crypto_config - else: - crypto_config = self._crypto_config(table_name) - - for pos in range(len(items)): - items[pos] = decrypt_python_item( - item=items[pos], - crypto_config=crypto_config - ) - return response - - def batch_write_item(self, **kwargs): - """Transparently encrypt multiple items before writing them with a batch write item request. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.ServiceResource.batch_write_item - """ - request_crypto_config = kwargs.pop('crypto_config', None) - - for table_name, items in kwargs['RequestItems'].items(): - if request_crypto_config is not None: - crypto_config = request_crypto_config - else: - crypto_config = self._crypto_config(table_name) - - for pos in range(len(items)): - for request_type, item in items[pos].items(): - # We don't encrypt primary indexes, so we can ignore DeleteItem requests - if request_type == 'PutRequest': - items[pos][request_type]['Item'] = encrypt_python_item( - item=item['Item'], - crypto_config=crypto_config - ) - return self._resource.batch_write_item(**kwargs) - def Table(self, name, **kwargs): + # naming chosen to align with boto3 resource name, so pylint: disable=invalid-name """Creates an EncryptedTable resource. If any of the optional configuration values are not provided, the corresponding values diff --git a/src/dynamodb_encryption_sdk/encrypted/table.py b/src/dynamodb_encryption_sdk/encrypted/table.py index 366056a9..92c375a8 100644 --- a/src/dynamodb_encryption_sdk/encrypted/table.py +++ b/src/dynamodb_encryption_sdk/encrypted/table.py @@ -11,12 +11,17 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """High-level helper class to provide a familiar interface to encrypted tables.""" +from functools import partial + import attr from boto3.resources.base import ServiceResource +from dynamodb_encryption_sdk.internal.utils import ( + crypto_config_from_kwargs, crypto_config_from_table_info, + decrypt_get_item, decrypt_multi_get, encrypt_put_item +) from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider -from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext, TableInfo -from . import CryptoConfig, validate_get_arguments +from dynamodb_encryption_sdk.structures import AttributeActions, TableInfo from .item import decrypt_python_item, encrypt_python_item __all__ = ('EncryptedTable',) @@ -24,6 +29,7 @@ @attr.s class EncryptedTable(object): + # pylint: disable=too-few-public-methods """High-level helper class to provide a familiar interface to encrypted tables. .. note:: @@ -69,7 +75,7 @@ class EncryptedTable(object): ) def __attrs_post_init__(self): - """Prepare table info is it was not set.""" + """Prepare table info is it was not set and set up translation methods.""" if self._table_info is None: self._table_info = TableInfo(name=self._table.name) @@ -80,6 +86,40 @@ def __attrs_post_init__(self): self._attribute_actions = self._attribute_actions.copy() self._attribute_actions.set_index_keys(*self._table_info.protected_index_keys()) + self._crypto_config = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + crypto_config_from_kwargs, + partial( + crypto_config_from_table_info, + self._materials_provider, + self._attribute_actions, + self._table_info + ) + ) + self.get_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + decrypt_get_item, + decrypt_python_item, + self._crypto_config, + self._table.get_item + ) + self.put_item = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + encrypt_put_item, + encrypt_python_item, + self._crypto_config, + self._table.put_item + ) + self.query = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + decrypt_multi_get, + decrypt_python_item, + self._crypto_config, + self._table.query + ) + self.scan = partial( # attrs confuses pylint: disable=attribute-defined-outside-init + decrypt_multi_get, + decrypt_python_item, + self._crypto_config, + self._table.scan + ) + def __getattr__(self, name): """Catch any method/attribute lookups that are not defined in this class and try to find them on the provided bridge object. @@ -93,78 +133,3 @@ def __getattr__(self, name): def update_item(self, **kwargs): """Update item is not yet supported.""" raise NotImplementedError('"update_item" is not yet implemented') - - def _crypto_config(self, **kwargs): - """Pull all encryption-specific parameters from the request and use them to build a crypto config. - - :returns: crypto config and updated kwargs - :rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict - """ - crypto_config = kwargs.pop('crypto_config', None) - - if crypto_config is not None: - return crypto_config, kwargs - - crypto_config = CryptoConfig( - materials_provider=self._materials_provider, - encryption_context=EncryptionContext(**self._table_info.encryption_context_values), - attribute_actions=self._attribute_actions - ) - return crypto_config, kwargs - - def get_item(self, **kwargs): - """Transparently decrypt an item after getting it from the table. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Table.get_item - """ - validate_get_arguments(kwargs) - crypto_config, ddb_kwargs = self._crypto_config(**kwargs) - response = self._table.get_item(**ddb_kwargs) - if 'Item' in response: - response['Item'] = decrypt_python_item( - item=response['Item'], - crypto_config=crypto_config - ) - return response - - def put_item(self, **kwargs): - """Transparently encrypt an item before putting it to the table. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Table.put_item - """ - crypto_config, ddb_kwargs = self._crypto_config(**kwargs) - ddb_kwargs['Item'] = encrypt_python_item( - item=ddb_kwargs['Item'], - crypto_config=crypto_config - ) - return self._table.put_item(**ddb_kwargs) - - def _encrypted_multi_get(self, method, **kwargs): - """Transparently decrypt multiple items after getting them from the table. - - :param method: Method from underlying DynamoDB table object to use - :type method: callable - """ - validate_get_arguments(kwargs) - crypto_config, ddb_kwargs = self._crypto_config(**kwargs) - response = method(**ddb_kwargs) - for pos in range(len(response['Items'])): - response['Items'][pos] = decrypt_python_item( - item=response['Items'][pos], - crypto_config=crypto_config - ) - return response - - def query(self, **kwargs): - """Transparently decrypt multiple items after getting them from a query request to the table. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Table.query - """ - return self._encrypted_multi_get(self._table.query, **kwargs) - - def scan(self, **kwargs): - """Transparently decrypt multiple items after getting them from a scan request to the table. - - https://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Table.scan - """ - return self._encrypted_multi_get(self._table.scan, **kwargs) diff --git a/src/dynamodb_encryption_sdk/internal/utils.py b/src/dynamodb_encryption_sdk/internal/utils.py index ddcde4e0..99d10702 100644 --- a/src/dynamodb_encryption_sdk/internal/utils.py +++ b/src/dynamodb_encryption_sdk/internal/utils.py @@ -14,16 +14,23 @@ import attr import botocore.client +from dynamodb_encryption_sdk.encrypted import CryptoConfig +from dynamodb_encryption_sdk.exceptions import InvalidArgumentError from dynamodb_encryption_sdk.internal.str_ops import to_bytes -from dynamodb_encryption_sdk.structures import TableInfo +from dynamodb_encryption_sdk.structures import EncryptionContext, TableInfo try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Dict, Text # noqa pylint: disable=unused-import + from typing import Callable, Dict, Text # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass -__all__ = ('sorted_key_map', 'TableInfoCache') +__all__ = ( + 'sorted_key_map', 'TableInfoCache', + 'crypto_config_from_kwargs', 'crypto_config_from_table_info', 'crypto_config_from_cache', + 'decrypt_get_item', 'decrypt_multi_get', 'decrypt_batch_get_item', + 'encrypt_put_item', 'encrypt_batch_write_item' +) def sorted_key_map(item, transform=to_bytes): @@ -44,6 +51,7 @@ def sorted_key_map(item, transform=to_bytes): @attr.s class TableInfoCache(object): + # pylint: disable=too-few-public-methods """Very simple cache of TableInfo objects, providing configuration information about DynamoDB tables. :param client: Boto3 DynamoDB client @@ -57,7 +65,7 @@ class TableInfoCache(object): def __attrs_post_init__(self): """Set up the empty cache.""" - self._all_tables_info = {} # type: Dict[Text, TableInfo] + self._all_tables_info = {} # type: Dict[Text, TableInfo] # pylint: disable=attribute-defined-outside-init def table_info(self, table_name): """Collect a TableInfo object for the specified table, creating and adding it to @@ -75,3 +83,175 @@ def table_info(self, table_name): _table_info.refresh_indexed_attributes(self._client) self._all_tables_info[table_name] = _table_info return _table_info + + +def validate_get_arguments(kwargs): + # type: (Dict[Text, Any]) -> None + """Verify that attribute filtering parameters are not found in the request. + + :raises InvalidArgumentError: if banned parameters are found + """ + for arg in ('AttributesToGet', 'ProjectionExpression'): + if arg in kwargs: + raise InvalidArgumentError('"{}" is not supported for this operation'.format(arg)) + + if kwargs.get('Select', None) in ('SPECIFIC_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES'): + raise InvalidArgumentError('Scan "Select" value of "{}" is not supported'.format(kwargs['Select'])) + + +def crypto_config_from_kwargs(fallback, **kwargs): + """Pull all encryption-specific parameters from the request and use them to build a crypto config. + + :returns: crypto config and updated kwargs + :rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict + """ + try: + crypto_config = kwargs.pop('crypto_config') + except KeyError: + try: + fallback_kwargs = {'table_name': kwargs['TableName']} + except KeyError: + fallback_kwargs = {} + crypto_config = fallback(**fallback_kwargs) + return crypto_config, kwargs + + +def crypto_config_from_table_info(materials_provider, attribute_actions, table_info): + """Build a crypto config from the provided values and table info. + + :returns: crypto config and updated kwargs + :rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict + """ + return CryptoConfig( + materials_provider=materials_provider, + encryption_context=EncryptionContext(**table_info.encryption_context_values), + attribute_actions=attribute_actions + ) + + +def crypto_config_from_cache(materials_provider, attribute_actions, table_info_cache, table_name): + """Build a crypto config from the provided values, loading the table info from the provided cache. + + :returns: crypto config and updated kwargs + :rtype: dynamodb_encryption_sdk.encrypted.CryptoConfig and dict + """ + table_info = table_info_cache.table_info(table_name) + + attribute_actions = attribute_actions.copy() + attribute_actions.set_index_keys(*table_info.protected_index_keys()) + + return crypto_config_from_table_info(materials_provider, attribute_actions, table_info) + + +def decrypt_multi_get(decrypt_method, crypto_config_method, read_method, **kwargs): + # type: (Callable, Callable, Callable, **Any) -> Dict + # TODO: narrow this down + """Transparently decrypt multiple items after getting them from the table. + + :param callable decrypt_method: Method to use to decrypt items + :param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig`` + :param callable read_method: Method that reads from the table + """ + validate_get_arguments(kwargs) + crypto_config, ddb_kwargs = crypto_config_method(**kwargs) + response = read_method(**ddb_kwargs) + for pos in range(len(response['Items'])): + response['Items'][pos] = decrypt_method( + item=response['Items'][pos], + crypto_config=crypto_config + ) + return response + + +def decrypt_get_item(decrypt_method, crypto_config_method, read_method, **kwargs): + # type: (Callable, Callable, Callable, **Any) -> Dict + # TODO: narrow this down + """Transparently decrypt an item after getting it from the table. + + :param callable decrypt_method: Method to use to decrypt item + :param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig`` + :param callable read_method: Method that reads from the table + """ + validate_get_arguments(kwargs) + crypto_config, ddb_kwargs = crypto_config_method(**kwargs) + response = read_method(**ddb_kwargs) + if 'Item' in response: + response['Item'] = decrypt_method( + item=response['Item'], + crypto_config=crypto_config + ) + return response + + +def decrypt_batch_get_item(decrypt_method, crypto_config_method, read_method, **kwargs): + # type: (Callable, Callable, Callable, **Any) -> Dict + # TODO: narrow this down + """Transparently decrypt multiple items after getting them in a batch request. + + :param callable decrypt_method: Method to use to decrypt items + :param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig`` + :param callable read_method: Method that reads from the table + """ + request_crypto_config = kwargs.pop('crypto_config', None) + + for _table_name, table_kwargs in kwargs['RequestItems'].items(): + validate_get_arguments(table_kwargs) + + response = read_method(**kwargs) + for table_name, items in response['Responses'].items(): + if request_crypto_config is not None: + crypto_config = request_crypto_config + else: + crypto_config = crypto_config_method(table_name=table_name) + + for pos, value in enumerate(items): + items[pos] = decrypt_method( + item=value, + crypto_config=crypto_config + ) + return response + + +def encrypt_put_item(encrypt_method, crypto_config_method, write_method, **kwargs): + # type: (Callable, Callable, Callable, **Any) -> Dict + # TODO: narrow this down + """Transparently encrypt an item before putting it to the table. + + :param callable encrypt_method: Method to use to encrypt items + :param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig`` + :param callable read_method: Method that reads from the table + """ + crypto_config, ddb_kwargs = crypto_config_method(**kwargs) + ddb_kwargs['Item'] = encrypt_method( + item=ddb_kwargs['Item'], + crypto_config=crypto_config + ) + return write_method(**ddb_kwargs) + + +def encrypt_batch_write_item(encrypt_method, crypto_config_method, write_method, **kwargs): + # type: (Callable, Callable, Callable, **Any) -> Dict + # TODO: narrow this down + """Transparently encrypt multiple items before putting them in a batch request. + + :param callable encrypt_method: Method to use to encrypt items + :param callable crypto_config_method: Method that accepts ``kwargs`` and provides a ``CryptoConfig`` + :param callable read_method: Method that reads from the table + """ + request_crypto_config = kwargs.pop('crypto_config', None) + + for table_name, items in kwargs['RequestItems'].items(): + if request_crypto_config is not None: + crypto_config = request_crypto_config + else: + crypto_config = crypto_config_method(table_name=table_name) + + for pos, value in enumerate(items): + for request_type, item in value.items(): + # We don't encrypt primary indexes, so we can ignore DeleteItem requests + if request_type == 'PutRequest': + items[pos][request_type]['Item'] = encrypt_method( + item=item['Item'], + crypto_config=crypto_config + ) + return write_method(**kwargs)