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",