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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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()
|
||||
164
barbican/tests/tasks/test_resources.py
Normal file
164
barbican/tests/tasks/test_resources.py
Normal 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)
|
||||
Reference in New Issue
Block a user