From db40a53c00fd9536a444d80bf8656bae73a94b00 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Fri, 10 May 2013 03:08:06 -0500 Subject: [PATCH] Added crypto plugin encryption to Secrets post. --- barbican/api/app.py | 11 +++++- barbican/api/resources.py | 54 +++++++++++++++++++-------- barbican/common/resources.py | 29 ++++++++------- barbican/crypto/extension_manager.py | 48 ++++++++++++++++++++++++ barbican/crypto/plugin.py | 55 ++++++++++++++++++++++++++++ barbican/model/models.py | 14 +++++++ barbican/tests/api/resources_test.py | 33 ++++++++++------- barbican/tests/crypto/__init__.py | 0 barbican/tests/crypto/test_plugin.py | 35 ++++++++++++++++++ barbican/tests/model/__init__.py | 0 barbican/tests/model/test_models.py | 36 ++++++++++++++++++ setup.py | 9 ++++- tools/pip-requires | 1 + 13 files changed, 280 insertions(+), 45 deletions(-) create mode 100644 barbican/crypto/extension_manager.py create mode 100644 barbican/crypto/plugin.py create mode 100644 barbican/tests/crypto/__init__.py create mode 100644 barbican/tests/crypto/test_plugin.py create mode 100644 barbican/tests/model/__init__.py create mode 100644 barbican/tests/model/test_models.py diff --git a/barbican/api/app.py b/barbican/api/app.py index 96cf43782..b92182c64 100644 --- a/barbican/api/app.py +++ b/barbican/api/app.py @@ -22,8 +22,9 @@ import falcon from barbican.api.resources import (VersionResource, SecretsResource, SecretResource, OrdersResource, OrderResource) -from barbican.openstack.common import log from barbican.common import config +from barbican.crypto.extension_manager import CryptoExtensionManager +from barbican.openstack.common import log def create_main_app(global_config, **local_conf): @@ -32,9 +33,15 @@ def create_main_app(global_config, **local_conf): config.parse_args() log.setup('barbican') + # Crypto Plugin Manager + crypto_mgr = CryptoExtensionManager( + 'barbican.crypto.extension', + ['simple_crypto'] # TODO: grab this list from cfg + ) + # Resources VERSIONS = VersionResource() - SECRETS = SecretsResource() + SECRETS = SecretsResource(crypto_mgr) SECRET = SecretResource() ORDERS = OrdersResource() ORDER = OrderResource() diff --git a/barbican/api/resources.py b/barbican/api/resources.py index f64595346..47e962704 100644 --- a/barbican/api/resources.py +++ b/barbican/api/resources.py @@ -20,22 +20,27 @@ API-facing resource controllers. import falcon -from barbican.version import __version__ from barbican.api import ApiResource, load_body, abort +from barbican.common.resources import (create_secret, + create_encrypted_datum, + get_or_create_tenant) +from barbican.common import utils +from barbican.crypto.extension_manager import ( + CryptoMimeTypeNotSupportedException +) +from barbican.crypto.fields import (encrypt, decrypt, + generate_response_for, + augment_fields_with_content_types) from barbican.model.models import (Tenant, Secret, TenantSecret, EncryptedDatum, Order, States) from barbican.model.repositories import (TenantRepo, SecretRepo, OrderRepo, TenantSecretRepo, EncryptedDatumRepo) -from barbican.common.resources import (create_secret, - create_encrypted_datum) -from barbican.crypto.fields import (encrypt, decrypt, - generate_response_for, - augment_fields_with_content_types) from barbican.openstack.common.gettextutils import _ from barbican.openstack.common import jsonutils as json from barbican.queue import get_queue_api -from barbican.common import utils +from barbican.version import __version__ + LOG = utils.getLogger(__name__) @@ -125,26 +130,43 @@ class VersionResource(ApiResource): class SecretsResource(ApiResource): """Handles Secret creation requests""" - def __init__(self, tenant_repo=None, secret_repo=None, + def __init__(self, crypto_manager, + tenant_repo=None, secret_repo=None, tenant_secret_repo=None, datum_repo=None): LOG.debug('Creating SecretsResource') self.tenant_repo = tenant_repo or TenantRepo() self.secret_repo = secret_repo or SecretRepo() self.tenant_secret_repo = tenant_secret_repo or TenantSecretRepo() self.datum_repo = datum_repo or EncryptedDatumRepo() + self.crypto_manager = crypto_manager def on_post(self, req, resp, tenant_id): - LOG.debug('Start on_post for tenant-ID {0}:'.format(tenant_id)) - body = load_body(req) + data = load_body(req) - # Create Secret - new_secret = create_secret(body, tenant_id, - self.tenant_repo, - self.secret_repo, - self.tenant_secret_repo, - self.datum_repo) + tenant = get_or_create_tenant(tenant_id, self.tenant_repo) + + new_secret = Secret(data) + self.secret_repo.create_from(new_secret) + + # Create Tenant/Secret entity. + new_assoc = TenantSecret() + new_assoc.tenant_id = tenant.id + new_assoc.secret_id = new_secret.id + new_assoc.role = "admin" + new_assoc.status = States.ACTIVE + self.tenant_secret_repo.create_from(new_assoc) + + if 'plain_text' in data: + LOG.debug('Encrypting plain_text secret') + try: + new_datum = self.crypto_manager.encrypt(data['plain_text'], + new_secret, tenant) + self.datum_repo.create_from(new_datum) + except CryptoMimeTypeNotSupportedException as e: + # TODO: return error + LOG.error(e.message) resp.status = falcon.HTTP_202 resp.set_header('Location', '/{0}/secrets/{1}'.format(tenant_id, diff --git a/barbican/common/resources.py b/barbican/common/resources.py index af6fe615e..797494c87 100644 --- a/barbican/common/resources.py +++ b/barbican/common/resources.py @@ -32,11 +32,9 @@ from barbican.common import utils LOG = utils.getLogger(__name__) -def create_secret(data, tenant_id, tenant_repo, - secret_repo, tenant_secret_repo, - datum_repo, ok_to_generate=False): - # Create a Secret and a single EncryptedDatum for that Secret. Create - # a Tenant if one doesn't already exist. +def get_or_create_tenant(tenant_id, tenant_repo): + """Returns tenant with matching tenant_id. Creates it if it does + not exist.""" tenant = tenant_repo.get(tenant_id, suppress_exception=True) if not tenant: LOG.debug('Creating tenant for {0}'.format(tenant_id)) @@ -44,6 +42,15 @@ def create_secret(data, tenant_id, tenant_repo, tenant.keystone_id = tenant_id tenant.status = States.ACTIVE tenant_repo.create_from(tenant) + return tenant + + +def create_secret(data, tenant_id, tenant_repo, + secret_repo, tenant_secret_repo, + datum_repo, ok_to_generate=False): + # Create a Secret and a single EncryptedDatum for that Secret. Create + # a Tenant if one doesn't already exist. + tenant = get_or_create_tenant(tenant_id, tenant_repo) # TODO: What if any criteria to restrict new secrets vs existing ones? # Verify secret doesn't already exist. @@ -63,14 +70,10 @@ def create_secret(data, tenant_id, tenant_repo, LOG.debug('Encrypted secret is {0}'.format(secret_value)) # Create Secret entity. - new_secret = Secret() - new_secret.name = data['name'] - new_secret.expiration = data.get('expiration', None) - new_secret.algorithm = data.get('algorithm', None) - new_secret.bit_length = data.get('bit_length', None) - new_secret.cypher_type = data.get('cypher_type', None) - new_secret.mime_type = data['mime_type'] - new_secret.status = States.ACTIVE + new_secret = Secret(data) + + # encrypt(new_secret) + secret_repo.create_from(new_secret) # Create Tenant/Secret entity. diff --git a/barbican/crypto/extension_manager.py b/barbican/crypto/extension_manager.py new file mode 100644 index 000000000..3ac1b36a6 --- /dev/null +++ b/barbican/crypto/extension_manager.py @@ -0,0 +1,48 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 stevedore import named + +from barbican.common.exception import BarbicanException +from barbican.openstack.common.gettextutils import _ + + +class CryptoMimeTypeNotSupportedException(BarbicanException): + """Raised when support for requested mime type is + not available in any active plugin.""" + def __init__(self, mime_type): + super(CryptoMimeTypeNotSupportedException, self).__init__( + _('Crypto Mime Type not supported {0}'.format(mime_type)) + ) + + +class CryptoExtensionManager(named.NamedExtensionManager): + def __init__(self, namespace, names, + invoke_on_load=True, invoke_args=(), invoke_kwargs={}): + super(CryptoExtensionManager, self).__init__( + namespace, + names, + invoke_on_load=invoke_on_load, + invoke_args=invoke_args, + invoke_kwds=invoke_kwargs + ) + + def encrypt(self, unencrypted, secret, tenant): + """Delegates encryption to active plugins.""" + for ext in self.extensions: + if ext.obj.supports(secret.mime_type): + return ext.obj.encrypt(unencrypted, secret, tenant) + else: + raise CryptoMimeTypeNotSupportedException(secret.mime_type) \ No newline at end of file diff --git a/barbican/crypto/plugin.py b/barbican/crypto/plugin.py new file mode 100644 index 000000000..bd1f7f95a --- /dev/null +++ b/barbican/crypto/plugin.py @@ -0,0 +1,55 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 abc + +from barbican.model.models import EncryptedDatum + + +class CryptoPluginBase(object): + """Base class for Crypto plugins.""" + + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def encrypt(self, unencrypted, secret, tenant): + """Encrypt unencrypted data in the context of the provided + secret and tenant""" + + @abc.abstractmethod + def create(self, secret_type): + """Create a new key.""" + + @abc.abstractmethod + def supports(self, secret_type): + """Whether the plugin supports the specified secret type.""" + + +class SimpleCryptoPlugin(CryptoPluginBase): + """Insecure implementation of the crypto plugin.""" + + def __init__(self): + self.supported_types = ['application/aes-256-cbc'] + + def encrypt(self, unencrypted, secret, tenant): + encrypted_datum = EncryptedDatum() + encrypted_datum.cypher_text = 'encrypted-data' + return encrypted_datum + + def create(self, secret_type): + return "insecure_key" + + def supports(self, secret_type): + return secret_type in self.supported_types diff --git a/barbican/model/models.py b/barbican/model/models.py index 159e65820..8db7679bc 100644 --- a/barbican/model/models.py +++ b/barbican/model/models.py @@ -192,6 +192,20 @@ class Secret(BASE, ModelBase): # datum attributes here. encrypted_data = relationship("EncryptedDatum", lazy='joined') + def __init__(self, parsed_request): + """Creates secret from a dict.""" + super(ModelBase, self).__init__() + + self.name = parsed_request['name'] + self.mime_type = parsed_request['mime_type'] + + self.expiration = parsed_request.get('expiration', None) + self.algorithm = parsed_request.get('algorithm', None) + self.bit_length = parsed_request.get('bit_length', None) + self.cypher_type = parsed_request.get('cypher_type', None) + + self.status = States.ACTIVE + def _do_extra_dict_fields(self): """Sub-class hook method: return dict of fields.""" return {'name': self.name, diff --git a/barbican/tests/api/resources_test.py b/barbican/tests/api/resources_test.py index d9e5573fc..9bcc14311 100644 --- a/barbican/tests/api/resources_test.py +++ b/barbican/tests/api/resources_test.py @@ -22,6 +22,7 @@ from datetime import datetime from barbican.api.resources import (VersionResource, SecretsResource, SecretResource, OrdersResource, OrderResource) +from barbican.crypto.extension_manager import CryptoExtensionManager from barbican.model.models import (Secret, Tenant, TenantSecret, Order, EncryptedDatum) from barbican.crypto.fields import decrypt_value, encrypt_value @@ -105,7 +106,13 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase): self.req.stream = self.stream self.resp = MagicMock() - self.resource = SecretsResource(self.tenant_repo, self.secret_repo, + self.crypto_mgr = CryptoExtensionManager( + 'barbican.test.crypto.extension', + ['test_crypto'] + ) + self.resource = SecretsResource(self.crypto_mgr, + self.tenant_repo, + self.secret_repo, self.tenant_secret_repo, self.datum_repo) @@ -129,10 +136,10 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase): args, kwargs = self.datum_repo.create_from.call_args datum = args[0] - assert isinstance(datum, EncryptedDatum) - assert encrypt_value(self.plain_text) == datum.cypher_text - assert self.mime_type == datum.mime_type - assert datum.kek_metadata is not None + self.assertIsInstance(datum, EncryptedDatum) + self.assertEqual('cypher_text', datum.cypher_text) + self.assertEqual(self.mime_type, datum.mime_type) + self.assertIsNotNone(datum.kek_metadata) def test_should_add_new_secret_tenant_not_exist(self): self.tenant_repo.get.return_value = None @@ -153,7 +160,7 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase): args, kwargs = self.datum_repo.create_from.call_args datum = args[0] assert isinstance(datum, EncryptedDatum) - assert encrypt_value(self.plain_text) == datum.cypher_text + self.assertEqual('cypher_text', datum.cypher_text) assert self.mime_type == datum.mime_type assert datum.kek_metadata is not None @@ -197,13 +204,13 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(unittest.TestCase): self.datum.cypher_text = "cypher_text" self.datum.kek_metadata = "kekedata" - self.secret = Secret() - self.secret.id = secret_id - self.secret.name = self.name - self.secret.mime_type = self.mime_type - self.secret.algorithm = self.secret_algorithm - self.secret.bit_length = self.secret_bit_length - self.secret.cypher_type = self.secret_cypher_type + self.parsed_data = {'id': secret_id, + 'name': self.name, + 'mime_type': self.mime_type, + 'algorithm': self.secret_algorithm, + 'bit_length': self.secret_bit_length, + 'cypher_type': self.secret_cypher_type} + self.secret = Secret(self.parsed_data) self.secret.encrypted_data = [self.datum] self.secret_repo = MagicMock() diff --git a/barbican/tests/crypto/__init__.py b/barbican/tests/crypto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/barbican/tests/crypto/test_plugin.py b/barbican/tests/crypto/test_plugin.py new file mode 100644 index 000000000..02ae0b593 --- /dev/null +++ b/barbican/tests/crypto/test_plugin.py @@ -0,0 +1,35 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 barbican.crypto.plugin import CryptoPluginBase +from barbican.model.models import EncryptedDatum +from barbican.openstack.common import jsonutils as json + + +class TestCryptoPlugin(CryptoPluginBase): + """Crypto plugin implementation for testing the plugin manager.""" + + def encrypt(self, unencrypted, secret, tenant): + datum = EncryptedDatum() + datum.cypher_text = 'cypher_text' + datum.mime_type = 'text/plain' + datum.kek_metadata = json.dumps({'plugin': 'TestCryptoPlugin'}) + return datum + + def create(self, secret_type): + return "insecure_key" + + def supports(self, secret_type): + return True \ No newline at end of file diff --git a/barbican/tests/model/__init__.py b/barbican/tests/model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/barbican/tests/model/test_models.py b/barbican/tests/model/test_models.py new file mode 100644 index 000000000..e9704b4e6 --- /dev/null +++ b/barbican/tests/model/test_models.py @@ -0,0 +1,36 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 unittest + +from barbican.model.models import Secret + + +class WhenCreatingNewSecret(unittest.TestCase): + def setUp(self): + self.parsed_body = {'name': 'name', + 'mime_type': 'text/plain', + 'algorithm': 'algorithm', + 'bit_length': 512, + 'cypher_type': 'cypher_type', + 'plain_text': 'not-encrypted'} + + def test_new_secret_is_created_from_dict(self): + secret = Secret(self.parsed_body) + self.assertEqual(secret.name, self.parsed_body['name']) + self.assertEqual(secret.mime_type, self.parsed_body['mime_type']) + self.assertEqual(secret.algorithm, self.parsed_body['algorithm']) + self.assertEqual(secret.bit_length, self.parsed_body['bit_length']) + self.assertEqual(secret.cypher_type, self.parsed_body['cypher_type']) \ No newline at end of file diff --git a/setup.py b/setup.py index de44c7058..8c5605386 100644 --- a/setup.py +++ b/setup.py @@ -72,5 +72,12 @@ setup( 'Environment :: No Input/Output (Daemon)', ], scripts=['bin/barbican-api'], - py_modules=[] + py_modules=[], + entry_points=""" + [barbican.crypto.extension] + simple_crypto = barbican.crypto.plugin:SimpleCryptoPlugin + + [barbican.test.crypto.extension] + test_crypto = barbican.tests.crypto.test_plugin:TestCryptoPlugin + """ ) diff --git a/tools/pip-requires b/tools/pip-requires index 874a21785..c8b8c8d13 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -10,6 +10,7 @@ webob>=1.2.3 PasteDeploy>=1.5.0 Celery>=3.0.19 python-keystoneclient>=0.2.0 +stevedore>=0.8 # SQLAlchemy 0.7.10 typically has issues installing via pip, since it # will be removed as a dependency soon we will just grab the tarball