From 039f9830da5970af956a7436bd2f2d897c1aaa90 Mon Sep 17 00:00:00 2001 From: Felipe Monteiro Date: Wed, 13 Jun 2018 22:06:35 +0100 Subject: [PATCH] Move retrieval of encrypted documents to Deckhand controller This patchset moves retrieval of encrypted documents to the Deckhand controller so that components like Pegleg and Promenade can consume the Deckhand engine offline without running into Barbican errors. Components can pass in `encryption_sources` to Deckhand's rendering module which Deckhand will now use instead to resolve secret references. `encryption_sources` is a dictionary that maps the reference contained in the destination document's data section to the actual unecrypted data. If encrypting data with Barbican, the reference will be a Barbican secret reference. Change-Id: I1a457d3bd37101d73a28882845c2ce74ac09fdf4 --- deckhand/barbican/client_wrapper.py | 3 +- deckhand/barbican/driver.py | 74 +++++++------------ deckhand/common/document.py | 21 ++++++ deckhand/control/buckets.py | 9 +-- deckhand/control/revision_documents.py | 46 ++++++++++-- deckhand/db/sqlalchemy/api.py | 2 +- deckhand/engine/layering.py | 12 ++- deckhand/engine/secrets_manager.py | 52 ++++++------- deckhand/errors.py | 48 +++++++++--- deckhand/tests/test_utils.py | 5 ++ deckhand/tests/unit/control/test_errors.py | 4 +- .../tests/unit/engine/test_secrets_manager.py | 62 +++++++++++----- doc/source/exceptions.rst | 14 +++- 13 files changed, 225 insertions(+), 127 deletions(-) diff --git a/deckhand/barbican/client_wrapper.py b/deckhand/barbican/client_wrapper.py index 96514e75..59b3265f 100644 --- a/deckhand/barbican/client_wrapper.py +++ b/deckhand/barbican/client_wrapper.py @@ -63,7 +63,8 @@ class BarbicanClientWrapper(object): except barbican_exc.HTTPAuthError as e: LOG.exception(str(e)) - raise errors.BarbicanException(details=str(e)) + raise errors.BarbicanClientException(code=e.status_code, + details=str(e)) return cli diff --git a/deckhand/barbican/driver.py b/deckhand/barbican/driver.py index f19b389a..6ed6443f 100644 --- a/deckhand/barbican/driver.py +++ b/deckhand/barbican/driver.py @@ -13,12 +13,11 @@ # limitations under the License. import ast -import re import barbicanclient from oslo_log import log as logging from oslo_serialization import base64 -from oslo_utils import uuidutils +from oslo_utils import excutils import six from deckhand.barbican import client_wrapper @@ -30,30 +29,9 @@ LOG = logging.getLogger(__name__) class BarbicanDriver(object): - _url_re = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|' - '(?:%[0-9a-fA-F][0-9a-fA-F]))+') - def __init__(self): self.barbicanclient = client_wrapper.BarbicanClientWrapper() - @classmethod - def is_barbican_ref(cls, secret_ref): - # TODO(felipemonteiro): Query Keystone service catalog for Barbican - # endpoint and cache it if Keystone is enabled. For now, it should be - # enough to check that ``secret_ref`` is a valid URL, contains - # 'secrets' substring, ends in a UUID and that the source document from - # which the reference is extracted is encrypted. - try: - secret_uuid = secret_ref.split('/')[-1] - except Exception: - secret_uuid = None - return ( - isinstance(secret_ref, six.string_types) and - cls._url_re.match(secret_ref) and - 'secrets' in secret_ref and - uuidutils.is_uuid_like(secret_uuid) - ) - @staticmethod def _get_secret_type(schema): """Get the Barbican secret type based on the following mapping: @@ -170,54 +148,54 @@ class BarbicanDriver(object): secret = self.barbicanclient.call("secrets.create", **kwargs) secret_ref = secret.store() except (barbicanclient.exceptions.HTTPAuthError, - barbicanclient.exceptions.HTTPClientError, - barbicanclient.exceptions.HTTPServerError) as e: + barbicanclient.exceptions.HTTPClientError) as e: + LOG.exception(str(e)) + raise errors.BarbicanClientException(code=e.status_code, + details=str(e)) + except barbicanclient.exceptions.HTTPServerError as e: LOG.error('Caught %s error from Barbican, likely due to a ' 'configuration or deployment issue.', e.__class__.__name__) - raise errors.BarbicanException(details=str(e)) + raise errors.BarbicanServerException(details=str(e)) except barbicanclient.exceptions.PayloadException as e: LOG.error('Caught %s error from Barbican, because the secret ' 'payload type is unsupported.', e.__class__.__name__) - raise errors.BarbicanException(details=str(e)) + raise errors.BarbicanServerException(details=str(e)) return secret_ref - def _base64_decode_payload(self, src_doc, dest_doc, payload): + def _base64_decode_payload(self, payload): + # If the secret_type is 'opaque' then this implies the + # payload was encoded to base64 previously. Reverse the + # operation. try: - # If the secret_type is 'opaque' then this implies the - # payload was encoded to base64 previously. Reverse the - # operation. - payload = ast.literal_eval(base64.decode_as_text(payload)) + return ast.literal_eval(base64.decode_as_text(payload)) except Exception: - message = ('Failed to unencode the original payload that ' - 'presumably was encoded to base64 with ' - 'secret_type=opaque for document [%s, %s] %s.' % - src_doc.meta) - LOG.error(message) - raise errors.UnknownSubstitutionError( - src_schema=src_doc.schema, src_layer=src_doc.layer, - src_name=src_doc.name, schema=dest_doc.schema, - layer=dest_doc.layer, name=dest_doc.name, - details=message) - return payload + with excutils.save_and_reraise_exception(): + message = ('Failed to unencode the original payload that ' + 'presumably was encoded to base64 with ' + 'secret_type: opaque.') + LOG.error(message) - def get_secret(self, src_doc, dest_doc, secret_ref): + def get_secret(self, secret_ref, src_doc): """Get a secret.""" try: secret = self.barbicanclient.call("secrets.get", secret_ref) except (barbicanclient.exceptions.HTTPAuthError, - barbicanclient.exceptions.HTTPClientError, - barbicanclient.exceptions.HTTPServerError, + barbicanclient.exceptions.HTTPClientError) as e: + LOG.exception(str(e)) + raise errors.BarbicanClientException(code=e.status_code, + details=str(e)) + except (barbicanclient.exceptions.HTTPServerError, ValueError) as e: LOG.exception(str(e)) - raise errors.BarbicanException(details=str(e)) + raise errors.BarbicanServerException(details=str(e)) payload = secret.payload if secret.secret_type == 'opaque': LOG.debug('Forcibly base64-decoding original non-string payload ' 'for document [%s, %s] %s.', *src_doc.meta) - secret = self._base64_decode_payload(src_doc, dest_doc, payload) + secret = self._base64_decode_payload(payload) else: secret = payload diff --git a/deckhand/common/document.py b/deckhand/common/document.py index 2c8e2fb6..54009272 100644 --- a/deckhand/common/document.py +++ b/deckhand/common/document.py @@ -15,12 +15,19 @@ import collections import functools import inspect +import re from oslo_serialization import jsonutils as json +from oslo_utils import uuidutils +import six from deckhand.common import utils +_URL_RE = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|' + '(?:%[0-9a-fA-F][0-9a-fA-F]))+') + + class DocumentDict(dict): """Wrapper for a document. @@ -136,6 +143,20 @@ class DocumentDict(dict): def is_encrypted(self): return self.storage_policy == 'encrypted' + @property + def has_barbican_ref(self): + try: + secret_ref = self.data + secret_uuid = secret_ref.split('/')[-1] + except Exception: + secret_uuid = None + return ( + isinstance(secret_ref, six.string_types) and + _URL_RE.match(secret_ref) and + 'secrets' in secret_ref and + uuidutils.is_uuid_like(secret_uuid) + ) + @property def is_replacement(self): return utils.jsonpath_parse(self, 'metadata.replacement') is True diff --git a/deckhand/control/buckets.py b/deckhand/control/buckets.py index 4124a393..027ec00a 100644 --- a/deckhand/control/buckets.py +++ b/deckhand/control/buckets.py @@ -57,12 +57,7 @@ class BucketsResource(api_base.BaseResource): 'deckhand:create_encrypted_documents', req.context) break - try: - documents = self._prepare_secret_documents(documents) - except deckhand_errors.BarbicanException: - with excutils.save_and_reraise_exception(): - LOG.error('An unknown exception occurred while trying to store' - ' a secret in Barbican.') + documents = self._encrypt_secret_documents(documents) created_documents = self._create_revision_documents( bucket_name, documents, validations) @@ -70,7 +65,7 @@ class BucketsResource(api_base.BaseResource): resp.body = self.view_builder.list(created_documents) resp.status = falcon.HTTP_200 - def _prepare_secret_documents(self, documents): + def _encrypt_secret_documents(self, documents): # Encrypt data for secret documents, if any. for document in documents: if secrets_manager.SecretsManager.requires_encryption(document): diff --git a/deckhand/control/revision_documents.py b/deckhand/control/revision_documents.py index ced619ae..9dc10678 100644 --- a/deckhand/control/revision_documents.py +++ b/deckhand/control/revision_documents.py @@ -25,6 +25,7 @@ from deckhand.control.views import document as document_view from deckhand.db.sqlalchemy import api as db_api from deckhand.engine import document_validation from deckhand.engine import layering +from deckhand.engine import secrets_manager from deckhand import errors from deckhand import policy from deckhand import types @@ -111,15 +112,19 @@ class RenderedDocumentsResource(api_base.BaseResource): documents = self._retrieve_documents_for_rendering(revision_id, **filters) + encryption_sources = self._retrieve_encrypted_documents(documents) try: # NOTE(fmontei): `validate` is False because documents have already # been pre-validated during ingestion. Documents are post-validated # below, regardless. document_layering = layering.DocumentLayering( - documents, validate=False) + documents, encryption_sources=encryption_sources, + validate=False) rendered_documents = document_layering.render() - except (errors.InvalidDocumentLayer, + except (errors.BarbicanClientException, + errors.BarbicanServerException, + errors.InvalidDocumentLayer, errors.InvalidDocumentParent, errors.InvalidDocumentReplacement, errors.IndeterminateDocumentParent, @@ -131,6 +136,12 @@ class RenderedDocumentsResource(api_base.BaseResource): errors.UnsupportedActionMethod) as e: with excutils.save_and_reraise_exception(): LOG.exception(e.format_message()) + except errors.EncryptionSourceNotFound as e: + # This branch should be unreachable, but if an encryption source + # wasn't found, then this indicates the controller fed bad data + # to the engine, in which case this is a 500. + e.code = 500 + raise e # Filters to be applied post-rendering, because many documents are # involved in rendering. User filters can only be applied once all @@ -183,6 +194,25 @@ class RenderedDocumentsResource(api_base.BaseResource): return documents + def _retrieve_encrypted_documents(self, documents): + encryption_sources = {} + for document in documents: + if document.is_encrypted and document.has_barbican_ref: + try: + unecrypted_data = secrets_manager.SecretsManager.get( + secret_ref=document.data, src_doc=document) + except Exception as e: + LOG.error( + 'An unknown exception occurred while trying to resolve' + ' a secret reference for substitution source document ' + '[%s, %s] %s.', document.schema, document.layer, + document.name) + raise errors.UnknownSubstitutionError( + src_schema=document.schema, src_layer=document.layer, + src_name=document.name, details=str(e)) + encryption_sources.setdefault(document.data, unecrypted_data) + return encryption_sources + def _post_validate(self, rendered_documents): # Perform schema validation post-rendering to ensure that rendering # and substitution didn't break anything. @@ -193,12 +223,12 @@ class RenderedDocumentsResource(api_base.BaseResource): try: validations = doc_validator.validate_all() except errors.InvalidDocumentFormat as e: - with excutils.save_and_reraise_exception(): - # Post-rendering validation errors likely indicate an internal - # rendering bug, so override the default code to 500. - e.code = 500 - LOG.error('Failed to post-validate rendered documents.') - LOG.exception(e.format_message()) + # Post-rendering validation errors likely indicate an internal + # rendering bug, so override the default code to 500. + e.code = 500 + LOG.error('Failed to post-validate rendered documents.') + LOG.exception(e.format_message()) + raise e else: error_list = [] diff --git a/deckhand/db/sqlalchemy/api.py b/deckhand/db/sqlalchemy/api.py index aba17e0b..84244e1c 100644 --- a/deckhand/db/sqlalchemy/api.py +++ b/deckhand/db/sqlalchemy/api.py @@ -374,7 +374,7 @@ def document_get(session=None, raw_dict=False, revision_id=None, **filters): for doc in documents: d = doc.to_dict(raw_dict=raw_dict) if utils.deepfilter(d, **nested_filters): - return d + return document_wrapper.DocumentDict(d) filters.update(nested_filters) raise errors.DocumentNotFound(filters=filters) diff --git a/deckhand/engine/layering.py b/deckhand/engine/layering.py index ab2c1fe9..4c1ed31b 100644 --- a/deckhand/engine/layering.py +++ b/deckhand/engine/layering.py @@ -382,7 +382,7 @@ class DocumentLayering(object): raise errors.InvalidDocumentFormat(error_list=error_list) def __init__(self, documents, substitution_sources=None, validate=True, - fail_on_missing_sub_src=True): + fail_on_missing_sub_src=True, encryption_sources=None): """Contructor for ``DocumentLayering``. :param layering_policy: The document with schema @@ -401,6 +401,11 @@ class DocumentLayering(object): :param fail_on_missing_sub_src: Whether to fail on a missing substitution source. Default is True. :type fail_on_missing_sub_src: bool + :param encryption_sources: A dictionary that maps the reference + contained in the destination document's data section to the + actual unecrypted data. If encrypting data with Barbican, the + reference will be a Barbican secret reference. + :type encryption_sources: List[dict] :raises LayeringPolicyNotFound: If no LayeringPolicy was found among list of ``documents``. @@ -489,6 +494,7 @@ class DocumentLayering(object): self.secrets_substitution = secrets_manager.SecretsSubstitution( substitution_sources, + encryption_sources=encryption_sources, fail_on_missing_sub_src=fail_on_missing_sub_src) self._sorted_documents = self._topologically_sort_documents( @@ -692,8 +698,8 @@ class DocumentLayering(object): # data has been encrypted so that future references use the actual # secret payload, rather than the Barbican secret reference. elif doc.is_encrypted: - encrypted_data = self.secrets_substitution.get_encrypted_data( - doc.data, doc, doc) + encrypted_data = self.secrets_substitution\ + .get_unencrypted_data(doc.data, doc, doc) if not doc.is_abstract: doc.data = encrypted_data self.secrets_substitution.update_substitution_sources( diff --git a/deckhand/engine/secrets_manager.py b/deckhand/engine/secrets_manager.py index 688a80be..f4fc048d 100644 --- a/deckhand/engine/secrets_manager.py +++ b/deckhand/engine/secrets_manager.py @@ -76,7 +76,7 @@ class SecretsManager(object): return payload @classmethod - def get(cls, secret_ref, src_doc, dest_doc): + def get(cls, secret_ref, src_doc): """Retrieve a secret payload from Barbican. Extracts {secret_uuid} from a secret reference and queries Barbican's @@ -88,8 +88,8 @@ class SecretsManager(object): """ LOG.debug('Resolving Barbican secret using source document ' 'reference...') - secret = cls.barbican_driver.get_secret(src_doc, dest_doc, - secret_ref=secret_ref) + secret = cls.barbican_driver.get_secret(secret_ref=secret_ref, + src_doc=src_doc) LOG.debug('Successfully retrieved Barbican secret using reference.') return secret @@ -97,7 +97,8 @@ class SecretsManager(object): class SecretsSubstitution(object): """Class for document substitution logic for YAML files.""" - __slots__ = ('_fail_on_missing_sub_src', '_substitution_sources') + __slots__ = ('_fail_on_missing_sub_src', '_substitution_sources', + '_encryption_sources') _insecure_reg_exps = ( re.compile(r'^.* is not of type .+$'), @@ -136,27 +137,21 @@ class SecretsSubstitution(object): return to_sanitize - @staticmethod - def get_encrypted_data(src_secret, src_doc, dest_doc): - try: - src_secret = SecretsManager.get(src_secret, src_doc, dest_doc) - except errors.BarbicanException as e: + def get_unencrypted_data(self, secret_ref, src_doc, dest_doc): + if secret_ref not in self._encryption_sources: LOG.error( - 'Failed to resolve a Barbican reference for substitution ' - 'source document [%s, %s] %s referenced in document [%s, %s] ' - '%s. Details: %s', src_doc.schema, src_doc.layer, src_doc.name, - dest_doc.schema, dest_doc.layer, dest_doc.name, - e.format_message()) - raise errors.UnknownSubstitutionError( - src_schema=src_doc.schema, src_layer=src_doc.layer, - src_name=src_doc.name, schema=dest_doc.schema, - layer=dest_doc.layer, name=dest_doc.name, - details=e.format_message()) - else: - return src_secret + 'Secret reference %s not found among `encryption_sources`, ' + 'referenced by source document [%s, %s] %s, needed by ' + 'destination document [%s, %s] %s.', secret_ref, + src_doc.schema, src_doc.layer, src_doc.name, + dest_doc.schema, dest_doc.layer, dest_doc.name) + raise errors.EncryptionSourceNotFound( + secret_ref=secret_ref, schema=src_doc.schema, + layer=src_doc.layer, name=src_doc.name) + return self._encryption_sources[secret_ref] def __init__(self, substitution_sources=None, - fail_on_missing_sub_src=True): + fail_on_missing_sub_src=True, encryption_sources=None): """SecretSubstitution constructor. This class will automatically detect documents that require @@ -170,6 +165,11 @@ class SecretsSubstitution(object): :type substitution_sources: List[dict] or dict :param bool fail_on_missing_sub_src: Whether to fail on a missing substitution source. Default is True. + :param encryption_sources: A dictionary that maps the reference + contained in the destination document's data section to the + actual unecrypted data. If encrypting data with Barbican, the + reference will be a Barbican secret reference. + :type encryption_sources: List[dict] """ # This maps a 2-tuple of (schema, name) to a document from which the @@ -177,6 +177,7 @@ class SecretsSubstitution(object): # name). This is necessary since the substitution format in the # document itself only provides a 2-tuple of (schema, name). self._substitution_sources = {} + self._encryption_sources = encryption_sources or {} self._fail_on_missing_sub_src = fail_on_missing_sub_src if isinstance(substitution_sources, dict): @@ -290,10 +291,9 @@ class SecretsSubstitution(object): # If the document has storagePolicy == encrypted then resolve # the Barbican reference into the actual secret. - if src_doc.is_encrypted and BarbicanDriver.is_barbican_ref( - src_secret): - src_secret = self.get_encrypted_data(src_secret, src_doc, - document) + if src_doc.is_encrypted and src_doc.has_barbican_ref: + src_secret = self.get_unencrypted_data(src_secret, src_doc, + document) if not isinstance(sub['dest'], list): dest_array = [sub['dest']] diff --git a/deckhand/errors.py b/deckhand/errors.py index c5303d57..99831be7 100644 --- a/deckhand/errors.py +++ b/deckhand/errors.py @@ -190,10 +190,9 @@ class DeckhandException(Exception): with the keyword arguments provided to the constructor. """ msg_fmt = "An unknown exception occurred." - code = 500 - def __init__(self, message=None, **kwargs): - kwargs.setdefault('code', DeckhandException.code) + def __init__(self, message=None, code=500, **kwargs): + kwargs.setdefault('code', code) if not message: try: @@ -372,6 +371,20 @@ class SubstitutionSourceDataNotFound(DeckhandException): code = 400 +class EncryptionSourceNotFound(DeckhandException): + """Required encryption source reference was not found. + + **Troubleshoot:** + + * Ensure that the secret reference exists among the encryption sources. + """ + msg_fmt = ( + "Required encryption source reference could not be resolved into a " + "secret because it was not found among encryption sources. Ref: " + "%(secret_ref)s. Referenced by: [%(schema)s, %(layer)s] %(name)s.") + code = 400 # Indicates bad data was passed in, causing a lookup to fail. + + class DocumentNotFound(DeckhandException): """The requested document could not be found. @@ -469,8 +482,8 @@ class PolicyNotAuthorized(DeckhandException): code = 403 -class BarbicanException(DeckhandException): - """An error occurred with Barbican. +class BarbicanClientException(DeckhandException): + """A client-side 4xx error occurred with Barbican. **Troubleshoot:** @@ -479,8 +492,13 @@ class BarbicanException(DeckhandException): * Ensure that Deckhand and Barbican are contained in the Keystone service catalog. """ - msg_fmt = ('An exception occurred while trying to communicate with ' - 'Barbican. Details: %(details)s') + msg_fmt = 'Barbican raised a client error. Details: %(details)s' + code = 400 # Needs to be overridden. + + +class BarbicanServerException(DeckhandException): + """A server-side 5xx error occurred with Barbican.""" + msg_fmt = ('Barbican raised a server error. Details: %(details)s') code = 500 @@ -489,8 +507,16 @@ class UnknownSubstitutionError(DeckhandException): **Troubleshoot:** """ - msg_fmt = ('An unknown exception occurred while trying to perform ' - 'substitution using source document [%(src_schema)s, ' - '%(src_layer)s] %(src_name)s contained in document [' - '%(schema)s, %(layer)s] %(name)s. Details: %(details)s') code = 500 + + def __init__(self, *args, **kwargs): + super(UnknownSubstitutionError, self).__init__(*args, **kwargs) + dest_args = ('schema', 'layer', 'name') + msg_format = ('An unknown exception occurred while trying to perform ' + 'substitution using source document [%(src_schema)s, ' + '%(src_layer)s] %(src_name)s') + if all(x in args for x in dest_args): + msg_format += (' contained in document [%(schema)s, %(layer)s]' + ' %(name)s') + msg_format += '. Details: %(detail)s' + self.msg_fmt = msg_format diff --git a/deckhand/tests/test_utils.py b/deckhand/tests/test_utils.py index 3f0b123f..72e5f101 100644 --- a/deckhand/tests/test_utils.py +++ b/deckhand/tests/test_utils.py @@ -84,3 +84,8 @@ def rand_password(length=15): pre = upper + digit + punc password = pre + ''.join(random.choice(seed) for x in range(length - 3)) return password + + +def rand_barbican_ref(): + secret_ref = "http://127.0.0.1/key-manager/v1/secrets/%s" % rand_uuid_hex() + return secret_ref diff --git a/deckhand/tests/unit/control/test_errors.py b/deckhand/tests/unit/control/test_errors.py index 1f443ef1..3afadc42 100644 --- a/deckhand/tests/unit/control/test_errors.py +++ b/deckhand/tests/unit/control/test_errors.py @@ -18,6 +18,7 @@ import yaml import falcon import mock +from deckhand.common import document as document_wrapper from deckhand import policy from deckhand.tests.unit.control import base as test_base @@ -187,7 +188,8 @@ class TestValidationMessageFormatting(test_base.BaseControllerTest): with mock.patch('deckhand.control.revision_documents.db_api' '.revision_documents_get', autospec=True) \ as mock_get_rev_documents: - invalid_document = yaml.safe_load(payload) + invalid_document = document_wrapper.DocumentDict( + yaml.safe_load(payload)) invalid_document.pop('metadata') mock_get_rev_documents.return_value = [invalid_document] diff --git a/deckhand/tests/unit/engine/test_secrets_manager.py b/deckhand/tests/unit/engine/test_secrets_manager.py index 55e09d4f..371fc116 100644 --- a/deckhand/tests/unit/engine/test_secrets_manager.py +++ b/deckhand/tests/unit/engine/test_secrets_manager.py @@ -116,7 +116,7 @@ class TestSecretsManager(test_base.TestDbBase): self.mock_barbicanclient.get_secret.return_value = ( mock.Mock(payload=expected_secret)) - secret_payload = secrets_manager.SecretsManager.get(secret_ref, {}, {}) + secret_payload = secrets_manager.SecretsManager.get(secret_ref, {}) self.assertEqual(expected_secret, secret_payload) self.mock_barbicanclient.call.assert_called_with( @@ -164,7 +164,7 @@ class TestSecretsManager(test_base.TestDbBase): dummy_document = document_wrapper.DocumentDict({}) retrieved_payload = secrets_manager.SecretsManager.get( - secret_ref, dummy_document, dummy_document) + secret_ref, dummy_document) self.assertEqual(payload, retrieved_payload) @@ -176,7 +176,7 @@ class TestSecretsSubstitution(test_base.TestDbBase): self.secrets_factory = factories.DocumentSecretFactory() def _test_doc_substitution(self, document_mapping, substitution_sources, - expected_data): + expected_data, encryption_sources=None): payload = self.document_factory.gen_test(document_mapping, global_abstract=False) bucket_name = test_utils.rand_name('bucket') @@ -187,7 +187,8 @@ class TestSecretsSubstitution(test_base.TestDbBase): expected_document['data'] = expected_data secret_substitution = secrets_manager.SecretsSubstitution( - substitution_sources) + encryption_sources=encryption_sources, + substitution_sources=substitution_sources) substituted_docs = list(secret_substitution.substitute_all(documents)) self.assertIn(expected_document, substituted_docs) @@ -221,9 +222,7 @@ class TestSecretsSubstitution(test_base.TestDbBase): self._test_doc_substitution( document_mapping, [certificate], expected_data) - @mock.patch.object(secrets_manager, 'SecretsManager', autospec=True) - def test_doc_substitution_single_encrypted(self, mock_secrets_manager): - mock_secrets_manager.get.return_value = 'test-certificate' + def test_doc_substitution_with_encryption_source(self): secret_ref = test_utils.rand_uuid_hex() secret_ref = ("http://127.0.0.1/key-manager/v1/secrets/%s" @@ -255,9 +254,9 @@ class TestSecretsSubstitution(test_base.TestDbBase): } } self._test_doc_substitution( - document_mapping, [certificate], expected_data) - mock_secrets_manager.get.assert_called_once_with( - secret_ref, certificate, mock.ANY) + document_mapping, substitution_sources=[certificate], + encryption_sources={secret_ref: 'test-certificate'}, + expected_data=expected_data) def test_create_destination_path_with_array(self): # Validate that the destination data will be populated with an array @@ -883,8 +882,7 @@ class TestSecretsSubstitutionNegative(test_base.TestDbBase): self.secrets_factory = factories.DocumentSecretFactory() def _test_secrets_substitution(self, secret_type, expected_exception): - secret_ref = ("http://127.0.0.1/key-manager/v1/secrets/%s" - % test_utils.rand_uuid_hex()) + secret_ref = test_utils.rand_barbican_ref() certificate = self.secrets_factory.gen_test( 'Certificate', secret_type, data=secret_ref) certificate['metadata']['name'] = 'example-cert' @@ -912,13 +910,6 @@ class TestSecretsSubstitutionNegative(test_base.TestDbBase): with testtools.ExpectedException(expected_exception): next(secrets_substitution.substitute_all(documents)) - @mock.patch.object(secrets_manager, 'SecretsManager', autospec=True) - def test_barbican_exception_raises_unknown_error( - self, mock_secrets_manager): - mock_secrets_manager.get.side_effect = errors.BarbicanException - self._test_secrets_substitution( - 'encrypted', errors.UnknownSubstitutionError) - @mock.patch('deckhand.engine.secrets_manager.utils', autospec=True) def test_generic_exception_raises_unknown_error( self, mock_utils): @@ -957,3 +948,36 @@ class TestSecretsSubstitutionNegative(test_base.TestDbBase): with testtools.ExpectedException( errors.SubstitutionSourceDataNotFound): next(secrets_substitution.substitute_all(documents)) + + def test_secret_substitution_missing_encryption_sources_raises_exc(self): + """Validate that when ``encryption_sources`` doesn't contain a + reference that a ``EncryptionSourceNotFound`` is raised. + """ + secret_ref = test_utils.rand_barbican_ref() + certificate = self.secrets_factory.gen_test( + 'Certificate', 'encrypted', data=secret_ref) + certificate['metadata']['name'] = 'example-cert' + + document_mapping = { + "_GLOBAL_SUBSTITUTIONS_1_": [{ + "dest": { + "path": ".chart.values.tls.certificate" + }, + "src": { + "schema": "deckhand/Certificate/v1", + "name": "example-cert", + "path": ".path-to-nowhere" + } + + }] + } + payload = self.document_factory.gen_test(document_mapping, + global_abstract=False) + bucket_name = test_utils.rand_name('bucket') + documents = self.create_documents( + bucket_name, [certificate] + [payload[-1]]) + + secrets_substitution = secrets_manager.SecretsSubstitution( + documents, encryption_sources={'foo': 'bar'}) + with testtools.ExpectedException(errors.EncryptionSourceNotFound): + next(secrets_substitution.substitute_all(documents)) diff --git a/doc/source/exceptions.rst b/doc/source/exceptions.rst index 693512ae..ed1c2f45 100644 --- a/doc/source/exceptions.rst +++ b/doc/source/exceptions.rst @@ -24,8 +24,13 @@ Deckhand Exceptions * - Exception Name - Description - * - BarbicanException - - .. autoexception:: deckhand.errors.BarbicanException + * - BarbicanClientException + - .. autoexception:: deckhand.errors.BarbicanClientException + :members: + :show-inheritance: + :undoc-members: + * - BarbicanServerException + - .. autoexception:: deckhand.errors.BarbicanServerException :members: :show-inheritance: :undoc-members: @@ -39,6 +44,11 @@ Deckhand Exceptions :members: :show-inheritance: :undoc-members: + * - EncryptionSourceNotFound + - .. autoexception:: deckhand.errors.EncryptionSourceNotFound + :members: + :show-inheritance: + :undoc-members: * - InvalidDocumentFormat - .. autoexception:: deckhand.errors.InvalidDocumentFormat :members: