Added crypto plugin encryption to Secrets post.
This commit is contained in:
parent
2bb497224f
commit
db40a53c00
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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'])
|
9
setup.py
9
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
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue