[feat] DECKHAND-38: Secrets DB model and secrets manager.

This commit adds a DocumentSecret model to the DB for
storing secrets directly in Deckhand as well as references
to secrets stored in Barbican if the encryption type
for the secret is encrypted.

This commit also adds a new class called SecretsManager
for managing the lifecycle of secrets from a higher level.

This commit also adds Postgres compliance. So now all
the DB models should work with Postgres.

Also includes unit tests.

Change-Id: Id7c4be8de2e70735f42b1f6710139d553ab4bea2
This commit is contained in:
Felipe Monteiro 2017-08-13 21:53:55 -04:00
parent 5dfcc600ac
commit c9cdd7514c
21 changed files with 524 additions and 158 deletions

View File

@ -46,16 +46,13 @@ class BarbicanClientWrapper(object):
# TODO(fmontei): Deckhand's configuration file needs to be populated
# with correct Keysone authentication values as well as the Barbican
# endpoint URL automatically.
barbican_url = (CONF.barbican.api_endpoint
if CONF.barbican.api_endpoint
else 'http://127.0.0.1:9311')
barbican_url = CONF.barbican.api_endpoint
keystone_auth = dict(CONF.keystone_authtoken)
auth = v3.Password(**keystone_auth)
sess = session.Session(auth=auth)
try:
# TODO(fmontei): replace with ``barbican_url``.
cli = barbican.client.Client(endpoint=barbican_url,
session=sess)
# Cache the client so we don't have to reconstruct and
@ -63,10 +60,10 @@ class BarbicanClientWrapper(object):
if retry_on_conflict:
self._cached_client = cli
except barbican_exc.HTTPAuthError:
msg = _("Unable to authenticate Barbican client.")
# TODO(fmontei): Log the error.
raise errors.ApiError(msg)
except barbican_exc.HTTPAuthError as e:
LOG.exception(e.message)
raise errors.BarbicanException(message=e.message,
code=e.status_code)
return cli

View File

@ -12,7 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import barbicanclient
from oslo_log import log as logging
from deckhand.barbican import client_wrapper
from deckhand import errors
from deckhand import utils
LOG = logging.getLogger(__name__)
class BarbicanDriver(object):
@ -22,4 +29,20 @@ class BarbicanDriver(object):
def create_secret(self, **kwargs):
"""Create a secret."""
return self.barbicanclient.call("secrets.create", **kwargs)
secret = self.barbicanclient.call("secrets.create", **kwargs)
try:
secret.store()
except (barbicanclient.exceptions.HTTPAuthError,
barbicanclient.exceptions.HTTPClientError,
barbicanclient.exceptions.HTTPServerError) as e:
LOG.exception(e.message)
raise errors.BarbicanException(message=e.message,
code=e.status_code)
# NOTE(fmontei): The dictionary representation of the Secret object by
# default has keys that are not snake case -- so make them snake case.
resp = secret.to_dict()
for key in resp.keys():
resp[utils.to_snake_case(key)] = resp.pop(key)
return resp

View File

@ -25,7 +25,6 @@ from deckhand.control import middleware
from deckhand.control import revision_documents
from deckhand.control import revision_tags
from deckhand.control import revisions
from deckhand.control import secrets
from deckhand.db.sqlalchemy import api as db_api
CONF = cfg.CONF
@ -102,9 +101,7 @@ def start_api(state_manager=None):
revision_documents.RevisionDocumentsResource()),
('revisions/{revision_id}/tags', revision_tags.RevisionTagsResource()),
('revisions/{revision_id}/tags/{tag}',
revision_tags.RevisionTagsResource()),
# TODO(fmontei): remove in follow-up commit.
('secrets', secrets.SecretsResource())
revision_tags.RevisionTagsResource())
]
for path, res in v1_0_routes:

View File

@ -23,7 +23,9 @@ from deckhand.control import base as api_base
from deckhand.control.views import document as document_view
from deckhand.db.sqlalchemy import api as db_api
from deckhand.engine import document_validation
from deckhand.engine import secrets_manager
from deckhand import errors as deckhand_errors
from deckhand import types
LOG = logging.getLogger(__name__)
@ -32,10 +34,10 @@ class BucketsResource(api_base.BaseResource):
"""API resource for realizing CRUD operations for buckets."""
view_builder = document_view.ViewBuilder()
secrets_mgr = secrets_manager.SecretsManager()
def on_put(self, req, resp, bucket_name=None):
document_data = req.stream.read(req.content_length or 0)
try:
documents = list(yaml.safe_load_all(document_data))
except yaml.YAMLError as e:
@ -52,6 +54,12 @@ class BucketsResource(api_base.BaseResource):
except (deckhand_errors.InvalidDocumentFormat) as e:
raise falcon.HTTPBadRequest(description=e.format_message())
for document in documents:
if any([document['schema'].startswith(t)
for t in types.DOCUMENT_SECRET_TYPES]):
secret_data = self.secrets_mgr.create(document)
document['data'] = secret_data
try:
documents.extend(validation_policies)
created_documents = db_api.documents_create(bucket_name, documents)

View File

@ -13,17 +13,6 @@
# limitations under the License.
import functools
import string
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
def to_camel_case(s):
return (s[0].lower() + string.capwords(s, sep='_').replace('_', '')[1:]
if s else s)
class ViewBuilder(object):

View File

@ -1,55 +0,0 @@
# 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 falcon
from oslo_serialization import jsonutils as json
from deckhand.barbican import driver
from deckhand.control import base as api_base
class SecretsResource(api_base.BaseResource):
"""API resource for interacting with Barbican.
NOTE: Currently only supports Barbican.
"""
def __init__(self, **kwargs):
super(SecretsResource, self).__init__(**kwargs)
self.authorized_roles = ['user']
self.barbican_driver = driver.BarbicanDriver()
def on_post(self, req, resp):
"""Create a secret.
:param name: The name of the secret. Required.
:param type: The type of the secret. Optional.
For a list of types, please refer to the following API documentation:
https://docs.openstack.org/barbican/latest/api/reference/secret_types.html
"""
secret_name = req.params.get('name')
secret_type = req.params.get('type')
if not secret_name:
resp.status = falcon.HTTP_400
# Do not allow users to call Barbican with all permitted kwargs.
# Selectively include only what we allow.
kwargs = {'name': secret_name, 'secret_type': secret_type}
secret = self.barbican_driver.create_secret(**kwargs)
resp.body = json.dumps(secret)
resp.status = falcon.HTTP_200

View File

@ -14,6 +14,7 @@
from deckhand.control import common
from deckhand import types
from deckhand import utils
class ViewBuilder(common.ViewBuilder):
@ -32,7 +33,7 @@ class ViewBuilder(common.ViewBuilder):
rev_documents = revision.pop('documents')
for attr in ('id', 'created_at'):
body[common.to_camel_case(attr)] = revision[attr]
body[utils.to_camel_case(attr)] = revision[attr]
body['tags'].update([t['tag'] for t in revision['tags']])
body['buckets'].update(

View File

@ -80,14 +80,16 @@ def clear_db_env():
_FACADE = None
def setup_db():
models.register_models(get_engine())
def drop_db():
models.unregister_models(get_engine())
def setup_db():
# Ensure the DB doesn't exist before creation.
drop_db()
models.register_models(get_engine())
def documents_create(bucket_name, documents, session=None):
session = session or get_session()
documents_created = _documents_create(documents, session)
@ -138,14 +140,7 @@ def _documents_create(values_list, session=None):
for values in values_list:
values['_metadata'] = values.pop('metadata')
values['name'] = values['_metadata']['name']
# NOTE(fmontei): Database requires that the 'data' column be a dict, so
# coerce the secret into a dictionary if it already isn't one.
if values['schema'] in (types.CERTIFICATE_SCHEMA,
types.CERTIFICATE_KEY_SCHEMA,
types.PASSPHRASE_SCHEMA):
if not isinstance(values['data'], dict):
values['data'] = {'secret': values['data']}
values['is_secret'] = 'secret' in values['data']
try:
existing_document = document_get(

View File

@ -12,8 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import uuid
from oslo_db.sqlalchemy import models
from oslo_db.sqlalchemy import types as oslo_types
from oslo_utils import timeutils
@ -26,7 +24,6 @@ from sqlalchemy import Integer
from sqlalchemy.orm import relationship
from sqlalchemy import schema
from sqlalchemy import String
from sqlalchemy import Unicode
# Declarative base class which maintains a catalog of classes and tables
@ -90,12 +87,12 @@ class DeckhandBase(models.ModelBase, models.TimestampMixin):
return d
@staticmethod
def gen_unqiue_contraint(*fields):
constraint_name = 'ix_' + DeckhandBase.__name__.lower() + '_'
for field in fields:
constraint_name = constraint_name + '_%s' % field
return schema.UniqueConstraint(*fields, name=constraint_name)
def gen_unique_constraint(table_name, *fields):
constraint_name = 'ix_' + table_name.lower()
for field in fields:
constraint_name = constraint_name + '_%s' % field
return schema.UniqueConstraint(*fields, name=constraint_name)
class Bucket(BASE, DeckhandBase):
@ -108,8 +105,7 @@ class Bucket(BASE, DeckhandBase):
class Revision(BASE, DeckhandBase):
__tablename__ = 'revisions'
id = Column(String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
id = Column(Integer, primary_key=True)
documents = relationship("Document")
tags = relationship("RevisionTag")
@ -123,9 +119,10 @@ class Revision(BASE, DeckhandBase):
class RevisionTag(BASE, DeckhandBase):
UNIQUE_CONSTRAINTS = ('tag', 'revision_id')
__tablename__ = 'revision_tags'
__table_args__ = (DeckhandBase.gen_unqiue_contraint(*UNIQUE_CONSTRAINTS),)
__table_args__ = (
gen_unique_constraint(__tablename__, *UNIQUE_CONSTRAINTS),)
tag = Column(Unicode(80), primary_key=True, nullable=False)
tag = Column(String(64), primary_key=True, nullable=False)
data = Column(oslo_types.JsonEncodedDict(), nullable=True, default={})
revision_id = Column(
Integer, ForeignKey('revisions.id', ondelete='CASCADE'),
@ -135,17 +132,17 @@ class RevisionTag(BASE, DeckhandBase):
class Document(BASE, DeckhandBase):
UNIQUE_CONSTRAINTS = ('schema', 'name', 'revision_id')
__tablename__ = 'documents'
__table_args__ = (DeckhandBase.gen_unqiue_contraint(*UNIQUE_CONSTRAINTS),)
__table_args__ = (gen_unique_constraint(*UNIQUE_CONSTRAINTS),)
id = Column(String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
id = Column(Integer, primary_key=True)
name = Column(String(64), nullable=False)
schema = Column(String(64), nullable=False)
# NOTE: Do not define a maximum length for these JSON data below. However,
# this approach is not compatible with all database types.
# "metadata" is reserved, so use "_metadata" instead.
_metadata = Column(oslo_types.JsonEncodedDict(), nullable=False)
data = Column(oslo_types.JsonEncodedDict(), nullable=False)
data = Column(oslo_types.JsonEncodedDict(), nullable=True)
is_secret = Column(Boolean, nullable=False, default=False)
bucket_id = Column(Integer, ForeignKey('buckets.name', ondelete='CASCADE'),
nullable=False)

View File

@ -0,0 +1,86 @@
# 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 deckhand.barbican import driver
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': secret_ref}
elif encryption_type == CLEARTEXT:
created_secret = {'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

View File

@ -125,3 +125,9 @@ class RevisionTagBadFormat(DeckhandException):
msg_fmt = ("The requested tag data %(data)s must either be null or "
"dictionary.")
code = 400
class BarbicanException(DeckhandException):
def __init__(self, message, code):
super(BarbicanException, self).__init__(message=message, code=code)

View File

@ -240,6 +240,65 @@ class DocumentFactory(DeckhandFactory):
return rendered_template
class DocumentSecretFactory(DeckhandFactory):
"""Class for auto-generating document secrets templates for testing.
Returns formats that adhere to the following supported schemas:
* deckhand/Certificate/v1
* deckhand/CertificateKey/v1
* deckhand/Passphrase/v1
"""
DOCUMENT_SECRET_TEMPLATE = {
"data": {
},
"metadata": {
"schema": "metadata/Document/v1",
"name": "application-api",
"storagePolicy": ""
},
"schema": "deckhand/%s/v1"
}
def __init__(self):
"""Constructor for ``DocumentSecretFactory``.
Returns a template whose YAML representation is of the form::
---
schema: deckhand/Certificate/v1
metadata:
schema: metadata/Document/v1
name: application-api
storagePolicy: cleartext
data: |-
-----BEGIN CERTIFICATE-----
MIIDYDCCAkigAwIBAgIUKG41PW4VtiphzASAMY4/3hL8OtAwDQYJKoZIhvcNAQEL
...snip...
P3WT9CfFARnsw2nKjnglQcwKkKLYip0WY2wh3FE7nrQZP6xKNaSRlh6p2pCGwwwH
HkvVwA==
-----END CERTIFICATE-----
...
"""
def gen(self):
pass
def gen_test(self, schema, storage_policy, data=None):
if data is None:
data = test_utils.rand_password()
document_secret_template = copy.deepcopy(self.DOCUMENT_SECRET_TEMPLATE)
document_secret_template['metadata']['storagePolicy'] = storage_policy
document_secret_template['schema'] = (
document_secret_template['schema'] % schema)
document_secret_template['data'] = data
return document_secret_template
class ValidationPolicyFactory(DeckhandFactory):
"""Class for auto-generating validation policy templates for testing."""
@ -274,7 +333,6 @@ class ValidationPolicyFactory(DeckhandFactory):
- name: armada-deployability-validation
...
"""
pass
def gen(self, validation_type, status):
if validation_type not in types.DECKHAND_VALIDATION_TYPES:

View File

@ -1,18 +1,20 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
# Copyright 2016 OpenStack Foundation
# All 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
# 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
# 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.
# 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 random
import string
import uuid
@ -60,3 +62,25 @@ def rand_int(min, max):
:rtype: integer
"""
return random.randint(min, max)
def rand_password(length=15):
"""Generate a random password
:param int length: The length of password that you expect to set
(If it's smaller than 3, it's same as 3.)
:return: a random password. The format is
'<random upper letter>-<random number>-<random special character>
-<random ascii letters or digit characters or special symbols>'
(e.g. 'G2*ac8&lKFFgh%2')
:rtype: string
"""
upper = random.choice(string.ascii_uppercase)
ascii_char = string.ascii_letters
digits = string.digits
digit = random.choice(string.digits)
puncs = '~!@#%^&*_=+'
punc = random.choice(puncs)
seed = ascii_char + digits + puncs
pre = upper + digit + punc
password = pre + ''.join(random.choice(seed) for x in range(length - 3))
return password

View File

@ -13,6 +13,7 @@
# limitations under the License.
import fixtures
import mock
from oslo_config import cfg
from oslo_log import log as logging
import testtools
@ -40,6 +41,36 @@ class DeckhandTestCase(testtools.TestCase):
elif isinstance(collection, dict):
self.assertEqual(0, len(collection.keys()))
def patch(self, target, autospec=True, **kwargs):
"""Returns a started `mock.patch` object for the supplied target.
The caller may then call the returned patcher to create a mock object.
The caller does not need to call stop() on the returned
patcher object, as this method automatically adds a cleanup
to the test class to stop the patcher.
:param target: String module.class or module.object expression to patch
:param **kwargs: Passed as-is to `mock.patch`. See mock documentation
for details.
"""
p = mock.patch(target, autospec=autospec, **kwargs)
m = p.start()
self.addCleanup(p.stop)
return m
def patchobject(self, target, attribute, new=mock.DEFAULT, autospec=True):
"""Convenient wrapper around `mock.patch.object`
Returns a started mock that will be automatically stopped after the
test ran.
"""
p = mock.patch.object(target, attribute, new, autospec=autospec)
m = p.start()
self.addCleanup(p.stop)
return m
class DeckhandWithDBTestCase(DeckhandTestCase):

View File

@ -20,7 +20,6 @@ from deckhand.control import buckets
from deckhand.control import revision_documents
from deckhand.control import revision_tags
from deckhand.control import revisions
from deckhand.control import secrets
from deckhand.tests.unit import base as test_base
@ -28,8 +27,8 @@ class TestApi(test_base.DeckhandTestCase):
def setUp(self):
super(TestApi, self).setUp()
for resource in (buckets, revision_documents, revision_tags, revisions,
secrets):
for resource in (buckets, revision_documents, revision_tags,
revisions):
resource_name = resource.__name__.split('.')[-1]
resource_obj = mock.patch.object(
resource, '%sResource' % resource_name.title().replace(
@ -59,8 +58,7 @@ class TestApi(test_base.DeckhandTestCase):
mock.call('/api/v1.0/revisions/{revision_id}/tags',
self.revision_tags_resource()),
mock.call('/api/v1.0/revisions/{revision_id}/tags/{tag}',
self.revision_tags_resource()),
mock.call('/api/v1.0/secrets', self.secrets_resource())
self.revision_tags_resource())
])
mock_config.parse_args.assert_called_once_with()
mock_db_api.setup_db.assert_called_once_with()

View File

@ -12,52 +12,67 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from deckhand import factories
from deckhand.tests import test_utils
from deckhand.tests.unit.db import base
class TestDocuments(base.TestDbBase):
def setUp(self):
super(TestDocuments, self).setUp()
# Will create 3 documents: layering policy, plus a global and site
# document.
self.secrets_factory = factories.DocumentSecretFactory()
self.documents_factory = factories.DocumentFactory(2, [1, 1])
self.document_mapping = {
"_GLOBAL_DATA_1_": {"data": {"a": {"x": 1, "y": 2}}},
"_SITE_DATA_1_": {"data": {"a": {"x": 7, "z": 3}, "b": 4}},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]}
}
def test_create_and_show_bucket(self):
payload = base.DocumentFixture.get_minimal_fixture()
payload = self.documents_factory.gen_test(
self.document_mapping)
bucket_name = test_utils.rand_name('bucket')
documents = self.create_documents(bucket_name, payload)
self.assertIsInstance(documents, list)
self.assertEqual(1, len(documents))
self.assertEqual(3, len(documents))
retrieved_document = self.show_document(id=documents[0]['id'])
self.assertEqual(documents[0], retrieved_document)
for idx in range(len(documents)):
retrieved_document = self.show_document(id=documents[idx]['id'])
self.assertEqual(documents[idx], retrieved_document)
def test_create_and_get_multiple_document(self):
payload = self.documents_factory.gen_test(
self.document_mapping)
bucket_name = test_utils.rand_name('bucket')
created_documents = self.create_documents(bucket_name, payload)
self.assertIsInstance(created_documents, list)
self.assertEqual(3, len(created_documents))
def test_create_document_conflict(self):
payload = base.DocumentFixture.get_minimal_fixture()
payload = self.documents_factory.gen_test(
self.document_mapping)
bucket_name = test_utils.rand_name('bucket')
self.create_documents(bucket_name, payload)
documents = self.create_documents(bucket_name, payload)
unchanged_documents = self.create_documents(bucket_name, payload)
self.assertIsInstance(documents, list)
self.assertEmpty(documents)
def test_create_document_and_show_revision(self):
payload = base.DocumentFixture.get_minimal_fixture()
bucket_name = test_utils.rand_name('bucket')
documents = self.create_documents(bucket_name, payload)
self.assertIsInstance(documents, list)
self.assertEqual(1, len(documents))
for document in documents:
revision = self.show_revision(document['revision_id'])
self.validate_revision(revision)
self.assertEqual(document['revision_id'], revision['id'])
self.assertIsInstance(unchanged_documents, list)
self.assertEmpty(unchanged_documents)
def test_list_documents_by_revision_id(self):
payload = base.DocumentFixture.get_minimal_fixture()
payload = self.documents_factory.gen_test(
self.document_mapping)
bucket_name = test_utils.rand_name('bucket')
documents = self.create_documents(bucket_name, payload)
revision = self.show_revision(documents[0]['revision_id'])
self.assertEqual(1, len(revision['documents']))
self.assertEqual(3, len(revision['documents']))
self.assertEqual(documents[0], revision['documents'][0])
def test_list_multiple_documents_by_revision_id(self):
@ -74,20 +89,113 @@ class TestDocuments(base.TestDbBase):
self.assertEqual(document['revision_id'], revision['id'])
def test_list_documents_by_revision_id_and_filters(self):
payload = base.DocumentFixture.get_minimal_fixture()
payload = self.documents_factory.gen_test(
self.document_mapping)
bucket_name = test_utils.rand_name('bucket')
document = self.create_documents(bucket_name, payload)[0]
document = self.create_documents(bucket_name, payload)[1]
filters = {
'schema': document['schema'],
'metadata.name': document['metadata']['name'],
'metadata.layeringDefinition.abstract':
document['metadata']['layeringDefinition']['abstract'],
'metadata.layeringDefinition.layer':
document['metadata']['layeringDefinition']['layer'],
'metadata.label': document['metadata']['label']
document['metadata']['layeringDefinition']['layer']
}
documents = self.list_revision_documents(
document['revision_id'], **filters)
self.assertEqual(1, len(documents))
self.assertEqual(document, documents[0])
def test_create_multiple_documents_and_get_revision(self):
payload = self.documents_factory.gen_test(
self.document_mapping)
bucket_name = test_utils.rand_name('bucket')
created_documents = self.create_documents(bucket_name, payload)
self.assertIsInstance(created_documents, list)
self.assertEqual(3, len(created_documents))
# Validate that each document references the same revision.
revisions = set(d['revision_id'] for d in created_documents)
self.assertEqual(1, len(revisions))
# Validate that the revision is valid.
for document in created_documents:
revision = self.show_revision(document['revision_id'])
self.assertEqual(3, len(revision['documents']))
self.assertIn(document, revision['documents'])
self.assertEqual(document['revision_id'], revision['id'])
def test_get_documents_by_revision_id_and_filters(self):
payload = self.documents_factory.gen_test(
self.document_mapping)
bucket_name = test_utils.rand_name('bucket')
created_documents = self.create_documents(bucket_name, payload)
for document in created_documents[1:]:
filters = {
'schema': document['schema'],
'metadata.name': document['metadata']['name'],
'metadata.layeringDefinition.abstract':
document['metadata']['layeringDefinition']['abstract'],
'metadata.layeringDefinition.layer':
document['metadata']['layeringDefinition']['layer']
}
filtered_documents = self.list_revision_documents(
document['revision_id'], **filters)
self.assertEqual(1, len(filtered_documents))
self.assertEqual(document, filtered_documents[0])
def test_create_certificate(self):
rand_secret = {'secret': test_utils.rand_password()}
bucket_name = test_utils.rand_name('bucket')
for storage_policy in ('encrypted', 'cleartext'):
secret_doc_payload = self.secrets_factory.gen_test(
'Certificate', storage_policy, rand_secret)
created_documents = self.create_documents(
bucket_name, secret_doc_payload)
self.assertEqual(1, len(created_documents))
self.assertIn('Certificate', created_documents[0]['schema'])
self.assertEqual(storage_policy, created_documents[0][
'metadata']['storagePolicy'])
self.assertTrue(created_documents[0]['is_secret'])
self.assertEqual(rand_secret, created_documents[0]['data'])
def test_create_certificate_key(self):
rand_secret = {'secret': test_utils.rand_password()}
bucket_name = test_utils.rand_name('bucket')
for storage_policy in ('encrypted', 'cleartext'):
secret_doc_payload = self.secrets_factory.gen_test(
'CertificateKey', storage_policy, rand_secret)
created_documents = self.create_documents(
bucket_name, secret_doc_payload)
self.assertEqual(1, len(created_documents))
self.assertIn('CertificateKey', created_documents[0]['schema'])
self.assertEqual(storage_policy, created_documents[0][
'metadata']['storagePolicy'])
self.assertTrue(created_documents[0]['is_secret'])
self.assertEqual(rand_secret, created_documents[0]['data'])
def test_create_passphrase(self):
rand_secret = {'secret': test_utils.rand_password()}
bucket_name = test_utils.rand_name('bucket')
for storage_policy in ('encrypted', 'cleartext'):
secret_doc_payload = self.secrets_factory.gen_test(
'Passphrase', storage_policy, rand_secret)
created_documents = self.create_documents(
bucket_name, secret_doc_payload)
self.assertEqual(1, len(created_documents))
self.assertIn('Passphrase', created_documents[0]['schema'])
self.assertEqual(storage_policy, created_documents[0][
'metadata']['storagePolicy'])
self.assertTrue(created_documents[0]['is_secret'])
self.assertEqual(rand_secret, created_documents[0]['data'])

View File

@ -77,8 +77,7 @@ class TestRevisionTags(base.TestDbBase):
for idx, tag in enumerate(tags):
expected_tag_names = sorted(tags[idx + 1:])
result = db_api.revision_tag_delete(self.revision_id, tag)
self.assertIsNone(result)
db_api.revision_tag_delete(self.revision_id, tag)
retrieved_tags = db_api.revision_tag_get_all(self.revision_id)
retrieved_tag_names = [t['tag'] for t in retrieved_tags]

View File

@ -0,0 +1,73 @@
# 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 deckhand.engine import secrets_manager
from deckhand import factories
from deckhand.tests import test_utils
from deckhand.tests.unit.db import base
class TestSecretsManager(base.TestDbBase):
def setUp(self):
super(TestSecretsManager, self).setUp()
self.mock_barbican_driver = self.patchobject(
secrets_manager.SecretsManager, 'barbican_driver')
self.secret_ref = 'https://path/to/fake_secret'
self.mock_barbican_driver.create_secret.return_value = (
{'secret_href': self.secret_ref})
self.secrets_manager = secrets_manager.SecretsManager()
self.factory = factories.DocumentSecretFactory()
def _test_create_secret(self, encryption_type, secret_type):
secret_data = test_utils.rand_password()
secret_doc = self.factory.gen_test(
secret_type, encryption_type, secret_data)
created_secret = self.secrets_manager.create(secret_doc)
if encryption_type == 'cleartext':
self.assertIn('secret', created_secret)
self.assertEqual(secret_data, created_secret['secret'])
elif encryption_type == 'encrypted':
expected_kwargs = {
'name': secret_doc['metadata']['name'],
'secret_type': ('private' if secret_type == 'CertificateKey'
else secret_type.lower()),
'payload': secret_doc['data']
}
self.mock_barbican_driver.create_secret.assert_called_once_with(
**expected_kwargs)
self.assertIn('secret', created_secret)
self.assertEqual(self.secret_ref, created_secret['secret'])
def test_create_cleartext_certificate(self):
self._test_create_secret('cleartext', 'Certificate')
def test_create_cleartext_certificate_key(self):
self._test_create_secret('cleartext', 'CertificateKey')
def test_create_cleartext_passphrase(self):
self._test_create_secret('cleartext', 'Passphrase')
def test_create_encrypted_certificate(self):
self._test_create_secret('encrypted', 'Certificate')
def test_create_encrypted_certificate_key(self):
self._test_create_secret('encrypted', 'CertificateKey')
def test_create_encrypted_passphrase(self):
self._test_create_secret('encrypted', 'Passphrase')

View File

@ -44,6 +44,9 @@ class TestRevisionViews(base.TestDbBase):
for attr in ('bucket', 'revision'):
self.assertIn(attr, document_view[idx]['status'])
revision_ids = set([v['status']['revision'] for v in document_view])
self.assertEqual([1], list(revision_ids))
def test_create_single_document(self):
self._test_document_creation_view(1)

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# TODO(fmontei): Make all these version-less.
DOCUMENT_SCHEMA_TYPES = (
CERTIFICATE_SCHEMA,
CERTIFICATE_KEY_SCHEMA,
@ -19,13 +20,25 @@ DOCUMENT_SCHEMA_TYPES = (
PASSPHRASE_SCHEMA,
VALIDATION_POLICY_SCHEMA,
) = (
'deckhand/Certificate/v1',
'deckhand/CertificateKey/v1',
'deckhand/Certificate',
'deckhand/CertificateKey',
'deckhand/LayeringPolicy/v1',
'deckhand/Passphrase/v1',
'deckhand/Passphrase',
'deckhand/ValidationPolicy/v1',
)
DOCUMENT_SECRET_TYPES = (
CERTIFICATE_KEY_SCHEMA,
CERTIFICATE_SCHEMA,
PASSPHRASE_SCHEMA
) = (
'deckhand/Certificate',
'deckhand/CertificateKey',
'deckhand/Passphrase'
)
DECKHAND_VALIDATION_TYPES = (
DECKHAND_SCHEMA_VALIDATION,
) = (

View File

@ -12,6 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import string
def to_camel_case(s):
"""Convert string to camel case."""
return (s[0].lower() + string.capwords(s, sep='_')
.replace('_', '')[1:] if s else s)
def to_snake_case(name):
"""Convert string to snake case."""
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def multi_getattr(multi_key, dict_data):
"""Iteratively check for nested attributes in the YAML data.