[test] Add integration test scenario for encrypting generic type
This PS adds an integration test scenario for validating that encrypting a generic document type and using it as a substitution source during document rendering works. Deckhand will now submit all generic documents to be encrypted to Barbican with a 'secret_type' of 'passphrase'. No encoding is provided Deckhand-side (i.e. base64) because encoding is deprecated in Barbican since it lead to strange behavior; Barbican will figure out what to encode the payload as automatically. For more information, see [0] and [1]. In addition, this PS handles 2 edge cases around secret payloads that are rejected by Barbican if not handled correctly by Deckhand: empty payloads and non-string type payloads [2]. For the first case Deckhand forcibly changes the document to cleartext because there is no point in encrypting a document with an empty payload. For the second case Deckhand sets overrides any previously set secret_type to 'opaque' and encodes the payload to base64 -- when it goes to render the secret it decodes the payload also using base64. Integration tests have been added to handle both edge cases described above. [0] https://bugs.launchpad.net/python-barbicanclient/+bug/1419166 [1]49505b9aec/barbicanclient/v1/secrets.py (L252)
[2]49505b9aec/barbicanclient/v1/secrets.py (L297)
Change-Id: I1964aa84ad07b6f310b39974f078b84a1dc84983
This commit is contained in:
parent
0143b2d727
commit
84ab5c5096
@ -12,49 +12,213 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
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_utils import uuidutils
|
||||||
|
import six
|
||||||
|
|
||||||
from deckhand.barbican import client_wrapper
|
from deckhand.barbican import client_wrapper
|
||||||
from deckhand.common import utils
|
|
||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
|
from deckhand import types
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
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()
|
||||||
|
|
||||||
def create_secret(self, **kwargs):
|
@classmethod
|
||||||
"""Create a secret."""
|
def is_barbican_ref(cls, secret_ref):
|
||||||
secret = self.barbicanclient.call("secrets.create", **kwargs)
|
# 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:
|
||||||
|
|
||||||
|
``deckhand/Certificate/v1`` => certificate
|
||||||
|
``deckhand/CertificateKey/v1`` => private
|
||||||
|
``deckhand/CertificateAuthority/v1`` => certificate
|
||||||
|
``deckhand/CertificateAuthorityKey/v1`` => private
|
||||||
|
``deckhand/Passphrase/v1`` => passphrase
|
||||||
|
``deckhand/PrivateKey/v1`` => private
|
||||||
|
``deckhand/PublicKey/v1`` => public
|
||||||
|
Other => passphrase
|
||||||
|
|
||||||
|
:param schema: The document's schema.
|
||||||
|
:returns: The value corresponding to the mapping above.
|
||||||
|
"""
|
||||||
|
parts = schema.split('/')
|
||||||
|
if len(parts) == 3:
|
||||||
|
namespace, kind, _ = parts
|
||||||
|
elif len(parts) == 2:
|
||||||
|
namespace, kind = parts
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Schema %s must consist of namespace/kind/version.' % schema)
|
||||||
|
|
||||||
|
is_generic = (
|
||||||
|
'/'.join([namespace, kind]) not in types.DOCUMENT_SECRET_TYPES
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the document kind is not a built-in secret type, then default to
|
||||||
|
# 'passphrase'.
|
||||||
|
if is_generic:
|
||||||
|
LOG.debug('Defaulting to secret_type="passphrase" for generic '
|
||||||
|
'document schema %s.', schema)
|
||||||
|
return 'passphrase'
|
||||||
|
|
||||||
|
kind = kind.lower()
|
||||||
|
|
||||||
|
if kind in [
|
||||||
|
'certificateauthoritykey', 'certificatekey', 'privatekey'
|
||||||
|
]:
|
||||||
|
return 'private'
|
||||||
|
elif kind == 'certificateauthority':
|
||||||
|
return 'certificate'
|
||||||
|
elif kind == 'publickey':
|
||||||
|
return 'public'
|
||||||
|
# NOTE(felipemonteiro): This branch below handles certificate and
|
||||||
|
# passphrase, both of which are supported secret types in Barbican.
|
||||||
|
return kind
|
||||||
|
|
||||||
|
def _base64_encode_payload(self, secret_doc):
|
||||||
|
"""Ensures secret document payload is compatible with Barbican."""
|
||||||
|
|
||||||
|
payload = secret_doc.data
|
||||||
|
secret_type = self._get_secret_type(secret_doc.schema)
|
||||||
|
|
||||||
|
# NOTE(felipemonteiro): The logic for the 2 conditions below is
|
||||||
|
# enforced from Barbican's Python client. Some pre-processing and
|
||||||
|
# transformation is needed to make Barbican work with non-compatible
|
||||||
|
# formats.
|
||||||
|
if not payload and payload is not False:
|
||||||
|
# There is no point in even bothering to encrypt an empty
|
||||||
|
# body, which just leads to needless overhead, so return
|
||||||
|
# early.
|
||||||
|
LOG.info('Barbican does not accept empty payloads so '
|
||||||
|
'Deckhand will not encrypt document [%s, %s] %s.',
|
||||||
|
secret_doc.schema, secret_doc.layer, secret_doc.name)
|
||||||
|
secret_doc.storage_policy = types.CLEARTEXT
|
||||||
|
elif not isinstance(
|
||||||
|
payload, (six.text_type, six.binary_type)):
|
||||||
|
LOG.debug('Forcibly setting secret_type=opaque and '
|
||||||
|
'base64-encoding non-string payload for '
|
||||||
|
'document [%s, %s] %s.', secret_doc.schema,
|
||||||
|
secret_doc.layer, secret_doc.name)
|
||||||
|
# NOTE(felipemonteiro): base64-encoding the non-string payload is
|
||||||
|
# done for serialization purposes, not for security purposes.
|
||||||
|
# 'opaque' is used to avoid Barbican doing any further
|
||||||
|
# serialization server-side.
|
||||||
|
secret_type = 'opaque'
|
||||||
|
try:
|
||||||
|
payload = base64.encode_as_text(six.text_type(payload))
|
||||||
|
except Exception:
|
||||||
|
message = ('Failed to base64-encode payload of type %s '
|
||||||
|
'for Barbican storage.', type(payload))
|
||||||
|
LOG.error(message)
|
||||||
|
raise errors.UnknownSubstitutionError(
|
||||||
|
src_schema=secret_doc.schema,
|
||||||
|
src_layer=secret_doc.layer, src_name=secret_doc.name,
|
||||||
|
schema='N/A', layer='N/A', name='N/A', details=message)
|
||||||
|
return secret_type, payload
|
||||||
|
|
||||||
|
def create_secret(self, secret_doc):
|
||||||
|
"""Create a secret.
|
||||||
|
|
||||||
|
:param secret_doc: Document with ``storagePolicy`` of "encrypted".
|
||||||
|
:type secret_doc: document.DocumentDict
|
||||||
|
:returns: Secret reference returned by Barbican
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
secret_type, payload = self._base64_encode_payload(secret_doc)
|
||||||
|
|
||||||
|
if secret_doc.storage_policy == types.CLEARTEXT:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
# Store secret_ref in database for `secret_doc`.
|
||||||
|
kwargs = {
|
||||||
|
'name': secret_doc['metadata']['name'],
|
||||||
|
'secret_type': secret_type,
|
||||||
|
'payload': payload
|
||||||
|
}
|
||||||
|
LOG.info('Storing encrypted document data in Barbican.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
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,
|
||||||
barbicanclient.exceptions.HTTPServerError) as e:
|
barbicanclient.exceptions.HTTPServerError) as e:
|
||||||
LOG.exception(str(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))
|
||||||
|
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.BarbicanException(details=str(e))
|
||||||
|
|
||||||
# NOTE(fmontei): The dictionary representation of the Secret object by
|
return secret_ref
|
||||||
# default has keys that are not snake case -- so make them snake case.
|
|
||||||
resp = secret.to_dict()
|
|
||||||
for key in resp:
|
|
||||||
resp[utils.to_snake_case(key)] = resp.pop(key)
|
|
||||||
resp['secret_ref'] = secret_ref
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def get_secret(self, secret_ref):
|
|
||||||
"""Get a secret."""
|
|
||||||
|
|
||||||
|
def _base64_decode_payload(self, src_doc, dest_doc, payload):
|
||||||
try:
|
try:
|
||||||
return self.barbicanclient.call("secrets.get", secret_ref)
|
# 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))
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_secret(self, src_doc, dest_doc, secret_ref):
|
||||||
|
"""Get a secret."""
|
||||||
|
try:
|
||||||
|
secret = self.barbicanclient.call("secrets.get", secret_ref)
|
||||||
except (barbicanclient.exceptions.HTTPAuthError,
|
except (barbicanclient.exceptions.HTTPAuthError,
|
||||||
barbicanclient.exceptions.HTTPClientError,
|
barbicanclient.exceptions.HTTPClientError,
|
||||||
barbicanclient.exceptions.HTTPServerError,
|
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.BarbicanException(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)
|
||||||
|
else:
|
||||||
|
secret = payload
|
||||||
|
|
||||||
|
return secret
|
||||||
|
@ -30,6 +30,11 @@ class DocumentDict(dict):
|
|||||||
Useful for accessing nested dictionary keys without having to worry about
|
Useful for accessing nested dictionary keys without having to worry about
|
||||||
exceptions getting thrown.
|
exceptions getting thrown.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
As a rule of thumb, setters for any metadata properties should be
|
||||||
|
avoided. Only implement or use for well-understood edge cases.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -112,7 +117,7 @@ class DocumentDict(dict):
|
|||||||
|
|
||||||
@substitutions.setter
|
@substitutions.setter
|
||||||
def substitutions(self, value):
|
def substitutions(self, value):
|
||||||
return utils.jsonpath_replace(self, value, 'metadata.substitutions')
|
return utils.jsonpath_replace(self, value, '.metadata.substitutions')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def actions(self):
|
def actions(self):
|
||||||
@ -123,6 +128,10 @@ class DocumentDict(dict):
|
|||||||
def storage_policy(self):
|
def storage_policy(self):
|
||||||
return utils.jsonpath_parse(self, 'metadata.storagePolicy') or ''
|
return utils.jsonpath_parse(self, 'metadata.storagePolicy') or ''
|
||||||
|
|
||||||
|
@storage_policy.setter
|
||||||
|
def storage_policy(self, value):
|
||||||
|
return utils.jsonpath_replace(self, value, '.metadata.storagePolicy')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_encrypted(self):
|
def is_encrypted(self):
|
||||||
return self.storage_policy == 'encrypted'
|
return self.storage_policy == 'encrypted'
|
||||||
|
@ -56,21 +56,26 @@ def sanitize_params(allowed_params):
|
|||||||
# This maps which type should be enforced per query parameter.
|
# This maps which type should be enforced per query parameter.
|
||||||
# Everything not included in type dict below is assumed to be a
|
# Everything not included in type dict below is assumed to be a
|
||||||
# string or a list of strings.
|
# string or a list of strings.
|
||||||
type_dict = {'limit': int}
|
type_dict = {
|
||||||
|
'limit': {
|
||||||
|
'func': lambda x: abs(int(x)),
|
||||||
|
'type': int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def _enforce_query_filter_type(key, val):
|
def _enforce_query_filter_type(key, val):
|
||||||
if key in type_dict:
|
cast_func = type_dict.get(key)
|
||||||
cast_type = type_dict[key]
|
if cast_func:
|
||||||
try:
|
try:
|
||||||
cast_val = cast_type(val)
|
cast_val = cast_func['func'](val)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise falcon.HTTPInvalidParam(
|
raise falcon.HTTPInvalidParam(
|
||||||
'Query parameter %s must be of type %s.' % (
|
'Query parameter %s must be of type %s.' % (
|
||||||
key, cast_type),
|
key, cast_func['type']),
|
||||||
key)
|
key)
|
||||||
return cast_val
|
|
||||||
else:
|
else:
|
||||||
return val
|
cast_val = val
|
||||||
|
return cast_val
|
||||||
|
|
||||||
def _convert_to_dict(sanitized_params, filter_key, filter_val):
|
def _convert_to_dict(sanitized_params, filter_key, filter_val):
|
||||||
# Key-value pairs like metadata.label=foo=bar need to be
|
# Key-value pairs like metadata.label=foo=bar need to be
|
||||||
|
@ -186,7 +186,8 @@ def documents_create(bucket_name, documents, validations=None,
|
|||||||
validation)
|
validation)
|
||||||
|
|
||||||
if documents_to_delete:
|
if documents_to_delete:
|
||||||
LOG.debug('Deleting documents: %s.', documents_to_delete)
|
LOG.debug('Deleting documents: %s.',
|
||||||
|
[d.meta for d in documents_to_delete])
|
||||||
deleted_documents = []
|
deleted_documents = []
|
||||||
|
|
||||||
for d in documents_to_delete:
|
for d in documents_to_delete:
|
||||||
@ -215,8 +216,9 @@ def documents_create(bucket_name, documents, validations=None,
|
|||||||
resp.append(doc.to_dict())
|
resp.append(doc.to_dict())
|
||||||
|
|
||||||
if documents_to_create:
|
if documents_to_create:
|
||||||
LOG.debug('Creating documents: %s.',
|
LOG.debug(
|
||||||
[(d['schema'], d['name']) for d in documents_to_create])
|
'Creating documents: %s.', [(d['schema'], d['layer'], d['name'])
|
||||||
|
for d in documents_to_create])
|
||||||
for doc in documents_to_create:
|
for doc in documents_to_create:
|
||||||
with session.begin():
|
with session.begin():
|
||||||
doc['bucket_id'] = bucket['id']
|
doc['bucket_id'] = bucket['id']
|
||||||
|
@ -17,10 +17,9 @@ import re
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import uuidutils
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from deckhand.barbican import driver
|
from deckhand.barbican.driver import BarbicanDriver
|
||||||
from deckhand.common import document as document_wrapper
|
from deckhand.common import document as document_wrapper
|
||||||
from deckhand.common import utils
|
from deckhand.common import utils
|
||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
@ -36,10 +35,7 @@ class SecretsManager(object):
|
|||||||
Currently only supports Barbican.
|
Currently only supports Barbican.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
barbican_driver = driver.BarbicanDriver()
|
barbican_driver = BarbicanDriver()
|
||||||
|
|
||||||
_url_re = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|'
|
|
||||||
'(?:%[0-9a-fA-F][0-9a-fA-F]))+')
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def requires_encryption(document):
|
def requires_encryption(document):
|
||||||
@ -48,76 +44,40 @@ class SecretsManager(object):
|
|||||||
document = clazz(document)
|
document = clazz(document)
|
||||||
return document.is_encrypted
|
return document.is_encrypted
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_barbican_ref(cls, secret_ref):
|
|
||||||
# TODO(fmontei): 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, secret_doc):
|
def create(cls, secret_doc):
|
||||||
"""Securely store secrets contained in ``secret_doc``.
|
"""Securely store secrets contained in ``secret_doc``.
|
||||||
|
|
||||||
Ordinarily, Deckhand documents are stored directly in Deckhand's
|
|
||||||
database. However, secret data (contained in the data section for the
|
|
||||||
documents with the schemas enumerated below) must be stored using a
|
|
||||||
secure storage service like Barbican.
|
|
||||||
|
|
||||||
Documents with ``metadata.storagePolicy`` == "clearText" have their
|
Documents with ``metadata.storagePolicy`` == "clearText" have their
|
||||||
secrets stored directly in Deckhand.
|
secrets stored directly in Deckhand.
|
||||||
|
|
||||||
Documents with ``metadata.storagePolicy`` == "encrypted" are stored in
|
Documents with ``metadata.storagePolicy`` == "encrypted" are stored in
|
||||||
Barbican directly. Deckhand in turn stores the reference returned
|
Barbican directly. Deckhand in turn stores the reference returned
|
||||||
by Barbican in Deckhand.
|
by Barbican in its own DB.
|
||||||
|
|
||||||
:param secret_doc: A Deckhand document with one of the following
|
:param secret_doc: A Deckhand document with a schema that belongs to
|
||||||
schemas:
|
``types.DOCUMENT_SECRET_TYPES``.
|
||||||
|
|
||||||
* ``deckhand/Certificate/v1``
|
:returns: Unecrypted data section from ``secret_doc`` if the document's
|
||||||
* ``deckhand/CertificateKey/v1``
|
``storagePolicy`` is "cleartext" or a Barbican secret reference
|
||||||
* ``deckhand/Passphrase/v1``
|
if the ``storagePolicy`` is "encrypted'.
|
||||||
|
|
||||||
:returns: Dictionary representation of
|
|
||||||
``deckhand.db.sqlalchemy.models.DocumentSecret``.
|
|
||||||
"""
|
"""
|
||||||
# TODO(fmontei): Look into POSTing Deckhand metadata into Barbican's
|
# TODO(fmontei): Look into POSTing Deckhand metadata into Barbican's
|
||||||
# Secrets Metadata API to make it easier to track stale secrets from
|
# Secrets Metadata API to make it easier to track stale secrets from
|
||||||
# prior revisions that need to be deleted.
|
# prior revisions that need to be deleted.
|
||||||
|
if not isinstance(secret_doc, document_wrapper.DocumentDict):
|
||||||
|
secret_doc = document_wrapper.DocumentDict(secret_doc)
|
||||||
|
|
||||||
encryption_type = secret_doc['metadata']['storagePolicy']
|
if secret_doc.storage_policy == types.ENCRYPTED:
|
||||||
secret_type = cls._get_secret_type(secret_doc['schema'])
|
payload = cls.barbican_driver.create_secret(secret_doc)
|
||||||
created_secret = secret_doc['data']
|
else:
|
||||||
|
payload = secret_doc.data
|
||||||
|
|
||||||
if encryption_type == types.ENCRYPTED:
|
return payload
|
||||||
# Store secret_ref in database for `secret_doc`.
|
|
||||||
kwargs = {
|
|
||||||
'name': secret_doc['metadata']['name'],
|
|
||||||
'secret_type': secret_type,
|
|
||||||
'payload': secret_doc['data']
|
|
||||||
}
|
|
||||||
LOG.info('Storing encrypted document data in Barbican.')
|
|
||||||
resp = cls.barbican_driver.create_secret(**kwargs)
|
|
||||||
|
|
||||||
secret_ref = resp['secret_ref']
|
|
||||||
created_secret = secret_ref
|
|
||||||
|
|
||||||
return created_secret
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, secret_ref):
|
def get(cls, secret_ref, src_doc, dest_doc):
|
||||||
"""Return 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
|
||||||
Secrets API with it.
|
Secrets API with it.
|
||||||
@ -125,43 +85,13 @@ class SecretsManager(object):
|
|||||||
:param str secret_ref: A string formatted like:
|
:param str secret_ref: A string formatted like:
|
||||||
"https://{barbican_host}/v1/secrets/{secret_uuid}"
|
"https://{barbican_host}/v1/secrets/{secret_uuid}"
|
||||||
:returns: Secret payload from Barbican.
|
:returns: Secret payload from Barbican.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
LOG.debug('Resolving Barbican secret using source document '
|
LOG.debug('Resolving Barbican secret using source document '
|
||||||
'reference...')
|
'reference...')
|
||||||
# TODO(fmontei): Need to avoid this call if Keystone is disabled.
|
secret = cls.barbican_driver.get_secret(src_doc, dest_doc,
|
||||||
secret = cls.barbican_driver.get_secret(secret_ref=secret_ref)
|
secret_ref=secret_ref)
|
||||||
payload = secret.payload
|
|
||||||
LOG.debug('Successfully retrieved Barbican secret using reference.')
|
LOG.debug('Successfully retrieved Barbican secret using reference.')
|
||||||
return payload
|
return secret
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_secret_type(cls, schema):
|
|
||||||
"""Get the Barbican secret type based on the following mapping:
|
|
||||||
|
|
||||||
``deckhand/Certificate/v1`` => certificate
|
|
||||||
``deckhand/CertificateKey/v1`` => private
|
|
||||||
``deckhand/CertificateAuthority/v1`` => certificate
|
|
||||||
``deckhand/CertificateAuthorityKey/v1`` => private
|
|
||||||
``deckhand/Passphrase/v1`` => passphrase
|
|
||||||
``deckhand/PrivateKey/v1`` => private
|
|
||||||
``deckhand/PublicKey/v1`` => public
|
|
||||||
|
|
||||||
:param schema: The document's schema.
|
|
||||||
:returns: The value corresponding to the mapping above.
|
|
||||||
"""
|
|
||||||
_schema = schema.split('/')[1].lower().strip()
|
|
||||||
if _schema in [
|
|
||||||
'certificateauthoritykey', 'certificatekey', 'privatekey'
|
|
||||||
]:
|
|
||||||
return 'private'
|
|
||||||
elif _schema == 'certificateauthority':
|
|
||||||
return 'certificate'
|
|
||||||
elif _schema == 'publickey':
|
|
||||||
return 'public'
|
|
||||||
# NOTE(fmontei): This branch below handles certificate and passphrase,
|
|
||||||
# both of which are supported secret types in Barbican.
|
|
||||||
return _schema
|
|
||||||
|
|
||||||
|
|
||||||
class SecretsSubstitution(object):
|
class SecretsSubstitution(object):
|
||||||
@ -209,7 +139,7 @@ class SecretsSubstitution(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_encrypted_data(src_secret, src_doc, dest_doc):
|
def get_encrypted_data(src_secret, src_doc, dest_doc):
|
||||||
try:
|
try:
|
||||||
src_secret = SecretsManager.get(src_secret)
|
src_secret = SecretsManager.get(src_secret, src_doc, dest_doc)
|
||||||
except errors.BarbicanException as e:
|
except errors.BarbicanException as e:
|
||||||
LOG.error(
|
LOG.error(
|
||||||
'Failed to resolve a Barbican reference for substitution '
|
'Failed to resolve a Barbican reference for substitution '
|
||||||
@ -270,24 +200,6 @@ class SecretsSubstitution(object):
|
|||||||
else:
|
else:
|
||||||
LOG.warning(exc_message)
|
LOG.warning(exc_message)
|
||||||
|
|
||||||
def _get_encrypted_secret(self, src_secret, src_doc, dest_doc):
|
|
||||||
try:
|
|
||||||
src_secret = SecretsManager.get(src_secret)
|
|
||||||
except errors.BarbicanException as e:
|
|
||||||
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
|
|
||||||
|
|
||||||
def _check_src_secret_is_not_none(self, src_secret, src_path, src_doc,
|
def _check_src_secret_is_not_none(self, src_secret, src_path, src_doc,
|
||||||
dest_doc):
|
dest_doc):
|
||||||
if src_secret is None:
|
if src_secret is None:
|
||||||
@ -378,7 +290,7 @@ 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 SecretsManager.is_barbican_ref(
|
if src_doc.is_encrypted and BarbicanDriver.is_barbican_ref(
|
||||||
src_secret):
|
src_secret):
|
||||||
src_secret = self.get_encrypted_data(src_secret, src_doc,
|
src_secret = self.get_encrypted_data(src_secret, src_doc,
|
||||||
document)
|
document)
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
# Tests success paths for edge cases around rendering with secrets.
|
||||||
|
#
|
||||||
|
# 1. Verifies that attempting to encrypt a secret passphrase with an empty
|
||||||
|
# string skips encryption and stores the document as cleartext instead
|
||||||
|
# and that rendering the document works (which should avoid Barbican
|
||||||
|
# API call).
|
||||||
|
# 2. Verifies that attempting to encrypt any document with an incompatible
|
||||||
|
# payload type (non-str, non-binary) results in the payload being properly
|
||||||
|
# encoded and decoded by Deckhand before and after storing and retrieving
|
||||||
|
# the encrypted data.
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
request_headers:
|
||||||
|
content-type: application/x-yaml
|
||||||
|
X-Auth-Token: $ENVIRON['TEST_AUTH_TOKEN']
|
||||||
|
response_headers:
|
||||||
|
content-type: application/x-yaml
|
||||||
|
verbose: true
|
||||||
|
|
||||||
|
tests:
|
||||||
|
### Scenario 1 ###
|
||||||
|
- name: attempt_create_encrypted_passphrase_empty_payload
|
||||||
|
desc: |
|
||||||
|
Attempting to create an encrypted passphrase with empty payload should
|
||||||
|
avoid encryption and return the empty payload instead.
|
||||||
|
PUT: /api/v1.0/buckets/secret/documents
|
||||||
|
status: 200
|
||||||
|
data: |-
|
||||||
|
---
|
||||||
|
schema: deckhand/LayeringPolicy/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Control/v1
|
||||||
|
name: layering-policy
|
||||||
|
data:
|
||||||
|
layerOrder:
|
||||||
|
- site
|
||||||
|
---
|
||||||
|
schema: deckhand/Passphrase/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Document/v1
|
||||||
|
name: my-passphrase
|
||||||
|
storagePolicy: encrypted
|
||||||
|
layeringDefinition:
|
||||||
|
layer: site
|
||||||
|
data: ''
|
||||||
|
...
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 2
|
||||||
|
$.[1].metadata.name: my-passphrase
|
||||||
|
$.[1].data: ''
|
||||||
|
|
||||||
|
- name: verify_revision_documents_returns_same_empty_payload
|
||||||
|
desc: Verify that the created document wasn't encrypted.
|
||||||
|
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/documents
|
||||||
|
status: 200
|
||||||
|
query_parameters:
|
||||||
|
metadata.name: my-passphrase
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 1
|
||||||
|
$.[0].data: ''
|
||||||
|
|
||||||
|
- name: verify_rendered_documents_returns_same_empty_payload
|
||||||
|
desc: |
|
||||||
|
Verify that rendering the document returns the same empty payload
|
||||||
|
which requires that the data be read directly from Deckhand's DB
|
||||||
|
rather than Barbican.
|
||||||
|
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/rendered-documents
|
||||||
|
status: 200
|
||||||
|
query_parameters:
|
||||||
|
metadata.name: my-passphrase
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 1
|
||||||
|
$.[0].data: ''
|
||||||
|
|
||||||
|
### Scenario 2 ###
|
||||||
|
- name: create_encrypted_passphrase_with_incompatible_payload
|
||||||
|
desc: |
|
||||||
|
Attempting to encrypt a non-str/non-bytes payload should result in
|
||||||
|
it first being base64-encoded then passed to Barbican. The response
|
||||||
|
should be a Barbican reference, which indicates the scenario passed
|
||||||
|
successfully.
|
||||||
|
PUT: /api/v1.0/buckets/secret/documents
|
||||||
|
status: 200
|
||||||
|
data: |-
|
||||||
|
---
|
||||||
|
schema: deckhand/LayeringPolicy/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Control/v1
|
||||||
|
name: layering-policy
|
||||||
|
data:
|
||||||
|
layerOrder:
|
||||||
|
- site
|
||||||
|
---
|
||||||
|
schema: armada/Generic/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Document/v1
|
||||||
|
name: armada-doc
|
||||||
|
storagePolicy: encrypted
|
||||||
|
layeringDefinition:
|
||||||
|
layer: site
|
||||||
|
data:
|
||||||
|
# This will be an object in memory requiring base64 encoding.
|
||||||
|
foo: bar
|
||||||
|
...
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 2
|
||||||
|
$.[1].metadata.name: armada-doc
|
||||||
|
# NOTE(fmontei): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
||||||
|
# leading to this nastiness:
|
||||||
|
$.[1].data.`split(:, 0, 1)` + "://" + $.[1].data.`split(/, 2, 3)`: $ENVIRON['TEST_BARBICAN_URL']
|
||||||
|
|
||||||
|
- name: verify_revision_documents_returns_barbican_ref
|
||||||
|
desc: Verify that the encrypted document returns a Barbican ref.
|
||||||
|
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/documents
|
||||||
|
status: 200
|
||||||
|
query_parameters:
|
||||||
|
metadata.name: armada-doc
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 1
|
||||||
|
$.[0].data.`split(:, 0, 1)` + "://" + $.[0].data.`split(/, 2, 3)`: $ENVIRON['TEST_BARBICAN_URL']
|
||||||
|
|
||||||
|
- name: verify_rendered_documents_returns_unencrypted_payload
|
||||||
|
desc: |
|
||||||
|
Verify that rendering the document returns the original payload which
|
||||||
|
means that Deckhand successfully encoded and decoded the non-compatible
|
||||||
|
payload using base64 encoding.
|
||||||
|
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/rendered-documents
|
||||||
|
status: 200
|
||||||
|
query_parameters:
|
||||||
|
metadata.name: armada-doc
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 1
|
||||||
|
$.[0].data:
|
||||||
|
foo: bar
|
@ -41,6 +41,11 @@ tests:
|
|||||||
layer: site
|
layer: site
|
||||||
data: not-a-real-password
|
data: not-a-real-password
|
||||||
...
|
...
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 2
|
||||||
|
# NOTE(fmontei): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
||||||
|
# leading to this nastiness:
|
||||||
|
$.[1].data.`split(:, 0, 1)` + "://" + $.[1].data.`split(/, 2, 3)`: $ENVIRON['TEST_BARBICAN_URL']
|
||||||
|
|
||||||
- name: verify_rendered_documents_returns_secret_payload
|
- name: verify_rendered_documents_returns_secret_payload
|
||||||
desc: Verify that the rendering the document returns the secret payload.
|
desc: Verify that the rendering the document returns the secret payload.
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
# Tests success paths for secret substitution using a generic document type.
|
||||||
|
# This entails setting storagePolicy=encrypted for a non-built-in secret
|
||||||
|
# document.
|
||||||
|
#
|
||||||
|
# 1. Tests that creating an encrypted generic document results in a
|
||||||
|
# Barbican reference being returned.
|
||||||
|
# 2. Tests that the encrypted payload is included in the destination
|
||||||
|
# and source documents after document rendering.
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
request_headers:
|
||||||
|
content-type: application/x-yaml
|
||||||
|
X-Auth-Token: $ENVIRON['TEST_AUTH_TOKEN']
|
||||||
|
response_headers:
|
||||||
|
content-type: application/x-yaml
|
||||||
|
verbose: true
|
||||||
|
|
||||||
|
tests:
|
||||||
|
- name: purge
|
||||||
|
desc: Begin testing from known state.
|
||||||
|
DELETE: /api/v1.0/revisions
|
||||||
|
status: 204
|
||||||
|
response_headers: null
|
||||||
|
|
||||||
|
- name: encrypt_generic_document_for_secret_substitution
|
||||||
|
desc: |
|
||||||
|
Create documents using a generic document type (armada/Generic/v1) as the
|
||||||
|
substitution source with storagePolicy=encrypted.
|
||||||
|
PUT: /api/v1.0/buckets/secret/documents
|
||||||
|
status: 200
|
||||||
|
data: |-
|
||||||
|
---
|
||||||
|
schema: deckhand/LayeringPolicy/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Control/v1
|
||||||
|
name: layering-policy
|
||||||
|
data:
|
||||||
|
layerOrder:
|
||||||
|
- site
|
||||||
|
---
|
||||||
|
# Generic document as substitution source.
|
||||||
|
schema: armada/Generic/v1
|
||||||
|
metadata:
|
||||||
|
name: example-armada-cert
|
||||||
|
schema: metadata/Document/v1
|
||||||
|
layeringDefinition:
|
||||||
|
layer: site
|
||||||
|
storagePolicy: encrypted
|
||||||
|
data: ARMADA CERTIFICATE DATA
|
||||||
|
---
|
||||||
|
schema: armada/Chart/v1
|
||||||
|
metadata:
|
||||||
|
schema: metadata/Document/v1
|
||||||
|
name: armada-chart-01
|
||||||
|
layeringDefinition:
|
||||||
|
layer: site
|
||||||
|
substitutions:
|
||||||
|
- dest:
|
||||||
|
path: .chart.values.tls.certificate
|
||||||
|
src:
|
||||||
|
schema: armada/Generic/v1
|
||||||
|
name: example-armada-cert
|
||||||
|
path: .
|
||||||
|
data: {}
|
||||||
|
...
|
||||||
|
|
||||||
|
- name: verify_multiple_revision_documents_returns_secret_ref
|
||||||
|
desc: Verify that secret ref was created for example-armada-cert among multiple created documents.
|
||||||
|
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/documents
|
||||||
|
status: 200
|
||||||
|
query_parameters:
|
||||||
|
metadata.name: example-armada-cert
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 1
|
||||||
|
# NOTE(fmontei): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
||||||
|
# leading to this nastiness:
|
||||||
|
$.[0].data.`split(:, 0, 1)` + "://" + $.[0].data.`split(/, 2, 3)`: $ENVIRON['TEST_BARBICAN_URL']
|
||||||
|
|
||||||
|
- name: verify_secret_payload_in_destination_document
|
||||||
|
desc: Verify secret payload is injected in destination document as well as example-armada-cert.
|
||||||
|
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/rendered-documents
|
||||||
|
status: 200
|
||||||
|
query_parameters:
|
||||||
|
metadata.name:
|
||||||
|
- armada-chart-01
|
||||||
|
- example-armada-cert
|
||||||
|
sort: metadata.name
|
||||||
|
response_multidoc_jsonpaths:
|
||||||
|
$.`len`: 2
|
||||||
|
$.[0].metadata.name: armada-chart-01
|
||||||
|
$.[0].data:
|
||||||
|
chart:
|
||||||
|
values:
|
||||||
|
tls:
|
||||||
|
certificate: ARMADA CERTIFICATE DATA
|
||||||
|
$.[1].metadata.name: example-armada-cert
|
||||||
|
$.[1].data: ARMADA CERTIFICATE DATA
|
@ -16,9 +16,13 @@ import copy
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
from oslo_serialization import base64
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
import six
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
|
from deckhand.barbican import driver
|
||||||
|
from deckhand.common import document as document_wrapper
|
||||||
from deckhand.engine import secrets_manager
|
from deckhand.engine import secrets_manager
|
||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
from deckhand import factories
|
from deckhand import factories
|
||||||
@ -30,38 +34,45 @@ class TestSecretsManager(test_base.TestDbBase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestSecretsManager, self).setUp()
|
super(TestSecretsManager, self).setUp()
|
||||||
self.mock_barbican_driver = self.patchobject(
|
self.mock_barbicanclient = self.patchobject(
|
||||||
secrets_manager.SecretsManager, 'barbican_driver')
|
secrets_manager.SecretsManager.barbican_driver, 'barbicanclient')
|
||||||
self.secret_ref = "https://barbican_host/v1/secrets/{secret_uuid}"\
|
self.secret_ref = "https://barbican_host/v1/secrets/{secret_uuid}"\
|
||||||
.format(**{'secret_uuid': uuidutils.generate_uuid()})
|
.format(**{'secret_uuid': uuidutils.generate_uuid()})
|
||||||
self.mock_barbican_driver.create_secret.return_value = (
|
|
||||||
{'secret_ref': self.secret_ref})
|
|
||||||
self.factory = factories.DocumentSecretFactory()
|
self.factory = factories.DocumentSecretFactory()
|
||||||
|
|
||||||
def _test_create_secret(self, encryption_type, secret_type):
|
def _mock_barbican_client_call(self, payload):
|
||||||
secret_data = test_utils.rand_password()
|
def fake_call(action, *args, **kwargs):
|
||||||
secret_doc = self.factory.gen_test(
|
if action == "secrets.create":
|
||||||
secret_type, encryption_type, secret_data)
|
return mock.Mock(**{'store.return_value': self.secret_ref})
|
||||||
payload = secret_doc['data']
|
elif action == "secrets.get":
|
||||||
self.mock_barbican_driver.get_secret.return_value = (
|
return mock.Mock(payload=payload)
|
||||||
mock.Mock(payload=payload))
|
|
||||||
|
|
||||||
created_secret = secrets_manager.SecretsManager.create(secret_doc)
|
fake_secret_obj = self.mock_barbicanclient.call
|
||||||
|
fake_secret_obj.side_effect = fake_call
|
||||||
|
|
||||||
|
def _test_create_secret(self, encryption_type, secret_type):
|
||||||
|
secret_payload = test_utils.rand_password()
|
||||||
|
secret_doc = self.factory.gen_test(
|
||||||
|
secret_type, encryption_type, secret_payload)
|
||||||
|
payload = secret_doc['data']
|
||||||
|
|
||||||
|
self._mock_barbican_client_call(payload)
|
||||||
|
secret_ref = secrets_manager.SecretsManager.create(secret_doc)
|
||||||
|
|
||||||
if encryption_type == 'cleartext':
|
if encryption_type == 'cleartext':
|
||||||
self.assertEqual(secret_data, created_secret)
|
self.assertEqual(secret_payload, secret_ref)
|
||||||
elif encryption_type == 'encrypted':
|
elif encryption_type == 'encrypted':
|
||||||
expected_kwargs = {
|
expected_kwargs = {
|
||||||
'name': secret_doc['metadata']['name'],
|
'name': secret_doc['metadata']['name'],
|
||||||
'secret_type': secrets_manager.SecretsManager._get_secret_type(
|
'secret_type': driver.BarbicanDriver._get_secret_type(
|
||||||
'deckhand/' + secret_type),
|
'deckhand/' + secret_type),
|
||||||
'payload': payload
|
'payload': payload
|
||||||
}
|
}
|
||||||
self.mock_barbican_driver.create_secret.assert_called_once_with(
|
self.assertEqual(self.secret_ref, secret_ref)
|
||||||
**expected_kwargs)
|
self.mock_barbicanclient.call.assert_called_once_with(
|
||||||
self.assertEqual(self.secret_ref, created_secret)
|
'secrets.create', **expected_kwargs)
|
||||||
|
|
||||||
return created_secret, payload
|
return secret_ref, payload
|
||||||
|
|
||||||
def test_create_cleartext_certificate(self):
|
def test_create_cleartext_certificate(self):
|
||||||
self._test_create_secret('cleartext', 'Certificate')
|
self._test_create_secret('cleartext', 'Certificate')
|
||||||
@ -102,11 +113,59 @@ class TestSecretsManager(test_base.TestDbBase):
|
|||||||
def test_retrieve_barbican_secret(self):
|
def test_retrieve_barbican_secret(self):
|
||||||
secret_ref, expected_secret = self._test_create_secret(
|
secret_ref, expected_secret = self._test_create_secret(
|
||||||
'encrypted', 'Certificate')
|
'encrypted', 'Certificate')
|
||||||
secret_payload = secrets_manager.SecretsManager.get(secret_ref)
|
self.mock_barbicanclient.get_secret.return_value = (
|
||||||
|
mock.Mock(payload=expected_secret))
|
||||||
|
|
||||||
|
secret_payload = secrets_manager.SecretsManager.get(secret_ref, {}, {})
|
||||||
|
|
||||||
self.assertEqual(expected_secret, secret_payload)
|
self.assertEqual(expected_secret, secret_payload)
|
||||||
self.mock_barbican_driver.get_secret.assert_called_once_with(
|
self.mock_barbicanclient.call.assert_called_with(
|
||||||
secret_ref=secret_ref)
|
'secrets.get', secret_ref)
|
||||||
|
|
||||||
|
def test_empty_payload_skips_encryption(self):
|
||||||
|
for empty_payload in ('', {}, []):
|
||||||
|
secret_doc = self.factory.gen_test(
|
||||||
|
'Certificate', 'encrypted', empty_payload)
|
||||||
|
|
||||||
|
self._mock_barbican_client_call(empty_payload)
|
||||||
|
retrieved_payload = secrets_manager.SecretsManager.create(
|
||||||
|
secret_doc)
|
||||||
|
|
||||||
|
self.assertEqual(empty_payload, retrieved_payload)
|
||||||
|
self.assertEqual('cleartext',
|
||||||
|
secret_doc['metadata']['storagePolicy'])
|
||||||
|
self.mock_barbicanclient.call.assert_not_called()
|
||||||
|
|
||||||
|
def test_create_and_retrieve_base64_encoded_payload(self):
|
||||||
|
# Validate base64-encoded encryption.
|
||||||
|
payload = {'foo': 'bar'}
|
||||||
|
secret_doc = self.factory.gen_test(
|
||||||
|
'Certificate', 'encrypted', payload)
|
||||||
|
|
||||||
|
expected_payload = base64.encode_as_text(six.text_type({'foo': 'bar'}))
|
||||||
|
expected_kwargs = {
|
||||||
|
'name': secret_doc['metadata']['name'],
|
||||||
|
'secret_type': 'opaque',
|
||||||
|
'payload': expected_payload
|
||||||
|
}
|
||||||
|
|
||||||
|
self._mock_barbican_client_call(payload)
|
||||||
|
secret_ref = secrets_manager.SecretsManager.create(secret_doc)
|
||||||
|
|
||||||
|
self.assertEqual(self.secret_ref, secret_ref)
|
||||||
|
self.assertEqual('encrypted',
|
||||||
|
secret_doc['metadata']['storagePolicy'])
|
||||||
|
self.mock_barbicanclient.call.assert_called_once_with(
|
||||||
|
"secrets.create", **expected_kwargs)
|
||||||
|
|
||||||
|
# Validate base64-encoded decryption.
|
||||||
|
self.mock_barbicanclient.get_secret.return_value = (
|
||||||
|
mock.Mock(payload=expected_payload, secret_type='opaque'))
|
||||||
|
|
||||||
|
dummy_document = document_wrapper.DocumentDict({})
|
||||||
|
retrieved_payload = secrets_manager.SecretsManager.get(
|
||||||
|
secret_ref, dummy_document, dummy_document)
|
||||||
|
self.assertEqual(payload, retrieved_payload)
|
||||||
|
|
||||||
|
|
||||||
class TestSecretsSubstitution(test_base.TestDbBase):
|
class TestSecretsSubstitution(test_base.TestDbBase):
|
||||||
@ -197,7 +256,8 @@ class TestSecretsSubstitution(test_base.TestDbBase):
|
|||||||
}
|
}
|
||||||
self._test_doc_substitution(
|
self._test_doc_substitution(
|
||||||
document_mapping, [certificate], expected_data)
|
document_mapping, [certificate], expected_data)
|
||||||
mock_secrets_manager.get.assert_called_once_with(secret_ref=secret_ref)
|
mock_secrets_manager.get.assert_called_once_with(
|
||||||
|
secret_ref, certificate, mock.ANY)
|
||||||
|
|
||||||
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
|
||||||
|
@ -12,33 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
DOCUMENT_SCHEMA_TYPES = (
|
|
||||||
CERTIFICATE_AUTHORITY_SCHEMA,
|
|
||||||
CERTIFICATE_KEY_AUTHORITY_SCHEMA,
|
|
||||||
CERTIFICATE_SCHEMA,
|
|
||||||
CERTIFICATE_KEY_SCHEMA,
|
|
||||||
PRIVATE_KEY_SCHEMA,
|
|
||||||
PUBLIC_KEY_SCHEMA,
|
|
||||||
PASSPHRASE_SCHEMA,
|
|
||||||
DATA_SCHEMA_SCHEMA,
|
|
||||||
LAYERING_POLICY_SCHEMA,
|
|
||||||
PASSPHRASE_SCHEMA,
|
|
||||||
VALIDATION_POLICY_SCHEMA,
|
|
||||||
) = (
|
|
||||||
'deckhand/CertificateAuthority',
|
|
||||||
'deckhand/CertificateAuthorityKey',
|
|
||||||
'deckhand/Certificate',
|
|
||||||
'deckhand/CertificateKey',
|
|
||||||
'deckhand/PrivateKey',
|
|
||||||
'deckhand/PublicKey',
|
|
||||||
'deckhand/Passphrase',
|
|
||||||
'deckhand/DataSchema',
|
|
||||||
'deckhand/LayeringPolicy',
|
|
||||||
'deckhand/Passphrase',
|
|
||||||
'deckhand/ValidationPolicy',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENT_SECRET_TYPES = (
|
DOCUMENT_SECRET_TYPES = (
|
||||||
CERTIFICATE_AUTHORITY_SCHEMA,
|
CERTIFICATE_AUTHORITY_SCHEMA,
|
||||||
CERTIFICATE_KEY_AUTHORITY_SCHEMA,
|
CERTIFICATE_KEY_AUTHORITY_SCHEMA,
|
||||||
@ -52,12 +25,26 @@ DOCUMENT_SECRET_TYPES = (
|
|||||||
'deckhand/CertificateAuthorityKey',
|
'deckhand/CertificateAuthorityKey',
|
||||||
'deckhand/Certificate',
|
'deckhand/Certificate',
|
||||||
'deckhand/CertificateKey',
|
'deckhand/CertificateKey',
|
||||||
|
'deckhand/Passphrase',
|
||||||
'deckhand/PrivateKey',
|
'deckhand/PrivateKey',
|
||||||
'deckhand/PublicKey',
|
'deckhand/PublicKey',
|
||||||
'deckhand/Passphrase'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENT_SCHEMA_TYPES = (
|
||||||
|
DATA_SCHEMA_SCHEMA,
|
||||||
|
LAYERING_POLICY_SCHEMA,
|
||||||
|
VALIDATION_POLICY_SCHEMA,
|
||||||
|
) = (
|
||||||
|
'deckhand/DataSchema',
|
||||||
|
'deckhand/LayeringPolicy',
|
||||||
|
'deckhand/ValidationPolicy',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENT_SCHEMA_TYPES += DOCUMENT_SECRET_TYPES
|
||||||
|
|
||||||
|
|
||||||
DECKHAND_VALIDATION_TYPES = (
|
DECKHAND_VALIDATION_TYPES = (
|
||||||
DECKHAND_SCHEMA_VALIDATION,
|
DECKHAND_SCHEMA_VALIDATION,
|
||||||
) = (
|
) = (
|
||||||
@ -70,5 +57,5 @@ ENCRYPTION_TYPES = (
|
|||||||
ENCRYPTED
|
ENCRYPTED
|
||||||
) = (
|
) = (
|
||||||
'cleartext',
|
'cleartext',
|
||||||
'encrypted'
|
'encrypted',
|
||||||
)
|
)
|
||||||
|
@ -71,8 +71,8 @@ Supported query string parameters:
|
|||||||
* ``metadata.name`` - string, optional
|
* ``metadata.name`` - string, optional
|
||||||
* ``metadata.layeringDefinition.abstract`` - string, optional - Valid values are
|
* ``metadata.layeringDefinition.abstract`` - string, optional - Valid values are
|
||||||
the "true" and "false".
|
the "true" and "false".
|
||||||
* ``metadata.layeringDefinition.layer`` - string, optional - Only return documents from
|
* ``metadata.layeringDefinition.layer`` - string, optional - Only return
|
||||||
the specified layer.
|
documents from the specified layer.
|
||||||
* ``metadata.label`` - string, optional, repeatable - Uses the format
|
* ``metadata.label`` - string, optional, repeatable - Uses the format
|
||||||
``metadata.label=key=value``. Repeating this parameter indicates all
|
``metadata.label=key=value``. Repeating this parameter indicates all
|
||||||
requested labels must apply (AND not OR).
|
requested labels must apply (AND not OR).
|
||||||
@ -86,7 +86,8 @@ Supported query string parameters:
|
|||||||
"asc". Controls the order in which the ``sort`` result is returned: "asc"
|
"asc". Controls the order in which the ``sort`` result is returned: "asc"
|
||||||
returns sorted results in ascending order, while "desc" returns results in
|
returns sorted results in ascending order, while "desc" returns results in
|
||||||
descending order.
|
descending order.
|
||||||
* ``limit`` - int - Controls number of documents returned by this endpoint.
|
* ``limit`` - int, optional - Controls number of documents returned by this
|
||||||
|
endpoint.
|
||||||
|
|
||||||
GET ``/revisions/{revision_id}/rendered-documents``
|
GET ``/revisions/{revision_id}/rendered-documents``
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Loading…
x
Reference in New Issue
Block a user