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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user