From 9fd4daac4da97ada8f1116f790b14b4e42b6aa6d Mon Sep 17 00:00:00 2001 From: Benjamin Farley Date: Wed, 20 Jan 2021 09:51:25 -0700 Subject: [PATCH] fix!: Remove the MostRecentProvider. BREAKING CHANGE: Removes the MostRecentProvider, which is replaced by the CachingMostRecentProvider. --- CHANGELOG.rst | 8 ++ src/dynamodb_encryption_sdk/identifiers.py | 2 +- .../material_providers/most_recent.py | 80 +++++------------ .../material_providers/test_most_recent.py | 90 ++++--------------- 4 files changed, 49 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d10b71e9..8a037cd1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,14 @@ Changelog ********* +2.0.0 -- 2021-02-04 +=================== + +Breaking Changes +---------------- +Removes MostRecentProvider. MostRecentProvider is replaced by CachingMostRecentProvider as of 1.3.0. + + 1.3.0 -- 2021-02-04 =================== Adds the CachingMostRecentProvider and deprecates MostRecentProvider. diff --git a/src/dynamodb_encryption_sdk/identifiers.py b/src/dynamodb_encryption_sdk/identifiers.py index f4edd670..ab0b9b33 100644 --- a/src/dynamodb_encryption_sdk/identifiers.py +++ b/src/dynamodb_encryption_sdk/identifiers.py @@ -14,7 +14,7 @@ from enum import Enum __all__ = ("LOGGER_NAME", "CryptoAction", "EncryptionKeyType", "KeyEncodingType") -__version__ = "1.3.0" +__version__ = "2.0.0" LOGGER_NAME = "dynamodb_encryption_sdk" USER_AGENT_SUFFIX = "DynamodbEncryptionSdkPython/{}".format(__version__) diff --git a/src/dynamodb_encryption_sdk/material_providers/most_recent.py b/src/dynamodb_encryption_sdk/material_providers/most_recent.py index 55b6376a..a1d493ba 100644 --- a/src/dynamodb_encryption_sdk/material_providers/most_recent.py +++ b/src/dynamodb_encryption_sdk/material_providers/most_recent.py @@ -13,7 +13,6 @@ """Cryptographic materials provider that uses a provider store to obtain cryptographic materials.""" import logging import time -import warnings from collections import OrderedDict from enum import Enum from threading import Lock, RLock @@ -37,7 +36,6 @@ __all__ = ( - "MostRecentProvider", "CachingMostRecentProvider", ) _LOGGER = logging.getLogger(LOGGER_NAME) @@ -135,10 +133,12 @@ def evict(self, name): @attr.s(init=False) -class MostRecentProvider(CryptographicMaterialsProvider): +@attr.s(init=False) +class CachingMostRecentProvider(CryptographicMaterialsProvider): # pylint: disable=too-many-instance-attributes """Cryptographic materials provider that uses a provider store to obtain cryptography - materials. + materials. Materials obtained from the store are cached for a user-defined amount of time, + then removed from the cache and re-retrieved from the store. When encrypting, the most recent provider that the provider store knows about will always be used. @@ -160,7 +160,6 @@ def __init__(self, provider_store, material_name, version_ttl, cache_size=1000): # Workaround pending resolution of attrs/mypy interaction. # https://github.com/python/mypy/issues/2088 # https://github.com/python-attrs/attrs/issues/215 - warnings.warn("MostRecentProvider is deprecated, use CachingMostRecentProvider instead.", DeprecationWarning) self._provider_store = provider_store self._material_name = material_name self._version_ttl = version_ttl @@ -185,15 +184,26 @@ def decryption_materials(self, encryption_context): :param EncryptionContext encryption_context: Encryption context for request :raises AttributeError: if no decryption materials are available """ + provider = None + version = self._provider_store.version_from_material_description(encryption_context.material_description) - try: - _LOGGER.debug("Looking in cache for decryption materials provider version %d", version) - _, provider = self._cache.get(version) - except KeyError: - _LOGGER.debug("Decryption materials provider not found in cache") + + ttl_action = self._ttl_action(version, _DECRYPT_ACTION) + + if ttl_action is TtlActions.EXPIRED: + self._cache.evict(self._version) + + _LOGGER.debug('TTL Action "%s" when getting decryption materials', ttl_action.name) + if ttl_action is TtlActions.LIVE: + try: + _LOGGER.debug("Looking in cache for encryption materials provider version %d", version) + _, provider = self._cache.get(version) + except KeyError: + _LOGGER.debug("Decryption materials provider not found in cache") + + if provider is None: try: - provider = self._provider_store.provider(self._material_name, version) - self._cache.put(version, (time.time(), provider)) + provider = self._get_provider_with_grace_period(version, ttl_action) except InvalidVersionError: _LOGGER.exception("Unable to get decryption materials from provider store.") raise AttributeError("No decryption materials available") @@ -385,52 +395,8 @@ def encryption_materials(self, encryption_context): def refresh(self): # type: () -> None """Clear all local caches for this provider.""" - _LOGGER.debug("Refreshing MostRecentProvider instance.") + _LOGGER.debug("Refreshing CachingMostRecentProvider instance.") with self._lock: self._cache.clear() self._version = None # type: int # pylint: disable=attribute-defined-outside-init self._last_updated = None # type: float # pylint: disable=attribute-defined-outside-init - - -@attr.s(init=False) -class CachingMostRecentProvider(MostRecentProvider): - """Cryptographic materials provider that uses a provider store to obtain cryptography - materials. Materials obtained from the store are cached for a user-defined amount of time, - then removed from the cache and re-retrieved from the store. - - When encrypting, the most recent provider that the provider store knows about will always - be used. - """ - - def decryption_materials(self, encryption_context): - # type: (EncryptionContext) -> CryptographicMaterials - """Return decryption materials. - - :param EncryptionContext encryption_context: Encryption context for request - :raises AttributeError: if no decryption materials are available - """ - provider = None - - version = self._provider_store.version_from_material_description(encryption_context.material_description) - - ttl_action = self._ttl_action(version, _DECRYPT_ACTION) - - if ttl_action is TtlActions.EXPIRED: - self._cache.evict(self._version) - - _LOGGER.debug('TTL Action "%s" when getting decryption materials', ttl_action.name) - if ttl_action is TtlActions.LIVE: - try: - _LOGGER.debug("Looking in cache for encryption materials provider version %d", version) - _, provider = self._cache.get(version) - except KeyError: - _LOGGER.debug("Decryption materials provider not found in cache") - - if provider is None: - try: - provider = self._get_provider_with_grace_period(version, ttl_action) - except InvalidVersionError: - _LOGGER.exception("Unable to get decryption materials from provider store.") - raise AttributeError("No decryption materials available") - - return provider.decryption_materials(encryption_context) diff --git a/test/functional/material_providers/test_most_recent.py b/test/functional/material_providers/test_most_recent.py index 3e46abe2..778e26af 100644 --- a/test/functional/material_providers/test_most_recent.py +++ b/test/functional/material_providers/test_most_recent.py @@ -12,7 +12,6 @@ # language governing permissions and limitations under the License. """Functional tests for ``dynamodb_encryption_sdk.material_providers.most_recent``.""" import time -import warnings from collections import defaultdict import pytest @@ -20,11 +19,7 @@ from dynamodb_encryption_sdk.exceptions import NoKnownVersionError from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider -from dynamodb_encryption_sdk.material_providers.most_recent import ( - CachingMostRecentProvider, - MostRecentProvider, - TtlActions, -) +from dynamodb_encryption_sdk.material_providers.most_recent import CachingMostRecentProvider, TtlActions from dynamodb_encryption_sdk.material_providers.store import ProviderStore from ..functional_test_utils import example_table # noqa=F401 pylint: disable=unused-import @@ -76,12 +71,11 @@ def version_from_material_description(self, material_description): return material_description -@pytest.mark.parametrize("provider_class", (MostRecentProvider, CachingMostRecentProvider)) -def test_constructor(provider_class): +def test_constructor(): """Tests that when the cache is expired on encrypt, we evict the entry from the cache.""" store = MockProviderStore() name = "material" - provider = provider_class(provider_store=store, material_name=name, version_ttl=1.0, cache_size=42) + provider = CachingMostRecentProvider(provider_store=store, material_name=name, version_ttl=1.0, cache_size=42) assert provider._provider_store == store assert provider._material_name == name @@ -277,10 +271,9 @@ def test_get_most_recent_version_grace_period_lock_not_acquired(): assert store.provider_calls == expected_calls -@pytest.mark.parametrize("provider_class", (MostRecentProvider, CachingMostRecentProvider)) -def test_failed_lock_acquisition(provider_class): +def test_failed_lock_acquisition(): store = MagicMock(__class__=ProviderStore) - provider = provider_class(provider_store=store, material_name="my material", version_ttl=10.0) + provider = CachingMostRecentProvider(provider_store=store, material_name="my material", version_ttl=10.0) provider._version = 9 provider._cache.put(provider._version, (time.time(), sentinel.nine)) @@ -291,11 +284,10 @@ def test_failed_lock_acquisition(provider_class): assert not store.mock_calls -@pytest.mark.parametrize("provider_class", (MostRecentProvider, CachingMostRecentProvider)) -def test_encryption_materials_cache_use(provider_class): +def test_encryption_materials_cache_use(): store = MockProviderStore() name = "material" - provider = provider_class(provider_store=store, material_name=name, version_ttl=10.0) + provider = CachingMostRecentProvider(provider_store=store, material_name=name, version_ttl=10.0) test1 = provider.encryption_materials(sentinel.encryption_context_1) assert test1 is sentinel.material_0_encryption @@ -320,11 +312,10 @@ def test_encryption_materials_cache_use(provider_class): assert store.provider_calls == expected_calls -@pytest.mark.parametrize("provider_class", (MostRecentProvider, CachingMostRecentProvider)) -def test_encryption_materials_cache_expired(provider_class): +def test_encryption_materials_cache_expired(): store = MockProviderStore() name = "material" - provider = provider_class(provider_store=store, material_name=name, version_ttl=0.0) + provider = CachingMostRecentProvider(provider_store=store, material_name=name, version_ttl=0.0) test1 = provider.encryption_materials(sentinel.encryption_context_1) assert test1 is sentinel.material_0_encryption @@ -354,12 +345,11 @@ def test_encryption_materials_cache_expired(provider_class): assert store.provider_calls == expected_calls -@pytest.mark.parametrize("provider_class", (MostRecentProvider, CachingMostRecentProvider)) -def test_encryption_materials_cache_expired_cache_removed(provider_class): +def test_encryption_materials_cache_expired_cache_removed(): """Tests that when the cache is expired on encrypt, we evict the entry from the cache.""" store = MockProviderStore() name = "material" - provider = provider_class(provider_store=store, material_name=name, version_ttl=0.0) + provider = CachingMostRecentProvider(provider_store=store, material_name=name, version_ttl=0.0) provider._cache = MagicMock() provider._cache.get.return_value = (0.0, MagicMock()) @@ -379,8 +369,7 @@ def test_decryption_materials_cache_expired_cache_removed(): provider._cache.evict.assert_called_once() -@pytest.mark.parametrize("provider_class", (MostRecentProvider, CachingMostRecentProvider)) -def test_encryption_materials_cache_in_grace_period_acquire_lock(provider_class): +def test_encryption_materials_cache_in_grace_period_acquire_lock(): """Test encryption grace period behavior. When the TTL is GRACE_PERIOD and we successfully acquire the lock for retrieving new materials, @@ -388,7 +377,7 @@ def test_encryption_materials_cache_in_grace_period_acquire_lock(provider_class) """ store = MockProviderStore() name = "material" - provider = provider_class(provider_store=store, material_name=name, version_ttl=0.0) + provider = CachingMostRecentProvider(provider_store=store, material_name=name, version_ttl=0.0) provider._grace_period = 10.0 test1 = provider.encryption_materials(sentinel.encryption_context_1) @@ -422,8 +411,7 @@ def test_encryption_materials_cache_in_grace_period_acquire_lock(provider_class) assert store.provider_calls == expected_calls -@pytest.mark.parametrize("provider_class", (MostRecentProvider, CachingMostRecentProvider)) -def test_encryption_materials_cache_in_grace_period_fail_to_acquire_lock(provider_class): +def test_encryption_materials_cache_in_grace_period_fail_to_acquire_lock(): """Test encryption grace period behavior. When the TTL is GRACE_PERIOD and we fail to acquire the lock for retrieving new materials, @@ -431,7 +419,7 @@ def test_encryption_materials_cache_in_grace_period_fail_to_acquire_lock(provide """ store = MockProviderStore() name = "material" - provider = provider_class(provider_store=store, material_name=name, version_ttl=0.0) + provider = CachingMostRecentProvider(provider_store=store, material_name=name, version_ttl=0.0) provider._grace_period = 10.0 test1 = provider.encryption_materials(sentinel.encryption_context_1) @@ -463,43 +451,10 @@ def test_encryption_materials_cache_in_grace_period_fail_to_acquire_lock(provide assert store.provider_calls == expected_calls -@pytest.mark.parametrize("provider_class", (CachingMostRecentProvider, CachingMostRecentProvider)) -def test_decryption_materials_cache_use(provider_class): - store = MockProviderStore() - name = "material" - provider = provider_class(provider_store=store, material_name=name, version_ttl=10.0) - - context = MagicMock(material_description=0) - - test1 = provider.decryption_materials(context) - assert test1 is sentinel.material_0_decryption - - assert len(provider._cache._cache) == 1 - - expected_calls = [("version_from_material_description", 0), ("get_or_create_provider", name, 0)] - - assert store.provider_calls == expected_calls - - test2 = provider.decryption_materials(context) - assert test2 is sentinel.material_0_decryption - - assert len(provider._cache._cache) == 1 - - expected_calls.append(("version_from_material_description", 0)) - - assert store.provider_calls == expected_calls - - -def test_most_recent_provider_decryption_materials_cache_expired(): - """Test decryption expiration behavior for MostRecentProvider. - - When using a MostRecentProvider and the cache is expired on decryption, we do not retrieve new - materials from the provider store. Note that this test only runs for MostRecentProvider, to ensure that our legacy - behavior has not changed. - """ +def test_decryption_materials_cache_use(): store = MockProviderStore() name = "material" - provider = MostRecentProvider(provider_store=store, material_name=name, version_ttl=0.0) + provider = CachingMostRecentProvider(provider_store=store, material_name=name, version_ttl=10.0) context = MagicMock(material_description=0) @@ -517,7 +472,6 @@ def test_most_recent_provider_decryption_materials_cache_expired(): assert len(provider._cache._cache) == 1 - # The MostRecentProvider does not use TTLs on decryption, so we should not see a new call to the provider store expected_calls.append(("version_from_material_description", 0)) assert store.provider_calls == expected_calls @@ -629,13 +583,3 @@ def test_caching_provider_decryption_materials_cache_in_grace_period_fail_to_acq def test_cache_use_encrypt(mock_metastore, example_table, caplog): check_metastore_cache_use_encrypt(mock_metastore, TEST_TABLE_NAME, caplog) - - -def test_most_recent_provider_deprecated(): - warnings.simplefilter("error") - - with pytest.raises(DeprecationWarning) as excinfo: - store = MockProviderStore() - name = "material" - MostRecentProvider(provider_store=store, material_name=name, version_ttl=0.0) - excinfo.match("MostRecentProvider is deprecated, use CachingMostRecentProvider instead")