Skip to content

Commit 2f7553c

Browse files
authored
Merge pull request #105 from mattsb42-aws/fixme
Actually fix MostRecentProvider reuse bug
2 parents 70e14e9 + becfc89 commit 2f7553c

File tree

11 files changed

+171
-35
lines changed

11 files changed

+171
-35
lines changed

CHANGELOG.rst

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
Changelog
33
*********
44

5+
1.0.7 -- 2018-01-xx
6+
===================
7+
8+
Bugfixes
9+
--------
10+
* Fix :class:`MostRecentProvider` cache reuse bug.
11+
`#105 <https://github.com/aws/aws-dynamodb-encryption-python/pull/105>`_
12+
513
1.0.6 -- 2018-01-15
614
===================
715

src/dynamodb_encryption_sdk/encrypted/item.py

-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ def encrypt_dynamodb_item(item, crypto_config):
7070
'Reserved attribute name "{}" is not allowed in plaintext item.'.format(reserved_name.value)
7171
)
7272

73-
crypto_config.materials_provider.refresh()
7473
encryption_materials = crypto_config.encryption_materials()
7574

7675
inner_material_description = encryption_materials.material_description.copy()

src/dynamodb_encryption_sdk/internal/utils.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,8 @@ class TableInfoCache(object):
5959
_client = attr.ib(validator=attr.validators.instance_of(botocore.client.BaseClient))
6060
_auto_refresh_table_indexes = attr.ib(validator=attr.validators.instance_of(bool))
6161

62-
def __init__(
63-
self, client, auto_refresh_table_indexes # type: botocore.client.BaseClient # type: bool
64-
): # noqa=D107
65-
# type: (...) -> None
62+
def __init__(self, client, auto_refresh_table_indexes): # noqa=D107
63+
# type: (botocore.client.BaseClient, bool) -> None
6664
# Workaround pending resolution of attrs/mypy interaction.
6765
# https://github.com/python/mypy/issues/2088
6866
# https://github.com/python-attrs/attrs/issues/215

src/dynamodb_encryption_sdk/material_providers/most_recent.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def get(self, name):
112112
def clear(self):
113113
# type: () -> None
114114
"""Clear the cache."""
115+
_LOGGER.debug("Clearing cache")
115116
with self._cache_lock:
116117
self._cache = OrderedDict() # type: OrderedDict[Any, Any] # pylint: disable=attribute-defined-outside-init
117118

@@ -163,8 +164,10 @@ def decryption_materials(self, encryption_context):
163164
"""
164165
version = self._provider_store.version_from_material_description(encryption_context.material_description)
165166
try:
167+
_LOGGER.debug("Looking in cache for decryption materials provider version %d", version)
166168
provider = self._cache.get(version)
167169
except KeyError:
170+
_LOGGER.debug("Decryption materials provider not found in cache")
168171
try:
169172
provider = self._provider_store.provider(self._material_name, version)
170173
except InvalidVersionError:
@@ -183,6 +186,7 @@ def _ttl_action(self):
183186
:rtype: TtlActions
184187
"""
185188
if self._version is None:
189+
_LOGGER.debug("TTL Expired because no version is known")
186190
return TtlActions.EXPIRED
187191

188192
time_since_updated = time.time() - self._last_updated
@@ -193,6 +197,7 @@ def _ttl_action(self):
193197
elif time_since_updated < self._version_ttl + _GRACE_PERIOD:
194198
return TtlActions.GRACE_PERIOD
195199

200+
_LOGGER.debug("TTL Expired because known version has expired")
196201
return TtlActions.EXPIRED
197202

198203
def _get_max_version(self):
@@ -260,6 +265,8 @@ def _get_most_recent_version(self, allow_local):
260265
finally:
261266
self._lock.release()
262267

268+
_LOGGER.debug("New latest version is %d", self._version)
269+
263270
return provider
264271

265272
def encryption_materials(self, encryption_context):
@@ -271,28 +278,32 @@ def encryption_materials(self, encryption_context):
271278
"""
272279
ttl_action = self._ttl_action()
273280

281+
_LOGGER.debug('TTL Action "%s" when getting encryption materials', ttl_action.name)
282+
274283
provider = None
275284

276285
if ttl_action is TtlActions.LIVE:
277286
try:
278-
_LOGGER.debug("Looking in cache for materials provider version %d", self._version)
287+
_LOGGER.debug("Looking in cache for encryption materials provider version %d", self._version)
279288
provider = self._cache.get(self._version)
280289
except KeyError:
281-
_LOGGER.debug("Provider not found in cache")
290+
_LOGGER.debug("Encryption materials provider not found in cache")
282291
ttl_action = TtlActions.EXPIRED
283292

284293
if provider is None:
285294
# Just get the latest local version if we cannot acquire the lock.
286295
# Otherwise, block until we can acquire the lock.
287296
allow_local = bool(ttl_action is TtlActions.GRACE_PERIOD)
288297

298+
_LOGGER.debug("Getting most recent materials provider version")
289299
provider = self._get_most_recent_version(allow_local)
290300

291301
return provider.encryption_materials(encryption_context)
292302

293303
def refresh(self):
294304
# type: () -> None
295305
"""Clear all local caches for this provider."""
306+
_LOGGER.debug("Refreshing MostRecentProvider instance.")
296307
with self._lock:
297308
self._cache.clear()
298309
self._version = None # type: int # pylint: disable=attribute-defined-outside-init

src/dynamodb_encryption_sdk/material_providers/store/meta.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
1313
"""Meta cryptographic provider store."""
14+
import logging
1415
from enum import Enum
1516

1617
import attr
@@ -22,7 +23,7 @@
2223
from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey
2324
from dynamodb_encryption_sdk.encrypted.table import EncryptedTable
2425
from dynamodb_encryption_sdk.exceptions import InvalidVersionError, NoKnownVersionError, VersionAlreadyExistsError
25-
from dynamodb_encryption_sdk.identifiers import EncryptionKeyType, KeyEncodingType
26+
from dynamodb_encryption_sdk.identifiers import LOGGER_NAME, EncryptionKeyType, KeyEncodingType
2627
from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider
2728
from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider
2829

@@ -36,6 +37,7 @@
3637

3738

3839
__all__ = ("MetaStore",)
40+
_LOGGER = logging.getLogger(LOGGER_NAME)
3941

4042

4143
class MetaStoreAttributeNames(Enum):
@@ -104,6 +106,7 @@ def create_table(cls, client, table_name, read_units, write_units):
104106
:param int read_units: Read capacity units to provision
105107
:param int write_units: Write capacity units to provision
106108
"""
109+
_LOGGER.debug("Creating MetaStore table")
107110
try:
108111
client.create_table(
109112
TableName=table_name,
@@ -127,6 +130,7 @@ def _load_materials(self, material_name, version):
127130
:returns: Materials loaded into delegated keys
128131
:rtype: tuple(JceNameLocalDelegatedKey)
129132
"""
133+
_LOGGER.debug('Loading material "%s" version %d from MetaStore table', material_name, version)
130134
key = {MetaStoreAttributeNames.PARTITION.value: material_name, MetaStoreAttributeNames.SORT.value: version}
131135
response = self._encrypted_table.get_item(Key=key)
132136
try:
@@ -168,6 +172,7 @@ def _save_materials(self, material_name, version, encryption_key, signing_key):
168172
:param int version: Version of material to locate
169173
:raises VersionAlreadyExistsError: if the specified version already exists
170174
"""
175+
_LOGGER.debug('Saving material "%s" version %d to MetaStore table', material_name, version)
171176
item = {
172177
MetaStoreAttributeNames.PARTITION.value: material_name,
173178
MetaStoreAttributeNames.SORT.value: version,

test/functional/functional_test_utils.py

+98-19
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616
import base64
1717
import copy
1818
import itertools
19-
import logging
2019
import os
2120
from collections import defaultdict
2221
from decimal import Decimal
2322

2423
import boto3
2524
import pytest
26-
import six
2725
from boto3.dynamodb.types import Binary
26+
from botocore.exceptions import NoRegionError
2827
from moto import mock_dynamodb2
2928

3029
from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey
@@ -34,6 +33,7 @@
3433
from dynamodb_encryption_sdk.encrypted.table import EncryptedTable
3534
from dynamodb_encryption_sdk.identifiers import CryptoAction
3635
from dynamodb_encryption_sdk.internal.identifiers import ReservedAttributes
36+
from dynamodb_encryption_sdk.material_providers.most_recent import MostRecentProvider
3737
from dynamodb_encryption_sdk.material_providers.static import StaticCryptographicMaterialsProvider
3838
from dynamodb_encryption_sdk.material_providers.store.meta import MetaStore
3939
from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider
@@ -44,11 +44,12 @@
4444
RUNNING_IN_TRAVIS = "TRAVIS" in os.environ
4545
_DELEGATED_KEY_CACHE = defaultdict(lambda: defaultdict(dict))
4646
TEST_TABLE_NAME = "my_table"
47+
TEST_REGION_NAME = "us-west-2"
4748
TEST_INDEX = {
4849
"partition_attribute": {"type": "S", "value": "test_value"},
4950
"sort_attribute": {"type": "N", "value": Decimal("99.233")},
5051
}
51-
SECONARY_INDEX = {
52+
SECONDARY_INDEX = {
5253
"secondary_index_1": {"type": "B", "value": Binary(b"\x00\x01\x02")},
5354
"secondary_index_2": {"type": "S", "value": "another_value"},
5455
}
@@ -76,8 +77,8 @@
7677

7778
@pytest.fixture
7879
def example_table():
79-
mock_dynamodb2().start()
80-
ddb = boto3.client("dynamodb", region_name="us-west-2")
80+
mock_dynamodb2().start(reset=False)
81+
ddb = boto3.client("dynamodb", region_name=TEST_REGION_NAME)
8182
ddb.create_table(
8283
TableName=TEST_TABLE_NAME,
8384
KeySchema=[
@@ -95,9 +96,9 @@ def example_table():
9596

9697

9798
@pytest.fixture
98-
def table_with_local_seconary_indexes():
99-
mock_dynamodb2().start()
100-
ddb = boto3.client("dynamodb", region_name="us-west-2")
99+
def table_with_local_secondary_indexes():
100+
mock_dynamodb2().start(reset=False)
101+
ddb = boto3.client("dynamodb", region_name=TEST_REGION_NAME)
101102
ddb.create_table(
102103
TableName=TEST_TABLE_NAME,
103104
KeySchema=[
@@ -118,7 +119,7 @@ def table_with_local_seconary_indexes():
118119
],
119120
AttributeDefinitions=[
120121
{"AttributeName": name, "AttributeType": value["type"]}
121-
for name, value in list(TEST_INDEX.items()) + list(SECONARY_INDEX.items())
122+
for name, value in list(TEST_INDEX.items()) + list(SECONDARY_INDEX.items())
122123
],
123124
ProvisionedThroughput={"ReadCapacityUnits": 100, "WriteCapacityUnits": 100},
124125
)
@@ -128,9 +129,9 @@ def table_with_local_seconary_indexes():
128129

129130

130131
@pytest.fixture
131-
def table_with_global_seconary_indexes():
132-
mock_dynamodb2().start()
133-
ddb = boto3.client("dynamodb", region_name="us-west-2")
132+
def table_with_global_secondary_indexes():
133+
mock_dynamodb2().start(reset=False)
134+
ddb = boto3.client("dynamodb", region_name=TEST_REGION_NAME)
134135
ddb.create_table(
135136
TableName=TEST_TABLE_NAME,
136137
KeySchema=[
@@ -153,7 +154,7 @@ def table_with_global_seconary_indexes():
153154
],
154155
AttributeDefinitions=[
155156
{"AttributeName": name, "AttributeType": value["type"]}
156-
for name, value in list(TEST_INDEX.items()) + list(SECONARY_INDEX.items())
157+
for name, value in list(TEST_INDEX.items()) + list(SECONDARY_INDEX.items())
157158
],
158159
ProvisionedThroughput={"ReadCapacityUnits": 100, "WriteCapacityUnits": 100},
159160
)
@@ -650,22 +651,100 @@ def client_cycle_batch_items_check_paginators(
650651

651652

652653
def build_metastore():
653-
client = boto3.client("dynamodb", region_name="us-west-2")
654+
client = boto3.client("dynamodb", region_name=TEST_REGION_NAME)
654655
table_name = base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8").replace("=", ".")
655656

656657
MetaStore.create_table(client, table_name, 1, 1)
657658
waiter = client.get_waiter("table_exists")
658659
waiter.wait(TableName=table_name)
659660

660-
table = boto3.resource("dynamodb", region_name="us-west-2").Table(table_name)
661-
yield MetaStore(table, build_static_jce_cmp("AES", 256, "HmacSHA256", 256))
661+
table = boto3.resource("dynamodb", region_name=TEST_REGION_NAME).Table(table_name)
662+
return MetaStore(table, build_static_jce_cmp("AES", 256, "HmacSHA256", 256)), table_name
662663

664+
665+
def delete_metastore(table_name):
666+
client = boto3.client("dynamodb", region_name=TEST_REGION_NAME)
663667
client.delete_table(TableName=table_name)
664-
waiter = client.get_waiter("table_not_exists")
665-
waiter.wait(TableName=table_name)
668+
# It sometimes takes a long time to delete a table.
669+
# If hanging, asynchronously deleting tables becomes an issue,
670+
# come back to this.
671+
# Otherwise, let's just let them take care of themselves.
672+
# waiter = client.get_waiter("table_not_exists")
673+
# waiter.wait(TableName=table_name)
666674

667675

668676
@pytest.fixture
669677
def mock_metastore():
670678
with mock_dynamodb2():
671-
yield next(build_metastore())
679+
metastore, table_name = build_metastore()
680+
yield metastore
681+
delete_metastore(table_name)
682+
683+
684+
def _count_entries(records, *messages):
685+
count = 0
686+
687+
for record in records:
688+
if all((message in record.getMessage() for message in messages)):
689+
count += 1
690+
691+
return count
692+
693+
694+
def _count_puts(records, table_name):
695+
return _count_entries(records, '"TableName": "{}"'.format(table_name), "OperationModel(name=PutItem)")
696+
697+
698+
def _count_gets(records, table_name):
699+
return _count_entries(records, '"TableName": "{}"'.format(table_name), "OperationModel(name=GetItem)")
700+
701+
702+
def check_metastore_cache_use_encrypt(metastore, table_name, log_capture):
703+
try:
704+
table = boto3.resource("dynamodb").Table(table_name)
705+
except NoRegionError:
706+
table = boto3.resource("dynamodb", region_name=TEST_REGION_NAME).Table(table_name)
707+
708+
most_recent_provider = MostRecentProvider(provider_store=metastore, material_name="test", version_ttl=600.0)
709+
e_table = EncryptedTable(table=table, materials_provider=most_recent_provider)
710+
711+
item = diverse_item()
712+
item.update(TEST_KEY)
713+
e_table.put_item(Item=item)
714+
e_table.put_item(Item=item)
715+
e_table.put_item(Item=item)
716+
e_table.put_item(Item=item)
717+
718+
try:
719+
primary_puts = _count_puts(log_capture.records, e_table.name)
720+
metastore_puts = _count_puts(log_capture.records, metastore._table.name)
721+
722+
assert primary_puts == 4
723+
assert metastore_puts == 1
724+
725+
e_table.get_item(Key=TEST_KEY)
726+
e_table.get_item(Key=TEST_KEY)
727+
e_table.get_item(Key=TEST_KEY)
728+
729+
primary_gets = _count_gets(log_capture.records, e_table.name)
730+
metastore_gets = _count_gets(log_capture.records, metastore._table.name)
731+
metastore_puts = _count_puts(log_capture.records, metastore._table.name)
732+
733+
assert primary_gets == 3
734+
assert metastore_gets == 0
735+
assert metastore_puts == 1
736+
737+
most_recent_provider.refresh()
738+
739+
e_table.get_item(Key=TEST_KEY)
740+
e_table.get_item(Key=TEST_KEY)
741+
e_table.get_item(Key=TEST_KEY)
742+
743+
primary_gets = _count_gets(log_capture.records, e_table.name)
744+
metastore_gets = _count_gets(log_capture.records, metastore._table.name)
745+
746+
assert primary_gets == 6
747+
assert metastore_gets == 1
748+
749+
finally:
750+
e_table.delete_item(Key=TEST_KEY)

test/functional/material_providers/test_most_recent.py

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
from dynamodb_encryption_sdk.material_providers.most_recent import MostRecentProvider
2222
from dynamodb_encryption_sdk.material_providers.store import ProviderStore
2323

24+
from ..functional_test_utils import ( # pylint: disable=unused-import
25+
TEST_TABLE_NAME,
26+
check_metastore_cache_use_encrypt,
27+
example_table,
28+
mock_metastore,
29+
)
30+
2431
pytestmark = [pytest.mark.functional, pytest.mark.local]
2532

2633

@@ -130,3 +137,7 @@ def test_decryption_materials_cache_use():
130137
expected_calls.append(("version_from_material_description", 0))
131138

132139
assert store.provider_calls == expected_calls
140+
141+
142+
def test_cache_use_encrypt(mock_metastore, example_table, caplog):
143+
check_metastore_cache_use_encrypt(mock_metastore, TEST_TABLE_NAME, caplog)

0 commit comments

Comments
 (0)