Skip to content

Commit c8e4059

Browse files
authored
Merge pull request #102 from mattsb42-aws/metastore
Fix MostRecentProvider.encryption_materials() caching bug
2 parents bdf167e + 0df01aa commit c8e4059

File tree

10 files changed

+267
-25
lines changed

10 files changed

+267
-25
lines changed

CHANGELOG.rst

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

5+
1.0.6 -- 2018-01-xx
6+
===================
7+
8+
Bugfixes
9+
--------
10+
* Fix :class:`MostRecentProvider` bug in providing invalid cached results.
11+
`#102 <https://github.com/aws/aws-dynamodb-encryption-python/pull/102>`_
12+
513
1.0.5 -- 2018-08-01
614
===================
715
* Move the ``aws-dynamodb-encryption-python`` repository from ``awslabs`` to ``aws``.

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ markers =
2828
veryslow: mark a test as being known to take a very long time to complete (order t > 60s)
2929
nope: mark a test as being so slow that it should only be very infrequently (order t > 30m)
3030
travis_isolation: mark a test that crashes Travis CI when run with other tests
31-
log_level=NOTSET
31+
log_level=DEBUG
3232

3333
# Flake8 Configuration
3434
[flake8]

src/dynamodb_encryption_sdk/material_providers/aws_kms.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ def __attrs_post_init__(self):
214214
default_algorithm=_DEFAULT_SIGNING_ALGORITHM,
215215
default_key_length=_DEFAULT_SIGNING_KEY_LENGTH,
216216
)
217-
self._regional_clients = {} # type: Dict[Text, botocore.client.BaseClient] # noqa pylint: disable=attribute-defined-outside-init
217+
self._regional_clients = (
218+
{}
219+
) # type: Dict[Text, botocore.client.BaseClient] # noqa pylint: disable=attribute-defined-outside-init
218220

219221
def _add_regional_client(self, region_name):
220222
# type: (Text) -> None

src/dynamodb_encryption_sdk/material_providers/most_recent.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,8 @@ class MostRecentProvider(CryptographicMaterialsProvider):
134134
_material_name = attr.ib(validator=attr.validators.instance_of(six.string_types))
135135
_version_ttl = attr.ib(validator=attr.validators.instance_of(float))
136136

137-
def __init__(
138-
self, provider_store, material_name, version_ttl # type: ProviderStore # type: Text # type: float
139-
): # noqa=D107
140-
# type: (...) -> None
137+
def __init__(self, provider_store, material_name, version_ttl): # noqa=D107
138+
# type: (ProviderStore, Text, float) -> None
141139
# Workaround pending resolution of attrs/mypy interaction.
142140
# https://github.com/python/mypy/issues/2088
143141
# https://github.com/python-attrs/attrs/issues/215
@@ -242,6 +240,7 @@ def _get_most_recent_version(self, allow_local):
242240
# We failed to acquire the lock.
243241
# If blocking, we will never reach this point.
244242
# If not blocking, we want whatever the latest local version is.
243+
_LOGGER.debug("Failed to acquire lock. Returning the last cached version.")
245244
version = self._version
246245
return self._cache.get(version)
247246

@@ -254,6 +253,7 @@ def _get_most_recent_version(self, allow_local):
254253
received_version = self._provider_store.version_from_material_description(provider._material_description)
255254
# TODO: ^ should we promote material description from hidden?
256255

256+
_LOGGER.debug("Caching materials provider version %d", received_version)
257257
self._version = received_version
258258
self._last_updated = time.time()
259259
self._cache.put(received_version, provider)
@@ -271,17 +271,22 @@ def encryption_materials(self, encryption_context):
271271
"""
272272
ttl_action = self._ttl_action()
273273

274+
provider = None
275+
274276
if ttl_action is TtlActions.LIVE:
275277
try:
276-
return self._cache.get(self._version)
278+
_LOGGER.debug("Looking in cache for materials provider version %d", self._version)
279+
provider = self._cache.get(self._version)
277280
except KeyError:
281+
_LOGGER.debug("Provider not found in cache")
278282
ttl_action = TtlActions.EXPIRED
279283

280-
# Just get the latest local version if we cannot acquire the lock.
281-
# Otherwise, block until we can acquire the lock.
282-
allow_local = bool(ttl_action is TtlActions.GRACE_PERIOD)
284+
if provider is None:
285+
# Just get the latest local version if we cannot acquire the lock.
286+
# Otherwise, block until we can acquire the lock.
287+
allow_local = bool(ttl_action is TtlActions.GRACE_PERIOD)
283288

284-
provider = self._get_most_recent_version(allow_local)
289+
provider = self._get_most_recent_version(allow_local)
285290

286291
return provider.encryption_materials(encryption_context)
287292

src/dynamodb_encryption_sdk/material_providers/store/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from dynamodb_encryption_sdk.exceptions import NoKnownVersionError
1919
from dynamodb_encryption_sdk.material_providers import ( # noqa pylint: disable=unused-import
20-
CryptographicMaterialsProvider
20+
CryptographicMaterialsProvider,
2121
)
2222

2323
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules

test/functional/functional_test_utils.py

+24
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"""Helper tools for use in tests."""
1414
from __future__ import division
1515

16+
import base64
1617
import copy
1718
import itertools
1819
import logging
@@ -34,6 +35,7 @@
3435
from dynamodb_encryption_sdk.identifiers import CryptoAction
3536
from dynamodb_encryption_sdk.internal.identifiers import ReservedAttributes
3637
from dynamodb_encryption_sdk.material_providers.static import StaticCryptographicMaterialsProvider
38+
from dynamodb_encryption_sdk.material_providers.store.meta import MetaStore
3739
from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider
3840
from dynamodb_encryption_sdk.materials.raw import RawDecryptionMaterials, RawEncryptionMaterials
3941
from dynamodb_encryption_sdk.structures import AttributeActions
@@ -645,3 +647,25 @@ def client_cycle_batch_items_check_paginators(
645647
e_scan_result = e_client.scan(TableName=table_name, ConsistentRead=True)
646648
assert not raw_scan_result["Items"]
647649
assert not e_scan_result["Items"]
650+
651+
652+
def build_metastore():
653+
client = boto3.client("dynamodb", region_name="us-west-2")
654+
table_name = base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8").replace("=", ".")
655+
656+
MetaStore.create_table(client, table_name, 1, 1)
657+
waiter = client.get_waiter("table_exists")
658+
waiter.wait(TableName=table_name)
659+
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))
662+
663+
client.delete_table(TableName=table_name)
664+
waiter = client.get_waiter("table_not_exists")
665+
waiter.wait(TableName=table_name)
666+
667+
668+
@pytest.fixture
669+
def mock_metastore():
670+
with mock_dynamodb2():
671+
yield next(build_metastore())

test/functional/material_providers/store/test_meta.py

+43-11
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,52 @@
1818
import pytest
1919
from moto import mock_dynamodb2
2020

21-
from dynamodb_encryption_sdk.material_providers.store.meta import MetaStore
21+
from dynamodb_encryption_sdk.exceptions import NoKnownVersionError
22+
from dynamodb_encryption_sdk.material_providers.store.meta import MetaStore, MetaStoreAttributeNames
23+
from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider
24+
25+
from ...functional_test_utils import build_static_jce_cmp, mock_metastore
2226

2327
pytestmark = [pytest.mark.functional, pytest.mark.local]
2428

2529

26-
@mock_dynamodb2
27-
def test_create_table():
28-
client = boto3.client("dynamodb", region_name="us-west-2")
29-
table_name = base64.b64encode(os.urandom(32)).decode("utf-8")
30+
def test_create_table(mock_metastore):
31+
# type: (MetaStore) -> None
32+
assert mock_metastore._table.key_schema == [
33+
{"AttributeName": MetaStoreAttributeNames.PARTITION.value, "KeyType": "HASH"},
34+
{"AttributeName": MetaStoreAttributeNames.SORT.value, "KeyType": "RANGE"},
35+
]
36+
assert mock_metastore._table.attribute_definitions == [
37+
{"AttributeName": MetaStoreAttributeNames.PARTITION.value, "AttributeType": "S"},
38+
{"AttributeName": MetaStoreAttributeNames.SORT.value, "AttributeType": "N"},
39+
]
40+
41+
42+
def test_max_version_empty(mock_metastore):
43+
# type: (MetaStore) -> None
44+
with pytest.raises(NoKnownVersionError) as excinfo:
45+
mock_metastore.max_version("example_name")
46+
47+
excinfo.match(r"No known version for name: ")
48+
49+
50+
def test_max_version_exists(mock_metastore):
51+
# type: (MetaStore) -> None
52+
mock_metastore.get_or_create_provider("example_name", 2)
53+
mock_metastore.get_or_create_provider("example_name", 5)
54+
55+
test = mock_metastore.max_version("example_name")
56+
57+
assert test == 5
58+
59+
60+
@pytest.mark.xfail(strict=True)
61+
def test_version_from_material_description():
62+
assert False
63+
3064

31-
MetaStore.create_table(client, table_name, 1, 1)
32-
waiter = client.get_waiter("table_exists")
33-
waiter.wait(TableName=table_name)
65+
def test_provider(mock_metastore):
66+
# type: (MetaStore) -> None
67+
test = mock_metastore.provider("example_name")
3468

35-
client.delete_table(TableName=table_name)
36-
waiter = client.get_waiter("table_not_exists")
37-
waiter.wait(TableName=table_name)
69+
assert isinstance(test, WrappedCryptographicMaterialsProvider)

test/functional/material_providers/test_most_recent.py

+99
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,60 @@
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
1313
"""Functional tests for ``dynamodb_encryption_sdk.material_providers.most_recent``."""
14+
from collections import defaultdict
15+
1416
import pytest
1517
from mock import MagicMock, sentinel
1618

19+
from dynamodb_encryption_sdk.exceptions import NoKnownVersionError
20+
from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider
1721
from dynamodb_encryption_sdk.material_providers.most_recent import MostRecentProvider
1822
from dynamodb_encryption_sdk.material_providers.store import ProviderStore
1923

2024
pytestmark = [pytest.mark.functional, pytest.mark.local]
2125

2226

27+
class SentinelCryptoMaterialsProvider(CryptographicMaterialsProvider):
28+
def __init__(self, name, version):
29+
self.name = name
30+
self.version = version
31+
self._material_description = version
32+
self.provider_calls = []
33+
34+
def encryption_materials(self, encryption_context):
35+
self.provider_calls.append(("encryption_materials", encryption_context))
36+
return getattr(sentinel, "{name}_{version}_encryption".format(name=self.name, version=self.version))
37+
38+
def decryption_materials(self, encryption_context):
39+
self.provider_calls.append(("decryption_materials", encryption_context))
40+
return getattr(sentinel, "{name}_{version}_decryption".format(name=self.name, version=self.version))
41+
42+
43+
class MockProviderStore(ProviderStore):
44+
def __init__(self):
45+
self.provider_calls = []
46+
self._providers = defaultdict(dict)
47+
48+
def get_or_create_provider(self, material_name, version):
49+
self.provider_calls.append(("get_or_create_provider", material_name, version))
50+
try:
51+
return self._providers[material_name][version]
52+
except KeyError:
53+
self._providers[material_name][version] = SentinelCryptoMaterialsProvider(material_name, version)
54+
return self._providers[material_name][version]
55+
56+
def max_version(self, material_name):
57+
self.provider_calls.append(("max_version", material_name))
58+
try:
59+
return sorted(self._providers[material_name].keys())[-1]
60+
except IndexError:
61+
raise NoKnownVersionError('No known version for name: "{}"'.format(material_name))
62+
63+
def version_from_material_description(self, material_description):
64+
self.provider_calls.append(("version_from_material_description", material_description))
65+
return material_description
66+
67+
2368
def test_failed_lock_acquisition():
2469
store = MagicMock(__class__=ProviderStore)
2570
provider = MostRecentProvider(provider_store=store, material_name="my material", version_ttl=10.0)
@@ -31,3 +76,57 @@ def test_failed_lock_acquisition():
3176

3277
assert test is sentinel.nine
3378
assert not store.mock_calls
79+
80+
81+
def test_encryption_materials_cache_use():
82+
store = MockProviderStore()
83+
name = "material"
84+
provider = MostRecentProvider(provider_store=store, material_name=name, version_ttl=10.0)
85+
86+
test1 = provider.encryption_materials(sentinel.encryption_context_1)
87+
assert test1 is sentinel.material_0_encryption
88+
89+
assert provider._version == 0
90+
assert len(provider._cache._cache) == 1
91+
92+
expected_calls = [
93+
("max_version", name),
94+
("get_or_create_provider", name, 0),
95+
("version_from_material_description", 0),
96+
]
97+
98+
assert store.provider_calls == expected_calls
99+
100+
test2 = provider.encryption_materials(sentinel.encryption_context_1)
101+
assert test2 is sentinel.material_0_encryption
102+
103+
assert provider._version == 0
104+
assert len(provider._cache._cache) == 1
105+
106+
assert store.provider_calls == expected_calls
107+
108+
109+
def test_decryption_materials_cache_use():
110+
store = MockProviderStore()
111+
name = "material"
112+
provider = MostRecentProvider(provider_store=store, material_name=name, version_ttl=10.0)
113+
114+
context = MagicMock(material_description=0)
115+
116+
test1 = provider.decryption_materials(context)
117+
assert test1 is sentinel.material_0_decryption
118+
119+
assert len(provider._cache._cache) == 1
120+
121+
expected_calls = [("version_from_material_description", 0), ("get_or_create_provider", name, 0)]
122+
123+
assert store.provider_calls == expected_calls
124+
125+
test2 = provider.decryption_materials(context)
126+
assert test2 is sentinel.material_0_decryption
127+
128+
assert len(provider._cache._cache) == 1
129+
130+
expected_calls.append(("version_from_material_description", 0))
131+
132+
assert store.provider_calls == expected_calls

test/integration/integration_test_utils.py

+5
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,8 @@ def ddb_table_name():
6565
" for integration tests to run"
6666
).format(DDB_TABLE_NAME)
6767
)
68+
69+
70+
@pytest.fixture
71+
def temp_metastore():
72+
yield next(functional_test_utils.build_metastore())

0 commit comments

Comments
 (0)