From 7fcf41551512a62cc21609cf358a1ce90b699ce0 Mon Sep 17 00:00:00 2001 From: Chelsea Winfree Date: Wed, 27 Aug 2014 16:29:09 -0500 Subject: [PATCH] Adding initial update logic for orders Adding the ability to update orders for certificates in the controller. Connects the update order all the way to the certificate resources. Also updated an issue if order type was not listed, nothing would happen. Implements: blueprint add-ssl-ca-support Change-Id: I08e58cc618c5e21520f4d46cc35e10da1a02d0c8 --- barbican/api/controllers/orders.py | 83 +++++++++++++++++-- barbican/queue/client.py | 8 ++ barbican/queue/server.py | 9 ++ barbican/tasks/certificate_resources.py | 9 ++ barbican/tasks/resources.py | 65 +++++++++++++++ barbican/tests/api/test_resources.py | 105 ++++++++++++++++++++++++ barbican/tests/queue/test_client.py | 11 +++ barbican/tests/queue/test_server.py | 12 +++ etc/barbican/policy.json | 1 + 9 files changed, 296 insertions(+), 7 deletions(-) diff --git a/barbican/api/controllers/orders.py b/barbican/api/controllers/orders.py index 8a62c96bd..b93bb4b36 100644 --- a/barbican/api/controllers/orders.py +++ b/barbican/api/controllers/orders.py @@ -44,20 +44,51 @@ def _order_update_not_supported(): pecan.abort(405, u._("Order update is not supported.")) +def _order_type_not_in_order(): + """Throw exception that order type is not available in the order.""" + pecan.abort(400, u._("Order type is expected but not received.")) + + +def _order_meta_not_in_update(): + """Throw exception that order meta is not available for an order update.""" + pecan.abort(400, u._("Order meta is expected for order updates.")) + + +def _order_update_not_supported_for_type(order_type): + """Throw exception that update is not supported.""" + pecan.abort(400, u._("Updates are not supported for order type " + "{0}.").format(order_type)) + + +def _order_cannot_be_updated_if_not_pending(order_status): + """Throw exception that order cannot be updated if not PENDING.""" + pecan.abort(400, u._("Only PENDING orders can be updated. Order is in the" + "{0} state.").format(order_status)) + + +def order_cannot_modify_order_type(): + """Throw exception that order type cannot be modified.""" + pecan.abort(400, u._("Cannot modify order type.")) + + class OrderController(object): """Handles Order retrieval and deletion requests.""" - def __init__(self, order_id, order_repo=None): + def __init__(self, order_id, order_repo=None, + queue_resource=None): self.order_id = order_id - self.repo = order_repo or repo.OrderRepo() + self.order_repo = order_repo or repo.OrderRepo() + self.queue = queue_resource or async_client.TaskClient() + self.type_order_validator = validators.TypeOrderValidator() @pecan.expose(generic=True, template='json') @controllers.handle_exceptions(u._('Order retrieval')) @controllers.enforce_rbac('order:get') def index(self, keystone_id): - order = self.repo.get(entity_id=self.order_id, keystone_id=keystone_id, - suppress_exception=True) + order = self.order_repo.get(entity_id=self.order_id, + keystone_id=keystone_id, + suppress_exception=True) if not order: _order_not_found() @@ -65,8 +96,46 @@ class OrderController(object): @index.when(method='PUT') @controllers.handle_exceptions(u._('Order update')) + @controllers.enforce_rbac('order:put') + @controllers.enforce_content_types(['application/json']) def on_put(self, keystone_id, **kwargs): - _order_update_not_supported() + raw_body = pecan.request.body + order_type = None + if raw_body: + order_type = json.loads(raw_body).get('type') + + if not order_type: + _order_type_not_in_order() + + order_model = self.order_repo.get(entity_id=self.order_id, + keystone_id=keystone_id, + suppress_exception=True) + + if not order_model: + _order_not_found() + + if order_model.type != order_type: + order_cannot_modify_order_type() + + if models.OrderType.CERTIFICATE != order_model.type: + _order_update_not_supported_for_type(order_type) + + if models.States.PENDING != order_model.status: + _order_cannot_be_updated_if_not_pending(order_model.status) + + body = api.load_body(pecan.request, + validator=self.type_order_validator) + + updated_meta = body.get('meta') + + if not updated_meta: + _order_meta_not_in_update() + + # TODO(chellygel): Put updated_meta into a separate order association + # entity. + self.queue.update_order(order_id=self.order_id, + keystone_id=keystone_id, + updated_meta=updated_meta) @index.when(method='DELETE') @controllers.handle_exceptions(u._('Order deletion')) @@ -74,8 +143,8 @@ class OrderController(object): def on_delete(self, keystone_id, **kwargs): try: - self.repo.delete_entity_by_id(entity_id=self.order_id, - keystone_id=keystone_id) + self.order_repo.delete_entity_by_id(entity_id=self.order_id, + keystone_id=keystone_id) except exception.NotFound: LOG.exception('Problem deleting order') _order_not_found() diff --git a/barbican/queue/client.py b/barbican/queue/client.py index f29632e10..79f7c31d6 100644 --- a/barbican/queue/client.py +++ b/barbican/queue/client.py @@ -49,6 +49,14 @@ class TaskClient(object): self._cast('process_type_order', order_id=order_id, keystone_id=keystone_id) + def update_order(self, order_id, keystone_id, updated_meta): + """Update Order.""" + + self._cast('update_order', + order_id=order_id, + keystone_id=keystone_id, + updated_meta=updated_meta) + def _cast(self, name, **kwargs): """Asynchronous call handler. Barbican probably only needs casts. diff --git a/barbican/queue/server.py b/barbican/queue/server.py index 44664e5ad..b868cc9ee 100644 --- a/barbican/queue/server.py +++ b/barbican/queue/server.py @@ -60,6 +60,15 @@ class Tasks(object): LOG.exception(">>>>> Task exception seen, details reported " "on the Orders entity.") + def update_order(self, context, order_id, keystone_id, updated_meta): + """Update Order.""" + task = resources.UpdateOrder() + try: + task.process(order_id, keystone_id, updated_meta) + except Exception: + LOG.exception(">>>>> Task exception seen, details reported " + "on the Orders entity.") + class TaskServer(Tasks, service.Service): """Server to process asynchronous tasking from Barbican API nodes. diff --git a/barbican/tasks/certificate_resources.py b/barbican/tasks/certificate_resources.py index 4f7e49860..364ccf16c 100644 --- a/barbican/tasks/certificate_resources.py +++ b/barbican/tasks/certificate_resources.py @@ -16,9 +16,11 @@ from barbican.common import hrefs import barbican.common.utils as utils from barbican.model import models + from barbican.plugin.interface import certificate_manager as cert from barbican.plugin import resources as plugin +LOG = utils.getLogger(__name__) # Order sub-status definitions ORDER_STATUS_REQUEST_PENDING = models.OrderStatus( @@ -163,6 +165,13 @@ def check_certificate_request(order_model, tenant_model, plugin_name, repos): return container_model +def modify_certificate_request(order_model, updated_meta, repos): + """Update the order with CA.""" + # TODO(chellygel): Add the modify certificate request logic. + LOG.debug('in modify_certificate_request') + raise NotImplementedError # pragma: no cover + + def _schedule_cert_retry_task(cert_result_dto, cert_plugin, order_model, plugin_meta, retry_method=None, diff --git a/barbican/tasks/resources.py b/barbican/tasks/resources.py index f192269c4..f020a01e0 100644 --- a/barbican/tasks/resources.py +++ b/barbican/tasks/resources.py @@ -301,3 +301,68 @@ class BeginTypeOrder(BaseTask): if new_container: order.container_id = new_container.id LOG.debug("...done requesting a certificate.") + else: + raise NotImplementedError( + 'Order type "{0}" not implemented.'.format(order_type)) + + +class UpdateOrder(BaseTask): + """Handles updating an order""" + def get_name(self): + return u._('Update Order') + + def __init__(self, tenant_repo=None, order_repo=None, + secret_repo=None, tenant_secret_repo=None, datum_repo=None, + kek_repo=None, container_repo=None, + container_secret_repo=None, secret_meta_repo=None): + LOG.debug('Creating UpdateOrder task processor') + self.repos = rep.Repositories( + tenant_repo=tenant_repo, + order_repo=order_repo, + secret_repo=secret_repo, + tenant_secret_repo=tenant_secret_repo, + datum_repo=datum_repo, + kek_repo=kek_repo, + container_repo=container_repo, + container_secret_repo=container_secret_repo, + secret_meta_repo=secret_meta_repo + ) + + def retrieve_entity(self, order_id, keystone_id): + return self.repos.order_repo.get(entity_id=order_id, + keystone_id=keystone_id) + + def handle_processing(self, order, updated_meta): + self.handle_order(order, updated_meta) + + def handle_error(self, order, status, message, exception, + *args, **kwargs): + order.status = models.States.ERROR + order.error_status_code = status + order.error_reason = message + LOG.exception(u._("An error has occurred updating the order.")) + self.repos.order_repo.save(order) + + def handle_success(self, order, *args, **kwargs): + # TODO(chellygel): Handle sub-status on a pending order. + order.status = models.States.ACTIVE + self.repos.order_repo.save(order) + + def handle_order(self, order, updated_meta): + """Handle Order Update + + :param order: Order to update. + """ + + order_info = order.to_dict_fields() + order_type = order_info.get('type') + + if order_type == models.OrderType.CERTIFICATE: + # Update a certificate request + cert.modify_certificate_request(order, updated_meta, self.repos) + LOG.debug("...done updating a certificate order.") + else: + raise NotImplementedError( + 'Order type "{0}" not implemented.'.format(order_type)) + + LOG.debug("...done updating order.") diff --git a/barbican/tests/api/test_resources.py b/barbican/tests/api/test_resources.py index 4d9123acd..9751ae91a 100644 --- a/barbican/tests/api/test_resources.py +++ b/barbican/tests/api/test_resources.py @@ -89,6 +89,17 @@ def create_order(id_ref="id", return order +def create_order_with_meta(id_ref="id", order_type="certificate", meta={}, + status='PENDING'): + """Generate an Order entity instance with Metadata.""" + order = models.Order() + order.id = id_ref + order.type = order_type + order.meta = meta + order.status = status + return order + + def validate_datum(test, datum): test.assertIsNone(datum.kek_meta_extended) test.assertIsNotNone(datum.kek_meta_tenant) @@ -1632,6 +1643,7 @@ class WhenGettingOrDeletingOrderUsingOrderResource(FunctionalTest): self.order_repo = mock.MagicMock() self.order_repo.get.return_value = self.order + self.order_repo.save.return_value = None self.order_repo.delete_entity_by_id.return_value = None self.tenant_repo = mock.MagicMock() @@ -1670,6 +1682,99 @@ class WhenGettingOrDeletingOrderUsingOrderResource(FunctionalTest): self.assertEqual(resp.content_type, "application/json") +class WhenPuttingOrderWithMetadataUsingOrderResource(FunctionalTest): + def setUp(self): + super( + WhenPuttingOrderWithMetadataUsingOrderResource, self + ).setUp() + self.app = webtest.TestApp(app.PecanAPI(self.root)) + self.app.extra_environ = get_barbican_env(self.tenant_keystone_id) + + @property + def root(self): + self._init() + + class RootController(object): + orders = controllers.orders.OrdersController(self.tenant_repo, + self.order_repo, + self.queue_resource) + + return RootController() + + def _init(self): + self.tenant_keystone_id = 'keystoneid1234' + self.requestor = 'requestor1234' + + self.order = create_order_with_meta( + id_ref='id1', + order_type='certificate', + meta={'email': 'email@email.com'}, + status="PENDING" + ) + + self.order_repo = mock.MagicMock() + self.order_repo.get.return_value = self.order + self.order_repo.save.return_value = None + self.order_repo.delete_entity_by_id.return_value = None + + self.type = 'certificate' + self.meta = {'email': 'newemail@email.com'} + + self.params = {'type': self.type, 'meta': self.meta} + + self.tenant_repo = mock.MagicMock() + self.queue_resource = mock.MagicMock() + + def test_should_put_order(self): + resp = self.app.put_json( + '/orders/{0}/'.format(self.order.id), + self.params, + headers={ + 'Content-Type': 'application/json' + } + ) + + self.assertEqual(resp.status_int, 204) + self.order_repo.get.assert_called_once_with( + entity_id=self.order.id, + keystone_id=self.tenant_keystone_id, + suppress_exception=True) + + def test_should_fail_bad_type(self): + self.order['type'] = 'secret' + resp = self.app.put_json( + '/orders/{0}/'.format(self.order.id), + self.params, + headers={ + 'Content-Type': 'application/json' + }, + expect_errors=True + ) + + self.assertEqual(resp.status_int, 400) + self.order_repo.get.assert_called_once_with( + entity_id=self.order.id, + keystone_id=self.tenant_keystone_id, + suppress_exception=True) + + def test_should_fail_bad_status(self): + self.order['status'] = 'DONE' + resp = self.app.put_json( + '/orders/{0}/'.format(self.order.id), + self.params, + headers={ + 'Content-Type': 'application/json' + }, + expect_errors=True + ) + + self.assertEqual(resp.status_int, 400) + self.order_repo.get.assert_called_once_with( + entity_id=self.order.id, + keystone_id=self.tenant_keystone_id, + suppress_exception=True) + + class WhenCreatingTypeOrdersUsingOrdersResource(FunctionalTest): def setUp(self): super( diff --git a/barbican/tests/queue/test_client.py b/barbican/tests/queue/test_client.py index be3c8cb94..1ffe7fde1 100644 --- a/barbican/tests/queue/test_client.py +++ b/barbican/tests/queue/test_client.py @@ -40,6 +40,17 @@ class WhenUsingAsyncTaskClient(utils.BaseTestCase): order_id=self.order_id, keystone_id=self.keystone_id) + def test_should_update_order(self): + updated_meta = {} + self.client.update_order(order_id=self.order_id, + keystone_id=self.keystone_id, + updated_meta=updated_meta) + queue.get_client.assert_called_with() + self.mock_client.cast.assert_called_with({}, 'update_order', + order_id=self.order_id, + keystone_id=self.keystone_id, + updated_meta=updated_meta) + class WhenCreatingDirectTaskClient(utils.BaseTestCase): """Test using the synchronous task client (i.e. standalone mode).""" diff --git a/barbican/tests/queue/test_server.py b/barbican/tests/queue/test_server.py index 296026d02..21b3ff61e 100644 --- a/barbican/tests/queue/test_server.py +++ b/barbican/tests/queue/test_server.py @@ -36,6 +36,18 @@ class WhenUsingBeginOrderTask(utils.BaseTestCase): mock_begin_order.return_value.process.assert_called_with( self.order_id, self.keystone_id) + @mock.patch('barbican.tasks.resources.UpdateOrder') + def test_should_update_order(self, mock_update_order): + mock_update_order.return_value.process.return_value = None + updated_meta = {} + self.tasks.update_order(context=None, + order_id=self.order_id, + keystone_id=self.keystone_id, + updated_meta=updated_meta) + mock_update_order.return_value.process.assert_called_with( + self.order_id, self.keystone_id, updated_meta + ) + class WhenUsingTaskServer(utils.BaseTestCase): """Test using the asynchronous task client.""" diff --git a/etc/barbican/policy.json b/etc/barbican/policy.json index 450944223..cd07951bb 100644 --- a/etc/barbican/policy.json +++ b/etc/barbican/policy.json @@ -9,6 +9,7 @@ "orders:post": "rule:admin_or_creator", "orders:get": "rule:all_but_audit", "order:get": "rule:all_users", + "order:put":"rule:admin_or_creator", "order:delete": "rule:admin", "consumer:get": "rule:all_users", "consumers:get": "rule:all_users",