deckhand/deckhand/engine/secrets_manager.py

168 lines
6.4 KiB
Python

# 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.
from oslo_log import log as logging
from deckhand.barbican import driver
from deckhand.db.sqlalchemy import api as db_api
from deckhand.engine import document as document_wrapper
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()
def create(self, 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 = self._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 = self.barbican_driver.create_secret(**kwargs)
secret_ref = resp['secret_href']
created_secret = secret_ref
elif encryption_type == CLEARTEXT:
created_secret = secret_doc['data']
return created_secret
def _get_secret_type(self, 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'
return _schema
class SecretsSubstitution(object):
"""Class for document substitution logic for YAML files."""
def __init__(self, documents):
"""SecretSubstitution constructor.
:param documents: List of documents that are candidates for secret
substitution. This class will automatically detect documents that
require substitution; documents need not be filtered prior to being
passed to the constructor.
"""
if not isinstance(documents, (list, tuple)):
documents = [documents]
self.docs_to_sub = []
for document in documents:
if not isinstance(document, document_wrapper.Document):
document_obj = document_wrapper.Document(document)
if document_obj.get_substitutions():
self.docs_to_sub.append(document_obj)
def substitute_all(self):
"""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.
:returns: List of fully substituted documents.
"""
LOG.debug('Substituting secrets for documents: %s',
self.docs_to_sub)
substituted_docs = []
for doc in self.docs_to_sub:
LOG.debug(
'Checking for substitutions in schema=%s, metadata.name=%s',
doc.get_name(), doc.get_schema())
for sub in doc.get_substitutions():
src_schema = sub['src']['schema']
src_name = sub['src']['name']
src_path = sub['src']['path']
# TODO(fmontei): Use SecretsManager for this logic. Need to
# check Barbican for the secret if it has been encrypted.
src_doc = db_api.document_get(
schema=src_schema, name=src_name, is_secret=True,
**{'metadata.layeringDefinition.abstract': False})
# 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['data']
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)
substituted_data = utils.jsonpath_replace(
doc['data'], src_secret, dest_path, dest_pattern)
doc['data'].update(substituted_data)
substituted_docs.append(doc.to_dict())
return substituted_docs