Added crypto plugin encryption to Secrets post.

This commit is contained in:
Douglas Mendizabal 2013-05-10 03:08:06 -05:00
parent 2bb497224f
commit db40a53c00
13 changed files with 280 additions and 45 deletions

View File

@ -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()

View File

@ -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,

View File

@ -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.

View File

@ -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)

55
barbican/crypto/plugin.py Normal file
View File

@ -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

View File

@ -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,

View File

@ -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()

View File

View File

@ -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

View File

View File

@ -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'])

View File

@ -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
"""
)

View File

@ -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