Add the error reason to the Order entity if async processing fails.

When an Order is used to perform asynchronous actions such as creating secrets,
the client doesn't know what problem occurred unless they retrieve the metadata
for the given order later. This metadata should both indicate that the order
status is ERROR, and provide an error reason for the failure.

Change-Id: Ib10e831cc98c20a77ed502e885687447e874e05c
Implements: blueprint api-orders-add-error-reason
This commit is contained in:
John Wood
2013-08-23 17:07:23 -05:00
parent 8e087334b0
commit 34fed4b9d1
8 changed files with 454 additions and 257 deletions

View File

@@ -16,11 +16,14 @@
"""
API handler for Cloudkeep's Barbican
"""
import falcon
from barbican.openstack.common import jsonutils as json
from barbican.common import exception
from barbican.common import utils
from barbican.crypto import extension_manager as em
from barbican.openstack.common import gettextutils as u
from barbican.openstack.common import jsonutils as json
from barbican.openstack.common import policy
LOG = utils.getLogger(__name__)
@@ -28,16 +31,20 @@ MAX_BYTES_REQUEST_INPUT_ACCEPTED = 1000000
class ApiResource(object):
"""
Base class for API resources
"""
"""Base class for API resources."""
pass
def abort(status=falcon.HTTP_500, message=None, req=None, resp=None):
"""
Helper function for aborting an API request process. Useful for error
reporting and expcetion handling.
"""Helper function for aborting an API request process.
This function is useful for error reporting and exception handling.
:param status: A falcon.HTTP_XXXX status code.
:param message: The message to associate with the Falcon exception.
:param req: The HTTP request.
:param resp: The HTTP response.
:return: None
:raise: falcon.HTTPError
"""
if resp and message:
if req and req.accept != 'application/json':
@@ -47,9 +54,13 @@ def abort(status=falcon.HTTP_500, message=None, req=None, resp=None):
def load_body(req, resp=None, validator=None):
"""
Helper function for loading an HTTP request body from JSON into a
Python dictionary
"""Helper function for loading an HTTP request body from JSON.
This body is placed into into a Python dictionary.
:param req: The HTTP request instance to load the body from.
:param resp: The HTTP response instance.
:param validator: The JSON validator to enforce.
:return: A dict of values from the JSON request.
"""
try:
raw_json = req.stream.read(MAX_BYTES_REQUEST_INPUT_ACCEPTED)
@@ -58,7 +69,8 @@ def load_body(req, resp=None, validator=None):
abort(falcon.HTTP_500, 'Read Error', req, resp)
try:
#TODO: Investigate how to get UTF8 format via openstack jsonutils:
#TODO(jwood): Investigate how to get UTF8 format via openstack
# jsonutils:
# parsed_body = json.loads(raw_json, 'utf-8')
parsed_body = json.loads(raw_json)
except ValueError:
@@ -79,3 +91,88 @@ def load_body(req, resp=None, validator=None):
abort(falcon.HTTP_413, str(e), req, resp)
return parsed_body
def generate_safe_exception_message(operation_name, excep):
"""Generates an exception message that is 'safe' for clients to consume.
A 'safe' message is one that doesn't contain sensitive information that
could be used for (say) cryptographic attacks on Barbican. That generally
means that em.CryptoXxxx should be captured here and with a simple
message created on behalf of them.
:param operation_name: Name of attempted operation, with a 'Verb noun'
format (e.g. 'Create Secret).
:param excep: The Exception instance that halted the operation.
:return: (status, message) where 'status' is one of the falcon.HTTP_xxxx
codes, and 'message' is the sanitized message
associated with the error.
"""
message = None
reason = None
status = falcon.HTTP_500
try:
raise excep
except falcon.HTTPError as f:
message = f.title
status = f.status
except policy.PolicyNotAuthorized:
message = u._('{0} attempt was not authorized - '
'please review your '
'user/tenant privileges').format(operation_name)
status = falcon.HTTP_401
except em.CryptoContentTypeNotSupportedException as cctnse:
reason = u._("content-type of '{0}' not "
"supported").format(cctnse.content_type)
status = falcon.HTTP_400
except em.CryptoContentEncodingNotSupportedException as cc:
reason = u._("content-encoding of '{0}' not "
"supported").format(cc.content_encoding)
status = falcon.HTTP_400
except em.CryptoAcceptNotSupportedException as canse:
reason = u._("accept of '{0}' not "
"supported").format(canse.accept)
status = falcon.HTTP_406
except em.CryptoAcceptEncodingNotSupportedException as caense:
reason = u._("accept-encoding of '{0}' not "
"supported").format(caense.accept_encoding)
status = falcon.HTTP_406
except em.CryptoNoPayloadProvidedException:
reason = u._("No payload provided")
status = falcon.HTTP_400
except em.CryptoNoSecretOrDataFoundException:
reason = u._("Not Found. Sorry but your secret is in "
"another castle")
status = falcon.HTTP_404
except em.CryptoPayloadDecodingError:
reason = u._("Problem decoding payload")
status = falcon.HTTP_400
except em.CryptoContentEncodingMustBeBase64:
reason = u._("Text-based binary secret payloads must "
"specify a content-encoding of 'base64'")
status = falcon.HTTP_400
except em.CryptoAlgorithmNotSupportedException:
reason = u._("No plugin was found that supports the "
"requested algorithm")
status = falcon.HTTP_400
except em.CryptoSupportedPluginNotFound:
reason = u._("No plugin was found that could support "
"your request")
status = falcon.HTTP_400
except exception.NoDataToProcess:
reason = u._("No information provided to process")
status = falcon.HTTP_400
except exception.LimitExceeded:
reason = u._("Provided information too large "
"to process")
status = falcon.HTTP_413
except Exception:
message = u._('{0} failure seen - please contact site '
'administrator.').format(operation_name)
if reason:
message = u._('{0} issue seen - {1}.').format(operation_name,
reason)
return status, message

View File

@@ -23,13 +23,11 @@ from barbican.common import exception
from barbican.common import resources as res
from barbican.common import utils
from barbican.common import validators
from barbican.crypto import extension_manager as em
from barbican.crypto import mime_types
from barbican.model import models
from barbican.model import repositories as repo
from barbican.openstack.common import gettextutils as u
from barbican.openstack.common import jsonutils as json
from barbican.openstack.common import policy
from barbican import queue
from barbican import version
@@ -37,20 +35,6 @@ from barbican import version
LOG = utils.getLogger(__name__)
def _general_failure(message, req, resp):
"""Throw exception a general processing failure."""
LOG.exception(message)
api.abort(falcon.HTTP_500, message, req, resp)
def _issue_failure(operation_name, reason, http_code, req, resp):
"""Generic issue handler for client-related problem responses."""
message = u._('{0} issue seen - {1}').format(operation_name,
reason)
LOG.exception(message)
api.abort(http_code, message, req, resp)
def _authorization_failed(message, req, resp):
"""Throw exception that authorization failed."""
api.abort(falcon.HTTP_401, message, req, resp)
@@ -247,73 +231,11 @@ def handle_exceptions(operation_name=u._('System')):
except falcon.HTTPError as f:
LOG.exception('Falcon error seen')
raise f # Already converted to Falcon exception, just reraise
except policy.PolicyNotAuthorized:
message = u._('{0} attempt was not authorized - '
'please review your '
'user/tenant privileges').format(operation_name)
except Exception as e:
status, message = api.generate_safe_exception_message(
operation_name, e)
LOG.exception(message)
_authorization_failed(message, req, resp)
except em.CryptoContentTypeNotSupportedException as cctnse:
_issue_failure(operation_name,
u._("content-type of '{0}' not "
"supported").format(cctnse.content_type),
falcon.HTTP_400, req, resp)
except em.CryptoContentEncodingNotSupportedException as cc:
_issue_failure(operation_name,
u._("content-encoding of '{0}' not "
"supported").format(cc.content_encoding),
falcon.HTTP_400, req, resp)
except em.CryptoAcceptNotSupportedException as canse:
_issue_failure(operation_name,
u._("accept of '{0}' not "
"supported").format(canse.accept),
falcon.HTTP_406, req, resp)
except em.CryptoAcceptEncodingNotSupportedException as caense:
_issue_failure(operation_name,
u._("accept-encoding of '{0}' not "
"supported").format(caense.accept_encoding),
falcon.HTTP_406, req, resp)
except em.CryptoNoPayloadProvidedException:
_issue_failure(operation_name,
u._("No payload provided"),
falcon.HTTP_400, req, resp)
except em.CryptoNoSecretOrDataFoundException:
_issue_failure(operation_name,
u._("Not Found. Sorry but your secret is in "
"another castle."),
falcon.HTTP_404, req, resp)
except em.CryptoPayloadDecodingError:
_issue_failure(operation_name,
u._("Problem decoding payload"),
falcon.HTTP_400, req, resp)
except em.CryptoContentEncodingMustBeBase64:
_issue_failure(operation_name,
u._("Text-based binary secret payloads must "
"specify a content-encoding of 'base64'"),
falcon.HTTP_400, req, resp)
except em.CryptoAlgorithmNotSupportedException:
_issue_failure(operation_name,
u._("No plugin was found that supports the "
"requested algorithm."),
falcon.HTTP_400, req, resp)
except em.CryptoSupportedPluginNotFound:
_issue_failure(operation_name,
u._("No plugin was found that could support "
"your request."),
falcon.HTTP_400, req, resp)
except exception.NoDataToProcess:
_issue_failure(operation_name,
u._("No information provided to process"),
falcon.HTTP_400, req, resp)
except exception.LimitExceeded:
_issue_failure(operation_name,
u._("Provided information too large "
"to process"),
falcon.HTTP_413, req, resp)
except Exception:
message = u._('{0} failure seen - please contact site '
'administrator').format(operation_name)
_general_failure(message, req, resp)
api.abort(status, message, req, resp)
return handler
@@ -536,11 +458,11 @@ class OrdersResource(api.ApiResource):
new_order = models.Order()
new_order.secret_name = secret_info.get('name')
new_order.secret_algorithm = secret_info['algorithm']
new_order.secret_bit_length = secret_info['bit_length']
new_order.secret_cypher_type = secret_info['cypher_type']
new_order.secret_payload_content_type = secret_info[
'payload_content_type']
new_order.secret_algorithm = secret_info.get('algorithm')
new_order.secret_bit_length = secret_info.get('bit_length', 0)
new_order.secret_cypher_type = secret_info.get('cypher_type')
new_order.secret_payload_content_type = secret_info.get(
'payload_content_type')
new_order.secret_expiration = secret_info.get('expiration')
new_order.tenant_id = tenant.id

View File

@@ -228,8 +228,14 @@ class NewOrderValidator(ValidatorBase):
reason=_("Only 'aes' "
"supported"))
bit_length = int(secret.get('bit_length', 0))
# TODO(reaperhulk): Future API change will move from bit to byte_length
bit_length = int(secret.get('bit_length', 0))
if bit_length <= 0:
raise exception.UnsupportedField(field="bit_length",
schema=schema_name,
reason=_("Must have non-zero "
"positive bit_length "
"to generate secret"))
if bit_length % 8 != 0:
raise exception.UnsupportedField(field="bit_length",
schema=schema_name,

View File

@@ -35,6 +35,7 @@ BASE = declarative_base()
class States(object):
PENDING = 'PENDING'
ACTIVE = 'ACTIVE'
ERROR = 'ERROR'
@classmethod
def is_valid(self, state_to_test):
@@ -316,6 +317,9 @@ class Order(BASE, ModelBase):
tenant_id = Column(String(36), ForeignKey('tenants.id'),
nullable=False)
error_status_code = Column(String(16))
error_reason = Column(String(255))
secret_name = Column(String(255))
secret_algorithm = Column(String(255))
secret_bit_length = Column(Integer)
@@ -328,15 +332,20 @@ class Order(BASE, ModelBase):
def _do_extra_dict_fields(self):
"""Sub-class hook method: return dict of fields."""
return {'secret': {'name': self.secret_name or self.secret_id,
'algorithm': self.secret_algorithm,
'bit_length': self.secret_bit_length,
'cypher_type': self.secret_cypher_type,
'expiration': self.secret_expiration,
'payload_content_type':
self.secret_payload_content_type},
'secret_id': self.secret_id,
'order_id': self.id}
ret = {'secret': {'name': self.secret_name or self.secret_id,
'algorithm': self.secret_algorithm,
'bit_length': self.secret_bit_length,
'cypher_type': self.secret_cypher_type,
'expiration': self.secret_expiration,
'payload_content_type':
self.secret_payload_content_type},
'secret_id': self.secret_id,
'order_id': self.id}
if self.error_status_code:
ret['error_status_code'] = self.error_status_code
if self.error_reason:
ret['error_reason'] = self.error_reason
return ret
# Keep this tuple synchronized with the models in the file

View File

@@ -30,4 +30,8 @@ def process_order(order_id, keystone_id):
"""Process Order."""
LOG.debug('Order id is {0}'.format(order_id))
task = BeginOrder()
return task.process(order_id, keystone_id)
try:
task.process(order_id, keystone_id)
except Exception:
LOG.exception(">>>>> Task exception seen, but simulating async "
"reporting via the Orders entity on the worker side.")

View File

@@ -16,18 +16,131 @@
"""
Task resources for the Barbican API.
"""
from barbican.crypto.extension_manager import CryptoExtensionManager
from barbican.model import repositories as rep
from barbican.model.models import States
from barbican.common.resources import create_secret
import abc
from barbican import api
from barbican.common import resources as res
from barbican.common import utils
from barbican.crypto import extension_manager as em
from barbican.model import models
from barbican.model import repositories as rep
from barbican.openstack.common import gettextutils as u
LOG = utils.getLogger(__name__)
class BeginOrder(object):
class BaseTask(object):
"""Base asychronous task."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def get_name(self):
"""A hook method to return a short localized name for this task.
The returned name in the form 'u.('Verb Noun')'. For example:
u._('Create Secret')
"""
def process(self, *args, **kwargs):
"""A template method for all asynchronous tasks.
:param args: List of arguments passed in from the client.
:param kwargs: Dict of arguments passed in from the client.
:return: None
"""
name = self.get_name()
# Retrieve the target entity (such as an models.Order instance).
try:
entity = self.retrieve_entity(*args, **kwargs)
except Exception as e:
# Serious error!
LOG.exception(u._("Could not retrieve information needed to "
"process task '{0}'.").format(name))
raise e
# Process the target entity.
try:
self.handle_processing(entity, *args, **kwargs)
except Exception as e_orig:
LOG.exception(u._("Could not perform processing for "
"task '{0}'.").format(name))
# Handle failure to process entity.
try:
status, message = api \
.generate_safe_exception_message(name, e_orig)
self.handle_error(entity, status, message, e_orig,
*args, **kwargs)
except Exception:
LOG.exception(u._("Problem handling an error for task '{0}', "
"raising original "
"exception.").format(name))
raise e_orig
# Handle successful conclusion of processing.
try:
self.handle_success(entity, *args, **kwargs)
except Exception as e:
LOG.exception(u._("Could not process after successfully executing"
" task '{0}'.").format(name))
raise e
@abc.abstractmethod
def retrieve_entity(self, *args, **kwargs):
"""A hook method to retrieve an entity for processing.
:param args: List of arguments passed in from the client.
:param kwargs: Dict of arguments passed in from the client.
:return: Entity instance to process in subsequent hook methods.
"""
@abc.abstractmethod
def handle_processing(self, entity, *args, **kwargs):
"""A hook method to handle processing on behalf of an entity.
:param args: List of arguments passed in from the client.
:param kwargs: Dict of arguments passed in from the client.
:return: None
"""
@abc.abstractmethod
def handle_error(self, entity, status, message, exception,
*args, **kwargs):
"""A hook method to deal with errors seen during processing.
This method could be used to mark entity as being in error, and/or
to record an error cause.
:param entity: Entity retrieved from _retrieve_entity() above.
:param status: Status code for exception.
:param message: Reason/message for the exception.
:param exception: Exception raised from handle_processing() above.
:param args: List of arguments passed in from the client.
:param kwargs: Dict of arguments passed in from the client.
:return: None
"""
@abc.abstractmethod
def handle_success(self, entity, *args, **kwargs):
"""A hook method to post-process after successful entity processing.
This method could be used to mark entity as being active, or to
add information/references to the entity.
:param entity: Entity retrieved from _retrieve_entity() above.
:param args: List of arguments passed in from the client.
:param kwargs: Dict of arguments passed in from the client.
:return: None
"""
class BeginOrder(BaseTask):
"""Handles beginning processing an Order"""
def get_name(self):
return u._('Create Secret')
def __init__(self, crypto_manager=None, tenant_repo=None, order_repo=None,
secret_repo=None, tenant_secret_repo=None,
datum_repo=None, kek_repo=None):
@@ -38,28 +151,34 @@ class BeginOrder(object):
self.tenant_secret_repo = tenant_secret_repo or rep.TenantSecretRepo()
self.datum_repo = datum_repo or rep.EncryptedDatumRepo()
self.kek_repo = kek_repo or rep.KEKDatumRepo()
self.crypto_manager = crypto_manager or CryptoExtensionManager()
self.crypto_manager = crypto_manager or em.CryptoExtensionManager()
def process(self, order_id, keystone_id):
"""Process the beginning of an Order."""
LOG.debug("Processing Order with ID = {0}".format(order_id))
def retrieve_entity(self, order_id, keystone_id):
return self.order_repo.get(entity_id=order_id,
keystone_id=keystone_id)
# Retrieve the order.
order = self.order_repo.get(entity_id=order_id,
keystone_id=keystone_id)
self._handle_order(order)
def handle_processing(self, order, *args, **kwargs):
self.handle_order(order)
# Indicate we are done with Order processing
order.status = States.ACTIVE
def handle_error(self, order, status, message, exception,
*args, **kwargs):
order.status = models.States.ERROR
order.error_status_code = status
order.error_reason = message
self.order_repo.save(order)
return None
def handle_success(self, order, *args, **kwargs):
order.status = models.States.ACTIVE
self.order_repo.save(order)
def handle_order(self, order):
"""Handle secret creation.
def _handle_order(self, order):
"""
Either creates a secret item here, or else begins the extended
process of creating a secret (such as for SSL certificate
generation.
:param order: Order to process on behalf of.
"""
order_info = order.to_dict_fields()
secret_info = order_info['secret']
@@ -68,11 +187,11 @@ class BeginOrder(object):
tenant = self.tenant_repo.get(order.tenant_id)
# Create Secret
new_secret = create_secret(secret_info, tenant,
self.crypto_manager, self.secret_repo,
self.tenant_secret_repo,
self.datum_repo, self.kek_repo,
ok_to_generate=True)
new_secret = res.create_secret(secret_info, tenant,
self.crypto_manager, self.secret_repo,
self.tenant_secret_repo,
self.datum_repo, self.kek_repo,
ok_to_generate=True)
order.secret_id = new_secret.id
LOG.debug("...done creating order's secret.")

View File

@@ -1,124 +0,0 @@
# 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 mock import MagicMock
from barbican.crypto.extension_manager import CryptoExtensionManager
from barbican.tasks.resources import BeginOrder
from barbican.model.models import (Tenant, Secret, TenantSecret,
EncryptedDatum, Order, States)
from barbican.openstack.common import timeutils
def suite():
suite = unittest.TestSuite()
suite.addTest(WhenBeginningOrder())
return suite
class WhenBeginningOrder(unittest.TestCase):
def setUp(self):
self.requestor = 'requestor1234'
self.order = Order()
self.order.id = "id1"
self.order.requestor = self.requestor
self.secret_name = "name"
self.secret_algorithm = "AES"
self.secret_bit_length = 256
self.secret_cypher_type = "CBC"
self.secret_expiration = timeutils.utcnow()
self.secret_payload_content_type = 'application/octet-stream'
self.keystone_id = 'keystone1234'
self.tenant_id = 'tenantid1234'
self.tenant = Tenant()
self.tenant.id = self.tenant_id
self.tenant.keystone_id = self.keystone_id
self.tenant_repo = MagicMock()
self.tenant_repo.get.return_value = self.tenant
self.order.status = States.PENDING
self.order.tenant_id = self.tenant_id
self.order.secret_name = self.secret_name
self.order.secret_algorithm = self.secret_algorithm
self.order.secret_bit_length = self.secret_bit_length
self.order.secret_cypher_type = self.secret_cypher_type
self.order.secret_expiration = self.secret_expiration
self.order.secret_payload_content_type = self\
.secret_payload_content_type
self.order_repo = MagicMock()
self.order_repo.get.return_value = self.order
self.secret_repo = MagicMock()
self.secret_repo.create_from.return_value = None
self.tenant_secret_repo = MagicMock()
self.tenant_secret_repo.create_from.return_value = None
self.datum_repo = MagicMock()
self.datum_repo.create_from.return_value = None
self.kek_repo = MagicMock()
self.conf = MagicMock()
self.conf.crypto.namespace = 'barbican.test.crypto.plugin'
self.conf.crypto.enabled_crypto_plugins = ['test_crypto']
self.crypto_mgr = CryptoExtensionManager(conf=self.conf)
self.resource = BeginOrder(self.crypto_mgr,
self.tenant_repo, self.order_repo,
self.secret_repo, self.tenant_secret_repo,
self.datum_repo, self.kek_repo)
def test_should_process_order(self):
self.resource.process(self.order.id, self.keystone_id)
self.order_repo.get \
.assert_called_once_with(entity_id=self.order.id,
keystone_id=self.keystone_id)
self.assertEqual(self.order.status, States.ACTIVE)
args, kwargs = self.secret_repo.create_from.call_args
secret = args[0]
self.assertIsInstance(secret, Secret)
self.assertEqual(secret.name, self.secret_name)
self.assertEqual(secret.expiration, self.secret_expiration)
args, kwargs = self.tenant_secret_repo.create_from.call_args
tenant_secret = args[0]
self.assertIsInstance(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]
self.assertIsInstance(datum, EncryptedDatum)
self.assertIsNotNone(datum.cypher_text)
self.assertIsNone(datum.kek_meta_extended)
self.assertIsNotNone(datum.kek_meta_tenant)
self.assertTrue(datum.kek_meta_tenant.bind_completed)
self.assertIsNotNone(datum.kek_meta_tenant.plugin_name)
self.assertIsNotNone(datum.kek_meta_tenant.kek_label)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,164 @@
# 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
import falcon
import mock
from barbican.crypto import extension_manager as em
from barbican.model import models
from barbican.openstack.common import gettextutils as u
from barbican.openstack.common import timeutils
from barbican.tasks import resources
class WhenBeginningOrder(unittest.TestCase):
def setUp(self):
self.requestor = 'requestor1234'
self.order = models.Order()
self.order.id = "id1"
self.order.requestor = self.requestor
self.secret_name = "name"
self.secret_algorithm = "AES"
self.secret_bit_length = 256
self.secret_cypher_type = "CBC"
self.secret_expiration = timeutils.utcnow()
self.secret_payload_content_type = 'application/octet-stream'
self.keystone_id = 'keystone1234'
self.tenant_id = 'tenantid1234'
self.tenant = models.Tenant()
self.tenant.id = self.tenant_id
self.tenant.keystone_id = self.keystone_id
self.tenant_repo = mock.MagicMock()
self.tenant_repo.get.return_value = self.tenant
self.order.status = models.States.PENDING
self.order.tenant_id = self.tenant_id
self.order.secret_name = self.secret_name
self.order.secret_algorithm = self.secret_algorithm
self.order.secret_bit_length = self.secret_bit_length
self.order.secret_cypher_type = self.secret_cypher_type
self.order.secret_expiration = self.secret_expiration
self.order.secret_payload_content_type = self\
.secret_payload_content_type
self.order_repo = mock.MagicMock()
self.order_repo.get.return_value = self.order
self.secret_repo = mock.MagicMock()
self.secret_repo.create_from.return_value = None
self.tenant_secret_repo = mock.MagicMock()
self.tenant_secret_repo.create_from.return_value = None
self.datum_repo = mock.MagicMock()
self.datum_repo.create_from.return_value = None
self.kek_repo = mock.MagicMock()
self.conf = mock.MagicMock()
self.conf.crypto.namespace = 'barbican.test.crypto.plugin'
self.conf.crypto.enabled_crypto_plugins = ['test_crypto']
self.crypto_mgr = em.CryptoExtensionManager(conf=self.conf)
self.resource = resources.BeginOrder(self.crypto_mgr,
self.tenant_repo, self.order_repo,
self.secret_repo,
self.tenant_secret_repo,
self.datum_repo, self.kek_repo)
def test_should_process_order(self):
self.resource.process(self.order.id, self.keystone_id)
self.order_repo.get \
.assert_called_once_with(entity_id=self.order.id,
keystone_id=self.keystone_id)
self.assertEqual(self.order.status, models.States.ACTIVE)
args, kwargs = self.secret_repo.create_from.call_args
secret = args[0]
self.assertIsInstance(secret, models.Secret)
self.assertEqual(secret.name, self.secret_name)
self.assertEqual(secret.expiration, self.secret_expiration)
args, kwargs = self.tenant_secret_repo.create_from.call_args
tenant_secret = args[0]
self.assertIsInstance(tenant_secret, models.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]
self.assertIsInstance(datum, models.EncryptedDatum)
self.assertIsNotNone(datum.cypher_text)
self.assertIsNone(datum.kek_meta_extended)
self.assertIsNotNone(datum.kek_meta_tenant)
self.assertTrue(datum.kek_meta_tenant.bind_completed)
self.assertIsNotNone(datum.kek_meta_tenant.plugin_name)
self.assertIsNotNone(datum.kek_meta_tenant.kek_label)
def test_should_fail_during_retrieval(self):
# Force an error during the order retrieval phase.
self.order_repo.get = mock.MagicMock(return_value=None,
side_effect=ValueError())
with self.assertRaises(ValueError):
self.resource.process(self.order.id, self.keystone_id)
# Order state doesn't change because can't retrieve it to change it.
self.assertEqual(models.States.PENDING, self.order.status)
def test_should_fail_during_processing(self):
# Force an error during the processing handler phase.
self.tenant_repo.get = mock.MagicMock(return_value=None,
side_effect=ValueError())
with self.assertRaises(ValueError):
self.resource.process(self.order.id, self.keystone_id)
self.assertEqual(models.States.ERROR, self.order.status)
self.assertEqual(falcon.HTTP_500, self.order.error_status_code)
self.assertEqual(u._('Create Secret failure seen - please contact '
'site administrator.'), self.order.error_reason)
def test_should_fail_during_success_report_fail(self):
# Force an error during the processing handler phase.
self.order_repo.save = mock.MagicMock(return_value=None,
side_effect=ValueError())
with self.assertRaises(ValueError):
self.resource.process(self.order.id, self.keystone_id)
def test_should_fail_during_error_report_fail(self):
# Force an error during the error-report handling after
# error in processing handler phase.
# Force an error during the processing handler phase.
self.tenant_repo.get = mock.MagicMock(return_value=None,
side_effect=TypeError())
# Force exception in the error-reporting phase.
self.order_repo.save = mock.MagicMock(return_value=None,
side_effect=ValueError())
# Should see the original exception (TypeError) instead of the
# secondary one (ValueError).
with self.assertRaises(TypeError):
self.resource.process(self.order.id, self.keystone_id)