Ejemplo n.º 1
0
    def create(cls, secret_doc):
        """Securely store secrets contained in ``secret_doc``.

        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 its own DB.

        :param secret_doc: A Deckhand document with a schema that belongs to
            ``types.DOCUMENT_SECRET_TYPES``.

        :returns: Unecrypted data section from ``secret_doc`` if the document's
            ``storagePolicy`` is "cleartext" or a Barbican secret reference
            if the ``storagePolicy`` is "encrypted'.
        """
        # TODO(fmontei): Look into POSTing Deckhand metadata into Barbican's
        # Secrets Metadata API to make it easier to track stale secrets from
        # prior revisions that need to be deleted.
        if not isinstance(secret_doc, dd):
            secret_doc = dd(secret_doc)

        if secret_doc.is_encrypted:
            payload = cls.barbican_driver.create_secret(secret_doc)
        else:
            payload = secret_doc.data
        return payload
Ejemplo n.º 2
0
    def __init__(self,
                 substitution_sources=None,
                 fail_on_missing_sub_src=True,
                 encryption_sources=None,
                 cleartext_secrets=False):
        """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. Or dict of documents keyed on tuple of
            (schema, metadata.name). Should only include concrete documents.
        :type substitution_sources: List[dict] or dict
        :param bool fail_on_missing_sub_src: Whether to fail on a missing
            substitution source. Default is True.
        :param encryption_sources: A dictionary that maps the reference
            contained in the destination document's data section to the
            actual unecrypted data. If encrypting data with Barbican, the
            reference will be a Barbican secret reference.
        :type encryption_sources: dict
        :param cleartext_secrets: Whether to show unencrypted data as
            cleartext.
        :type cleartext_secrets: bool
        """

        # This maps a 2-tuple of (schema, name) to a document from which the
        # document.meta can be extracted which is a 3-tuple of (schema, layer,
        # name). This is necessary since the substitution format in the
        # document itself only provides a 2-tuple of (schema, name).
        self._substitution_sources = {}
        self._encryption_sources = encryption_sources or {}
        self._fail_on_missing_sub_src = fail_on_missing_sub_src
        self._cleartext_secrets = cleartext_secrets

        if isinstance(substitution_sources, dict):
            self._substitution_sources = substitution_sources
        else:
            self._substitution_sources = dict()
            for document in substitution_sources:
                if not isinstance(document, dd):
                    document = dd(document)
                if document.schema and document.name:
                    self._substitution_sources.setdefault(
                        (document.schema, document.name), document)
Ejemplo n.º 3
0
    def delete(cls, document):
        """Delete a secret from Barbican.

        :param dict document: Document with secret_ref in ``data`` section with
            format: "https://{barbican_host}/v1/secrets/{secret_uuid}"
        :returns: None

        """
        if not isinstance(document, dd):
            document = dd(document)

        secret_ref = document.data
        if document.is_encrypted and document.has_barbican_ref:
            LOG.debug('Deleting Barbican secret: %s.', secret_ref)
            cls.barbican_driver.delete_secret(secret_ref=secret_ref)
        else:
            LOG.warning('%s is not a valid Barbican secret. Could not delete.',
                        secret_ref)
Ejemplo n.º 4
0
    def _calc_replacements_and_substitutions(
            self, substitution_sources):

        # Used to track document names and schemas for documents that are not
        # replacement documents
        non_replacement_documents = set()

        for document in self._documents_by_index.values():
            parent_meta = self._parents.get(document.meta)
            parent = self._documents_by_index.get(parent_meta)

            if document.is_replacement:
                replacement.check_document_with_replacement_field_has_parent(
                    parent_meta, parent, document)
                replacement.check_replacement_and_parent_same_schema_and_name(
                    parent, document)
                parent.replaced_by = document
            else:
                # Handles case where parent and child have replacement: false
                # as in this case both documents should not be replacement
                # documents, requiring them to have different schema/name pair.
                replacement.check_child_and_parent_different_metadata_name(
                    parent, document)
                replacement.check_replacement_is_false_uniqueness(
                    document, non_replacement_documents)

        # Since a substitution source only provides the document's
        # `metadata.name` and `schema`, their tuple acts as the dictionary key.
        # If a substitution source has a replacement, the replacement is used
        # instead.
        substitution_source_map = {}

        for src in substitution_sources:
            src_ref = dd(src)
            if src_ref.meta in self._documents_by_index:
                src_ref = self._documents_by_index[src_ref.meta]
                if src_ref.has_replacement:
                    replacement.check_only_one_level_of_replacement(src_ref)
                    src_ref = src_ref.replaced_by
            substitution_source_map[(src_ref.schema, src_ref.name)] = src_ref

        return substitution_source_map
Ejemplo n.º 5
0
    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, dd):
                document = dd(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] %s' % d.meta for d in documents_to_substitute]))

        for document in documents_to_substitute:
            redact_dest = False
            LOG.debug('Checking for substitutions for document [%s, %s] %s.',
                      *document.meta)
            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 src_doc.is_encrypted:
                    redact_dest = True

                # 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')

                self._check_src_secret_is_not_none(src_secret, src_path,
                                                   src_doc, document)

                # If the document has storagePolicy == encrypted then resolve
                # the Barbican reference into the actual secret.
                if src_doc.is_encrypted and src_doc.has_barbican_ref:
                    src_secret = self.get_unencrypted_data(
                        src_secret, src_doc, document)

                if not isinstance(sub['dest'], list):
                    dest_array = [sub['dest']]
                    dest_is_list = False
                else:
                    dest_array = sub['dest']
                    dest_is_list = True

                for i, each_dest_path in enumerate(dest_array):
                    dest_path = each_dest_path['path']
                    dest_pattern = each_dest_path.get('pattern', None)
                    dest_recurse = each_dest_path.get('recurse', {})

                    # If the source document is encrypted and cleartext_secrets
                    # is False, then redact the substitution metadata in the
                    # destination document to prevent reverse-engineering of
                    # where the sensitive data came from.
                    if src_doc.is_encrypted and not self._cleartext_secrets:
                        sub['src']['path'] = dd.redact(src_path)
                        if dest_is_list:
                            sub['dest'][i]['path'] = dd.redact(dest_path)
                        else:
                            sub['dest']['path'] = dd.redact(dest_path)

                    LOG.debug(
                        'Substituting from schema=%s layer=%s name=%s '
                        'src_path=%s into dest_path=%s, dest_pattern=%s',
                        src_schema, src_doc.layer, src_name, src_path,
                        dest_path, dest_pattern)

                    document = self._substitute_one(document,
                                                    src_doc=src_doc,
                                                    src_secret=src_secret,
                                                    dest_path=dest_path,
                                                    dest_pattern=dest_pattern,
                                                    dest_recurse=dest_recurse)

            # If we just substituted from an encrypted document
            # into a cleartext document, we need to redact the
            # dest document as well so the secret stays hidden
            if (not document.is_encrypted and redact_dest
                    and not self._cleartext_secrets):
                document.storage_policy = 'encrypted'

        yield document
Ejemplo n.º 6
0
    def __init__(self,
                 documents,
                 validate=True,
                 fail_on_missing_sub_src=True,
                 encryption_sources=None,
                 cleartext_secrets=False):
        """Contructor for ``DocumentLayering``.

        :param layering_policy: The document with schema
            ``deckhand/LayeringPolicy`` needed for layering.
        :param documents: List of all other documents to be layered together
            in accordance with the ``layerOrder`` defined by the
            LayeringPolicy document.
        :type documents: List[dict]
        :param validate: Whether to pre-validate documents using built-in
            schema validation. Skips over externally registered ``DataSchema``
            documents to avoid false positives. Default is True.
        :type validate: bool
        :param fail_on_missing_sub_src: Whether to fail on a missing
            substitution source. Default is True.
        :type fail_on_missing_sub_src: bool
        :param encryption_sources: A dictionary that maps the reference
            contained in the destination document's data section to the
            actual unecrypted data. If encrypting data with Barbican, the
            reference will be a Barbican secret reference.
        :type encryption_sources: dict
        :param cleartext_secrets: Whether to show unencrypted data as
            cleartext.
        :type cleartext_secrets: bool

        :raises LayeringPolicyNotFound: If no LayeringPolicy was found among
            list of ``documents``.
        :raises InvalidDocumentLayer: If document layer not found in layerOrder
            for provided LayeringPolicy.
        :raises InvalidDocumentParent: If child references parent but they
            don't have the same schema or their layers are incompatible.
        :raises IndeterminateDocumentParent: If more than one parent document
            was found for a document.
        """
        self._documents_by_layer = {}
        self._documents_by_labels = {}
        self._layering_policy = None
        self._sorted_documents = {}
        self._documents_by_index = {}

        # TODO(felipemonteiro): Add a hook for post-validation too.
        if validate:
            self._pre_validate_documents(documents)

        layering_policies = list(
            filter(lambda x: x.get('schema').startswith(
                   types.LAYERING_POLICY_SCHEMA), documents))
        if layering_policies:
            self._layering_policy = dd(layering_policies[0])
            if len(layering_policies) > 1:
                LOG.warning('More than one layering policy document was '
                            'passed in. Using the first one found: [%s] %s.',
                            self._layering_policy.schema,
                            self._layering_policy.name)

        if self._layering_policy is None:
            error_msg = (
                'No layering policy found in the system so could not render '
                'documents.')
            LOG.error(error_msg)
            raise errors.LayeringPolicyNotFound()

        for document in documents:
            document = dd(document)

            self._documents_by_index.setdefault(document.meta, document)

            if document.layer:
                if document.layer not in self._layering_policy.layer_order:
                    LOG.error('Document layer %s for document [%s] %s not '
                              'in layerOrder: %s.', document.layer,
                              document.schema, document.name,
                              self._layering_policy.layer_order)
                    raise errors.InvalidDocumentLayer(
                        document_layer=document.layer,
                        document_schema=document.schema,
                        document_name=document.name,
                        layer_order=', '.join(
                            self._layering_policy.layer_order),
                        layering_policy_name=self._layering_policy.name)
                self._documents_by_layer.setdefault(document.layer, [])
                self._documents_by_layer[document.layer].append(document)
            if document.parent_selector:
                for label_key, label_val in document.parent_selector.items():
                    self._documents_by_labels.setdefault(
                        (label_key, label_val), [])
                    self._documents_by_labels[
                        (label_key, label_val)].append(document)

        self._layer_order = self._get_layering_order(self._layering_policy)
        self._calc_all_document_children()

        substitution_sources = self._calc_replacements_and_substitutions(
            [
                d for d in self._documents_by_index.values()
                if not d.is_abstract
            ])

        self.secrets_substitution = secrets_manager.SecretsSubstitution(
            substitution_sources,
            encryption_sources=encryption_sources,
            fail_on_missing_sub_src=fail_on_missing_sub_src,
            cleartext_secrets=cleartext_secrets)

        self._sorted_documents = self._topologically_sort_documents(
            substitution_sources)

        del self._documents_by_layer
        del self._documents_by_labels
Ejemplo n.º 7
0
    def _calc_replacements_and_substitutions(self, substitution_sources):
        def _check_document_with_replacement_field_has_parent(
                parent_meta, parent, document):
            if not parent_meta or not parent:
                error_message = (
                    'Document replacement requires that the document with '
                    '`replacement: true` have a parent.')
                raise errors.InvalidDocumentReplacement(schema=document.schema,
                                                        name=document.name,
                                                        layer=document.layer,
                                                        reason=error_message)

        def _check_replacement_and_parent_same_schema_and_name(
                parent, document):
            # This checks that a document can only be a replacement for
            # another document with the same `metadata.name` and `schema`.
            if not (document.schema == parent.schema
                    and document.name == parent.name):
                error_message = (
                    'Document replacement requires that both documents '
                    'have the same `schema` and `metadata.name`.')
                raise errors.InvalidDocumentReplacement(schema=document.schema,
                                                        name=document.name,
                                                        layer=document.layer,
                                                        reason=error_message)

        def _check_non_replacement_and_parent_different_schema_and_name(
                parent, document):
            if (parent and document.schema == parent.schema
                    and document.name == parent.name):
                error_message = (
                    'Non-replacement documents cannot have the same `schema` '
                    'and `metadata.name` as their parent. Either add '
                    '`replacement: true` to the document or give the document '
                    'a different name.')
                raise errors.InvalidDocumentReplacement(schema=document.schema,
                                                        name=document.name,
                                                        layer=document.layer,
                                                        reason=error_message)

        def _check_replacement_not_itself_replaced_by_another(src_ref):
            # If the document has a replacement, use the replacement as the
            # substitution source instead.
            if src_ref.is_replacement:
                error_message = ('A replacement document cannot itself'
                                 ' be replaced by another document.')
                raise errors.InvalidDocumentReplacement(schema=src_ref.schema,
                                                        name=src_ref.name,
                                                        layer=src_ref.layer,
                                                        reason=error_message)

        for document in self._documents_by_index.values():
            parent_meta = self._parents.get(document.meta)
            parent = self._documents_by_index.get(parent_meta)

            if document.is_replacement:
                _check_document_with_replacement_field_has_parent(
                    parent_meta, parent, document)
                _check_replacement_and_parent_same_schema_and_name(
                    parent, document)
                parent.replaced_by = document
            else:
                _check_non_replacement_and_parent_different_schema_and_name(
                    parent, document)

        # Since a substitution source only provides the document's
        # `metadata.name` and `schema`, their tuple acts as the dictionary key.
        # If a substitution source has a replacement, the replacement is used
        # instead.
        substitution_source_map = {}

        for src in substitution_sources:
            src_ref = dd(src)
            if src_ref.meta in self._documents_by_index:
                src_ref = self._documents_by_index[src_ref.meta]
                if src_ref.has_replacement:
                    _check_replacement_not_itself_replaced_by_another(src_ref)
                    src_ref = src_ref.replaced_by
            substitution_source_map[(src_ref.schema, src_ref.name)] = src_ref

        return substitution_source_map