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
This commit is contained in:
parent
e25ea04985
commit
039f9830da
@ -63,7 +63,8 @@ class BarbicanClientWrapper(object):
|
|||||||
|
|
||||||
except barbican_exc.HTTPAuthError as e:
|
except barbican_exc.HTTPAuthError as e:
|
||||||
LOG.exception(str(e))
|
LOG.exception(str(e))
|
||||||
raise errors.BarbicanException(details=str(e))
|
raise errors.BarbicanClientException(code=e.status_code,
|
||||||
|
details=str(e))
|
||||||
|
|
||||||
return cli
|
return cli
|
||||||
|
|
||||||
|
@ -13,12 +13,11 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
import re
|
|
||||||
|
|
||||||
import barbicanclient
|
import barbicanclient
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_serialization import base64
|
from oslo_serialization import base64
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import excutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from deckhand.barbican import client_wrapper
|
from deckhand.barbican import client_wrapper
|
||||||
@ -30,30 +29,9 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class BarbicanDriver(object):
|
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):
|
def __init__(self):
|
||||||
self.barbicanclient = client_wrapper.BarbicanClientWrapper()
|
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
|
@staticmethod
|
||||||
def _get_secret_type(schema):
|
def _get_secret_type(schema):
|
||||||
"""Get the Barbican secret type based on the following mapping:
|
"""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 = self.barbicanclient.call("secrets.create", **kwargs)
|
||||||
secret_ref = secret.store()
|
secret_ref = secret.store()
|
||||||
except (barbicanclient.exceptions.HTTPAuthError,
|
except (barbicanclient.exceptions.HTTPAuthError,
|
||||||
barbicanclient.exceptions.HTTPClientError,
|
barbicanclient.exceptions.HTTPClientError) as e:
|
||||||
barbicanclient.exceptions.HTTPServerError) 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 '
|
LOG.error('Caught %s error from Barbican, likely due to a '
|
||||||
'configuration or deployment issue.',
|
'configuration or deployment issue.',
|
||||||
e.__class__.__name__)
|
e.__class__.__name__)
|
||||||
raise errors.BarbicanException(details=str(e))
|
raise errors.BarbicanServerException(details=str(e))
|
||||||
except barbicanclient.exceptions.PayloadException as e:
|
except barbicanclient.exceptions.PayloadException as e:
|
||||||
LOG.error('Caught %s error from Barbican, because the secret '
|
LOG.error('Caught %s error from Barbican, because the secret '
|
||||||
'payload type is unsupported.', e.__class__.__name__)
|
'payload type is unsupported.', e.__class__.__name__)
|
||||||
raise errors.BarbicanException(details=str(e))
|
raise errors.BarbicanServerException(details=str(e))
|
||||||
|
|
||||||
return secret_ref
|
return secret_ref
|
||||||
|
|
||||||
def _base64_decode_payload(self, src_doc, dest_doc, payload):
|
def _base64_decode_payload(self, payload):
|
||||||
try:
|
|
||||||
# If the secret_type is 'opaque' then this implies the
|
# If the secret_type is 'opaque' then this implies the
|
||||||
# payload was encoded to base64 previously. Reverse the
|
# payload was encoded to base64 previously. Reverse the
|
||||||
# operation.
|
# operation.
|
||||||
payload = ast.literal_eval(base64.decode_as_text(payload))
|
try:
|
||||||
|
return ast.literal_eval(base64.decode_as_text(payload))
|
||||||
except Exception:
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
message = ('Failed to unencode the original payload that '
|
message = ('Failed to unencode the original payload that '
|
||||||
'presumably was encoded to base64 with '
|
'presumably was encoded to base64 with '
|
||||||
'secret_type=opaque for document [%s, %s] %s.' %
|
'secret_type: opaque.')
|
||||||
src_doc.meta)
|
|
||||||
LOG.error(message)
|
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
|
|
||||||
|
|
||||||
def get_secret(self, src_doc, dest_doc, secret_ref):
|
def get_secret(self, secret_ref, src_doc):
|
||||||
"""Get a secret."""
|
"""Get a secret."""
|
||||||
try:
|
try:
|
||||||
secret = self.barbicanclient.call("secrets.get", secret_ref)
|
secret = self.barbicanclient.call("secrets.get", secret_ref)
|
||||||
except (barbicanclient.exceptions.HTTPAuthError,
|
except (barbicanclient.exceptions.HTTPAuthError,
|
||||||
barbicanclient.exceptions.HTTPClientError,
|
barbicanclient.exceptions.HTTPClientError) as e:
|
||||||
barbicanclient.exceptions.HTTPServerError,
|
LOG.exception(str(e))
|
||||||
|
raise errors.BarbicanClientException(code=e.status_code,
|
||||||
|
details=str(e))
|
||||||
|
except (barbicanclient.exceptions.HTTPServerError,
|
||||||
ValueError) as e:
|
ValueError) as e:
|
||||||
LOG.exception(str(e))
|
LOG.exception(str(e))
|
||||||
raise errors.BarbicanException(details=str(e))
|
raise errors.BarbicanServerException(details=str(e))
|
||||||
|
|
||||||
payload = secret.payload
|
payload = secret.payload
|
||||||
if secret.secret_type == 'opaque':
|
if secret.secret_type == 'opaque':
|
||||||
LOG.debug('Forcibly base64-decoding original non-string payload '
|
LOG.debug('Forcibly base64-decoding original non-string payload '
|
||||||
'for document [%s, %s] %s.', *src_doc.meta)
|
'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:
|
else:
|
||||||
secret = payload
|
secret = payload
|
||||||
|
|
||||||
|
@ -15,12 +15,19 @@
|
|||||||
import collections
|
import collections
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
import re
|
||||||
|
|
||||||
from oslo_serialization import jsonutils as json
|
from oslo_serialization import jsonutils as json
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
import six
|
||||||
|
|
||||||
from deckhand.common import utils
|
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):
|
class DocumentDict(dict):
|
||||||
"""Wrapper for a document.
|
"""Wrapper for a document.
|
||||||
|
|
||||||
@ -136,6 +143,20 @@ class DocumentDict(dict):
|
|||||||
def is_encrypted(self):
|
def is_encrypted(self):
|
||||||
return self.storage_policy == 'encrypted'
|
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
|
@property
|
||||||
def is_replacement(self):
|
def is_replacement(self):
|
||||||
return utils.jsonpath_parse(self, 'metadata.replacement') is True
|
return utils.jsonpath_parse(self, 'metadata.replacement') is True
|
||||||
|
@ -57,12 +57,7 @@ class BucketsResource(api_base.BaseResource):
|
|||||||
'deckhand:create_encrypted_documents', req.context)
|
'deckhand:create_encrypted_documents', req.context)
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
documents = self._encrypt_secret_documents(documents)
|
||||||
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.')
|
|
||||||
|
|
||||||
created_documents = self._create_revision_documents(
|
created_documents = self._create_revision_documents(
|
||||||
bucket_name, documents, validations)
|
bucket_name, documents, validations)
|
||||||
@ -70,7 +65,7 @@ class BucketsResource(api_base.BaseResource):
|
|||||||
resp.body = self.view_builder.list(created_documents)
|
resp.body = self.view_builder.list(created_documents)
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
|
|
||||||
def _prepare_secret_documents(self, documents):
|
def _encrypt_secret_documents(self, documents):
|
||||||
# Encrypt data for secret documents, if any.
|
# Encrypt data for secret documents, if any.
|
||||||
for document in documents:
|
for document in documents:
|
||||||
if secrets_manager.SecretsManager.requires_encryption(document):
|
if secrets_manager.SecretsManager.requires_encryption(document):
|
||||||
|
@ -25,6 +25,7 @@ from deckhand.control.views import document as document_view
|
|||||||
from deckhand.db.sqlalchemy import api as db_api
|
from deckhand.db.sqlalchemy import api as db_api
|
||||||
from deckhand.engine import document_validation
|
from deckhand.engine import document_validation
|
||||||
from deckhand.engine import layering
|
from deckhand.engine import layering
|
||||||
|
from deckhand.engine import secrets_manager
|
||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
from deckhand import policy
|
from deckhand import policy
|
||||||
from deckhand import types
|
from deckhand import types
|
||||||
@ -111,15 +112,19 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
|||||||
|
|
||||||
documents = self._retrieve_documents_for_rendering(revision_id,
|
documents = self._retrieve_documents_for_rendering(revision_id,
|
||||||
**filters)
|
**filters)
|
||||||
|
encryption_sources = self._retrieve_encrypted_documents(documents)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# NOTE(fmontei): `validate` is False because documents have already
|
# NOTE(fmontei): `validate` is False because documents have already
|
||||||
# been pre-validated during ingestion. Documents are post-validated
|
# been pre-validated during ingestion. Documents are post-validated
|
||||||
# below, regardless.
|
# below, regardless.
|
||||||
document_layering = layering.DocumentLayering(
|
document_layering = layering.DocumentLayering(
|
||||||
documents, validate=False)
|
documents, encryption_sources=encryption_sources,
|
||||||
|
validate=False)
|
||||||
rendered_documents = document_layering.render()
|
rendered_documents = document_layering.render()
|
||||||
except (errors.InvalidDocumentLayer,
|
except (errors.BarbicanClientException,
|
||||||
|
errors.BarbicanServerException,
|
||||||
|
errors.InvalidDocumentLayer,
|
||||||
errors.InvalidDocumentParent,
|
errors.InvalidDocumentParent,
|
||||||
errors.InvalidDocumentReplacement,
|
errors.InvalidDocumentReplacement,
|
||||||
errors.IndeterminateDocumentParent,
|
errors.IndeterminateDocumentParent,
|
||||||
@ -131,6 +136,12 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
|||||||
errors.UnsupportedActionMethod) as e:
|
errors.UnsupportedActionMethod) as e:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.exception(e.format_message())
|
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
|
# Filters to be applied post-rendering, because many documents are
|
||||||
# involved in rendering. User filters can only be applied once all
|
# involved in rendering. User filters can only be applied once all
|
||||||
@ -183,6 +194,25 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
|||||||
|
|
||||||
return documents
|
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):
|
def _post_validate(self, rendered_documents):
|
||||||
# Perform schema validation post-rendering to ensure that rendering
|
# Perform schema validation post-rendering to ensure that rendering
|
||||||
# and substitution didn't break anything.
|
# and substitution didn't break anything.
|
||||||
@ -193,12 +223,12 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
validations = doc_validator.validate_all()
|
validations = doc_validator.validate_all()
|
||||||
except errors.InvalidDocumentFormat as e:
|
except errors.InvalidDocumentFormat as e:
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
# Post-rendering validation errors likely indicate an internal
|
# Post-rendering validation errors likely indicate an internal
|
||||||
# rendering bug, so override the default code to 500.
|
# rendering bug, so override the default code to 500.
|
||||||
e.code = 500
|
e.code = 500
|
||||||
LOG.error('Failed to post-validate rendered documents.')
|
LOG.error('Failed to post-validate rendered documents.')
|
||||||
LOG.exception(e.format_message())
|
LOG.exception(e.format_message())
|
||||||
|
raise e
|
||||||
else:
|
else:
|
||||||
error_list = []
|
error_list = []
|
||||||
|
|
||||||
|
@ -374,7 +374,7 @@ def document_get(session=None, raw_dict=False, revision_id=None, **filters):
|
|||||||
for doc in documents:
|
for doc in documents:
|
||||||
d = doc.to_dict(raw_dict=raw_dict)
|
d = doc.to_dict(raw_dict=raw_dict)
|
||||||
if utils.deepfilter(d, **nested_filters):
|
if utils.deepfilter(d, **nested_filters):
|
||||||
return d
|
return document_wrapper.DocumentDict(d)
|
||||||
|
|
||||||
filters.update(nested_filters)
|
filters.update(nested_filters)
|
||||||
raise errors.DocumentNotFound(filters=filters)
|
raise errors.DocumentNotFound(filters=filters)
|
||||||
|
@ -382,7 +382,7 @@ class DocumentLayering(object):
|
|||||||
raise errors.InvalidDocumentFormat(error_list=error_list)
|
raise errors.InvalidDocumentFormat(error_list=error_list)
|
||||||
|
|
||||||
def __init__(self, documents, substitution_sources=None, validate=True,
|
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``.
|
"""Contructor for ``DocumentLayering``.
|
||||||
|
|
||||||
:param layering_policy: The document with schema
|
: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
|
:param fail_on_missing_sub_src: Whether to fail on a missing
|
||||||
substitution source. Default is True.
|
substitution source. Default is True.
|
||||||
:type fail_on_missing_sub_src: bool
|
: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
|
:raises LayeringPolicyNotFound: If no LayeringPolicy was found among
|
||||||
list of ``documents``.
|
list of ``documents``.
|
||||||
@ -489,6 +494,7 @@ class DocumentLayering(object):
|
|||||||
|
|
||||||
self.secrets_substitution = secrets_manager.SecretsSubstitution(
|
self.secrets_substitution = secrets_manager.SecretsSubstitution(
|
||||||
substitution_sources,
|
substitution_sources,
|
||||||
|
encryption_sources=encryption_sources,
|
||||||
fail_on_missing_sub_src=fail_on_missing_sub_src)
|
fail_on_missing_sub_src=fail_on_missing_sub_src)
|
||||||
|
|
||||||
self._sorted_documents = self._topologically_sort_documents(
|
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
|
# data has been encrypted so that future references use the actual
|
||||||
# secret payload, rather than the Barbican secret reference.
|
# secret payload, rather than the Barbican secret reference.
|
||||||
elif doc.is_encrypted:
|
elif doc.is_encrypted:
|
||||||
encrypted_data = self.secrets_substitution.get_encrypted_data(
|
encrypted_data = self.secrets_substitution\
|
||||||
doc.data, doc, doc)
|
.get_unencrypted_data(doc.data, doc, doc)
|
||||||
if not doc.is_abstract:
|
if not doc.is_abstract:
|
||||||
doc.data = encrypted_data
|
doc.data = encrypted_data
|
||||||
self.secrets_substitution.update_substitution_sources(
|
self.secrets_substitution.update_substitution_sources(
|
||||||
|
@ -76,7 +76,7 @@ class SecretsManager(object):
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, secret_ref, src_doc, dest_doc):
|
def get(cls, secret_ref, src_doc):
|
||||||
"""Retrieve a secret payload from Barbican.
|
"""Retrieve a secret payload from Barbican.
|
||||||
|
|
||||||
Extracts {secret_uuid} from a secret reference and queries Barbican's
|
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 '
|
LOG.debug('Resolving Barbican secret using source document '
|
||||||
'reference...')
|
'reference...')
|
||||||
secret = cls.barbican_driver.get_secret(src_doc, dest_doc,
|
secret = cls.barbican_driver.get_secret(secret_ref=secret_ref,
|
||||||
secret_ref=secret_ref)
|
src_doc=src_doc)
|
||||||
LOG.debug('Successfully retrieved Barbican secret using reference.')
|
LOG.debug('Successfully retrieved Barbican secret using reference.')
|
||||||
return secret
|
return secret
|
||||||
|
|
||||||
@ -97,7 +97,8 @@ class SecretsManager(object):
|
|||||||
class SecretsSubstitution(object):
|
class SecretsSubstitution(object):
|
||||||
"""Class for document substitution logic for YAML files."""
|
"""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 = (
|
_insecure_reg_exps = (
|
||||||
re.compile(r'^.* is not of type .+$'),
|
re.compile(r'^.* is not of type .+$'),
|
||||||
@ -136,27 +137,21 @@ class SecretsSubstitution(object):
|
|||||||
|
|
||||||
return to_sanitize
|
return to_sanitize
|
||||||
|
|
||||||
@staticmethod
|
def get_unencrypted_data(self, secret_ref, src_doc, dest_doc):
|
||||||
def get_encrypted_data(src_secret, src_doc, dest_doc):
|
if secret_ref not in self._encryption_sources:
|
||||||
try:
|
|
||||||
src_secret = SecretsManager.get(src_secret, src_doc, dest_doc)
|
|
||||||
except errors.BarbicanException as e:
|
|
||||||
LOG.error(
|
LOG.error(
|
||||||
'Failed to resolve a Barbican reference for substitution '
|
'Secret reference %s not found among `encryption_sources`, '
|
||||||
'source document [%s, %s] %s referenced in document [%s, %s] '
|
'referenced by source document [%s, %s] %s, needed by '
|
||||||
'%s. Details: %s', src_doc.schema, src_doc.layer, src_doc.name,
|
'destination document [%s, %s] %s.', secret_ref,
|
||||||
dest_doc.schema, dest_doc.layer, dest_doc.name,
|
src_doc.schema, src_doc.layer, src_doc.name,
|
||||||
e.format_message())
|
dest_doc.schema, dest_doc.layer, dest_doc.name)
|
||||||
raise errors.UnknownSubstitutionError(
|
raise errors.EncryptionSourceNotFound(
|
||||||
src_schema=src_doc.schema, src_layer=src_doc.layer,
|
secret_ref=secret_ref, schema=src_doc.schema,
|
||||||
src_name=src_doc.name, schema=dest_doc.schema,
|
layer=src_doc.layer, name=src_doc.name)
|
||||||
layer=dest_doc.layer, name=dest_doc.name,
|
return self._encryption_sources[secret_ref]
|
||||||
details=e.format_message())
|
|
||||||
else:
|
|
||||||
return src_secret
|
|
||||||
|
|
||||||
def __init__(self, substitution_sources=None,
|
def __init__(self, substitution_sources=None,
|
||||||
fail_on_missing_sub_src=True):
|
fail_on_missing_sub_src=True, encryption_sources=None):
|
||||||
"""SecretSubstitution constructor.
|
"""SecretSubstitution constructor.
|
||||||
|
|
||||||
This class will automatically detect documents that require
|
This class will automatically detect documents that require
|
||||||
@ -170,6 +165,11 @@ class SecretsSubstitution(object):
|
|||||||
:type substitution_sources: List[dict] or dict
|
:type substitution_sources: List[dict] or dict
|
||||||
:param bool fail_on_missing_sub_src: Whether to fail on a missing
|
:param bool fail_on_missing_sub_src: Whether to fail on a missing
|
||||||
substitution source. Default is True.
|
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
|
# 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
|
# name). This is necessary since the substitution format in the
|
||||||
# document itself only provides a 2-tuple of (schema, name).
|
# document itself only provides a 2-tuple of (schema, name).
|
||||||
self._substitution_sources = {}
|
self._substitution_sources = {}
|
||||||
|
self._encryption_sources = encryption_sources or {}
|
||||||
self._fail_on_missing_sub_src = fail_on_missing_sub_src
|
self._fail_on_missing_sub_src = fail_on_missing_sub_src
|
||||||
|
|
||||||
if isinstance(substitution_sources, dict):
|
if isinstance(substitution_sources, dict):
|
||||||
@ -290,9 +291,8 @@ class SecretsSubstitution(object):
|
|||||||
|
|
||||||
# If the document has storagePolicy == encrypted then resolve
|
# If the document has storagePolicy == encrypted then resolve
|
||||||
# the Barbican reference into the actual secret.
|
# the Barbican reference into the actual secret.
|
||||||
if src_doc.is_encrypted and BarbicanDriver.is_barbican_ref(
|
if src_doc.is_encrypted and src_doc.has_barbican_ref:
|
||||||
src_secret):
|
src_secret = self.get_unencrypted_data(src_secret, src_doc,
|
||||||
src_secret = self.get_encrypted_data(src_secret, src_doc,
|
|
||||||
document)
|
document)
|
||||||
|
|
||||||
if not isinstance(sub['dest'], list):
|
if not isinstance(sub['dest'], list):
|
||||||
|
@ -190,10 +190,9 @@ class DeckhandException(Exception):
|
|||||||
with the keyword arguments provided to the constructor.
|
with the keyword arguments provided to the constructor.
|
||||||
"""
|
"""
|
||||||
msg_fmt = "An unknown exception occurred."
|
msg_fmt = "An unknown exception occurred."
|
||||||
code = 500
|
|
||||||
|
|
||||||
def __init__(self, message=None, **kwargs):
|
def __init__(self, message=None, code=500, **kwargs):
|
||||||
kwargs.setdefault('code', DeckhandException.code)
|
kwargs.setdefault('code', code)
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
try:
|
try:
|
||||||
@ -372,6 +371,20 @@ class SubstitutionSourceDataNotFound(DeckhandException):
|
|||||||
code = 400
|
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):
|
class DocumentNotFound(DeckhandException):
|
||||||
"""The requested document could not be found.
|
"""The requested document could not be found.
|
||||||
|
|
||||||
@ -469,8 +482,8 @@ class PolicyNotAuthorized(DeckhandException):
|
|||||||
code = 403
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
class BarbicanException(DeckhandException):
|
class BarbicanClientException(DeckhandException):
|
||||||
"""An error occurred with Barbican.
|
"""A client-side 4xx error occurred with Barbican.
|
||||||
|
|
||||||
**Troubleshoot:**
|
**Troubleshoot:**
|
||||||
|
|
||||||
@ -479,8 +492,13 @@ class BarbicanException(DeckhandException):
|
|||||||
* Ensure that Deckhand and Barbican are contained in the Keystone service
|
* Ensure that Deckhand and Barbican are contained in the Keystone service
|
||||||
catalog.
|
catalog.
|
||||||
"""
|
"""
|
||||||
msg_fmt = ('An exception occurred while trying to communicate with '
|
msg_fmt = 'Barbican raised a client error. Details: %(details)s'
|
||||||
'Barbican. 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
|
code = 500
|
||||||
|
|
||||||
|
|
||||||
@ -489,8 +507,16 @@ class UnknownSubstitutionError(DeckhandException):
|
|||||||
|
|
||||||
**Troubleshoot:**
|
**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
|
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
|
||||||
|
@ -84,3 +84,8 @@ def rand_password(length=15):
|
|||||||
pre = upper + digit + punc
|
pre = upper + digit + punc
|
||||||
password = pre + ''.join(random.choice(seed) for x in range(length - 3))
|
password = pre + ''.join(random.choice(seed) for x in range(length - 3))
|
||||||
return password
|
return password
|
||||||
|
|
||||||
|
|
||||||
|
def rand_barbican_ref():
|
||||||
|
secret_ref = "http://127.0.0.1/key-manager/v1/secrets/%s" % rand_uuid_hex()
|
||||||
|
return secret_ref
|
||||||
|
@ -18,6 +18,7 @@ import yaml
|
|||||||
import falcon
|
import falcon
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from deckhand.common import document as document_wrapper
|
||||||
from deckhand import policy
|
from deckhand import policy
|
||||||
from deckhand.tests.unit.control import base as test_base
|
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'
|
with mock.patch('deckhand.control.revision_documents.db_api'
|
||||||
'.revision_documents_get', autospec=True) \
|
'.revision_documents_get', autospec=True) \
|
||||||
as mock_get_rev_documents:
|
as mock_get_rev_documents:
|
||||||
invalid_document = yaml.safe_load(payload)
|
invalid_document = document_wrapper.DocumentDict(
|
||||||
|
yaml.safe_load(payload))
|
||||||
invalid_document.pop('metadata')
|
invalid_document.pop('metadata')
|
||||||
|
|
||||||
mock_get_rev_documents.return_value = [invalid_document]
|
mock_get_rev_documents.return_value = [invalid_document]
|
||||||
|
@ -116,7 +116,7 @@ class TestSecretsManager(test_base.TestDbBase):
|
|||||||
self.mock_barbicanclient.get_secret.return_value = (
|
self.mock_barbicanclient.get_secret.return_value = (
|
||||||
mock.Mock(payload=expected_secret))
|
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.assertEqual(expected_secret, secret_payload)
|
||||||
self.mock_barbicanclient.call.assert_called_with(
|
self.mock_barbicanclient.call.assert_called_with(
|
||||||
@ -164,7 +164,7 @@ class TestSecretsManager(test_base.TestDbBase):
|
|||||||
|
|
||||||
dummy_document = document_wrapper.DocumentDict({})
|
dummy_document = document_wrapper.DocumentDict({})
|
||||||
retrieved_payload = secrets_manager.SecretsManager.get(
|
retrieved_payload = secrets_manager.SecretsManager.get(
|
||||||
secret_ref, dummy_document, dummy_document)
|
secret_ref, dummy_document)
|
||||||
self.assertEqual(payload, retrieved_payload)
|
self.assertEqual(payload, retrieved_payload)
|
||||||
|
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ class TestSecretsSubstitution(test_base.TestDbBase):
|
|||||||
self.secrets_factory = factories.DocumentSecretFactory()
|
self.secrets_factory = factories.DocumentSecretFactory()
|
||||||
|
|
||||||
def _test_doc_substitution(self, document_mapping, substitution_sources,
|
def _test_doc_substitution(self, document_mapping, substitution_sources,
|
||||||
expected_data):
|
expected_data, encryption_sources=None):
|
||||||
payload = self.document_factory.gen_test(document_mapping,
|
payload = self.document_factory.gen_test(document_mapping,
|
||||||
global_abstract=False)
|
global_abstract=False)
|
||||||
bucket_name = test_utils.rand_name('bucket')
|
bucket_name = test_utils.rand_name('bucket')
|
||||||
@ -187,7 +187,8 @@ class TestSecretsSubstitution(test_base.TestDbBase):
|
|||||||
expected_document['data'] = expected_data
|
expected_document['data'] = expected_data
|
||||||
|
|
||||||
secret_substitution = secrets_manager.SecretsSubstitution(
|
secret_substitution = secrets_manager.SecretsSubstitution(
|
||||||
substitution_sources)
|
encryption_sources=encryption_sources,
|
||||||
|
substitution_sources=substitution_sources)
|
||||||
substituted_docs = list(secret_substitution.substitute_all(documents))
|
substituted_docs = list(secret_substitution.substitute_all(documents))
|
||||||
self.assertIn(expected_document, substituted_docs)
|
self.assertIn(expected_document, substituted_docs)
|
||||||
|
|
||||||
@ -221,9 +222,7 @@ class TestSecretsSubstitution(test_base.TestDbBase):
|
|||||||
self._test_doc_substitution(
|
self._test_doc_substitution(
|
||||||
document_mapping, [certificate], expected_data)
|
document_mapping, [certificate], expected_data)
|
||||||
|
|
||||||
@mock.patch.object(secrets_manager, 'SecretsManager', autospec=True)
|
def test_doc_substitution_with_encryption_source(self):
|
||||||
def test_doc_substitution_single_encrypted(self, mock_secrets_manager):
|
|
||||||
mock_secrets_manager.get.return_value = 'test-certificate'
|
|
||||||
secret_ref = test_utils.rand_uuid_hex()
|
secret_ref = test_utils.rand_uuid_hex()
|
||||||
|
|
||||||
secret_ref = ("http://127.0.0.1/key-manager/v1/secrets/%s"
|
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(
|
self._test_doc_substitution(
|
||||||
document_mapping, [certificate], expected_data)
|
document_mapping, substitution_sources=[certificate],
|
||||||
mock_secrets_manager.get.assert_called_once_with(
|
encryption_sources={secret_ref: 'test-certificate'},
|
||||||
secret_ref, certificate, mock.ANY)
|
expected_data=expected_data)
|
||||||
|
|
||||||
def test_create_destination_path_with_array(self):
|
def test_create_destination_path_with_array(self):
|
||||||
# Validate that the destination data will be populated with an array
|
# 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()
|
self.secrets_factory = factories.DocumentSecretFactory()
|
||||||
|
|
||||||
def _test_secrets_substitution(self, secret_type, expected_exception):
|
def _test_secrets_substitution(self, secret_type, expected_exception):
|
||||||
secret_ref = ("http://127.0.0.1/key-manager/v1/secrets/%s"
|
secret_ref = test_utils.rand_barbican_ref()
|
||||||
% test_utils.rand_uuid_hex())
|
|
||||||
certificate = self.secrets_factory.gen_test(
|
certificate = self.secrets_factory.gen_test(
|
||||||
'Certificate', secret_type, data=secret_ref)
|
'Certificate', secret_type, data=secret_ref)
|
||||||
certificate['metadata']['name'] = 'example-cert'
|
certificate['metadata']['name'] = 'example-cert'
|
||||||
@ -912,13 +910,6 @@ class TestSecretsSubstitutionNegative(test_base.TestDbBase):
|
|||||||
with testtools.ExpectedException(expected_exception):
|
with testtools.ExpectedException(expected_exception):
|
||||||
next(secrets_substitution.substitute_all(documents))
|
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)
|
@mock.patch('deckhand.engine.secrets_manager.utils', autospec=True)
|
||||||
def test_generic_exception_raises_unknown_error(
|
def test_generic_exception_raises_unknown_error(
|
||||||
self, mock_utils):
|
self, mock_utils):
|
||||||
@ -957,3 +948,36 @@ class TestSecretsSubstitutionNegative(test_base.TestDbBase):
|
|||||||
with testtools.ExpectedException(
|
with testtools.ExpectedException(
|
||||||
errors.SubstitutionSourceDataNotFound):
|
errors.SubstitutionSourceDataNotFound):
|
||||||
next(secrets_substitution.substitute_all(documents))
|
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))
|
||||||
|
@ -24,8 +24,13 @@ Deckhand Exceptions
|
|||||||
|
|
||||||
* - Exception Name
|
* - Exception Name
|
||||||
- Description
|
- Description
|
||||||
* - BarbicanException
|
* - BarbicanClientException
|
||||||
- .. autoexception:: deckhand.errors.BarbicanException
|
- .. autoexception:: deckhand.errors.BarbicanClientException
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
:undoc-members:
|
||||||
|
* - BarbicanServerException
|
||||||
|
- .. autoexception:: deckhand.errors.BarbicanServerException
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
@ -39,6 +44,11 @@ Deckhand Exceptions
|
|||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
* - EncryptionSourceNotFound
|
||||||
|
- .. autoexception:: deckhand.errors.EncryptionSourceNotFound
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
:undoc-members:
|
||||||
* - InvalidDocumentFormat
|
* - InvalidDocumentFormat
|
||||||
- .. autoexception:: deckhand.errors.InvalidDocumentFormat
|
- .. autoexception:: deckhand.errors.InvalidDocumentFormat
|
||||||
:members:
|
:members:
|
||||||
|
Loading…
Reference in New Issue
Block a user