From 5cdfd8aed135d9a4fa1538e4dbcc50d08546ccba Mon Sep 17 00:00:00 2001 From: Adam Harwell Date: Fri, 21 Nov 2014 15:55:50 -0600 Subject: [PATCH] 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 --- barbicanclient/secrets.py | 150 ++++++++++++++------- barbicanclient/test/test_client_secrets.py | 75 ++++++++++- 2 files changed, 178 insertions(+), 47 deletions(-) diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index 027672cd..67c1d404 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -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, diff --git a/barbicanclient/test/test_client_secrets.py b/barbicanclient/test/test_client_secrets.py index 23ecd259..3dd747d2 100644 --- a/barbicanclient/test/test_client_secrets.py +++ b/barbicanclient/test/test_client_secrets.py @@ -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)