Merge pull request #69 from jfwood/master

Added methods to the crypto plugin manager to handle key gen and is-supports API flows; Added more code to deal with corner cases in API flow;
This commit is contained in:
Douglas Mendizabal
2013-05-17 15:28:52 -07:00
12 changed files with 386 additions and 156 deletions
+4 -1
View File
@@ -21,6 +21,9 @@ import falcon
from barbican.openstack.common import jsonutils as json
MAX_SIZE_REQUEST_INPUT_ACCEPTED_IN_BYTES = 1000000
class ApiResource(object):
"""
Base class for API resources
@@ -42,7 +45,7 @@ def load_body(req):
Python dictionary
"""
try:
raw_json = req.stream.read()
raw_json = req.stream.read(MAX_SIZE_REQUEST_INPUT_ACCEPTED_IN_BYTES)
except IOError:
abort(falcon.HTTP_500, 'Read Error')
+1
View File
@@ -30,6 +30,7 @@ from barbican.openstack.common import log
def create_main_app(global_config, **local_conf):
"""uWSGI factory method for the Barbican-API application"""
# Configure oslo logging and configuration services.
config.parse_args()
log.setup('barbican')
+78 -31
View File
@@ -30,6 +30,7 @@ from barbican.model.models import (Tenant, Secret, TenantSecret,
from barbican.model.repositories import (TenantRepo, SecretRepo,
OrderRepo, TenantSecretRepo,
EncryptedDatumRepo)
from barbican.common import exception
from barbican.crypto import extension_manager as em
from barbican.openstack.common.gettextutils import _
from barbican.openstack.common import jsonutils as json
@@ -47,12 +48,12 @@ def _general_failure(message):
def _secret_not_found():
"""Throw exception indicating secret not found."""
abort(falcon.HTTP_400, _('Unable to locate secret.'))
abort(falcon.HTTP_404, _('Unable to locate secret.'))
def _order_not_found():
"""Throw exception indicating order not found."""
abort(falcon.HTTP_400, _('Unable to locate order.'))
abort(falcon.HTTP_404, _('Unable to locate order.'))
def _put_accept_incorrect(ct):
@@ -67,6 +68,12 @@ def _get_accept_not_supported(accept):
"is not supported.").format(accept))
def _get_secret_info_not_found(mime_type):
"""Throw exception indicating request's accept is not supported."""
abort(falcon.HTTP_404, _("Secret information of type '{0}' not available "
"for decryption.").format(mime_type))
def _secret_mime_type_not_supported(mt, exception=None):
"""Throw exception indicating secret mime-type is not supported."""
abort(falcon.HTTP_400, _("Mime-type of '{0}' "
@@ -82,6 +89,16 @@ def _client_content_mismatch_to_secret(expected, actual):
"secret's of '{1}'.").format(actual, expected))
def _secret_data_too_large():
"""Throw exception indicating plain-text was too big."""
abort(falcon.HTTP_413, _("Could not add secret data as it was too large"))
def _secret_plain_text_empty():
"""Throw exception indicating empty plain-text was supplied."""
abort(falcon.HTTP_400, _("Could not add secret with empty 'plain_text'"))
def _failed_to_create_encrypted_datum():
"""
Throw exception we could not create an EncryptedDatum
@@ -90,6 +107,11 @@ def _failed_to_create_encrypted_datum():
abort(falcon.HTTP_400, _("Could not add secret data to Barbican."))
def _failed_to_decrypt_data():
"""Throw exception if failed to decrypt secret information."""
abort(falcon.HTTP_500, _("Problem decrypting secret information."))
def _secret_already_has_data():
"""
Throw exception that the secret already has data.
@@ -177,16 +199,17 @@ def next_href(resources_name, tenant_id, offset, limit):
return convert_list_to_href(resources_name, tenant_id, offset, limit)
def add_nav_hrefs(resources_name, tenant_id, offset, limit, data):
def add_nav_hrefs(resources_name, tenant_id, offset, limit, num_elements, data):
if offset > 0:
data.update({'previous': previous_href(resources_name,
tenant_id,
offset,
limit)})
data.update({'next': next_href(resources_name,
tenant_id,
offset,
limit)})
if num_elements >= limit:
data.update({'next': next_href(resources_name,
tenant_id,
offset,
limit)})
return data
@@ -232,6 +255,12 @@ class SecretsResource(ApiResource):
except em.CryptoMimeTypeNotSupportedException as cmtnse:
LOG.exception('Secret creation failed - mime-type not supported')
_secret_mime_type_not_supported(cmtnse.mime_type)
except exception.NoDataToProcess:
LOG.exception('No secret data to process')
_secret_plain_text_empty()
except exception.LimitExceeded:
LOG.exception('Secret data too big to process')
_secret_data_too_large()
except Exception as e:
LOG.exception('Secret creation failed - unknown')
_general_failure('Secret creation failed - unknown')
@@ -248,21 +277,23 @@ class SecretsResource(ApiResource):
params = req._params
result = self.secret_repo.get_by_create_date(
offset_arg=params.get('offset', None),
limit_arg=params.get('limit', None),
suppress_exception=True)
result = self.secret_repo \
.get_by_create_date(offset_arg=params.get('offset',
None),
limit_arg=params.get('limit',
None),
suppress_exception=True)
secrets, offset, limit = result
if not secrets:
_secret_not_found()
else:
secrets_resp = [convert_to_hrefs(tenant_id,
augment_fields_with_content_types(s)) for s in
secrets]
secrets_resp_overall = add_nav_hrefs('secrets',
tenant_id, offset, limit,
{'secrets': secrets_resp})
secret_fields = lambda s: augment_fields_with_content_types(s)
secrets_resp = [convert_to_hrefs(tenant_id, secret_fields(s)) for
s in secrets]
secrets_resp_overall = add_nav_hrefs('secrets', tenant_id,
offset, limit, len(secrets),
{'secrets': secrets_resp})
resp.body = json.dumps(secrets_resp_overall,
default=json_handler)
@@ -292,10 +323,10 @@ class SecretResource(ApiResource):
if not req.accept or req.accept == 'application/json':
# Metadata-only response, no decryption necessary.
resp.set_header('Content-Type', 'application/json')
resp.body = json.dumps(
convert_to_hrefs(tenant_id,
augment_fields_with_content_types(secret)),
default=json_handler)
secret_fields = augment_fields_with_content_types(secret)
resp.body = json.dumps(convert_to_hrefs(tenant_id,
secret_fields),
default=json_handler)
else:
tenant = get_or_create_tenant(tenant_id, self.tenant_repo)
resp.set_header('Content-Type', req.accept)
@@ -306,6 +337,10 @@ class SecretResource(ApiResource):
LOG.exception('Secret decryption failed - '
'accept not supported')
_get_accept_not_supported(canse.accept)
except em.CryptoNoSecretOrDataException as cnsode:
LOG.exception('Secret information of type {0} not '
'found for decryption.'.format(cnsode.mime_type))
_get_secret_info_not_found(cnsode.mime_type)
except Exception as e:
LOG.exception('Secret decryption failed - unknown')
_failed_to_decrypt_data()
@@ -341,14 +376,23 @@ class SecretResource(ApiResource):
except em.CryptoMimeTypeNotSupportedException as cmtnse:
LOG.exception('Secret creation failed - mime-type not supported')
_secret_mime_type_not_supported(cmtnse.mime_type)
except exception.NoDataToProcess:
LOG.exception('No secret data to process')
_secret_plain_text_empty()
except exception.LimitExceeded:
LOG.exception('Secret data too big to process')
_secret_data_too_large()
except Exception as e:
LOG.exception('Secret creation failed - unknown')
_failed_to_create_encrypted_datum()
def on_delete(self, req, resp, tenant_id, secret_id):
secret = self.repo.get(entity_id=secret_id)
self.repo.delete_entity(secret)
try:
self.repo.delete_entity_by_id(entity_id=secret_id)
except exception.NotFound:
LOG.exception('Problem deleting secret')
_secret_not_found()
resp.status = falcon.HTTP_200
@@ -419,10 +463,10 @@ class OrdersResource(ApiResource):
params = req._params
result = self.order_repo.get_by_create_date(
offset_arg=params.get('offset', None),
limit_arg=params.get('limit', None),
suppress_exception=True)
result = self.order_repo \
.get_by_create_date(offset_arg=params.get('offset', None),
limit_arg=params.get('limit', None),
suppress_exception=True)
orders, offset, limit = result
if not orders:
@@ -430,8 +474,8 @@ class OrdersResource(ApiResource):
else:
orders_resp = [convert_to_hrefs(tenant_id, o.to_dict_fields())
for o in orders]
orders_resp_overall = add_nav_hrefs('orders', tenant_id, offset,
limit,
orders_resp_overall = add_nav_hrefs('orders', tenant_id,
offset, limit, len(orders),
{'orders': orders_resp})
resp.body = json.dumps(orders_resp_overall,
default=json_handler)
@@ -455,8 +499,11 @@ class OrderResource(ApiResource):
default=json_handler)
def on_delete(self, req, resp, tenant_id, order_id):
order = self.repo.get(entity_id=order_id)
self.repo.delete_entity(order)
try:
self.repo.delete_entity_by_id(entity_id=order_id)
except exception.NotFound:
LOG.exception('Problem deleting order')
_order_not_found()
resp.status = falcon.HTTP_200
+4
View File
@@ -128,6 +128,10 @@ class Invalid(BarbicanException):
message = _("Data supplied was not valid.")
class NoDataToProcess(BarbicanException):
message = _("No data supplied to process.")
class InvalidSortKey(Invalid):
message = _("Sort key supplied was not valid.")
+31 -6
View File
@@ -16,6 +16,9 @@
"""
Shared business logic.
"""
from sys import getsizeof
from oslo.config import cfg
from barbican.common import exception
from barbican.crypto.extension_manager import (
CryptoMimeTypeNotSupportedException
)
@@ -25,6 +28,15 @@ from barbican.common import utils
LOG = utils.getLogger(__name__)
DEFAULT_MAX_SECRET_BYTES = 10000
common_opts = [
cfg.IntOpt('max_allowed_secret_in_bytes', default=DEFAULT_MAX_SECRET_BYTES),
]
CONF = cfg.CONF
CONF.register_opts(common_opts)
def get_or_create_tenant(tenant_id, tenant_repo):
"""Returns tenant with matching tenant_id. Creates it if it does
not exist."""
@@ -67,6 +79,15 @@ def create_secret(data, tenant, crypto_manager,
tenant_secret_repo.create_from(new_assoc)
if 'plain_text' in data:
plain_text = data['plain_text']
if not plain_text:
raise exception.NoDataToProcess()
if getsizeof(plain_text) > CONF.max_allowed_secret_in_bytes:
raise exception.LimitExceeded()
LOG.debug('Encrypting plain_text secret...')
new_datum = crypto_manager.encrypt(data['plain_text'],
new_secret,
@@ -74,14 +95,15 @@ def create_secret(data, tenant, crypto_manager,
datum_repo.create_from(new_datum)
elif ok_to_generate:
LOG.debug('Generating new secret...')
# TODO: Generate a good key
new_datum = crypto_manager.encrypt('generated_plain_text_key',
new_secret,
tenant)
new_datum = crypto_manager.generate_data_encryption_key(new_secret,
tenant)
datum_repo.create_from(new_datum)
else:
LOG.debug('Only creating metadata for the new secret.')
LOG.debug('Creating metadata only for the new secret. '
'A subsequent PUT is required')
crypto_manager.supports(new_secret, tenant)
return new_secret
@@ -100,7 +122,10 @@ def create_encrypted_datum(secret, plain_text, tenant, crypto_manager,
:retval The response body, None if N/A
"""
if not plain_text:
raise ValueError('Must provide plain-text to encrypt.')
raise exception.NoDataToProcess()
if getsizeof(plain_text) > CONF.max_allowed_secret_in_bytes:
raise exception.LimitExceeded()
if secret.encrypted_data:
raise ValueError('Secret already has encrypted data stored for it.')
+52 -3
View File
@@ -45,7 +45,7 @@ class CryptoMimeTypeNotSupportedException(BarbicanException):
not available in any active plugin."""
def __init__(self, mime_type):
super(CryptoMimeTypeNotSupportedException, self).__init__(
_('Crypto Mime Type not supported {0}'.format(mime_type))
_("Crypto Mime Type of '{0}' not supported").format(mime_type)
)
self.mime_type = mime_type
@@ -55,11 +55,22 @@ class CryptoAcceptNotSupportedException(BarbicanException):
available in any active plugin."""
def __init__(self, accept):
super(CryptoAcceptNotSupportedException, self).__init__(
_('Crypto Accept not supported {0}'.format(accept))
_("Crypto Accept of '{0}' not supported").format(accept)
)
self.accept = accept
class CryptoNoSecretOrDataException(BarbicanException):
"""Raised when secret information is not available for the specified
secret mime-type."""
def __init__(self, mime_type):
super(CryptoNoSecretOrDataException, self).__init__(
_('No secret information available for '
'Mime Type of {0}').format(mime_type)
)
self.mime_type = mime_type
class CryptoExtensionManager(named.NamedExtensionManager):
def __init__(self, conf=CONF, invoke_on_load=True,
invoke_args=(), invoke_kwargs={}):
@@ -81,8 +92,46 @@ class CryptoExtensionManager(named.NamedExtensionManager):
def decrypt(self, accept, secret, tenant):
"""Delegates decryption to active plugins."""
if not secret or not secret.encrypted_data:
raise CryptoNoSecretOrDataException(accept)
plain_text = None
for ext in self.extensions:
if ext.obj.supports(accept):
return ext.obj.decrypt(accept, secret, tenant)
plain_text = ext.obj.decrypt(accept, secret, tenant)
break
else:
raise CryptoAcceptNotSupportedException(accept)
if not plain_text:
raise CryptoNoSecretOrDataException(accept)
return plain_text
def generate_data_encryption_key(self, secret, tenant):
"""
Delegates generating a data-encryption key to active plugins.
Note that this key can be used by clients for their encryption
processes. This generated key is then be encrypted via
the plug-in key encryption process, and that encrypted datum
is then returned from this method.
"""
for ext in self.extensions:
if ext.obj.supports(secret.mime_type):
# TODO: Call plugin's key generation processes.
# Note: It could be the *data* key to generate (for the
# secret algo type) uses a different plug in than that
# used to encrypted the key.
data_key = ext.obj.create(secret.mime_type)
return ext.obj.encrypt(data_key, secret, tenant)
else:
raise CryptoMimeTypeNotSupportedException(secret.mime_type)
def supports(self, secret, tenant):
"""Tests if at least one plug-in supports the secret type."""
for ext in self.extensions:
if ext.obj.supports(secret.mime_type):
return True
else:
raise CryptoMimeTypeNotSupportedException(secret.mime_type)
+3
View File
@@ -20,8 +20,10 @@ Barbican defined mime-types
# Maps mime-types used to specify secret data formats to the types that can
# be requested for secrets via GET calls.
CTYPES_PLAIN = {'default': 'text/plain'}
CTYPES_BINARY = {'default': 'application/octet-stream'}
CTYPES_AES = {'default': 'application/aes'}
CTYPES_MAPPINGS = {'text/plain': CTYPES_PLAIN,
'application/octet-stream': CTYPES_BINARY,
'application/aes': CTYPES_AES}
@@ -38,5 +40,6 @@ def augment_fields_with_content_types(secret):
for datum in secret.encrypted_data:
if datum.mime_type in CTYPES_MAPPINGS:
fields.update({'content_types': CTYPES_MAPPINGS[datum.mime_type]})
break
return fields
+7 -4
View File
@@ -48,16 +48,19 @@ class SimpleCryptoPlugin(CryptoPluginBase):
#TODO: Use PyCrypto to aes encode secrets
def __init__(self):
self.supported_types = ['application/aes-256-cbc', 'text/plain']
self.supported_types = ['text/plain', 'application/octet-stream']
def encrypt(self, unencrypted, secret, tenant):
encrypted_datum = EncryptedDatum(secret)
encrypted_datum.cypher_text = 'encrypted-data'
encrypted_datum.cypher_text = '[ENcrypt this:{0}]'.format(unencrypted)
encrypted_datum.kek_metadata = "kek_metadata here"
return encrypted_datum
def decrypt(self, secret_type, secret, tenant):
encrypted_datum = secret.encrypted_data
return 'plain-data'
for encrypted_datum in secret.encrypted_data:
if secret_type == encrypted_datum.mime_type:
return '[DEcrypt this:{0}]'.format(encrypted_datum.cypher_text)
return None
def create(self, secret_type):
return "insecure_key"
+22 -5
View File
@@ -38,6 +38,11 @@ class States(object):
PENDING = 'PENDING'
ACTIVE = 'ACTIVE'
@classmethod
def is_valid(self, state_to_test):
"""Tests if a state is a valid one."""
return state_to_test in self.__dict__
@compiles(BigInteger, 'sqlite')
def compile_big_int_sqlite(type_, compiler, **kw):
@@ -74,12 +79,17 @@ class ModelBase(object):
"""Delete this object"""
import barbican.model.repositories
session = session or barbican.model.repositories.get_session()
session.delete(self)
self.deleted = True
self.deleted_at = timeutils.utcnow()
self.save(session=session)
#TODO: Soft delete instead?
# self.deleted = True
# self.deleted_at = timeutils.utcnow()
# self.save(session=session)
self._do_delete_children(session)
def _do_delete_children(self, session):
"""
Sub-class hook: delete children relationships.
"""
pass
def update(self, values):
"""dict.update() behaviour."""
@@ -206,6 +216,13 @@ class Secret(BASE, ModelBase):
self.status = States.ACTIVE
def _do_delete_children(self, session):
"""
Sub-class hook: delete children relationships.
"""
for datum in self.encrypted_data:
datam.delete(session)
def _do_extra_dict_fields(self):
"""Sub-class hook method: return dict of fields."""
return {'secret_id': self.id,
+39 -35
View File
@@ -49,9 +49,6 @@ BASE = models.BASE
sa_logger = None
STATUSES = ['active', 'saving', 'queued', 'killed', 'pending_delete',
'deleted']
db_opts = [
cfg.IntOpt('sql_idle_timeout', default=3600),
cfg.IntOpt('sql_max_retries', default=60),
@@ -205,6 +202,17 @@ def wrap_db_error(f):
return _wrap
def clean_paging_values(offset_arg=None, limit_arg=None):
"""Cleans and safely limits raw paging offset/limit values."""
offset = int(offset_arg) if offset_arg else 0
offset = offset if offset >= 0 else 0
limit = int(limit_arg) if limit_arg else CONF.max_limit_paging
limit = limit if limit >= 2 else 2
return (offset, limit)
class BaseRepo(object):
"""
Base repository for the barbican entities.
@@ -330,13 +338,20 @@ class BaseRepo(object):
"""
return self._update(entity_id, values, purge_props)
def delete_entity(self, entity):
"""Remove the entity"""
def delete_entity_by_id(self, entity_id):
"""Remove the entity by its ID"""
session = get_session()
with session.begin():
entity.deleted_at = timeutils.utcnow()
entity.delete(session=session)
entity = self.get(entity_id=entity_id, session=session)
try:
entity.delete(session=session)
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem finding entity to delete')
raise exception.NotFound("Entity ID %s not found"
% entity_id)
def _do_entity_name(self):
"""Sub-class hook: return entity name, such as for debugging."""
@@ -378,7 +393,7 @@ class BaseRepo(object):
msg = "{0} status is required.".format(self._do_entity_name())
raise exception.Invalid(msg)
if status not in STATUSES:
if not models.States.is_valid(status):
msg = "Invalid status '{0}' for {1}.".format(
status, self._do_entity_name())
raise exception.Invalid(msg)
@@ -455,10 +470,6 @@ class TenantRepo(BaseRepo):
"""Sub-class hook: build a retrieve query."""
return session.query(models.Tenant).filter_by(id=entity_id)
def _do_validate(self, values):
"""Sub-class hook: validate values."""
pass
def find_by_keystone_id(self, keystone_id, suppress_exception=False,
session=None):
session = self.get_session(session)
@@ -491,19 +502,16 @@ class SecretRepo(BaseRepo):
and paged based on the offset and limit fields.
"""
offset = int(offset_arg) if offset_arg else 0
offset = offset if offset >= 0 else 0
limit = int(limit_arg) if limit_arg else CONF.max_limit_paging
limit = limit if limit >= 2 else 2
offset, limit = clean_paging_values(offset_arg, limit_arg)
session = self.get_session(session)
utcnow = timeutils.utcnow()
try:
query = session.query(models.Secret).order_by(
models.Secret.created_at).filter_by(deleted=False)
query = session.query(models.Secret) \
.order_by(models.Secret.created_at) \
.filter_by(deleted=False)
# Note: Must use '== None' below, not 'is None'.
query = query.filter(or_(models.Secret.expiration == None,
models.Secret.expiration > utcnow))
@@ -528,20 +536,20 @@ class SecretRepo(BaseRepo):
def _do_build_query_by_name(self, name, session):
"""Sub-class hook: find entity by name."""
utcnow = timeutils.utcnow()
# Note: Must use '== None' below, not 'is None'.
return session.query(models.Secret).filter_by(name=name).filter(
or_(models.Secret.expiration == None,
models.Secret.expiration > utcnow))
return session.query(models.Secret).filter_by(name=name) \
.filter(or_(models.Secret.expiration == None,
models.Secret.expiration > utcnow))
def _do_build_get_query(self, entity_id, session):
"""Sub-class hook: build a retrieve query."""
utcnow = timeutils.utcnow()
# Note: Must use '== None' below, not 'is None'.
return session.query(models.Secret).filter_by(id=entity_id).filter(
or_(models.Secret.expiration == None,
models.Secret.expiration > utcnow))
return session.query(models.Secret).filter_by(id=entity_id) \
.filter(or_(models.Secret.expiration == None,
models.Secret.expiration > utcnow))
def _do_validate(self, values):
"""Sub-class hook: validate values."""
@@ -609,17 +617,13 @@ class OrderRepo(BaseRepo):
and paged based on the offset and limit fields.
"""
offset = int(offset_arg) if offset_arg else 0
offset = offset if offset >= 0 else 0
limit = int(limit_arg) if limit_arg else CONF.max_limit_paging
limit = limit if limit >= 2 else 2
offset, limit = clean_paging_values(offset_arg, limit_arg)
session = self.get_session(session)
try:
query = session.query(models.Order).order_by(
models.Order.created_at)
query = session.query(models.Order) \
.order_by(models.Order.created_at)
query = query.filter_by(deleted=False)
entities = query[offset:(offset + limit)]
+145 -70
View File
@@ -26,7 +26,8 @@ from barbican.crypto.extension_manager import CryptoExtensionManager
from barbican.model.models import (Secret, Tenant, TenantSecret,
Order, EncryptedDatum)
from barbican.common import config
from barbican.common import exception
from barbican.common import exception as excep
from barbican.common.resources import DEFAULT_MAX_SECRET_BYTES
from barbican.openstack.common import jsonutils
@@ -59,8 +60,8 @@ def create_secret(mime_type, id="id", name="name",
def create_order(mime_type, id="id", name="name",
algorithm=None, bit_length=None,
cypher_type=None):
algorithm=None, bit_length=None,
cypher_type=None):
"""Generate an Order entity instance."""
order = Order()
order.id = id
@@ -156,18 +157,18 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
args, kwargs = self.secret_repo.create_from.call_args
secret = args[0]
assert isinstance(secret, Secret)
assert secret.name == self.name
assert secret.algorithm == self.secret_algorithm
assert secret.bit_length == self.secret_bit_length
assert secret.cypher_type == self.secret_cypher_type
assert secret.mime_type == self.mime_type
self.assertTrue(isinstance(secret, Secret))
self.assertEqual(secret.name, self.name)
self.assertEqual(secret.algorithm, self.secret_algorithm)
self.assertEqual(secret.bit_length, self.secret_bit_length)
self.assertEqual(secret.cypher_type, self.secret_cypher_type)
self.assertEqual(secret.mime_type, self.mime_type)
args, kwargs = self.tenant_secret_repo.create_from.call_args
tenant_secret = args[0]
assert isinstance(tenant_secret, TenantSecret)
assert tenant_secret.tenant_id == self.tenant_id
assert tenant_secret.secret_id == secret.id
self.assertTrue(isinstance(tenant_secret, TenantSecret))
self.assertEqual(tenant_secret.tenant_id, self.tenant_id)
self.assertEqual(tenant_secret.secret_id, secret.id)
args, kwargs = self.datum_repo.create_from.call_args
datum = args[0]
@@ -183,21 +184,21 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
args, kwargs = self.secret_repo.create_from.call_args
secret = args[0]
assert isinstance(secret, Secret)
assert secret.name == self.name
self.assertTrue(isinstance(secret, Secret))
self.assertEqual(secret.name, self.name)
args, kwargs = self.tenant_secret_repo.create_from.call_args
tenant_secret = args[0]
assert isinstance(tenant_secret, TenantSecret)
assert not tenant_secret.tenant_id
assert tenant_secret.secret_id == secret.id
self.assertTrue(isinstance(tenant_secret, TenantSecret))
self.assertIsNone(tenant_secret.tenant_id)
self.assertEqual(tenant_secret.secret_id, secret.id)
args, kwargs = self.datum_repo.create_from.call_args
datum = args[0]
assert isinstance(datum, EncryptedDatum)
self.assertTrue(isinstance(datum, EncryptedDatum))
self.assertEqual('cypher_text', datum.cypher_text)
assert self.mime_type == datum.mime_type
assert datum.kek_metadata is not None
self.assertEqual(self.mime_type, datum.mime_type)
self.assertIsNotNone(datum.kek_metadata)
def test_should_add_new_secret_no_plain_text(self):
json_template = u'{{"name":"{0}", "mime_type":"{1}"}}'
@@ -208,16 +209,48 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
args, kwargs = self.secret_repo.create_from.call_args
secret = args[0]
assert isinstance(secret, Secret)
assert secret.name == self.name
self.assertTrue(isinstance(secret, Secret))
self.assertEqual(secret.name, self.name)
args, kwargs = self.tenant_secret_repo.create_from.call_args
tenant_secret = args[0]
assert isinstance(tenant_secret, TenantSecret)
assert tenant_secret.tenant_id == self.tenant_id
assert tenant_secret.secret_id == secret.id
self.assertTrue(isinstance(tenant_secret, TenantSecret))
self.assertEqual(tenant_secret.tenant_id, self.tenant_id)
self.assertEqual(tenant_secret.secret_id, secret.id)
assert not self.datum_repo.create_from.called
self.assertFalse(self.datum_repo.create_from.called)
def test_should_fail_due_to_empty_plain_text(self):
self.secret_req = {'name': self.name,
'mime_type': self.mime_type,
'algorithm': self.secret_algorithm,
'bit_length': self.secret_bit_length,
'cypher_type': self.secret_cypher_type,
'plain_text': ''}
self.stream.read.return_value = json.dumps(self.secret_req)
with self.assertRaises(falcon.HTTPError) as cm:
self.resource.on_post(self.req, self.resp, self.tenant_id)
exception = cm.exception
self.assertEqual(falcon.HTTP_400, exception.status)
def test_should_fail_due_to_plain_text_too_large(self):
big_text = ['A' for x in xrange(2 * DEFAULT_MAX_SECRET_BYTES)]
self.secret_req = {'name': self.name,
'mime_type': self.mime_type,
'algorithm': self.secret_algorithm,
'bit_length': self.secret_bit_length,
'cypher_type': self.secret_cypher_type,
'plain_text': big_text}
self.stream.read.return_value = json.dumps(self.secret_req)
with self.assertRaises(falcon.HTTPError) as cm:
self.resource.on_post(self.req, self.resp, self.tenant_id)
exception = cm.exception
self.assertEqual(falcon.HTTP_413, exception.status)
def test_should_fail_due_to_unsupported_mime(self):
self.secret_req = {'name': self.name,
@@ -232,7 +265,7 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
self.resource.on_post(self.req, self.resp, self.tenant_id)
exception = cm.exception
assert falcon.HTTP_400 == exception.status
self.assertEqual(falcon.HTTP_400, exception.status)
class WhenGettingSecretsListUsingSecretsResource(unittest.TestCase):
@@ -293,12 +326,12 @@ class WhenGettingSecretsListUsingSecretsResource(unittest.TestCase):
def test_should_get_list_secrets(self):
self.resource.on_get(self.req, self.resp, self.tenant_id)
self.secret_repo.get_by_create_date.assert_called_once_with(
offset_arg=self.params.get('offset',
self.offset),
limit_arg=self.params.get('limit',
self.limit),
suppress_exception=True)
self.secret_repo.get_by_create_date \
.assert_called_once_with(offset_arg=self.params.get('offset',
self.offset),
limit_arg=self.params.get('limit',
self.limit),
suppress_exception=True)
resp_body = jsonutils.loads(self.resp.body)
self.assertTrue('previous' in resp_body)
@@ -324,16 +357,15 @@ class WhenGettingSecretsListUsingSecretsResource(unittest.TestCase):
self.resource.on_get(self.req, self.resp, self.tenant_id)
exception = cm.exception
assert falcon.HTTP_400 == exception.status
self.assertEqual(falcon.HTTP_404, exception.status)
def _create_url(self, tenant_id, offset_arg=None, limit_arg=None):
if limit_arg:
offset = int(offset_arg)
limit = int(limit_arg)
return '/v1/{0}/secrets?limit={1}&offset={2}'.format(
tenant_id,
limit,
offset)
return '/v1/{0}/secrets?limit={1}&offset={2}'.format(tenant_id,
limit,
offset)
else:
return '/v1/{0}/secrets'.format(self.tenant_id)
@@ -374,7 +406,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
self.secret_repo = MagicMock()
self.secret_repo.get.return_value = self.secret
self.secret_repo.delete_entity.return_value = None
self.secret_repo.delete_entity_by_id.return_value = None
self.tenant_secret_repo = MagicMock()
self.tenant_secret_repo.create_from.return_value = None
@@ -426,7 +458,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
self.assertEquals(self.resp.status, falcon.HTTP_200)
resp_body = self.resp.body
assert resp_body
self.assertIsNotNone(resp_body)
def test_should_throw_exception_for_get_when_secret_not_found(self):
self.secret_repo.get.return_value = None
@@ -436,8 +468,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
self.secret.id)
exception = cm.exception
assert falcon.HTTP_400 == exception.status
self.assertEqual(falcon.HTTP_404, exception.status)
def test_should_throw_exception_for_get_when_accept_not_supported(self):
@@ -448,8 +479,19 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
self.secret.id)
exception = cm.exception
assert falcon.HTTP_406 == exception.status
self.assertEqual(falcon.HTTP_406, exception.status)
def test_should_throw_exception_for_get_when_datum_not_available(self):
self.req.accept = 'text/plain'
self.secret.encrypted_data = []
with self.assertRaises(falcon.HTTPError) as cm:
self.resource.on_get(self.req, self.resp, self.tenant_id,
self.secret.id)
exception = cm.exception
self.assertEqual(falcon.HTTP_404, exception.status)
def test_should_put_secret_as_plain(self):
self._setup_for_puts()
@@ -459,10 +501,10 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
args, kwargs = self.datum_repo.create_from.call_args
datum = args[0]
assert isinstance(datum, EncryptedDatum)
self.assertTrue(isinstance(datum, EncryptedDatum))
self.assertEqual('cypher_text', datum.cypher_text)
assert self.mime_type == datum.mime_type
assert datum.kek_metadata is not None
self.assertEqual(self.mime_type, datum.mime_type)
self.assertIsNotNone(datum.kek_metadata)
def test_should_fail_put_secret_as_json(self):
self._setup_for_puts()
@@ -476,7 +518,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
self.secret.id)
exception = cm.exception
assert falcon.HTTP_415 == exception.status
self.assertEqual(falcon.HTTP_415, exception.status)
def test_should_fail_put_secret_not_found(self):
self._setup_for_puts()
@@ -489,7 +531,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
self.secret.id)
exception = cm.exception
assert falcon.HTTP_400 == exception.status
self.assertEqual(falcon.HTTP_404, exception.status)
def test_should_fail_put_secret_no_plain_text(self):
self._setup_for_puts()
@@ -502,7 +544,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
self.secret.id)
exception = cm.exception
assert falcon.HTTP_400 == exception.status
self.assertEqual(falcon.HTTP_400, exception.status)
def test_should_fail_put_secret_with_existing_datum(self):
self._setup_for_puts()
@@ -515,23 +557,51 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(
self.secret.id)
exception = cm.exception
assert falcon.HTTP_409 == exception.status
self.assertEqual(falcon.HTTP_409, exception.status)
def test_should_fail_due_to_empty_plain_text(self):
self._setup_for_puts()
self.stream.read.return_value = ''
with self.assertRaises(falcon.HTTPError) as cm:
self.resource.on_put(self.req, self.resp, self.tenant_id,
self.secret.id)
exception = cm.exception
self.assertEqual(falcon.HTTP_400, exception.status)
def test_should_fail_due_to_plain_text_too_large(self):
self._setup_for_puts()
big_text = ['A' for x in xrange(2 * DEFAULT_MAX_SECRET_BYTES)]
self.stream.read.return_value = big_text
with self.assertRaises(falcon.HTTPError) as cm:
self.resource.on_put(self.req, self.resp, self.tenant_id,
self.secret.id)
exception = cm.exception
self.assertEqual(falcon.HTTP_413, exception.status)
def test_should_delete_secret(self):
self.resource.on_delete(self.req, self.resp, self.tenant_id,
self.secret.id)
self.secret_repo.get.assert_called_once_with(entity_id=self.secret.id)
self.secret_repo.delete_entity.assert_called_once_with(self.secret)
self.secret_repo.delete_entity_by_id \
.assert_called_once_with(entity_id=self.secret.id)
def test_should_throw_exception_for_delete_when_secret_not_found(self):
self.secret_repo.get.side_effect = exception.NotFound(
self.secret_repo.delete_entity_by_id.side_effect = excep.NotFound(
"Test not found exception")
with self.assertRaises(exception.NotFound):
with self.assertRaises(falcon.HTTPError) as cm:
self.resource.on_delete(self.req, self.resp, self.tenant_id,
self.secret.id)
exception = cm.exception
self.assertEqual(falcon.HTTP_404, exception.status)
def _setup_for_puts(self):
self.plain_text = "plain_text"
self.req.accept = self.mime_type
@@ -599,7 +669,7 @@ class WhenCreatingOrdersUsingOrdersResource(unittest.TestCase):
None)
args, kwargs = self.order_repo.create_from.call_args
assert isinstance(args[0], Order)
self.assertTrue(isinstance(args[0], Order))
class WhenGettingOrdersListUsingOrdersResource(unittest.TestCase):
@@ -649,12 +719,12 @@ class WhenGettingOrdersListUsingOrdersResource(unittest.TestCase):
def test_should_get_list_orders(self):
self.resource.on_get(self.req, self.resp, self.tenant_id)
self.order_repo.get_by_create_date.assert_called_once_with(
offset_arg=self.params.get('offset',
self.offset),
limit_arg=self.params.get('limit',
self.limit),
suppress_exception=True)
self.order_repo.get_by_create_date \
.assert_called_once_with(offset_arg=self.params.get('offset',
self.offset),
limit_arg=self.params.get('limit',
self.limit),
suppress_exception=True)
resp_body = jsonutils.loads(self.resp.body)
self.assertTrue('previous' in resp_body)
@@ -680,7 +750,7 @@ class WhenGettingOrdersListUsingOrdersResource(unittest.TestCase):
self.resource.on_get(self.req, self.resp, self.tenant_id)
exception = cm.exception
assert falcon.HTTP_400 == exception.status
self.assertEqual(falcon.HTTP_404, exception.status)
def _create_url(self, tenant_id, offset_arg=None, limit_arg=None):
if limit_arg:
@@ -704,7 +774,7 @@ class WhenGettingOrDeletingOrderUsingOrderResource(unittest.TestCase):
self.order_repo = MagicMock()
self.order_repo.get.return_value = self.order
self.order_repo.delete_entity.return_value = None
self.order_repo.delete_entity_by_id.return_value = None
self.policy = MagicMock()
@@ -726,26 +796,31 @@ class WhenGettingOrDeletingOrderUsingOrderResource(unittest.TestCase):
self.resource.on_delete(self.req, self.resp, self.tenant_keystone_id,
self.order.id)
self.order_repo.get.assert_called_once_with(entity_id=self.order.id)
self.order_repo.delete_entity.assert_called_once_with(self.order)
self.order_repo.delete_entity_by_id \
.assert_called_once_with(entity_id=self.order.id)
def test_should_throw_exception_for_get_when_order_not_found(self):
self.order_repo.get.side_effect = exception.NotFound(
"Test not found exception")
self.order_repo.get.return_value = None
with self.assertRaises(exception.NotFound):
with self.assertRaises(falcon.HTTPError) as cm:
self.resource.on_get(self.req, self.resp, self.tenant_keystone_id,
self.order.id)
exception = cm.exception
self.assertEqual(falcon.HTTP_404, exception.status)
def test_should_throw_exception_for_delete_when_order_not_found(self):
self.order_repo.get.side_effect = exception.NotFound(
self.order_repo.delete_entity_by_id.side_effect = excep.NotFound(
"Test not found exception")
with self.assertRaises(exception.NotFound):
with self.assertRaises(falcon.HTTPError) as cm:
self.resource.on_delete(self.req, self.resp,
self.tenant_keystone_id,
self.order.id)
exception = cm.exception
self.assertEqual(falcon.HTTP_404, exception.status)
if __name__ == '__main__':
unittest.main()
-1
View File
@@ -24,7 +24,6 @@ class TestCryptoPlugin(CryptoPluginBase):
def encrypt(self, unencrypted, secret, tenant):
datum = EncryptedDatum(secret)
datum.cypher_text = 'cypher_text'
datum.mime_type = 'text/plain'
datum.kek_metadata = json.dumps({'plugin': 'TestCryptoPlugin'})
return datum