Skip to content

fix!: Remove the MostRecentProvider. #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/dynamodb_encryption_sdk/identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
80 changes: 23 additions & 57 deletions src/dynamodb_encryption_sdk/material_providers/most_recent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,7 +36,6 @@


__all__ = (
"MostRecentProvider",
"CachingMostRecentProvider",
)
_LOGGER = logging.getLogger(LOGGER_NAME)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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)
90 changes: 17 additions & 73 deletions test/functional/material_providers/test_most_recent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,14 @@
# 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
from mock import MagicMock, sentinel

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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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())

Expand All @@ -379,16 +369,15 @@ 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,
we call to the provider store for new materials.
"""
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)
Expand Down Expand Up @@ -422,16 +411,15 @@ 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,
we use the materials 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._grace_period = 10.0

test1 = provider.encryption_materials(sentinel.encryption_context_1)
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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")