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: