Defer loading Secret meta-data until requested

This change will cause all attributes on a Secret to be lazy-loaded,
meaning it will be possible to get the payload from a Secret object
without also pulling the meta-data (which was not previously an option).

Change-Id: Iea51f9aae580eb5cada81de697c3b14c179a01af
This commit is contained in:
Adam Harwell
2014-11-21 15:55:50 -06:00
parent c54b93989e
commit 5cdfd8aed1
2 changed files with 178 additions and 47 deletions

View File

@@ -25,6 +25,14 @@ from barbicanclient import formatter
LOG = logging.getLogger(__name__)
def lazy(func):
@functools.wraps(func)
def wrapper(self, *args):
self._fill_lazy_properties()
return func(self, *args)
return wrapper
def immutable_after_save(func):
@functools.wraps(func)
def wrapper(self, *args):
@@ -74,83 +82,67 @@ class Secret(SecretFormatter):
content_types=None, status=None):
self._api = api
self._secret_ref = secret_ref
self._name = name
self._algorithm = algorithm
self._bit_length = bit_length
self._mode = mode
self._payload = payload
self._payload_content_encoding = payload_content_encoding
self._expiration = expiration
if self._expiration:
self._expiration = parse_isotime(self._expiration)
if secret_ref:
self._content_types = content_types
self._status = status
self._created = created
self._updated = updated
if self._created:
self._created = parse_isotime(self._created)
if self._updated:
self._updated = parse_isotime(self._updated)
else:
self._content_types = None
self._status = None
self._created = None
self._updated = None
if not self._content_types:
self._payload_content_type = payload_content_type
else:
self._payload_content_type = self._content_types.get('default',
None)
self._fill_from_data(
name=name,
expiration=expiration,
algorithm=algorithm,
bit_length=bit_length,
mode=mode,
payload=payload,
payload_content_type=payload_content_type,
payload_content_encoding=payload_content_encoding,
created=created,
updated=updated,
content_types=content_types,
status=status
)
@property
def secret_ref(self):
return self._secret_ref
@property
@lazy
def name(self):
return self._name
@property
@lazy
def expiration(self):
return self._expiration
@property
@lazy
def algorithm(self):
return self._algorithm
@property
@lazy
def bit_length(self):
return self._bit_length
@property
@lazy
def mode(self):
return self._mode
@property
def payload(self):
if not self._payload:
self._fetch_payload()
return self._payload
@property
def payload_content_type(self):
return self._payload_content_type
@property
@lazy
def payload_content_encoding(self):
return self._payload_content_encoding
@property
@lazy
def created(self):
return self._created
@property
@lazy
def updated(self):
return self._updated
@property
@lazy
def content_types(self):
if self._content_types:
return self._content_types
@@ -159,9 +151,22 @@ class Secret(SecretFormatter):
return None
@property
@lazy
def status(self):
return self._status
@property
def payload_content_type(self):
if not self._payload_content_type and self.content_types:
self._payload_content_type = self.content_types.get('default')
return self._payload_content_type
@property
def payload(self):
if not self._payload:
self._fetch_payload()
return self._payload
@name.setter
@immutable_after_save
def name(self, value):
@@ -203,13 +208,13 @@ class Secret(SecretFormatter):
self._payload_content_encoding = value
def _fetch_payload(self):
if not self._payload_content_type and not self._content_types:
if not self.payload_content_type and not self.content_types:
raise ValueError('Secret has no encrypted data to decrypt.')
elif not self._payload_content_type:
elif not self.payload_content_type:
raise ValueError("Must specify decrypt content-type as "
"secret does not specify a 'default' "
"content-type.")
headers = {'Accept': self._payload_content_type}
headers = {'Accept': self.payload_content_type}
self._payload = self._api._get_raw(self._secret_ref, headers)
@immutable_after_save
@@ -240,8 +245,64 @@ class Secret(SecretFormatter):
else:
raise LookupError("Secret is not yet stored.")
def _fill_from_data(self, name=None, expiration=None, algorithm=None,
bit_length=None, mode=None, payload=None,
payload_content_type=None,
payload_content_encoding=None, created=None,
updated=None, content_types=None, status=None):
self._name = name
self._algorithm = algorithm
self._bit_length = bit_length
self._mode = mode
self._payload = payload
self._payload_content_encoding = payload_content_encoding
self._expiration = expiration
if self._expiration:
self._expiration = parse_isotime(self._expiration)
if self._secret_ref:
self._content_types = content_types
self._status = status
self._created = created
self._updated = updated
if self._created:
self._created = parse_isotime(self._created)
if self._updated:
self._updated = parse_isotime(self._updated)
else:
self._content_types = None
self._status = None
self._created = None
self._updated = None
if not self._content_types:
self._payload_content_type = payload_content_type
else:
self._payload_content_type = self._content_types.get('default',
None)
def _fill_lazy_properties(self):
if self._secret_ref and not self._name:
result = self._api._get(self._secret_ref)
self._fill_from_data(
name=result.get('name'),
expiration=result.get('expiration'),
algorithm=result.get('algorithm'),
bit_length=result.get('bit_length'),
mode=result.get('mode'),
payload_content_type=result.get('payload_content_type'),
payload_content_encoding=result.get(
'payload_content_encoding'
),
created=result.get('created'),
updated=result.get('updated'),
content_types=result.get('content_types'),
status=result.get('status')
)
def __repr__(self):
return 'Secret(name="{0}")'.format(self.name)
if self._secret_ref:
return 'Secret(secret_ref="{0}")'.format(self._secret_ref)
return 'Secret(name="{0}")'.format(self._name)
class SecretManager(base.BaseEntityManager):
@@ -259,11 +320,10 @@ class SecretManager(base.BaseEntityManager):
"""
LOG.debug("Getting secret - Secret href: {0}".format(secret_ref))
base.validate_ref(secret_ref, 'Secret')
response = self._api._get(secret_ref)
return Secret(
api=self._api,
payload_content_type=payload_content_type,
**response
secret_ref=secret_ref
)
def create(self, name=None, payload=None,

View File

@@ -138,18 +138,89 @@ class WhenTestingSecrets(test_client.BaseEntityResource):
except AttributeError:
pass
def test_should_get(self):
def test_should_get_lazy(self):
self.api._get.return_value = self.secret.get_dict(self.entity_href)
secret = self.manager.get(secret_ref=self.entity_href)
self.assertIsInstance(secret, secrets.Secret)
self.assertEqual(self.entity_href, secret.secret_ref)
# Verify the correct URL was used to make the call.
# Verify GET wasn't called yet
self.assertFalse(self.api._get.called)
# Check an attribute to trigger lazy-load
self.assertEqual(self.secret.name, secret.name)
# Verify the correct URL was used to make the GET call
args, kwargs = self.api._get.call_args
url = args[0]
self.assertEqual(self.entity_href, url)
def test_should_get_payload_only(self):
self.api._get.return_value = self.secret.get_dict(self.entity_href)
self.api._get_raw.return_value = self.secret.payload
secret = self.manager.get(
secret_ref=self.entity_href,
payload_content_type=self.secret.payload_content_type
)
self.assertIsInstance(secret, secrets.Secret)
self.assertEqual(self.entity_href, secret.secret_ref)
# Verify `get` wasn't called yet (metadata)
self.assertFalse(self.api._get.called)
# Verify `get_raw` wasn't called yet (payload)
self.assertFalse(self.api._get_raw.called)
# GET payload (with payload_content_type)
self.assertEqual(self.secret.payload, secret.payload)
# Verify `get` still wasn't called (metadata)
self.assertFalse(self.api._get.called)
# Verify `get_raw` was called (payload)
self.assertTrue(self.api._get_raw.called)
# Verify the correct URL was used to make the `get_raw` call
args, kwargs = self.api._get_raw.call_args
url = args[0]
self.assertEqual(self.entity_href, url)
def test_should_fetch_metadata_to_get_payload_if_no_content_type_set(self):
content_types_dict = {'default': 'application/octet-stream'}
self.api._get.return_value = self.secret.get_dict(
self.entity_href, content_types_dict=content_types_dict)
self.api._get_raw.return_value = self.secret.payload
secret = self.manager.get(secret_ref=self.entity_href)
self.assertIsInstance(secret, secrets.Secret)
self.assertEqual(self.entity_href, secret.secret_ref)
# Verify `get` wasn't called yet (metadata)
self.assertFalse(self.api._get.called)
# Verify `get_raw` wasn't called yet (payload)
self.assertFalse(self.api._get_raw.called)
# GET payload (with no payload_content_type) trigger lazy-load
self.assertEqual(self.secret.payload, secret.payload)
# Verify `get` was called (metadata)
self.assertTrue(self.api._get.called)
# Verify `get_raw` was called (payload)
self.assertTrue(self.api._get_raw.called)
# Verify the correct URL was used to make the `get` calls
args, kwargs = self.api._get.call_args
url = args[0]
self.assertEqual(self.entity_href, url)
args, kwargs = self.api._get_raw.call_args
url = args[0]
self.assertEqual(self.entity_href, url)
def test_should_decrypt_with_content_type(self):
self.api._get.return_value = self.secret.get_dict(self.entity_href)