Skip to content

Commit 5f9ea84

Browse files
authored
Merge pull request #42 from mattsb42-aws/mrp
most recent provider and provider stores
2 parents 6892c07 + 10f7731 commit 5f9ea84

File tree

16 files changed

+1447
-31
lines changed

16 files changed

+1447
-31
lines changed

doc/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ Modules
2121
dynamodb_encryption_sdk.encrypted.table
2222
dynamodb_encryption_sdk.material_providers
2323
dynamodb_encryption_sdk.material_providers.aws_kms
24+
dynamodb_encryption_sdk.material_providers.most_recent
2425
dynamodb_encryption_sdk.material_providers.static
2526
dynamodb_encryption_sdk.material_providers.wrapped
27+
dynamodb_encryption_sdk.material_providers.store
28+
dynamodb_encryption_sdk.material_providers.store.meta
2629
dynamodb_encryption_sdk.materials
2730
dynamodb_encryption_sdk.materials.raw
2831
dynamodb_encryption_sdk.materials.wrapped

src/dynamodb_encryption_sdk/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,19 @@ class SigningError(DynamodbEncryptionSdkError):
8787

8888
class SignatureVerificationError(DynamodbEncryptionSdkError):
8989
"""Otherwise undifferentiated error encountered while verifying a signature."""
90+
91+
92+
class ProviderStoreError(DynamodbEncryptionSdkError):
93+
"""Otherwise undifferentiated error encountered by a provider store."""
94+
95+
96+
class NoKnownVersionError(ProviderStoreError):
97+
"""Raised if a provider store cannot locate any version of the requested material."""
98+
99+
100+
class InvalidVersionError(ProviderStoreError):
101+
"""Raised if an invalid version of a material is requested."""
102+
103+
104+
class VersionAlreadyExistsError(ProviderStoreError):
105+
"""Raised if a version that is being added to a provider store already exists."""
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
"""Cryptographic materials provider that uses a provider store to obtain cryptographic materials."""
14+
from collections import OrderedDict
15+
from enum import Enum
16+
import logging
17+
from threading import Lock, RLock
18+
import time
19+
20+
import attr
21+
import six
22+
23+
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
24+
from typing import Any, Text # noqa pylint: disable=unused-import
25+
except ImportError: # pragma: no cover
26+
# We only actually need these imports when running the mypy checks
27+
pass
28+
29+
from dynamodb_encryption_sdk.exceptions import InvalidVersionError, NoKnownVersionError
30+
from dynamodb_encryption_sdk.identifiers import LOGGER_NAME
31+
from dynamodb_encryption_sdk.materials import CryptographicMaterials # noqa pylint: disable=unused-import
32+
from dynamodb_encryption_sdk.structures import EncryptionContext # noqa pylint: disable=unused-import
33+
from . import CryptographicMaterialsProvider
34+
from .store import ProviderStore
35+
36+
__all__ = ('MostRecentProvider',)
37+
_LOGGER = logging.getLogger(LOGGER_NAME)
38+
#: Grace period during which we will return the latest local materials. This allows multiple
39+
#: threads to be using this same provider without risking lock contention or many threads
40+
#: all attempting to create new versions simultaneously.
41+
_GRACE_PERIOD = 0.5
42+
43+
44+
class TtlActions(Enum):
45+
"""Actions that can be taken based on the version TTl state."""
46+
EXPIRED = 0
47+
GRACE_PERIOD = 1
48+
LIVE = 2
49+
50+
51+
def _min_capacity_validator(instance, attribute, value):
52+
"""Attrs validator to require that value is at least 1."""
53+
if value < 1:
54+
raise ValueError('Cache capacity must be at least 1')
55+
56+
57+
@attr.s(init=False)
58+
class BasicCache(object):
59+
"""Most basic LRU cache."""
60+
61+
capacity = attr.ib(validator=(
62+
attr.validators.instance_of(int),
63+
_min_capacity_validator
64+
))
65+
66+
def __init__(self, capacity):
67+
# type: (int) -> None
68+
"""Workaround pending resolution of attrs/mypy interaction.
69+
https://github.com/python/mypy/issues/2088
70+
https://github.com/python-attrs/attrs/issues/215
71+
"""
72+
self.capacity = capacity
73+
attr.validate(self)
74+
self.__attrs_post_init__()
75+
76+
def __attrs_post_init__(self):
77+
# type; () -> None
78+
"""Initialize the internal cache."""
79+
self._cache_lock = RLock() # attrs confuses pylint: disable=attribute-defined-outside-init
80+
self.clear()
81+
82+
def _prune(self):
83+
# type: () -> None
84+
"""Prunes internal cache until internal cache is within the defined limit."""
85+
with self._cache_lock:
86+
while len(self._cache) > self.capacity:
87+
self._cache.popitem(last=False)
88+
89+
def put(self, name, value):
90+
# type: (Any, Any) -> None
91+
"""Add a value to the cache.
92+
93+
:param name: Hashable object to identify the value in the cache
94+
:param value: Value to add to cache
95+
"""
96+
with self._cache_lock:
97+
self._cache[name] = value
98+
self._prune()
99+
100+
def get(self, name):
101+
# type: (Any) -> Any
102+
"""Get a value from the cache."""
103+
with self._cache_lock:
104+
value = self._cache.pop(name)
105+
self.put(name, value) # bump to the from of the LRU
106+
return value
107+
108+
def clear(self):
109+
# type: () -> None
110+
"""Clear the cache."""
111+
with self._cache_lock:
112+
self._cache = OrderedDict() # type: OrderedDict[Any, Any]
113+
114+
115+
@attr.s(init=False)
116+
class MostRecentProvider(CryptographicMaterialsProvider):
117+
"""Cryptographic materials provider that uses a provider store to obtain cryptography
118+
materials.
119+
120+
When encrypting, the most recent provider that the provider store knows about will always
121+
be used.
122+
123+
:param provider_store: Provider store to use
124+
:type provider_store: dynamodb_encryption_sdk.material_providers.store.ProviderStore
125+
:param str material_name: Name of materials for which to ask the provider store
126+
:param float version_ttl: Max time in seconds to go until checking with provider store
127+
for a more recent version
128+
"""
129+
130+
_provider_store = attr.ib(validator=attr.validators.instance_of(ProviderStore))
131+
_material_name = attr.ib(validator=attr.validators.instance_of(six.string_types))
132+
_version_ttl = attr.ib(validator=attr.validators.instance_of(float))
133+
134+
def __init__(
135+
self,
136+
provider_store, # type: ProviderStore
137+
material_name, # type: Text
138+
version_ttl # type: float
139+
):
140+
# type: (...) -> None
141+
"""Workaround pending resolution of attrs/mypy interaction.
142+
https://github.com/python/mypy/issues/2088
143+
https://github.com/python-attrs/attrs/issues/215
144+
"""
145+
self._provider_store = provider_store
146+
self._material_name = material_name
147+
self._version_ttl = version_ttl
148+
attr.validate(self)
149+
self.__attrs_post_init__()
150+
151+
def __attrs_post_init__(self):
152+
# type: () -> None
153+
"""Initialize the cache."""
154+
self._lock = Lock()
155+
self._cache = BasicCache(1000)
156+
self.refresh()
157+
158+
def decryption_materials(self, encryption_context):
159+
# type: (EncryptionContext) -> CryptographicMaterials
160+
"""Return decryption materials.
161+
162+
:param encryption_context: Encryption context for request
163+
:type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext
164+
:raises AttributeError: if no decryption materials are available
165+
"""
166+
version = self._provider_store.version_from_material_description(encryption_context.material_description)
167+
try:
168+
provider = self._cache.get(version)
169+
except KeyError:
170+
try:
171+
provider = self._provider_store.provider(self._material_name, version)
172+
except InvalidVersionError:
173+
_LOGGER.exception('Unable to get decryption materials from provider store.')
174+
raise AttributeError('No decryption materials available')
175+
176+
self._cache.put(version, provider)
177+
178+
return provider.decryption_materials(encryption_context)
179+
180+
def _ttl_action(self):
181+
# type: () -> bool
182+
"""Determine the correct action to take based on the local resources and TTL.
183+
184+
:returns: decision
185+
:rtype: TtlActions
186+
"""
187+
if self._version is None:
188+
return TtlActions.EXPIRED
189+
190+
time_since_updated = time.time() - self._last_updated
191+
192+
if time_since_updated < self._version_ttl:
193+
return TtlActions.LIVE
194+
195+
elif time_since_updated < self._version_ttl + _GRACE_PERIOD:
196+
return TtlActions.GRACE_PERIOD
197+
198+
return TtlActions.EXPIRED
199+
200+
def _get_max_version(self):
201+
# type: () -> int
202+
"""Ask the provider store for the most recent version of this material.
203+
204+
:returns: Latest version in the provider store (0 if not found)
205+
:rtype: int
206+
"""
207+
try:
208+
return self._provider_store.max_version(self._material_name)
209+
except NoKnownVersionError:
210+
return 0
211+
212+
def _get_provider(self, version):
213+
# type: (int) -> CryptographicMaterialsProvider
214+
"""Ask the provider for a specific version of this material.
215+
216+
:param int version: Version to request
217+
:returns: Cryptographic materials provider for the requested version
218+
:rtype: CryptographicMaterialsProvider
219+
:raises AttributeError: if provider could not locate version
220+
"""
221+
try:
222+
return self._provider_store.get_or_create_provider(self._material_name, version)
223+
except InvalidVersionError:
224+
_LOGGER.exception('Unable to get encryption materials from provider store.')
225+
raise AttributeError('No encryption materials available')
226+
227+
def _get_most_recent_version(self, allow_local):
228+
# type: (bool) -> CryptographicMaterialsProvider
229+
"""Get the most recent version of the provider.
230+
231+
If allowing local and we cannot obtain the lock, just return the most recent local
232+
version. Otherwise, wait for the lock and ask the provider store for the most recent
233+
version of the provider.
234+
235+
:param bool allow_local: Should we allow returning the local version if we cannot obtain the lock?
236+
:returns: version and corresponding cryptographic materials provider
237+
:rtype: CryptographicMaterialsProvider
238+
"""
239+
acquired = self._lock.acquire(blocking=not allow_local)
240+
241+
if not acquired:
242+
# We failed to acquire the lock.
243+
# If blocking, we will never reach this point.
244+
# If not blocking, we want whatever the latest local version is.
245+
version = self._version
246+
return version, self._cache.get(version)
247+
248+
try:
249+
max_version = self._get_max_version()
250+
try:
251+
provider = self._cache.get(max_version)
252+
except KeyError:
253+
provider = self._get_provider(max_version)
254+
received_version = self._provider_store.version_from_material_description(provider._material_description)
255+
# TODO: ^ should we promote material description from hidden?
256+
257+
self._version = received_version
258+
self._last_updated = time.time()
259+
self._cache.put(received_version, provider)
260+
finally:
261+
self._lock.release()
262+
263+
return provider
264+
265+
def encryption_materials(self, encryption_context):
266+
# type: (EncryptionContext) -> CryptographicMaterials
267+
"""Return encryption materials.
268+
269+
:param encryption_context: Encryption context for request
270+
:type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext
271+
:raises AttributeError: if no encryption materials are available
272+
"""
273+
ttl_action = self._ttl_action()
274+
275+
if ttl_action is TtlActions.LIVE:
276+
try:
277+
return self._cache.get(self._version)
278+
except KeyError:
279+
ttl_action = TtlActions.EXPIRED
280+
281+
if ttl_action is TtlActions.GRACE_PERIOD:
282+
# Just get the latest local version if we cannot acquire the lock.
283+
allow_local = True
284+
else:
285+
# Block until we can acquire the lock.
286+
allow_local = False
287+
288+
provider = self._get_most_recent_version(allow_local)
289+
290+
return provider.encryption_materials(encryption_context)
291+
292+
def refresh(self):
293+
# type: () -> None
294+
"""Clear all local caches for this provider."""
295+
with self._lock:
296+
self._cache.clear()
297+
self._version = None # type: int
298+
self._last_updated = None # type: float

0 commit comments

Comments
 (0)