# Copyright 2017 AT&T Intellectual Property. All other rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy from oslo_log import log as logging import six from deckhand.barbican import driver from deckhand.engine import document_wrapper from deckhand import errors from deckhand import utils LOG = logging.getLogger(__name__) CLEARTEXT = 'cleartext' ENCRYPTED = 'encrypted' class SecretsManager(object): """Internal API resource for interacting with Barbican. Currently only supports Barbican. """ barbican_driver = driver.BarbicanDriver() @classmethod def create(cls, 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 secrets stored directly in Deckhand. Documents with ``metadata.storagePolicy`` == "encrypted" are stored in Barbican directly. Deckhand in turn stores the reference returned by Barbican in Deckhand. :param secret_doc: A Deckhand document with one of the following schemas: * ``deckhand/Certificate/v1`` * ``deckhand/CertificateKey/v1`` * ``deckhand/Passphrase/v1`` :returns: Dictionary representation of ``deckhand.db.sqlalchemy.models.DocumentSecret``. """ encryption_type = secret_doc['metadata']['storagePolicy'] secret_type = cls._get_secret_type(secret_doc['schema']) if encryption_type == ENCRYPTED: # Store secret_ref in database for `secret_doc`. kwargs = { 'name': secret_doc['metadata']['name'], 'secret_type': secret_type, 'payload': secret_doc['data'] } resp = cls.barbican_driver.create_secret(**kwargs) secret_ref = resp['secret_ref'] created_secret = secret_ref elif encryption_type == CLEARTEXT: created_secret = secret_doc['data'] return created_secret @classmethod def get(cls, secret_ref): secret = cls.barbican_driver.get_secret(secret_ref=secret_ref) payload = secret.payload return payload @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/Passphrase/v1`` => passphrase :param schema: The document's schema. :returns: The value corresponding to the mapping above. """ _schema = schema.split('/')[1].lower().strip() if _schema == 'certificatekey': return 'private' elif _schema == 'certificateauthority': return 'certificate' elif _schema == 'certificateauthoritykey': return 'private' elif _schema == 'publickey': return 'public' return _schema class SecretsSubstitution(object): """Class for document substitution logic for YAML files.""" @staticmethod def sanitize_potential_secrets(document): """Sanitize all secret data that may have been substituted into the document. Uses references in ``document.substitutions`` to determine which values to sanitize. Only meaningful to call this on post-rendered documents. :param DocumentDict document: Document to sanitize. """ to_sanitize = copy.deepcopy(document) safe_message = 'Sanitized to avoid exposing secret.' for sub in document.substitutions: replaced_data = utils.jsonpath_replace( to_sanitize['data'], safe_message, sub['dest']['path']) if replaced_data: to_sanitize['data'] = replaced_data return to_sanitize def __init__(self, substitution_sources=None, fail_on_missing_sub_src=True): """SecretSubstitution constructor. This class will automatically detect documents that require substitution; documents need not be filtered prior to being passed to the constructor. :param substitution_sources: List of documents that are potential sources for substitution. Should only include concrete documents. :type substitution_sources: List[dict] :param bool fail_on_missing_sub_src: Whether to fail on a missing substitution source. Default is True. """ self._substitution_sources = {} self._fail_on_missing_sub_src = fail_on_missing_sub_src for document in substitution_sources: if not isinstance(document, document_wrapper.DocumentDict): document = document_wrapper.DocumentDict(document) if document.schema and document.name: self._substitution_sources.setdefault( (document.schema, document.name), document) def _is_barbican_ref(self, src_secret): # TODO(fmontei): Make this more robust. return (isinstance(src_secret, six.string_types) and 'key-manager/v1/secrets' in src_secret) def substitute_all(self, documents): """Substitute all documents that have a `metadata.substitutions` field. Concrete (non-abstract) documents can be used as a source of substitution into other documents. This substitution is layer-independent, a document in the region layer could insert data from a document in the site layer. :param documents: List of documents that are candidates for substitution. :type documents: dict or List[dict] :returns: List of fully substituted documents. :rtype: Generator[:class:`DocumentDict`] :raises SubstitutionSourceNotFound: If a substitution source document is referenced by another document but wasn't found. :raises UnknownSubstitutionError: If an unknown error occurred during substitution. """ documents_to_substitute = [] if not isinstance(documents, list): documents = [documents] for document in documents: if not isinstance(document, document_wrapper.DocumentDict): document = document_wrapper.DocumentDict(document) # If the document has substitutions include it. if document.substitutions: documents_to_substitute.append(document) LOG.debug('Performing substitution on following documents: %s', ', '.join(['[%s] %s' % (d.schema, d.name) for d in documents_to_substitute])) for document in documents_to_substitute: LOG.debug('Checking for substitutions for document [%s] %s.', document.schema, document.name) for sub in document.substitutions: src_schema = sub['src']['schema'] src_name = sub['src']['name'] src_path = sub['src']['path'] if (src_schema, src_name) in self._substitution_sources: src_doc = self._substitution_sources[ (src_schema, src_name)] else: message = ('Could not find substitution source document ' '[%s] %s among the provided ' '`substitution_sources`.', src_schema, src_name) if self._fail_on_missing_sub_src: LOG.error(message) raise errors.SubstitutionSourceNotFound( src_schema=src_schema, src_name=src_name, document_schema=document.schema, document_name=document.name) else: LOG.warning(message) continue # If the data is a dictionary, retrieve the nested secret # via jsonpath_parse, else the secret is the primitive/string # stored in the data section itself. if isinstance(src_doc.get('data'), dict): src_secret = utils.jsonpath_parse(src_doc.get('data', {}), src_path) else: src_secret = src_doc.get('data') # Check if src_secret is Barbican secret reference. if self._is_barbican_ref(src_secret): LOG.debug('Resolving Barbican reference for %s.', src_secret) src_secret = SecretsManager.get(src_secret) dest_path = sub['dest']['path'] dest_pattern = sub['dest'].get('pattern', None) LOG.debug('Substituting from schema=%s name=%s src_path=%s ' 'into dest_path=%s, dest_pattern=%s', src_schema, src_name, src_path, dest_path, dest_pattern) try: substituted_data = utils.jsonpath_replace( document['data'], src_secret, dest_path, dest_pattern) if (isinstance(document['data'], dict) and isinstance(substituted_data, dict)): document['data'].update(substituted_data) elif substituted_data: document['data'] = substituted_data else: message = ( 'Failed to create JSON path "%s" in the ' 'destination document [%s] %s. No data was ' 'substituted.', dest_path, document.schema, document.name) LOG.error(message) raise errors.UnknownSubstitutionError(details=message) except errors.BarbicanException as e: LOG.error('Failed to resolve a Barbican reference.') raise errors.UnknownSubstitutionError( details=e.format_message()) except Exception as e: LOG.error('Unexpected exception occurred while attempting ' 'secret substitution. %s', six.text_type(e)) raise errors.UnknownSubstitutionError( details=six.text_type(e)) yield document def update_substitution_sources(self, schema, name, data): if (schema, name) not in self._substitution_sources: return substitution_src = self._substitution_sources[(schema, name)] if isinstance(data, dict) and isinstance(substitution_src.data, dict): substitution_src.data.update(data) else: substitution_src.data = data