diff --git a/barbican/api/__init__.py b/barbican/api/__init__.py index efe29fc32..346b95701 100644 --- a/barbican/api/__init__.py +++ b/barbican/api/__init__.py @@ -16,7 +16,8 @@ """ API handler for Cloudkeep's Barbican """ -import falcon +import pecan +from webob import exc from oslo.config import cfg from pkgutil import simplegeneric @@ -44,28 +45,6 @@ class ApiResource(object): pass -def abort(status=falcon.HTTP_500, message=None, req=None, resp=None): - """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 - """ - # Deal with odd Falcon behavior, whereby it does not encode error - # response messages if requests specify a non-JSON Accept header. - # If the Accept header does specify JSON, then Falcon properly - # JSON-ifies the error message. - if resp and message: - if req and req.accept != 'application/json': - resp.set_header('Content-Type', 'text/plain') - resp.body = message - raise falcon.HTTPError(status, message) - - def load_body(req, resp=None, validator=None): """Helper function for loading an HTTP request body from JSON. This body is placed into into a Python dictionary. @@ -76,33 +55,33 @@ def load_body(req, resp=None, validator=None): :return: A dict of values from the JSON request. """ try: - raw_json = req.stream.read(CONF.max_allowed_request_size_in_bytes) + body = req.body_file.read(CONF.max_allowed_request_size_in_bytes) except IOError: LOG.exception("Problem reading request JSON stream.") - abort(falcon.HTTP_500, 'Read Error', req, resp) + pecan.abort(500, 'Read Error') try: #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) + parsed_body = json.loads(body) strip_whitespace(parsed_body) except ValueError: LOG.exception("Problem loading request JSON.") - abort(falcon.HTTP_400, 'Malformed JSON', req, resp) + pecan.abort(400, 'Malformed JSON') if validator: try: parsed_body = validator.validate(parsed_body) except exception.InvalidObject as e: LOG.exception("Failed to validate JSON information") - abort(falcon.HTTP_400, str(e), req, resp) + pecan.abort(400, str(e)) except exception.UnsupportedField as e: LOG.exception("Provided field value is not supported") - abort(falcon.HTTP_400, str(e), req, resp) + pecan.abort(400, str(e)) except exception.LimitExceeded as e: LOG.exception("Data limit exceeded") - abort(falcon.HTTP_413, str(e), req, resp) + pecan.abort(413, str(e)) return parsed_body @@ -118,65 +97,65 @@ def generate_safe_exception_message(operation_name, excep): :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 + :return: (status, message) where 'status' is one of the webob.exc.HTTP_xxx codes, and 'message' is the sanitized message associated with the error. """ message = None reason = None - status = falcon.HTTP_500 + status = 500 try: raise excep - except falcon.HTTPError as f: + except exc.HTTPError as f: message = f.title status = f.status except policy.PolicyNotAuthorized: message = u._('{0} attempt not allowed - ' 'please review your ' 'user/tenant privileges').format(operation_name) - status = falcon.HTTP_403 + status = 403 except em.CryptoContentTypeNotSupportedException as cctnse: reason = u._("content-type of '{0}' not " "supported").format(cctnse.content_type) - status = falcon.HTTP_400 + status = 400 except em.CryptoContentEncodingNotSupportedException as cc: reason = u._("content-encoding of '{0}' not " "supported").format(cc.content_encoding) - status = falcon.HTTP_400 + status = 400 except em.CryptoAcceptNotSupportedException as canse: reason = u._("accept of '{0}' not " "supported").format(canse.accept) - status = falcon.HTTP_406 + status = 406 except em.CryptoNoPayloadProvidedException: reason = u._("No payload provided") - status = falcon.HTTP_400 + status = 400 except em.CryptoNoSecretOrDataFoundException: reason = u._("Not Found. Sorry but your secret is in " "another castle") - status = falcon.HTTP_404 + status = 404 except em.CryptoPayloadDecodingError: reason = u._("Problem decoding payload") - status = falcon.HTTP_400 + status = 400 except em.CryptoContentEncodingMustBeBase64: reason = u._("Text-based binary secret payloads must " "specify a content-encoding of 'base64'") - status = falcon.HTTP_400 + status = 400 except em.CryptoAlgorithmNotSupportedException: reason = u._("No plugin was found that supports the " "requested algorithm") - status = falcon.HTTP_400 + status = 400 except em.CryptoSupportedPluginNotFound: reason = u._("No plugin was found that could support " "your request") - status = falcon.HTTP_400 + status = 400 except exception.NoDataToProcess: reason = u._("No information provided to process") - status = falcon.HTTP_400 + status = 400 except exception.LimitExceeded: reason = u._("Provided information too large " "to process") - status = falcon.HTTP_413 + status = 413 except Exception: message = u._('{0} failure seen - please contact site ' 'administrator.').format(operation_name) diff --git a/barbican/api/app.py b/barbican/api/app.py index 624c37770..3cc1f7a2e 100644 --- a/barbican/api/app.py +++ b/barbican/api/app.py @@ -16,8 +16,10 @@ """ API application handler for Cloudkeep's Barbican """ +import json -import falcon +import pecan +from webob import exc as webob_exc try: import newrelic.agent @@ -27,7 +29,8 @@ except ImportError: from oslo.config import cfg -from barbican.api import resources as res +from barbican.api.controllers import (performance, orders, secrets, containers, + versions) from barbican.common import config from barbican.crypto import extension_manager as ext from barbican.openstack.common import log @@ -37,6 +40,46 @@ if newrelic_loaded: newrelic.agent.initialize('/etc/newrelic/newrelic.ini') +class JSONErrorHook(pecan.hooks.PecanHook): + + def on_error(self, state, exc): + if isinstance(exc, webob_exc.HTTPError): + exc.body = json.dumps({ + 'code': exc.status_int, + 'title': exc.title, + 'description': exc.detail + }) + return exc.body + + +class PecanAPI(pecan.Pecan): + + # For performance testing only + performance_uri = 'mu-1a90dfd0-7e7abba4-4e459908-fc097d60' + performance_controller = performance.PerformanceController() + + def __init__(self, *args, **kwargs): + kwargs.setdefault('hooks', []).append(JSONErrorHook()) + super(PecanAPI, self).__init__(*args, **kwargs) + + def route(self, req, node, path): + # Pop the tenant ID from the path + path = path.split('/')[1:] + first_path = path.pop(0) + + # Route to the special performance controller + if first_path == self.performance_uri: + return self.performance_controller.index, [] + + path = '/%s' % '/'.join(path) + controller, remainder = super(PecanAPI, self).route(req, node, path) + + # Pass the tenant ID as the first argument to the controller + remainder = list(remainder) + remainder.insert(0, first_path) + return controller, remainder + + def create_main_app(global_config, **local_conf): """uWSGI factory method for the Barbican-API application""" @@ -51,50 +94,21 @@ def create_main_app(global_config, **local_conf): CONF = cfg.CONF queue.init(CONF) - # Resources - secrets = res.SecretsResource(crypto_mgr) - secret = res.SecretResource(crypto_mgr) - orders = res.OrdersResource() - order = res.OrderResource() - containers = res.ContainersResource() - container = res.ContainerResource() + class RootController(object): + secrets = secrets.SecretsController(crypto_mgr) + orders = orders.OrdersController() + containers = containers.ContainersController() - # For performance testing only - performance = res.PerformanceResource() - performance_uri = 'mu-1a90dfd0-7e7abba4-4e459908-fc097d60' - - wsgi_app = api = falcon.API() + wsgi_app = PecanAPI(RootController(), force_canonical=False) if newrelic_loaded: wsgi_app = newrelic.agent.WSGIApplicationWrapper(wsgi_app) - - api.add_route('/{keystone_id}/secrets', secrets) - api.add_route('/{keystone_id}/secrets/{secret_id}', secret) - api.add_route('/{keystone_id}/orders', orders) - api.add_route('/{keystone_id}/orders/{order_id}', order) - api.add_route('/{keystone_id}/containers/', containers) - api.add_route('/{keystone_id}/containers/{container_id}', container) - - # For performance testing only - api.add_route('/{0}'.format(performance_uri), performance) - return wsgi_app def create_admin_app(global_config, **local_conf): config.parse_args() - - versions = res.VersionResource() - wsgi_app = api = falcon.API() - api.add_route('/', versions) - + wsgi_app = pecan.make_app(versions.VersionController()) return wsgi_app -def create_version_app(global_config, **local_conf): - config.parse_args() - - versions = res.VersionResource() - wsgi_app = api = falcon.API() - api.add_route('/', versions) - - return wsgi_app +create_version_app = create_admin_app diff --git a/barbican/api/controllers/__init__.py b/barbican/api/controllers/__init__.py new file mode 100644 index 000000000..54d34b0cf --- /dev/null +++ b/barbican/api/controllers/__init__.py @@ -0,0 +1,97 @@ +# 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 pecan +from webob import exc + +from barbican import api +from barbican.common import utils +from barbican.openstack.common import gettextutils as u + +LOG = utils.getLogger(__name__) + + +def is_json_request_accept(req): + """Test if http request 'accept' header configured for JSON response. + + :param req: HTTP request + :return: True if need to return JSON response. + """ + return not req.accept or req.accept.header_value == 'application/json' \ + or req.accept.header_value == '*/*' + + +def enforce_rbac(req, action_name, keystone_id=None): + """Enforce RBAC based on 'request' information.""" + if action_name and 'barbican.context' in req.environ: + + # Prepare credentials information. + ctx = req.environ['barbican.context'] # Placed here by context.py + # middleware + credentials = { + 'roles': ctx.roles, + 'user': ctx.user, + 'tenant': ctx.tenant, + } + + # Verify keystone_id matches the tenant ID. + if keystone_id and keystone_id != ctx.tenant: + pecan.abort(403, u._("URI tenant does not match " + "authenticated tenant.")) + + # Enforce special case: secret GET decryption + if 'secret:get' == action_name and not is_json_request_accept(req): + action_name = 'secret:decrypt' # Override to perform special rules + + # Enforce access controls. + ctx.policy_enforcer.enforce(action_name, {}, credentials, + do_raise=True) + + +def handle_rbac(action_name='default'): + """Decorator handling RBAC enforcement on behalf of REST verb methods.""" + + def rbac_decorator(fn): + def enforcer(inst, *args, **kwargs): + + # Enforce RBAC rules. + enforce_rbac(pecan.request, action_name, + keystone_id=kwargs.get('keystone_id')) + + # Execute guarded method now. + return fn(inst, *args, **kwargs) + + return enforcer + + return rbac_decorator + + +def handle_exceptions(operation_name=u._('System')): + """Decorator handling generic exceptions from REST methods.""" + + def exceptions_decorator(fn): + + def handler(inst, *args, **kwargs): + try: + return fn(inst, *args, **kwargs) + except exc.HTTPError as f: + LOG.exception('Webob error seen') + raise f # Already converted to Webob exception, just reraise + except Exception as e: + status, message = api.generate_safe_exception_message( + operation_name, e) + LOG.exception(message) + pecan.abort(status, message) + + return handler + + return exceptions_decorator diff --git a/barbican/api/controllers/containers.py b/barbican/api/controllers/containers.py new file mode 100644 index 000000000..7855d6cb4 --- /dev/null +++ b/barbican/api/controllers/containers.py @@ -0,0 +1,154 @@ +# 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 pecan + +from barbican import api +from barbican.api.controllers import hrefs, handle_exceptions, handle_rbac +from barbican.openstack.common import gettextutils as u +from barbican.common import exception +from barbican.common import resources as res +from barbican.common import utils +from barbican.common import validators +from barbican.model import models +from barbican.model import repositories as repo + +LOG = utils.getLogger(__name__) + + +def _container_not_found(): + """Throw exception indicating container not found.""" + pecan.abort(404, u._('Not Found. Sorry but your container is in ' + 'another castle.')) + + +class ContainerController(object): + """Handles Container entity retrieval and deletion requests.""" + + def __init__(self, container_id, tenant_repo=None, container_repo=None): + self.container_id = container_id + self.tenant_repo = tenant_repo or repo.TenantRepo() + self.container_repo = container_repo or repo.ContainerRepo() + self.validator = validators.ContainerValidator() + + @pecan.expose(generic=True, template='json') + @handle_exceptions(u._('Container retrieval')) + @handle_rbac('container:get') + def index(self, keystone_id): + container = self.container_repo.get(entity_id=self.container_id, + keystone_id=keystone_id, + suppress_exception=True) + if not container: + _container_not_found() + + dict_fields = container.to_dict_fields() + + for secret_ref in dict_fields['secret_refs']: + hrefs.convert_to_hrefs(keystone_id, secret_ref) + + return hrefs.convert_to_hrefs( + keystone_id, + hrefs.convert_to_hrefs(keystone_id, dict_fields) + ) + + @index.when(method='DELETE', template='') + @handle_exceptions(u._('Container deletion')) + @handle_rbac('container:delete') + def on_delete(self, keystone_id): + + try: + self.container_repo.delete_entity_by_id( + entity_id=self.container_id, + keystone_id=keystone_id + ) + except exception.NotFound: + LOG.exception('Problem deleting container') + _container_not_found() + + +class ContainersController(object): + """ Handles Container creation requests. """ + + def __init__(self, tenant_repo=None, container_repo=None, + secret_repo=None): + + self.tenant_repo = tenant_repo or repo.TenantRepo() + self.container_repo = container_repo or repo.ContainerRepo() + self.secret_repo = secret_repo or repo.SecretRepo() + self.validator = validators.ContainerValidator() + + @pecan.expose() + def _lookup(self, container_id, *remainder): + return ContainerController(container_id, self.tenant_repo, + self.container_repo), remainder + + @pecan.expose(generic=True, template='json') + @handle_exceptions(u._('Containers(s) retrieval')) + @handle_rbac('containers:get') + def index(self, keystone_id, **kw): + LOG.debug('Start containers on_get ' + 'for tenant-ID {0}:'.format(keystone_id)) + + result = self.container_repo.get_by_create_date( + keystone_id, + offset_arg=int(kw.get('offset')), + limit_arg=int(kw.get('limit')), + suppress_exception=True + ) + + containers, offset, limit, total = result + + if not containers: + resp_ctrs_overall = {'containers': [], 'total': total} + else: + resp_ctrs = [ + hrefs.convert_to_hrefs(keystone_id, c.to_dict_fields()) + for c in containers + ] + resp_ctrs_overall = hrefs.add_nav_hrefs('containers', + keystone_id, offset, + limit, total, + {'containers': resp_ctrs}) + resp_ctrs_overall.update({'total': total}) + + return resp_ctrs_overall + + @index.when(method='POST', template='json') + @handle_exceptions(u._('Container creation')) + @handle_rbac('containers:post') + def on_post(self, keystone_id): + + tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) + + data = api.load_body(pecan.request, validator=self.validator) + LOG.debug('Start on_post...{0}'.format(data)) + + new_container = models.Container(data) + new_container.tenant_id = tenant.id + + #TODO: (hgedikli) performance optimizations + for secret_ref in new_container.container_secrets: + secret = self.secret_repo.get(entity_id=secret_ref.secret_id, + keystone_id=keystone_id, + suppress_exception=True) + if not secret: + pecan.abort(404, u._("Secret provided for '%s'" + " doesn't exist." % secret_ref.name)) + + self.container_repo.create_from(new_container) + + pecan.response.status = 202 + pecan.response.headers['Location'] = '/{0}/containers/{1}'.format( + keystone_id, new_container.id + ) + url = hrefs.convert_container_to_href(keystone_id, new_container.id) + return {'container_ref': url} diff --git a/barbican/api/controllers/hrefs.py b/barbican/api/controllers/hrefs.py new file mode 100644 index 000000000..1489b470d --- /dev/null +++ b/barbican/api/controllers/hrefs.py @@ -0,0 +1,118 @@ +# 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. + +from barbican.common import utils + + +def convert_secret_to_href(keystone_id, secret_id): + """Convert the tenant/secret IDs to a HATEOS-style href.""" + if secret_id: + resource = 'secrets/' + secret_id + else: + resource = 'secrets/????' + return utils.hostname_for_refs(keystone_id=keystone_id, resource=resource) + + +def convert_order_to_href(keystone_id, order_id): + """Convert the tenant/order IDs to a HATEOS-style href.""" + if order_id: + resource = 'orders/' + order_id + else: + resource = 'orders/????' + return utils.hostname_for_refs(keystone_id=keystone_id, resource=resource) + + +def convert_container_to_href(keystone_id, container_id): + """Convert the tenant/container IDs to a HATEOS-style href.""" + if container_id: + resource = 'containers/' + container_id + else: + resource = 'containers/????' + return utils.hostname_for_refs(keystone_id=keystone_id, resource=resource) + + +#TODO: (hgedikli) handle list of fields in here +def convert_to_hrefs(keystone_id, fields): + """Convert id's within a fields dict to HATEOS-style hrefs.""" + if 'secret_id' in fields: + fields['secret_ref'] = convert_secret_to_href(keystone_id, + fields['secret_id']) + del fields['secret_id'] + + if 'order_id' in fields: + fields['order_ref'] = convert_order_to_href(keystone_id, + fields['order_id']) + del fields['order_id'] + + if 'container_id' in fields: + fields['container_ref'] = \ + convert_container_to_href(keystone_id, fields['container_id']) + del fields['container_id'] + + return fields + + +def convert_list_to_href(resources_name, keystone_id, offset, limit): + """Supports pretty output of paged-list hrefs. + + Convert the tenant ID and offset/limit info to a HATEOS-style href + suitable for use in a list navigation paging interface. + """ + resource = '{0}?limit={1}&offset={2}'.format(resources_name, limit, + offset) + return utils.hostname_for_refs(keystone_id=keystone_id, resource=resource) + + +def previous_href(resources_name, keystone_id, offset, limit): + """Supports pretty output of previous-page hrefs. + + Create a HATEOS-style 'previous' href suitable for use in a list + navigation paging interface, assuming the provided values are the + currently viewed page. + """ + offset = max(0, offset - limit) + return convert_list_to_href(resources_name, keystone_id, offset, limit) + + +def next_href(resources_name, keystone_id, offset, limit): + """Supports pretty output of next-page hrefs. + + Create a HATEOS-style 'next' href suitable for use in a list + navigation paging interface, assuming the provided values are the + currently viewed page. + """ + offset = offset + limit + return convert_list_to_href(resources_name, keystone_id, offset, limit) + + +def add_nav_hrefs(resources_name, keystone_id, offset, limit, + total_elements, data): + """Adds next and/or previous hrefs to paged list responses. + + :param resources_name: Name of api resource + :param keystone_id: Keystone id of the tenant + :param offset: Element number (ie. index) where current page starts + :param limit: Max amount of elements listed on current page + :param num_elements: Total number of elements + :returns: augmented dictionary with next and/or previous hrefs + """ + if offset > 0: + data.update({'previous': previous_href(resources_name, + keystone_id, + offset, + limit)}) + if total_elements > (offset + limit): + data.update({'next': next_href(resources_name, + keystone_id, + offset, + limit)}) + return data diff --git a/barbican/api/controllers/orders.py b/barbican/api/controllers/orders.py new file mode 100644 index 000000000..e3dc57e50 --- /dev/null +++ b/barbican/api/controllers/orders.py @@ -0,0 +1,188 @@ +# 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 pecan + +from barbican import api +from barbican.api.controllers import hrefs, handle_exceptions, handle_rbac +from barbican.openstack.common import gettextutils as u +from barbican.common import exception +from barbican.common import resources as res +from barbican.common import utils +from barbican.common import validators +from barbican.model import models +from barbican.model import repositories as repo +from barbican.queue import client as async_client + +LOG = utils.getLogger(__name__) + + +def _order_not_found(): + """Throw exception indicating order not found.""" + pecan.abort(404, u._('Not Found. Sorry but your order is in ' + 'another castle.')) + + +def _secret_not_in_order(): + """Throw exception that secret info is not available in the order.""" + pecan.abort(400, u._("Secret metadata expected but not received.")) + + +def _order_update_not_supported(): + """Throw exception that PUT operation is not supported for orders.""" + pecan.abort(405, u._("Order update is not supported.")) + + +class OrderController(object): + + """Handles Order retrieval and deletion requests.""" + + def __init__(self, order_id, order_repo=None): + self.order_id = order_id + self.repo = order_repo or repo.OrderRepo() + + @pecan.expose(generic=True, template='json') + @handle_exceptions(u._('Order retrieval')) + @handle_rbac('order:get') + def index(self, keystone_id): + order = self.repo.get(entity_id=self.order_id, keystone_id=keystone_id, + suppress_exception=True) + if not order: + _order_not_found() + + return hrefs.convert_to_hrefs(keystone_id, order.to_dict_fields()) + + @index.when(method='PUT') + @handle_exceptions(u._('Order update')) + def on_put(self, keystone_id): + _order_update_not_supported() + + @index.when(method='DELETE') + @handle_exceptions(u._('Order deletion')) + @handle_rbac('order:delete') + def on_delete(self, keystone_id): + + try: + self.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() + + +class OrdersController(object): + """Handles Order requests for Secret creation.""" + + def __init__(self, tenant_repo=None, order_repo=None, + queue_resource=None): + + LOG.debug('Creating OrdersController') + self.tenant_repo = tenant_repo or repo.TenantRepo() + self.order_repo = order_repo or repo.OrderRepo() + self.queue = queue_resource or async_client.TaskClient() + self.validator = validators.NewOrderValidator() + + @pecan.expose() + def _lookup(self, order_id, *remainder): + return OrderController(order_id, self.order_repo), remainder + + @pecan.expose(generic=True, template='json') + @handle_exceptions(u._('Order(s) retrieval')) + @handle_rbac('orders:get') + def index(self, keystone_id, **kw): + LOG.debug('Start orders on_get ' + 'for tenant-ID {0}:'.format(keystone_id)) + + offset = kw.get('offset') + if offset is not None: + try: + offset = int(offset) + except ValueError: + # as per Github issue 171, if offset is invalid then + # the default should be used. + offset = None + + limit = kw.get('limit') + if limit is not None: + try: + limit = int(limit) + except ValueError: + # as per Github issue 171, if limit is invalid then + # the default should be used. + limit = None + + result = self.order_repo \ + .get_by_create_date(keystone_id, + offset_arg=offset, + limit_arg=limit, + suppress_exception=True) + orders, offset, limit, total = result + + if not orders: + orders_resp_overall = {'orders': [], + 'total': total} + else: + orders_resp = [ + hrefs.convert_to_hrefs(keystone_id, o.to_dict_fields()) + for o in orders + ] + orders_resp_overall = hrefs.add_nav_hrefs('orders', keystone_id, + offset, limit, total, + {'orders': orders_resp}) + orders_resp_overall.update({'total': total}) + + return orders_resp_overall + + @pecan.expose(generic=True, template='json') + @handle_exceptions(u._('Order update')) + @handle_rbac('orders:put') + def on_put(self, keystone_id): + _order_update_not_supported() + + @index.when(method='POST', template='json') + @handle_exceptions(u._('Order creation')) + @handle_rbac('orders:post') + def on_post(self, keystone_id): + + tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) + + body = api.load_body(pecan.request, validator=self.validator) + LOG.debug('Start on_post...{0}'.format(body)) + + if 'secret' not in body: + _secret_not_in_order() + secret_info = body['secret'] + name = secret_info.get('name') + LOG.debug('Secret to create is {0}'.format(name)) + + new_order = models.Order() + new_order.secret_name = secret_info.get('name') + new_order.secret_algorithm = secret_info.get('algorithm') + new_order.secret_bit_length = secret_info.get('bit_length', 0) + new_order.secret_mode = secret_info.get('mode') + 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 + self.order_repo.create_from(new_order) + + # Send to workers to process. + self.queue.process_order(order_id=new_order.id, + keystone_id=keystone_id) + + pecan.response.status = 202 + pecan.response.headers['Location'] = '/{0}/orders/{1}'.format( + keystone_id, new_order.id + ) + url = hrefs.convert_order_to_href(keystone_id, new_order.id) + return {'order_ref': url} diff --git a/barbican/api/controllers/performance.py b/barbican/api/controllers/performance.py new file mode 100644 index 000000000..21de059ee --- /dev/null +++ b/barbican/api/controllers/performance.py @@ -0,0 +1,27 @@ +# 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 pecan + +from barbican.common import utils + +LOG = utils.getLogger(__name__) + + +class PerformanceController(object): + + def __init__(self): + LOG.debug('=== Creating PerformanceController ===') + + @pecan.expose() + def index(self): + return '42' diff --git a/barbican/api/controllers/secrets.py b/barbican/api/controllers/secrets.py new file mode 100644 index 000000000..b93a1c512 --- /dev/null +++ b/barbican/api/controllers/secrets.py @@ -0,0 +1,252 @@ +# 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 urllib +import mimetypes + +import pecan + +from barbican import api +from barbican.api.controllers import (hrefs, handle_exceptions, handle_rbac, + is_json_request_accept) +from barbican.openstack.common import gettextutils as u +from barbican.model import repositories as repo +from barbican.crypto import mime_types +from barbican.common import exception +from barbican.common import resources as res +from barbican.common import utils +from barbican.common import validators + +LOG = utils.getLogger(__name__) + + +def allow_all_content_types(f): + cfg = pecan.util._cfg(f) + for value in mimetypes.types_map.values(): + cfg.setdefault('content_types', {})[value] = '' + return f + + +def _secret_not_found(): + """Throw exception indicating secret not found.""" + pecan.abort(404, u._('Not Found. Sorry but your secret is in ' + 'another castle.')) + + +def _secret_already_has_data(): + """Throw exception that the secret already has data.""" + pecan.abort(409, u._("Secret already has data, cannot modify it.")) + + +class SecretController(object): + """Handles Secret retrieval and deletion requests.""" + + def __init__(self, secret_id, crypto_manager, + tenant_repo=None, secret_repo=None, datum_repo=None, + kek_repo=None): + LOG.debug('=== Creating SecretController ===') + self.secret_id = secret_id + self.crypto_manager = crypto_manager + self.tenant_repo = tenant_repo or repo.TenantRepo() + self.repo = secret_repo or repo.SecretRepo() + self.datum_repo = datum_repo or repo.EncryptedDatumRepo() + self.kek_repo = kek_repo or repo.KEKDatumRepo() + + @pecan.expose(generic=True) + @allow_all_content_types + @handle_exceptions(u._('Secret retrieval')) + @handle_rbac('secret:get') + def index(self, keystone_id): + + secret = self.repo.get(entity_id=self.secret_id, + keystone_id=keystone_id, + suppress_exception=True) + if not secret: + _secret_not_found() + + if is_json_request_accept(pecan.request): + # Metadata-only response, no decryption necessary. + pecan.override_template('json', 'application/json') + secret_fields = mime_types.augment_fields_with_content_types( + secret) + return hrefs.convert_to_hrefs(keystone_id, secret_fields) + else: + tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) + pecan.override_template('', pecan.request.accept.header_value) + return self.crypto_manager.decrypt( + pecan.request.accept.header_value, + secret, + tenant + ) + + @index.when(method='PUT') + @allow_all_content_types + @handle_exceptions(u._('Secret update')) + @handle_rbac('secret:put') + def on_put(self, keystone_id): + + if not pecan.request.content_type or \ + pecan.request.content_type == 'application/json': + pecan.abort( + 415, + u._("Content-Type of '{0}' is not supported for PUT.").format( + pecan.request.content_type + ) + ) + + secret = self.repo.get(entity_id=self.secret_id, + keystone_id=keystone_id, + suppress_exception=True) + if not secret: + _secret_not_found() + + if secret.encrypted_data: + _secret_already_has_data() + + tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) + content_type = pecan.request.content_type + content_encoding = pecan.request.headers.get('Content-Encoding') + + res.create_encrypted_datum(secret, + pecan.request.body, + content_type, + content_encoding, + tenant, + self.crypto_manager, + self.datum_repo, + self.kek_repo) + + @index.when(method='DELETE') + @handle_exceptions(u._('Secret deletion')) + @handle_rbac('secret:delete') + def on_delete(self, keystone_id): + + try: + self.repo.delete_entity_by_id(entity_id=self.secret_id, + keystone_id=keystone_id) + except exception.NotFound: + LOG.exception('Problem deleting secret') + _secret_not_found() + + +class SecretsController(object): + """Handles Secret creation requests.""" + + def __init__(self, crypto_manager, + tenant_repo=None, secret_repo=None, + tenant_secret_repo=None, datum_repo=None, kek_repo=None): + LOG.debug('Creating SecretsController') + self.tenant_repo = tenant_repo or repo.TenantRepo() + self.secret_repo = secret_repo or repo.SecretRepo() + self.tenant_secret_repo = tenant_secret_repo or repo.TenantSecretRepo() + self.datum_repo = datum_repo or repo.EncryptedDatumRepo() + self.kek_repo = kek_repo or repo.KEKDatumRepo() + self.crypto_manager = crypto_manager + self.validator = validators.NewSecretValidator() + + @pecan.expose() + def _lookup(self, secret_id, *remainder): + return SecretController(secret_id, self.crypto_manager, + self.tenant_repo, self.secret_repo, + self.datum_repo, self.kek_repo), remainder + + @pecan.expose(generic=True, template='json') + @handle_exceptions(u._('Secret(s) retrieval')) + @handle_rbac('secrets:get') + def index(self, keystone_id, **kw): + LOG.debug('Start secrets on_get ' + 'for tenant-ID {0}:'.format(keystone_id)) + + name = kw.get('name') + if name: + name = urllib.unquote_plus(name) + + offset = kw.get('offset') + if offset is not None: + try: + offset = int(offset) + except ValueError: + # as per Github issue 171, if offset is invalid then + # the default should be used. + offset = None + + limit = kw.get('limit') + if limit is not None: + try: + limit = int(limit) + except ValueError: + # as per Github issue 171, if limit is invalid then + # the default should be used. + limit = None + + bits = kw.get('bits') + if bits is not None: + try: + bits = int(bits) + except ValueError: + # as per Github issue 171, if bits is invalid then + # the default should be used. + bits = None + + result = self.secret_repo.get_by_create_date( + keystone_id, + offset_arg=offset, + limit_arg=limit, + name=name, + alg=kw.get('alg'), + mode=kw.get('mode'), + bits=bits, + suppress_exception=True + ) + + secrets, offset, limit, total = result + + if not secrets: + secrets_resp_overall = {'secrets': [], + 'total': total} + else: + secret_fields = lambda s: mime_types\ + .augment_fields_with_content_types(s) + secrets_resp = [ + hrefs.convert_to_hrefs(keystone_id, secret_fields(s)) + for s in secrets + ] + secrets_resp_overall = hrefs.add_nav_hrefs( + 'secrets', keystone_id, offset, limit, total, + {'secrets': secrets_resp} + ) + secrets_resp_overall.update({'total': total}) + + return secrets_resp_overall + + @index.when(method='POST', template='json') + @handle_exceptions(u._('Secret creation')) + @handle_rbac('secrets:post') + def on_post(self, keystone_id): + LOG.debug('Start on_post for tenant-ID {0}:...'.format(keystone_id)) + + data = api.load_body(pecan.request, validator=self.validator) + tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) + + new_secret = res.create_secret(data, tenant, self.crypto_manager, + self.secret_repo, + self.tenant_secret_repo, + self.datum_repo, + self.kek_repo) + + pecan.response.status = 201 + pecan.response.headers['Location'] = '/{0}/secrets/{1}'.format( + keystone_id, new_secret.id + ) + url = hrefs.convert_secret_to_href(keystone_id, new_secret.id) + LOG.debug('URI to secret is {0}'.format(url)) + return {'secret_ref': url} diff --git a/barbican/api/controllers/versions.py b/barbican/api/controllers/versions.py new file mode 100644 index 000000000..69fe10df6 --- /dev/null +++ b/barbican/api/controllers/versions.py @@ -0,0 +1,35 @@ +# 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 pecan + +from barbican.api.controllers import handle_exceptions, handle_rbac +from barbican.openstack.common import gettextutils as u +from barbican.common import utils +from barbican import version + +LOG = utils.getLogger(__name__) + + +class VersionController(object): + + def __init__(self): + LOG.debug('=== Creating VersionController ===') + + @pecan.expose('json') + @handle_exceptions(u._('Version retrieval')) + @handle_rbac('version:get') + def index(self): + return { + 'v1': 'current', + 'build': version.__version__ + } diff --git a/barbican/api/resources.py b/barbican/api/resources.py deleted file mode 100644 index 82e2a3941..000000000 --- a/barbican/api/resources.py +++ /dev/null @@ -1,689 +0,0 @@ -# Copyright (c) 2013-2014 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. - -""" -API-facing resource controllers. -""" -import urllib -import falcon - -from barbican import api -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 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.queue import client as async_client -from barbican import version - - -LOG = utils.getLogger(__name__) - - -def _not_allowed(message, req, resp): - """Throw exception for forbidden resource.""" - api.abort(falcon.HTTP_403, message, req, resp) - - -def _secret_not_found(req, resp): - """Throw exception indicating secret not found.""" - api.abort(falcon.HTTP_404, u._('Not Found. Sorry but your secret is in ' - 'another castle.'), req, resp) - - -def _order_not_found(req, resp): - """Throw exception indicating order not found.""" - api.abort(falcon.HTTP_404, u._('Not Found. Sorry but your order is in ' - 'another castle.'), req, resp) - - -def _container_not_found(req, resp): - """Throw exception indicating container not found.""" - api.abort(falcon.HTTP_404, u._('Not Found. Sorry but your container ' - 'is in ' - 'another castle.'), req, resp) - - -def _put_accept_incorrect(ct, req, resp): - """Throw exception indicating request content-type is not supported.""" - api.abort(falcon.HTTP_415, - u._("Content-Type of '{0}' is not " - "supported for PUT.").format(ct), - req, resp) - - -def _secret_already_has_data(req, resp): - """Throw exception that the secret already has data.""" - api.abort(falcon.HTTP_409, - u._("Secret already has data, cannot modify it."), req, resp) - - -def _secret_not_in_order(req, resp): - """Throw exception that secret info is not available in the order.""" - api.abort(falcon.HTTP_400, - u._("Secret metadata expected but not received."), req, resp) - - -def json_handler(obj): - """Convert objects into json-friendly equivalents.""" - return obj.isoformat() if hasattr(obj, 'isoformat') else obj - - -def convert_secret_to_href(keystone_id, secret_id): - """Convert the tenant/secret IDs to a HATEOS-style href.""" - if secret_id: - resource = 'secrets/' + secret_id - else: - resource = 'secrets/????' - return utils.hostname_for_refs(keystone_id=keystone_id, resource=resource) - - -def convert_order_to_href(keystone_id, order_id): - """Convert the tenant/order IDs to a HATEOS-style href.""" - if order_id: - resource = 'orders/' + order_id - else: - resource = 'orders/????' - return utils.hostname_for_refs(keystone_id=keystone_id, resource=resource) - - -def convert_container_to_href(keystone_id, container_id): - """Convert the tenant/container IDs to a HATEOS-style href.""" - if container_id: - resource = 'containers/' + container_id - else: - resource = 'containers/????' - return utils.hostname_for_refs(keystone_id=keystone_id, resource=resource) - - -#TODO: (hgedikli) handle list of fields in here -def convert_to_hrefs(keystone_id, fields): - """Convert id's within a fields dict to HATEOS-style hrefs.""" - if 'secret_id' in fields: - fields['secret_ref'] = convert_secret_to_href(keystone_id, - fields['secret_id']) - del fields['secret_id'] - - if 'order_id' in fields: - fields['order_ref'] = convert_order_to_href(keystone_id, - fields['order_id']) - del fields['order_id'] - - if 'container_id' in fields: - fields['container_ref'] = \ - convert_container_to_href(keystone_id, fields['container_id']) - del fields['container_id'] - - return fields - - -def convert_list_to_href(resources_name, keystone_id, offset, limit): - """Supports pretty output of paged-list hrefs. - - Convert the tenant ID and offset/limit info to a HATEOS-style href - suitable for use in a list navigation paging interface. - """ - resource = '{0}?limit={1}&offset={2}'.format(resources_name, limit, - offset) - return utils.hostname_for_refs(keystone_id=keystone_id, resource=resource) - - -def previous_href(resources_name, keystone_id, offset, limit): - """Supports pretty output of previous-page hrefs. - - Create a HATEOS-style 'previous' href suitable for use in a list - navigation paging interface, assuming the provided values are the - currently viewed page. - """ - offset = max(0, offset - limit) - return convert_list_to_href(resources_name, keystone_id, offset, limit) - - -def next_href(resources_name, keystone_id, offset, limit): - """Supports pretty output of next-page hrefs. - - Create a HATEOS-style 'next' href suitable for use in a list - navigation paging interface, assuming the provided values are the - currently viewed page. - """ - offset = offset + limit - return convert_list_to_href(resources_name, keystone_id, offset, limit) - - -def add_nav_hrefs(resources_name, keystone_id, offset, limit, - total_elements, data): - """Adds next and/or previous hrefs to paged list responses. - - :param resources_name: Name of api resource - :param keystone_id: Keystone id of the tenant - :param offset: Element number (ie. index) where current page starts - :param limit: Max amount of elements listed on current page - :param num_elements: Total number of elements - :returns: augmented dictionary with next and/or previous hrefs - """ - if offset > 0: - data.update({'previous': previous_href(resources_name, - keystone_id, - offset, - limit)}) - if total_elements > (offset + limit): - data.update({'next': next_href(resources_name, - keystone_id, - offset, - limit)}) - return data - - -def is_json_request_accept(req): - """Test if http request 'accept' header configured for JSON response. - - :param req: HTTP request - :return: True if need to return JSON response. - """ - return not req.accept or req.accept == 'application/json' \ - or req.accept == '*/*' - - -def enforce_rbac(req, resp, action_name, keystone_id=None): - """Enforce RBAC based on 'request' information.""" - if action_name and 'barbican.context' in req.env: - - # Prepare credentials information. - ctx = req.env['barbican.context'] # Placed here by context.py - # middleware - credentials = { - 'roles': ctx.roles, - 'user': ctx.user, - 'tenant': ctx.tenant, - } - - # Verify keystone_id matches the tenant ID. - if keystone_id and keystone_id != ctx.tenant: - _not_allowed(u._("URI tenant does not match " - "authenticated tenant."), req, resp) - - # Enforce special case: secret GET decryption - if 'secret:get' == action_name and not is_json_request_accept(req): - action_name = 'secret:decrypt' # Override to perform special rules - - # Enforce access controls. - ctx.policy_enforcer.enforce(action_name, {}, credentials, - do_raise=True) - - -def handle_rbac(action_name='default'): - """Decorator handling RBAC enforcement on behalf of REST verb methods.""" - - def rbac_decorator(fn): - def enforcer(inst, req, resp, *args, **kwargs): - - # Enforce RBAC rules. - enforce_rbac(req, resp, action_name, - keystone_id=kwargs.get('keystone_id')) - - # Execute guarded method now. - fn(inst, req, resp, *args, **kwargs) - - return enforcer - - return rbac_decorator - - -def handle_exceptions(operation_name=u._('System')): - """Decorator handling generic exceptions from REST methods.""" - - def exceptions_decorator(fn): - - def handler(inst, req, resp, *args, **kwargs): - try: - fn(inst, req, resp, *args, **kwargs) - except falcon.HTTPError as f: - LOG.exception('Falcon error seen') - raise f # Already converted to Falcon exception, just reraise - except Exception as e: - status, message = api.generate_safe_exception_message( - operation_name, e) - LOG.exception(message) - api.abort(status, message, req, resp) - - return handler - - return exceptions_decorator - - -class PerformanceResource(api.ApiResource): - """Supports a static response to support performance testing.""" - - def __init__(self): - LOG.debug('=== Creating PerformanceResource ===') - - def on_get(self, req, resp): - resp.status = falcon.HTTP_200 - resp.body = '42' - - -class VersionResource(api.ApiResource): - """Returns service and build version information.""" - - def __init__(self): - LOG.debug('=== Creating VersionResource ===') - - @handle_exceptions(u._('Version retrieval')) - @handle_rbac('version:get') - def on_get(self, req, resp): - resp.status = falcon.HTTP_200 - resp.body = json.dumps({'v1': 'current', - 'build': version.__version__}) - - -class SecretsResource(api.ApiResource): - """Handles Secret creation requests.""" - - def __init__(self, crypto_manager, - tenant_repo=None, secret_repo=None, - tenant_secret_repo=None, datum_repo=None, kek_repo=None): - LOG.debug('Creating SecretsResource') - self.tenant_repo = tenant_repo or repo.TenantRepo() - self.secret_repo = secret_repo or repo.SecretRepo() - self.tenant_secret_repo = tenant_secret_repo or repo.TenantSecretRepo() - self.datum_repo = datum_repo or repo.EncryptedDatumRepo() - self.kek_repo = kek_repo or repo.KEKDatumRepo() - self.crypto_manager = crypto_manager - self.validator = validators.NewSecretValidator() - - @handle_exceptions(u._('Secret creation')) - @handle_rbac('secrets:post') - def on_post(self, req, resp, keystone_id): - LOG.debug('Start on_post for tenant-ID {0}:...'.format(keystone_id)) - - data = api.load_body(req, resp, self.validator) - tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) - - new_secret = res.create_secret(data, tenant, self.crypto_manager, - self.secret_repo, - self.tenant_secret_repo, - self.datum_repo, - self.kek_repo) - - resp.status = falcon.HTTP_201 - resp.set_header('Location', '/{0}/secrets/{1}'.format(keystone_id, - new_secret.id)) - url = convert_secret_to_href(keystone_id, new_secret.id) - LOG.debug('URI to secret is {0}'.format(url)) - resp.body = json.dumps({'secret_ref': url}) - - @handle_exceptions(u._('Secret(s) retrieval')) - @handle_rbac('secrets:get') - def on_get(self, req, resp, keystone_id): - LOG.debug('Start secrets on_get ' - 'for tenant-ID {0}:'.format(keystone_id)) - - name = req.get_param('name') - if name: - name = urllib.unquote_plus(name) - - result = self.secret_repo.get_by_create_date( - keystone_id, - offset_arg=req.get_param('offset'), - limit_arg=req.get_param('limit'), - name=name, - alg=req.get_param('alg'), - mode=req.get_param('mode'), - bits=req.get_param('bits'), - suppress_exception=True - ) - - secrets, offset, limit, total = result - - if not secrets: - secrets_resp_overall = {'secrets': [], - 'total': total} - else: - secret_fields = lambda s: mime_types\ - .augment_fields_with_content_types(s) - secrets_resp = [convert_to_hrefs(keystone_id, secret_fields(s)) for - s in secrets] - secrets_resp_overall = add_nav_hrefs('secrets', keystone_id, - offset, limit, total, - {'secrets': secrets_resp}) - secrets_resp_overall.update({'total': total}) - - resp.status = falcon.HTTP_200 - resp.body = json.dumps(secrets_resp_overall, - default=json_handler) - - -class SecretResource(api.ApiResource): - """Handles Secret retrieval and deletion requests.""" - - def __init__(self, crypto_manager, - tenant_repo=None, secret_repo=None, datum_repo=None, - kek_repo=None): - self.crypto_manager = crypto_manager - self.tenant_repo = tenant_repo or repo.TenantRepo() - self.repo = secret_repo or repo.SecretRepo() - self.datum_repo = datum_repo or repo.EncryptedDatumRepo() - self.kek_repo = kek_repo or repo.KEKDatumRepo() - - @handle_exceptions(u._('Secret retrieval')) - @handle_rbac('secret:get') - def on_get(self, req, resp, keystone_id, secret_id): - - secret = self.repo.get(entity_id=secret_id, keystone_id=keystone_id, - suppress_exception=True) - if not secret: - _secret_not_found(req, resp) - - resp.status = falcon.HTTP_200 - - if is_json_request_accept(req): - # Metadata-only response, no decryption necessary. - resp.set_header('Content-Type', 'application/json') - secret_fields = mime_types.augment_fields_with_content_types( - secret) - resp.body = json.dumps(convert_to_hrefs(keystone_id, - secret_fields), - default=json_handler) - else: - tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) - resp.set_header('Content-Type', req.accept) - - resp.body = self.crypto_manager \ - .decrypt(req.accept, - secret, - tenant) - - @handle_exceptions(u._('Secret update')) - @handle_rbac('secret:put') - def on_put(self, req, resp, keystone_id, secret_id): - - if not req.content_type or req.content_type == 'application/json': - _put_accept_incorrect(req.content_type, req, resp) - - secret = self.repo.get(entity_id=secret_id, keystone_id=keystone_id, - suppress_exception=True) - if not secret: - _secret_not_found(req, resp) - - if secret.encrypted_data: - _secret_already_has_data(req, resp) - - tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) - payload = None - content_type = req.content_type - content_encoding = req.get_header('Content-Encoding') - - try: - payload = req.stream.read(api.MAX_BYTES_REQUEST_INPUT_ACCEPTED) - except IOError: - api.abort(falcon.HTTP_500, 'Read Error') - - res.create_encrypted_datum(secret, - payload, - content_type, - content_encoding, - tenant, - self.crypto_manager, - self.datum_repo, - self.kek_repo) - - resp.status = falcon.HTTP_200 - - @handle_exceptions(u._('Secret deletion')) - @handle_rbac('secret:delete') - def on_delete(self, req, resp, keystone_id, secret_id): - - try: - self.repo.delete_entity_by_id(entity_id=secret_id, - keystone_id=keystone_id) - except exception.NotFound: - LOG.exception('Problem deleting secret') - _secret_not_found(req, resp) - - resp.status = falcon.HTTP_200 - - -class OrdersResource(api.ApiResource): - """Handles Order requests for Secret creation.""" - - def __init__(self, tenant_repo=None, order_repo=None, - queue_resource=None): - - LOG.debug('Creating OrdersResource') - self.tenant_repo = tenant_repo or repo.TenantRepo() - self.order_repo = order_repo or repo.OrderRepo() - self.queue = queue_resource or async_client.TaskClient() - self.validator = validators.NewOrderValidator() - - @handle_exceptions(u._('Order creation')) - @handle_rbac('orders:post') - def on_post(self, req, resp, keystone_id): - - tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) - - body = api.load_body(req, resp, self.validator) - LOG.debug('Start on_post...{0}'.format(body)) - - if 'secret' not in body: - _secret_not_in_order(req, resp) - secret_info = body['secret'] - name = secret_info.get('name') - LOG.debug('Secret to create is {0}'.format(name)) - - new_order = models.Order() - new_order.secret_name = secret_info.get('name') - new_order.secret_algorithm = secret_info.get('algorithm') - new_order.secret_bit_length = secret_info.get('bit_length', 0) - new_order.secret_mode = secret_info.get('mode') - 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 - self.order_repo.create_from(new_order) - - # Send to workers to process. - self.queue.process_order(order_id=new_order.id, - keystone_id=keystone_id) - - resp.status = falcon.HTTP_202 - resp.set_header('Location', '/{0}/orders/{1}'.format(keystone_id, - new_order.id)) - url = convert_order_to_href(keystone_id, new_order.id) - resp.body = json.dumps({'order_ref': url}) - - @handle_exceptions(u._('Order(s) retrieval')) - @handle_rbac('orders:get') - def on_get(self, req, resp, keystone_id): - LOG.debug('Start orders on_get ' - 'for tenant-ID {0}:'.format(keystone_id)) - - result = self.order_repo \ - .get_by_create_date(keystone_id, - offset_arg=req.get_param('offset'), - limit_arg=req.get_param('limit'), - suppress_exception=True) - orders, offset, limit, total = result - - if not orders: - orders_resp_overall = {'orders': [], - 'total': total} - else: - orders_resp = [convert_to_hrefs(keystone_id, o.to_dict_fields()) - for o in orders] - orders_resp_overall = add_nav_hrefs('orders', keystone_id, - offset, limit, total, - {'orders': orders_resp}) - orders_resp_overall.update({'total': total}) - - resp.status = falcon.HTTP_200 - resp.body = json.dumps(orders_resp_overall, - default=json_handler) - - -class OrderResource(api.ApiResource): - """Handles Order retrieval and deletion requests.""" - - def __init__(self, order_repo=None): - self.repo = order_repo or repo.OrderRepo() - - @handle_exceptions(u._('Order retrieval')) - @handle_rbac('order:get') - def on_get(self, req, resp, keystone_id, order_id): - order = self.repo.get(entity_id=order_id, keystone_id=keystone_id, - suppress_exception=True) - if not order: - _order_not_found(req, resp) - - resp.status = falcon.HTTP_200 - resp.body = json.dumps(convert_to_hrefs(keystone_id, - order.to_dict_fields()), - default=json_handler) - - @handle_exceptions(u._('Order deletion')) - @handle_rbac('order:delete') - def on_delete(self, req, resp, keystone_id, order_id): - - try: - self.repo.delete_entity_by_id(entity_id=order_id, - keystone_id=keystone_id) - except exception.NotFound: - LOG.exception('Problem deleting order') - _order_not_found(req, resp) - - resp.status = falcon.HTTP_200 - - -class ContainersResource(api.ApiResource): - """ Handles Container creation requests. """ - - def __init__(self, tenant_repo=None, container_repo=None, - secret_repo=None): - - self.tenant_repo = tenant_repo or repo.TenantRepo() - self.container_repo = container_repo or repo.ContainerRepo() - self.secret_repo = secret_repo or repo.SecretRepo() - self.validator = validators.ContainerValidator() - - @handle_exceptions(u._('Container creation')) - @handle_rbac('containers:post') - def on_post(self, req, resp, keystone_id): - - tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) - - data = api.load_body(req, resp, self.validator) - LOG.debug('Start on_post...{0}'.format(data)) - - new_container = models.Container(data) - new_container.tenant_id = tenant.id - - #TODO: (hgedikli) performance optimizations - for secret_ref in new_container.container_secrets: - secret = self.secret_repo.get(entity_id=secret_ref.secret_id, - keystone_id=keystone_id, - suppress_exception=True) - if not secret: - api.abort(falcon.HTTP_404, - u._("Secret provided for '%s'" - " doesn't exist." % secret_ref.name), - req, resp) - - self.container_repo.create_from(new_container) - - resp.status = falcon.HTTP_202 - resp.set_header('Location', - '/{0}/containers/{1}'.format(keystone_id, - new_container.id)) - url = convert_container_to_href(keystone_id, new_container.id) - resp.body = json.dumps({'container_ref': url}) - - @handle_exceptions(u._('Containers(s) retrieval')) - @handle_rbac('containers:get') - def on_get(self, req, resp, keystone_id): - LOG.debug('Start containers on_get ' - 'for tenant-ID {0}:'.format(keystone_id)) - - result = self.container_repo.get_by_create_date( - keystone_id, - offset_arg=req.get_param('offset'), - limit_arg=req.get_param('limit'), - suppress_exception=True - ) - - containers, offset, limit, total = result - - if not containers: - resp_ctrs_overall = {'containers': [], 'total': total} - else: - resp_ctrs = [convert_to_hrefs(keystone_id, - c.to_dict_fields()) - for c in containers] - resp_ctrs_overall = add_nav_hrefs('containers', - keystone_id, offset, - limit, total, - {'containers': resp_ctrs}) - resp_ctrs_overall.update({'total': total}) - - resp.status = falcon.HTTP_200 - resp.body = json.dumps(resp_ctrs_overall, - default=json_handler) - - -class ContainerResource(api.ApiResource): - """Handles Container entity retrieval and deletion requests.""" - - def __init__(self, tenant_repo=None, container_repo=None): - self.tenant_repo = tenant_repo or repo.TenantRepo() - self.container_repo = container_repo or repo.ContainerRepo() - self.validator = validators.ContainerValidator() - - @handle_exceptions(u._('Container retrieval')) - @handle_rbac('container:get') - def on_get(self, req, resp, keystone_id, container_id): - container = self.container_repo.get(entity_id=container_id, - keystone_id=keystone_id, - suppress_exception=True) - if not container: - _container_not_found(req, resp) - - resp.status = falcon.HTTP_200 - - dict_fields = container.to_dict_fields() - - for secret_ref in dict_fields['secret_refs']: - convert_to_hrefs(keystone_id, secret_ref) - - resp.body = json.dumps( - convert_to_hrefs(keystone_id, - convert_to_hrefs(keystone_id, dict_fields)), - default=json_handler) - - @handle_exceptions(u._('Container deletion')) - @handle_rbac('container:delete') - def on_delete(self, req, resp, keystone_id, container_id): - - try: - - self.container_repo.delete_entity_by_id(entity_id=container_id, - keystone_id=keystone_id) - except exception.NotFound: - LOG.exception('Problem deleting container') - _container_not_found(req, resp) - - resp.status = falcon.HTTP_200 diff --git a/barbican/tests/api/test_resources.py b/barbican/tests/api/test_resources.py index 549c3986e..8ebf70776 100644 --- a/barbican/tests/api/test_resources.py +++ b/barbican/tests/api/test_resources.py @@ -20,22 +20,21 @@ resource classes. For RBAC tests of these classes, see the """ import base64 -import json import urllib -import falcon import mock - import testtools +import pecan +from webtest import TestApp from barbican.api import strip_whitespace -from barbican.api import resources as res +from barbican.api import app +from barbican.api import controllers from barbican.common import exception as excep from barbican.common import utils from barbican.common import validators from barbican.crypto import extension_manager as em from barbican.model import models -from barbican.openstack.common import jsonutils from barbican.tests.crypto import test_plugin as ctp @@ -49,6 +48,7 @@ def create_secret(id_ref="id", name="name", 'bit_length': bit_length, 'mode': mode} secret = models.Secret(info) + secret.id = id_ref if encrypted_datum: secret.encrypted_data = [encrypted_datum] return secret @@ -90,30 +90,56 @@ def create_container(id_ref): return container -class WhenTestingVersionResource(testtools.TestCase): - def setUp(self): - super(WhenTestingVersionResource, self).setUp() +class FunctionalTest(testtools.TestCase): - self.req = mock.MagicMock() - self.resp = mock.MagicMock() - self.resource = res.VersionResource() + def setUp(self): + super(FunctionalTest, self).setUp() + root = self.root + config = {'app': {'root': root}} + pecan.set_config(config, overwrite=True) + self.app = TestApp(pecan.make_app(root)) + + def tearDown(self): + super(FunctionalTest, self).tearDown() + pecan.set_config({}, overwrite=True) + + @property + def root(self): + return controllers.versions.VersionController() + + +class WhenTestingVersionResource(FunctionalTest): def test_should_return_200_on_get(self): - self.resource.on_get(self.req, self.resp) - self.assertEqual(falcon.HTTP_200, self.resp.status) + resp = self.app.get('/') + self.assertEqual(200, resp.status_int) def test_should_return_version_json(self): - self.resource.on_get(self.req, self.resp) + resp = self.app.get('/') - parsed_body = json.loads(self.resp.body) - - self.assertTrue('v1' in parsed_body) - self.assertEqual('current', parsed_body['v1']) + self.assertTrue('v1' in resp.json) + self.assertEqual('current', resp.json['v1']) -class BaseSecretsResource(testtools.TestCase): +class BaseSecretsResource(FunctionalTest): """Base test class for the Secrets resource.""" + def setUp(self): + super(BaseSecretsResource, self).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + + @property + def root(self): + self._init() + + class RootController(object): + secrets = controllers.secrets.SecretsController( + self.crypto_mgr, self.tenant_repo, self.secret_repo, + self.tenant_secret_repo, self.datum_repo, self.kek_repo + ) + + return RootController() + def _init(self, payload=b'not-encrypted', payload_content_type='text/plain', payload_content_encoding=None): @@ -135,7 +161,6 @@ class BaseSecretsResource(testtools.TestCase): if payload_content_encoding: self.secret_req['payload_content_encoding'] = \ payload_content_encoding - self.json = json.dumps(self.secret_req) self.keystone_id = 'keystone1234' self.tenant_entity_id = 'tid1234' @@ -162,33 +187,21 @@ class BaseSecretsResource(testtools.TestCase): self.kek_repo = mock.MagicMock() self.kek_repo.find_or_create_kek_metadata.return_value = self.kek_datum - self.stream = mock.MagicMock() - self.stream.read.return_value = self.json - - self.req = mock.MagicMock() - self.req.stream = self.stream - - self.resp = 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 = res.SecretsResource(self.crypto_mgr, - self.tenant_repo, - self.secret_repo, - self.tenant_secret_repo, - self.datum_repo, - self.kek_repo) - def _test_should_add_new_secret_with_expiration(self): expiration = '2114-02-28 12:14:44.180394-05:00' self.secret_req.update({'expiration': expiration}) - self.stream.read.return_value = json.dumps(self.secret_req) - self.resource.on_post(self.req, self.resp, self.keystone_id) + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req + ) - self.assertEqual(self.resp.status, falcon.HTTP_201) + self.assertEqual(resp.status_int, 201) args, kwargs = self.secret_repo.create_from.call_args secret = args[0] @@ -201,9 +214,11 @@ class BaseSecretsResource(testtools.TestCase): :param check_tenant_id: True if the retrieved Tenant id needs to be verified, False to skip this check (necessary for new-Tenant flows). """ - self.resource.on_post(self.req, self.resp, self.keystone_id) - - self.assertEqual(self.resp.status, falcon.HTTP_201) + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req + ) + self.assertEqual(resp.status_int, 201) args, kwargs = self.secret_repo.create_from.call_args secret = args[0] @@ -240,9 +255,10 @@ class BaseSecretsResource(testtools.TestCase): self.assertEqual(self.keystone_id, tenant.keystone_id) def _test_should_add_new_secret_metadata_without_payload(self): - self.stream.read.return_value = json.dumps({'name': self.name}) - - self.resource.on_post(self.req, self.resp, self.keystone_id) + self.app.post_json( + '/%s/secrets/' % self.keystone_id, + {'name': self.name} + ) args, kwargs = self.secret_repo.create_from.call_args secret = args[0] @@ -275,9 +291,7 @@ class BaseSecretsResource(testtools.TestCase): if self.payload_content_encoding: self.secret_req['payload_content_encoding'] = \ self.payload_content_encoding - self.stream.read.return_value = json.dumps(self.secret_req) - - self.resource.on_post(self.req, self.resp, self.keystone_id) + self.app.post_json('/%s/secrets/' % self.keystone_id, self.secret_req) def _test_should_fail_due_to_payload_too_large(self): big_text = ''.join(['A' for x @@ -293,14 +307,13 @@ class BaseSecretsResource(testtools.TestCase): if self.payload_content_encoding: self.secret_req['payload_content_encoding'] = \ self.payload_content_encoding - self.stream.read.return_value = json.dumps(self.secret_req) - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.keystone_id + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req, + expect_errors=True ) - self.assertEqual(falcon.HTTP_413, exception.status) + self.assertEqual(resp.status_int, 413) def _test_should_fail_due_to_empty_payload(self): self.secret_req = {'name': self.name, @@ -313,20 +326,16 @@ class BaseSecretsResource(testtools.TestCase): if self.payload_content_encoding: self.secret_req['payload_content_encoding'] = \ self.payload_content_encoding - self.stream.read.return_value = json.dumps(self.secret_req) - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.keystone_id + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + assert resp.status_int == 400 class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource): - def setUp(self): - super(WhenCreatingPlainTextSecretsUsingSecretsResource, self).setUp() - self._init() # Default settings setup a plain-text secret. def test_should_add_new_secret_one_step(self): self._test_should_add_new_secret_one_step() @@ -356,14 +365,13 @@ class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource): 'bit_length': self.secret_bit_length, 'mode': self.secret_mode, 'payload': self.payload} - self.stream.read.return_value = json.dumps(self.secret_req) - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.keystone_id, + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEqual(resp.status_int, 400) def test_create_secret_content_type_text_plain(self): # payload_content_type has trailing space @@ -373,10 +381,12 @@ class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource): 'bit_length': self.secret_bit_length, 'mode': self.secret_mode, 'payload': self.payload} - self.stream.read.return_value = json.dumps(self.secret_req) - self.resource.on_post(self.req, self.resp, self.keystone_id) - self.assertEqual(self.resp.status, falcon.HTTP_201) + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req + ) + self.assertEqual(resp.status_int, 201) self.secret_req = {'name': self.name, 'payload_content_type': ' text/plain', @@ -384,10 +394,12 @@ class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource): 'bit_length': self.secret_bit_length, 'mode': self.secret_mode, 'payload': self.payload} - self.stream.read.return_value = json.dumps(self.secret_req) - self.resource.on_post(self.req, self.resp, self.keystone_id) - self.assertEqual(self.resp.status, falcon.HTTP_201) + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req + ) + self.assertEqual(resp.status_int, 201) def test_create_secret_content_type_text_plain_space_charset_utf8(self): # payload_content_type has trailing space @@ -398,10 +410,12 @@ class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource): 'bit_length': self.secret_bit_length, 'mode': self.secret_mode, 'payload': self.payload} - self.stream.read.return_value = json.dumps(self.secret_req) - self.resource.on_post(self.req, self.resp, self.keystone_id) - self.assertEqual(self.resp.status, falcon.HTTP_201) + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req + ) + self.assertEqual(resp.status_int, 201) self.secret_req = {'name': self.name, 'payload_content_type': @@ -410,38 +424,42 @@ class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource): 'bit_length': self.secret_bit_length, 'mode': self.secret_mode, 'payload': self.payload} - self.stream.read.return_value = json.dumps(self.secret_req) - self.resource.on_post(self.req, self.resp, self.keystone_id) - self.assertEqual(self.resp.status, falcon.HTTP_201) + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req + ) + self.assertEqual(resp.status_int, 201) def test_create_secret_with_only_content_type(self): # No payload just content_type self.secret_req = {'payload_content_type': 'text/plain'} - self.stream.read.return_value = json.dumps(self.secret_req) - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.keystone_id, + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEqual(resp.status_int, 400) self.secret_req = {'payload_content_type': 'text/plain', 'payload': 'somejunk'} - self.stream.read.return_value = json.dumps(self.secret_req) - self.resource.on_post(self.req, self.resp, self.keystone_id) - self.assertEqual(self.resp.status, falcon.HTTP_201) + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req + ) + self.assertEqual(resp.status_int, 201) class WhenCreatingBinarySecretsUsingSecretsResource(BaseSecretsResource): - def setUp(self): - super(WhenCreatingBinarySecretsUsingSecretsResource, self).setUp() + @property + def root(self): self._init(payload="...lOtfqHaUUpe6NqLABgquYQ==", payload_content_type='application/octet-stream', payload_content_encoding='base64') + return super(WhenCreatingBinarySecretsUsingSecretsResource, self).root def test_should_add_new_secret_one_step(self): self._test_should_add_new_secret_one_step() @@ -465,80 +483,94 @@ class WhenCreatingBinarySecretsUsingSecretsResource(BaseSecretsResource): self._test_should_fail_due_to_empty_payload() def test_create_secret_fails_with_binary_payload_no_encoding(self): - self.stream.read.return_value = json.dumps( - {'name': self.name, - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode, - 'payload': 'lOtfqHaUUpe6NqLABgquYQ==', - 'payload_content_type': 'application/octet-stream'} + self.secret_req = { + 'name': self.name, + 'algorithm': self.secret_algorithm, + 'bit_length': self.secret_bit_length, + 'mode': self.secret_mode, + 'payload': 'lOtfqHaUUpe6NqLABgquYQ==', + 'payload_content_type': 'application/octet-stream' + } + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req, + expect_errors=True ) - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.keystone_id - ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEqual(resp.status_int, 400) def test_create_secret_fails_with_binary_payload_bad_encoding(self): - self.stream.read.return_value = json.dumps( - {'name': self.name, - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode, - 'payload': 'lOtfqHaUUpe6NqLABgquYQ==', - 'payload_content_type': 'application/octet-stream', - 'payload_content_encoding': 'bogus64'} - ) + self.secret_req = { + 'name': self.name, + 'algorithm': self.secret_algorithm, + 'bit_length': self.secret_bit_length, + 'mode': self.secret_mode, + 'payload': 'lOtfqHaUUpe6NqLABgquYQ==', + 'payload_content_type': 'application/octet-stream', + 'payload_content_encoding': 'bogus64' + } - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.keystone_id + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEqual(resp.status_int, 400) def test_create_secret_fails_with_binary_payload_no_content_type(self): - self.stream.read.return_value = json.dumps( - {'name': self.name, - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode, - 'payload': 'lOtfqHaUUpe6NqLABgquYQ==', - 'payload_content_encoding': 'base64'} - ) + self.secret_req = { + 'name': self.name, + 'algorithm': self.secret_algorithm, + 'bit_length': self.secret_bit_length, + 'mode': self.secret_mode, + 'payload': 'lOtfqHaUUpe6NqLABgquYQ==', + 'payload_content_encoding': 'base64' + } - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.keystone_id + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEqual(resp.status_int, 400) def test_create_secret_fails_with_bad_payload(self): - self.stream.read.return_value = json.dumps( - {'name': self.name, - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode, - 'payload': 'AAAAAAAAA', - 'payload_content_type': 'application/octet-stream', - 'payload_content_encoding': 'base64'} + self.secret_req = { + 'name': self.name, + 'algorithm': self.secret_algorithm, + 'bit_length': self.secret_bit_length, + 'mode': self.secret_mode, + 'payload': 'AAAAAAAAA', + 'payload_content_type': 'application/octet-stream', + 'payload_content_encoding': 'base64' + } + + resp = self.app.post_json( + '/%s/secrets/' % self.keystone_id, + self.secret_req, + expect_errors=True ) - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.keystone_id - ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEqual(resp.status_int, 400) -class WhenGettingSecretsListUsingSecretsResource(testtools.TestCase): +class WhenGettingSecretsListUsingSecretsResource(FunctionalTest): + def setUp(self): super(WhenGettingSecretsListUsingSecretsResource, self).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + @property + def root(self): + self._init() + + class RootController(object): + secrets = controllers.secrets.SecretsController( + self.crypto_mgr, self.tenant_repo, self.secret_repo, + self.tenant_secret_repo, self.datum_repo, self.kek_repo + ) + + return RootController() + + def _init(self): self.tenant_id = 'tenant1234' self.keystone_id = 'keystone1234' self.name = 'name 1234 !@#$%^&*()_+=-{}[];:<>,./?' @@ -582,31 +614,22 @@ class WhenGettingSecretsListUsingSecretsResource(testtools.TestCase): self.conf.crypto.enabled_crypto_plugins = ['test_crypto'] self.crypto_mgr = em.CryptoExtensionManager(conf=self.conf) - self.req = mock.MagicMock() - self.req.accept = 'application/json' - self.req.get_param = mock.Mock() self.params = {'offset': self.offset, 'limit': self.limit, 'name': None, 'alg': None, 'bits': 0, 'mode': None} - self.req.get_param.side_effect = self.params.get - - self.resp = mock.MagicMock() - self.resource = res.SecretsResource(self.crypto_mgr, self.tenant_repo, - self.secret_repo, - self.tenant_secret_repo, - self.datum_repo, - self.kek_repo) def test_should_list_secrets_by_name(self): # Quote the name parameter to simulate how it would be # received in practice via a REST-ful GET query. self.params['name'] = urllib.quote_plus(self.name) - self.resource.on_get(self.req, self.resp, self.keystone_id) - + resp = self.app.get( + '/%s/secrets/' % self.keystone_id, + dict((k, v) for k, v in self.params.items() if v is not None) + ) # Verify that the name is unquoted correctly in the # secrets.on_get function prior to searching the repo. self.secret_repo.get_by_create_date \ @@ -618,14 +641,16 @@ class WhenGettingSecretsListUsingSecretsResource(testtools.TestCase): alg=None, mode=None, bits=0) - resp_body = jsonutils.loads(self.resp.body) - self.assertIn('secrets', resp_body) - secrets = resp_body['secrets'] + self.assertIn('secrets', resp.namespace) + secrets = resp.namespace['secrets'] # The result should be the unquoted name self.assertEqual(secrets[0]['name'], self.name) def test_should_get_list_secrets(self): - self.resource.on_get(self.req, self.resp, self.keystone_id) + resp = self.app.get( + '/%s/secrets/' % self.keystone_id, + dict((k, v) for k, v in self.params.items() if v is not None) + ) self.secret_repo.get_by_create_date \ .assert_called_once_with(self.keystone_id, @@ -635,33 +660,38 @@ class WhenGettingSecretsListUsingSecretsResource(testtools.TestCase): name=None, alg=None, mode=None, bits=0) - resp_body = jsonutils.loads(self.resp.body) - self.assertTrue('previous' in resp_body) - self.assertTrue('next' in resp_body) + self.assertTrue('previous' in resp.namespace) + self.assertTrue('next' in resp.namespace) url_nav_next = self._create_url(self.keystone_id, self.offset + self.limit, self.limit) - self.assertTrue(self.resp.body.count(url_nav_next) == 1) + self.assertTrue(resp.body.count(url_nav_next) == 1) url_nav_prev = self._create_url(self.keystone_id, 0, self.limit) - self.assertTrue(self.resp.body.count(url_nav_prev) == 1) + self.assertTrue(resp.body.count(url_nav_prev) == 1) url_hrefs = self._create_url(self.keystone_id) - self.assertTrue(self.resp.body.count(url_hrefs) == + self.assertTrue(resp.body.count(url_hrefs) == (self.num_secrets + 2)) def test_response_should_include_total(self): - self.resource.on_get(self.req, self.resp, self.keystone_id) - resp_body = jsonutils.loads(self.resp.body) - self.assertIn('total', resp_body) - self.assertEqual(resp_body['total'], self.total) + resp = self.app.get( + '/%s/secrets/' % self.keystone_id, + dict((k, v) for k, v in self.params.items() if v is not None) + ) + + self.assertIn('total', resp.namespace) + self.assertEqual(resp.namespace['total'], self.total) def test_should_handle_no_secrets(self): del self.secrets[:] - self.resource.on_get(self.req, self.resp, self.keystone_id) + resp = self.app.get( + '/%s/secrets/' % self.keystone_id, + dict((k, v) for k, v in self.params.items() if v is not None) + ) self.secret_repo.get_by_create_date \ .assert_called_once_with(self.keystone_id, @@ -671,28 +701,40 @@ class WhenGettingSecretsListUsingSecretsResource(testtools.TestCase): name=None, alg=None, mode=None, bits=0) - resp_body = jsonutils.loads(self.resp.body) - self.assertFalse('previous' in resp_body) - self.assertFalse('next' in resp_body) + self.assertFalse('previous' in resp.namespace) + self.assertFalse('next' in resp.namespace) def _create_url(self, keystone_id, offset_arg=None, limit_arg=None): if limit_arg: offset = int(offset_arg) limit = int(limit_arg) - return '/v1/{0}/secrets?limit={1}&offset={2}'.format(keystone_id, - limit, - offset) + return '/{0}/secrets?limit={1}&offset={2}'.format(keystone_id, + limit, + offset) else: - return '/v1/{0}/secrets'.format(keystone_id) + return '/{0}/secrets'.format(keystone_id) -class WhenGettingPuttingOrDeletingSecretUsingSecretResource( - testtools.TestCase): +class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): def setUp(self): super( - WhenGettingPuttingOrDeletingSecretUsingSecretResource, - self, + WhenGettingPuttingOrDeletingSecretUsingSecretResource, self ).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + + @property + def root(self): + self._init() + + class RootController(object): + secrets = controllers.secrets.SecretsController( + self.crypto_mgr, self.tenant_repo, self.secret_repo, + self.tenant_secret_repo, self.datum_repo, self.kek_repo + ) + + return RootController() + + def _init(self): self.tenant_id = 'tenantid1234' self.keystone_id = 'keystone1234' self.name = 'name1234' @@ -738,132 +780,122 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource( self.secret_repo.get.return_value = self.secret self.secret_repo.delete_entity_by_id.return_value = None + self.tenant_secret_repo = mock.MagicMock() + self.datum_repo = mock.MagicMock() self.datum_repo.create_from.return_value = None self.kek_repo = mock.MagicMock() - self.req = mock.MagicMock() - self.req.accept = 'application/json' - self.req.accept_encoding = 'gzip' - self.resp = 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 = res.SecretResource(self.crypto_mgr, - self.tenant_repo, - self.secret_repo, - self.datum_repo, - self.kek_repo) - def test_should_get_secret_as_json(self): - self.resource.on_get(self.req, self.resp, self.keystone_id, - self.secret.id) - + resp = self.app.get( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + headers={'Accept': 'application/json', 'Accept-Encoding': 'gzip'} + ) self.secret_repo \ .get.assert_called_once_with(entity_id=self.secret.id, keystone_id=self.keystone_id, suppress_exception=True) + self.assertEquals(resp.status_int, 200) - self.assertEqual(self.resp.status, falcon.HTTP_200) - - resp_body = jsonutils.loads(self.resp.body) - self.assertNotIn('content_encodings', resp_body) - self.assertIn('content_types', resp_body) + self.assertNotIn('content_encodings', resp.namespace) + self.assertIn('content_types', resp.namespace) self.assertIn(self.datum.content_type, - resp_body['content_types'].itervalues()) - self.assertNotIn('mime_type', resp_body) + resp.namespace['content_types'].itervalues()) + self.assertNotIn('mime_type', resp.namespace) def test_should_get_secret_as_plain(self): - self.req.accept = 'text/plain' - - self.resource.on_get(self.req, self.resp, self.keystone_id, - self.secret.id) + resp = self.app.get( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + headers={'Accept': 'text/plain'} + ) self.secret_repo \ .get.assert_called_once_with(entity_id=self.secret.id, keystone_id=self.keystone_id, suppress_exception=True) + self.assertEquals(resp.status_int, 200) - self.assertEqual(self.resp.status, falcon.HTTP_200) - - resp_body = self.resp.body - self.assertIsNotNone(resp_body) + self.assertIsNotNone(resp.body) def test_should_get_secret_meta_for_binary(self): - self.req.accept = 'application/json' self.datum.content_type = "application/octet-stream" self.datum.cypher_text = 'aaaa' - self.resource.on_get(self.req, self.resp, self.keystone_id, - self.secret.id) + resp = self.app.get( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + headers={'Accept': 'application/json', 'Accept-Encoding': 'gzip'} + ) self.secret_repo \ .get.assert_called_once_with(entity_id=self.secret.id, keystone_id=self.keystone_id, suppress_exception=True) - self.assertEqual(self.resp.status, falcon.HTTP_200) + self.assertEqual(resp.status_int, 200) - resp_body = jsonutils.loads(self.resp.body) - self.assertIsNotNone(resp_body) - self.assertIn('content_types', resp_body) + self.assertIsNotNone(resp.namespace) + self.assertIn('content_types', resp.namespace) self.assertIn(self.datum.content_type, - resp_body['content_types'].itervalues()) + resp.namespace['content_types'].itervalues()) def test_should_get_secret_as_binary(self): - self.req.accept = 'application/octet-stream' - # mock Content-Encoding header - self.req.get_header.return_value = None - self.datum.content_type = 'application/octet-stream' + self.datum.content_type = "application/octet-stream" self.datum.cypher_text = 'aaaa' - self.resource.on_get(self.req, self.resp, self.keystone_id, - self.secret.id) + resp = self.app.get( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + headers={ + 'Accept': 'application/octet-stream', + 'Accept-Encoding': 'gzip' + } + ) - self.assertEqual(self.resp.body, 'unencrypted_data') + self.assertEqual(resp.body, 'unencrypted_data') def test_should_throw_exception_for_get_when_secret_not_found(self): self.secret_repo.get.return_value = None - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_get, - self.req, self.resp, self.keystone_id, self.secret.id, + resp = self.app.get( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + headers={'Accept': 'application/json', 'Accept-Encoding': 'gzip'}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_404, exception.status) + self.assertEqual(resp.status_int, 404) def test_should_throw_exception_for_get_when_accept_not_supported(self): - self.req.accept = 'bogusaccept' - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_get, - self.req, self.resp, self.keystone_id, self.secret.id, + resp = self.app.get( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + headers={'Accept': 'bogusaccept', 'Accept-Encoding': 'gzip'}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_406, exception.status) + self.assertEqual(resp.status_int, 406) def test_should_throw_exception_for_get_when_datum_not_available(self): - self.req.accept = 'text/plain' self.secret.encrypted_data = [] - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_get, - self.req, self.resp, self.keystone_id, self.secret.id, + resp = self.app.get( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + headers={'Accept': 'text/plain'}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_404, exception.status) + self.assertEqual(resp.status_int, 404) def test_should_put_secret_as_plain(self): - self._setup_for_puts() + self.secret.encrypted_data = [] - self.resource.on_put(self.req, self.resp, self.keystone_id, - self.secret.id) + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + 'plain text', + headers={'Accept': 'text/plain', 'Content-Type': 'text/plain'}, + ) - self.assertEqual(self.resp.status, falcon.HTTP_200) + self.assertEqual(resp.status_int, 200) args, kwargs = self.datum_repo.create_from.call_args datum = args[0] @@ -873,124 +905,132 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource( validate_datum(self, datum) def test_should_put_secret_as_binary(self): - self._setup_for_puts() - self.req.content_type = 'application/octet-stream' + self.secret.encrypted_data = [] - self.resource.on_put(self.req, self.resp, self.keystone_id, - self.secret.id) + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + 'plain text', + headers={ + 'Accept': 'text/plain', + 'Content-Type': 'application/octet-stream' + }, + ) - self.assertEqual(self.resp.status, falcon.HTTP_200) + self.assertEqual(resp.status_int, 200) args, kwargs = self.datum_repo.create_from.call_args datum = args[0] self.assertIsInstance(datum, models.EncryptedDatum) def test_should_put_encoded_secret_as_binary(self): - self._setup_for_puts() - self.stream.read.return_value = base64.b64encode(self.payload) - self.req.content_type = 'application/octet-stream' - # mock Content-Encoding header - self.req.get_header.return_value = 'base64' + self.secret.encrypted_data = [] + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + base64.b64encode('plain text'), + headers={ + 'Accept': 'text/plain', + 'Content-Type': 'application/octet-stream', + 'Content-Encoding': 'base64' + }, + ) - self.resource.on_put(self.req, self.resp, self.keystone_id, - self.secret.id) - - self.assertEqual(self.resp.status, falcon.HTTP_200) + self.assertEqual(resp.status_int, 200) def test_should_fail_to_put_secret_with_unsupported_encoding(self): - self._setup_for_puts() - self.req.content_type = 'application/octet-stream' - # mock Content-Encoding header - self.req.get_header.return_value = 'bogusencoding' - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_put, - self.req, self.resp, self.keystone_id, self.secret.id, + self.secret.encrypted_data = [] + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + 'plain text', + headers={ + 'Accept': 'text/plain', + 'Content-Type': 'application/octet-stream', + 'Content-Encoding': 'bogusencoding' + }, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + + self.assertEqual(resp.status_int, 400) def test_should_fail_put_secret_as_json(self): - self._setup_for_puts() - - self.req.content_type = 'application/json' - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_put, - self.req, self.resp, self.keystone_id, self.secret.id, + self.secret.encrypted_data = [] + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + 'plain text', + headers={ + 'Accept': 'text/plain', + 'Content-Type': 'application/json' + }, + expect_errors=True ) - self.assertEqual(falcon.HTTP_415, exception.status) + + self.assertEqual(resp.status_int, 415) def test_should_fail_put_secret_not_found(self): - self._setup_for_puts() - # Force error, due to secret not found. self.secret_repo.get.return_value = None - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_put, - self.req, self.resp, self.keystone_id, self.secret.id, + self.secret.encrypted_data = [] + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + 'plain text', + headers={'Accept': 'text/plain', 'Content-Type': 'text/plain'}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_404, exception.status) + + self.assertEqual(resp.status_int, 404) def test_should_fail_put_secret_no_payload(self): - self._setup_for_puts() - - # Force error due to no data passed in the request. - self.stream.read.return_value = None - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_put, - self.req, self.resp, self.keystone_id, self.secret.id, + self.secret.encrypted_data = [] + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + # response.body = None + headers={'Accept': 'text/plain', 'Content-Type': 'text/plain'}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + + self.assertEqual(resp.status_int, 400) def test_should_fail_put_secret_with_existing_datum(self): - self._setup_for_puts() - # Force error due to secret already having data self.secret.encrypted_data = [self.datum] - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_put, - self.req, self.resp, self.keystone_id, self.secret.id, + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + 'plain text', + headers={'Accept': 'text/plain', 'Content-Type': 'text/plain'}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_409, exception.status) + self.assertEqual(resp.status_int, 409) def test_should_fail_due_to_empty_payload(self): - self._setup_for_puts() + self.secret.encrypted_data = [] - self.stream.read.return_value = '' - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_put, - self.req, self.resp, self.keystone_id, self.secret.id, + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + '', + headers={'Accept': 'text/plain', 'Content-Type': 'text/plain'}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEqual(resp.status_int, 400) def test_should_fail_due_to_plain_text_too_large(self): - self._setup_for_puts() - big_text = ''.join(['A' for x in xrange( 2 * validators.DEFAULT_MAX_SECRET_BYTES)]) - self.stream.read.return_value = big_text - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_put, - self.req, self.resp, self.keystone_id, self.secret.id, + self.secret.encrypted_data = [] + + resp = self.app.put( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + big_text, + headers={'Accept': 'text/plain', 'Content-Type': 'text/plain'}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_413, exception.status) + self.assertEqual(resp.status_int, 413) def test_should_delete_secret(self): - self.resource.on_delete(self.req, self.resp, self.keystone_id, - self.secret.id) - + self.app.delete( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id) + ) self.secret_repo.delete_entity_by_id \ .assert_called_once_with(entity_id=self.secret.id, keystone_id=self.keystone_id) @@ -999,31 +1039,32 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource( self.secret_repo.delete_entity_by_id.side_effect = excep.NotFound( "Test not found exception") - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_delete, - self.req, self.resp, self.keystone_id, self.secret.id, + resp = self.app.delete( + '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), + expect_errors=True ) - self.assertEqual(falcon.HTTP_404, exception.status) - - def _setup_for_puts(self): - self.payload = "plain_text" - self.req.accept = "text/plain" - self.req.content_type = "text/plain" - # mock Content-Encoding header - self.req.get_header.return_value = None - - self.secret.encrypted_data = [] - - self.stream = mock.MagicMock() - self.stream.read.return_value = self.payload - self.req.stream = self.stream + self.assertEqual(resp.status_int, 404) -class WhenCreatingOrdersUsingOrdersResource(testtools.TestCase): +class WhenCreatingOrdersUsingOrdersResource(FunctionalTest): def setUp(self): - super(WhenCreatingOrdersUsingOrdersResource, self).setUp() + super( + WhenCreatingOrdersUsingOrdersResource, self + ).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + @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.secret_name = 'name' self.secret_payload_content_type = 'application/octet-stream' self.secret_algorithm = "aes" @@ -1046,28 +1087,23 @@ class WhenCreatingOrdersUsingOrdersResource(testtools.TestCase): self.queue_resource = mock.MagicMock() self.queue_resource.process_order.return_value = None - self.stream = mock.MagicMock() - - order_req = {'secret': {'name': self.secret_name, - 'payload_content_type': - self.secret_payload_content_type, - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode}} - self.json = json.dumps(order_req) - self.stream.read.return_value = self.json - - self.req = mock.MagicMock() - self.req.stream = self.stream - - self.resp = mock.MagicMock() - self.resource = res.OrdersResource(self.tenant_repo, self.order_repo, - self.queue_resource) + self.order_req = { + 'secret': { + 'name': self.secret_name, + 'payload_content_type': + self.secret_payload_content_type, + 'algorithm': self.secret_algorithm, + 'bit_length': self.secret_bit_length, + 'mode': self.secret_mode + } + } def test_should_add_new_order(self): - self.resource.on_post(self.req, self.resp, self.tenant_keystone_id) - - self.assertEqual(falcon.HTTP_202, self.resp.status) + resp = self.app.post_json( + '/%s/orders/' % self.tenant_keystone_id, + self.order_req + ) + self.assertEqual(resp.status_int, 202) self.queue_resource.process_order \ .assert_called_once_with(order_id=None, @@ -1078,30 +1114,41 @@ class WhenCreatingOrdersUsingOrdersResource(testtools.TestCase): self.assertIsInstance(order, models.Order) def test_should_fail_add_new_order_no_secret(self): - self.stream.read.return_value = '{}' - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.tenant_keystone_id, + resp = self.app.post_json( + '/%s/orders/' % self.tenant_keystone_id, + {}, + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEquals(resp.status_int, 400) def test_should_fail_add_new_order_bad_json(self): - self.stream.read.return_value = '' - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.tenant_keystone_id, + resp = self.app.post( + '/%s/orders/' % self.tenant_keystone_id, + '', + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEquals(resp.status_int, 400) -class WhenGettingOrdersListUsingOrdersResource(testtools.TestCase): +class WhenGettingOrdersListUsingOrdersResource(FunctionalTest): def setUp(self): - super(WhenGettingOrdersListUsingOrdersResource, self).setUp() + super( + WhenGettingOrdersListUsingOrdersResource, self + ).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + @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_id = 'tenant1234' self.keystone_id = 'keystoneid1234' self.name = 'name1234' @@ -1133,16 +1180,13 @@ class WhenGettingOrdersListUsingOrdersResource(testtools.TestCase): self.queue_resource = mock.MagicMock() self.queue_resource.process_order.return_value = None - self.req = mock.MagicMock() - self.req.accept = 'application/json' - self.req.get_param = mock.Mock() - self.req.get_param.side_effect = [self.offset, self.limit] - self.resp = mock.MagicMock() - self.resource = res.OrdersResource(self.tenant_repo, self.order_repo, - self.queue_resource) + self.params = { + 'offset': self.offset, + 'limit': self.limit + } def test_should_get_list_orders(self): - self.resource.on_get(self.req, self.resp, self.keystone_id) + resp = self.app.get('/%s/orders/' % self.keystone_id, self.params) self.order_repo.get_by_create_date \ .assert_called_once_with(self.keystone_id, @@ -1150,33 +1194,31 @@ class WhenGettingOrdersListUsingOrdersResource(testtools.TestCase): limit_arg=self.limit, suppress_exception=True) - resp_body = jsonutils.loads(self.resp.body) - self.assertTrue('previous' in resp_body) - self.assertTrue('next' in resp_body) + self.assertTrue('previous' in resp.namespace) + self.assertTrue('next' in resp.namespace) url_nav_next = self._create_url(self.keystone_id, self.offset + self.limit, self.limit) - self.assertTrue(self.resp.body.count(url_nav_next) == 1) + self.assertTrue(resp.body.count(url_nav_next) == 1) url_nav_prev = self._create_url(self.keystone_id, 0, self.limit) - self.assertTrue(self.resp.body.count(url_nav_prev) == 1) + self.assertTrue(resp.body.count(url_nav_prev) == 1) url_hrefs = self._create_url(self.keystone_id) - self.assertTrue(self.resp.body.count(url_hrefs) == + self.assertTrue(resp.body.count(url_hrefs) == (self.num_orders + 2)) def test_response_should_include_total(self): - self.resource.on_get(self.req, self.resp, self.keystone_id) - resp_body = jsonutils.loads(self.resp.body) - self.assertIn('total', resp_body) - self.assertEqual(resp_body['total'], self.total) + resp = self.app.get('/%s/orders/' % self.keystone_id, self.params) + self.assertIn('total', resp.namespace) + self.assertEqual(resp.namespace['total'], self.total) def test_should_handle_no_orders(self): del self.orders[:] - self.resource.on_get(self.req, self.resp, self.keystone_id) + resp = self.app.get('/%s/orders/' % self.keystone_id, self.params) self.order_repo.get_by_create_date \ .assert_called_once_with(self.keystone_id, @@ -1186,24 +1228,39 @@ class WhenGettingOrdersListUsingOrdersResource(testtools.TestCase): self.limit), suppress_exception=True) - resp_body = jsonutils.loads(self.resp.body) - self.assertFalse('previous' in resp_body) - self.assertFalse('next' in resp_body) + self.assertFalse('previous' in resp.namespace) + self.assertFalse('next' in resp.namespace) def _create_url(self, keystone_id, offset_arg=None, limit_arg=None): if limit_arg: offset = int(offset_arg) limit = int(limit_arg) - return '/v1/{0}/orders?limit={1}&offset={2}'.format(keystone_id, - limit, - offset) + return '/{0}/orders?limit={1}&offset={2}'.format(keystone_id, + limit, + offset) else: - return '/v1/{0}/orders'.format(self.keystone_id) + return '/{0}/orders'.format(self.keystone_id) -class WhenGettingOrDeletingOrderUsingOrderResource(testtools.TestCase): +class WhenGettingOrDeletingOrderUsingOrderResource(FunctionalTest): def setUp(self): - super(WhenGettingOrDeletingOrderUsingOrderResource, self).setUp() + super( + WhenGettingOrDeletingOrderUsingOrderResource, self + ).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + + @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' @@ -1213,14 +1270,12 @@ class WhenGettingOrDeletingOrderUsingOrderResource(testtools.TestCase): self.order_repo.get.return_value = self.order self.order_repo.delete_entity_by_id.return_value = None - self.req = mock.MagicMock() - self.resp = mock.MagicMock() - - self.resource = res.OrderResource(self.order_repo) + self.tenant_repo = mock.MagicMock() + self.queue_resource = mock.MagicMock() def test_should_get_order(self): - self.resource.on_get(self.req, self.resp, self.tenant_keystone_id, - self.order.id) + self.app.get('/%s/orders/%s/' % (self.tenant_keystone_id, + self.order.id)) self.order_repo.get \ .assert_called_once_with(entity_id=self.order.id, @@ -1228,33 +1283,28 @@ class WhenGettingOrDeletingOrderUsingOrderResource(testtools.TestCase): suppress_exception=True) def test_should_delete_order(self): - self.resource.on_delete(self.req, self.resp, self.tenant_keystone_id, - self.order.id) - + self.app.delete('/%s/orders/%s/' % (self.tenant_keystone_id, + self.order.id)) self.order_repo.delete_entity_by_id \ .assert_called_once_with(entity_id=self.order.id, keystone_id=self.tenant_keystone_id) def test_should_throw_exception_for_get_when_order_not_found(self): self.order_repo.get.return_value = None - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_get, - self.req, self.resp, self.tenant_keystone_id, self.order.id, + resp = self.app.get( + '/%s/orders/%s/' % (self.tenant_keystone_id, self.order.id), + expect_errors=True ) - self.assertEqual(falcon.HTTP_404, exception.status) + self.assertEqual(resp.status_int, 404) def test_should_throw_exception_for_delete_when_order_not_found(self): self.order_repo.delete_entity_by_id.side_effect = excep.NotFound( "Test not found exception") - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_delete, - self.req, self.resp, self.tenant_keystone_id, self.order.id, + resp = self.app.delete( + '/%s/orders/%s/' % (self.tenant_keystone_id, self.order.id), + expect_errors=True ) - self.assertEqual(falcon.HTTP_404, exception.status) + self.assertEqual(resp.status_int, 404) class WhenAddingNavigationHrefs(testtools.TestCase): @@ -1271,11 +1321,11 @@ class WhenAddingNavigationHrefs(testtools.TestCase): offset = 0 limit = 10 - data_with_hrefs = res.add_nav_hrefs(self.resource_name, - self.keystone_id, - offset, limit, - self.num_elements, - self.data) + data_with_hrefs = controllers.hrefs.add_nav_hrefs(self.resource_name, + self.keystone_id, + offset, limit, + self.num_elements, + self.data) self.assertNotIn('previous', data_with_hrefs) self.assertIn('next', data_with_hrefs) @@ -1284,11 +1334,11 @@ class WhenAddingNavigationHrefs(testtools.TestCase): offset = 10 limit = 10 - data_with_hrefs = res.add_nav_hrefs(self.resource_name, - self.keystone_id, - offset, limit, - self.num_elements, - self.data) + data_with_hrefs = controllers.hrefs.add_nav_hrefs(self.resource_name, + self.keystone_id, + offset, limit, + self.num_elements, + self.data) self.assertIn('previous', data_with_hrefs) self.assertIn('next', data_with_hrefs) @@ -1297,11 +1347,11 @@ class WhenAddingNavigationHrefs(testtools.TestCase): offset = 90 limit = 10 - data_with_hrefs = res.add_nav_hrefs(self.resource_name, - self.keystone_id, - offset, limit, - self.num_elements, - self.data) + data_with_hrefs = controllers.hrefs.add_nav_hrefs(self.resource_name, + self.keystone_id, + offset, limit, + self.num_elements, + self.data) self.assertIn('previous', data_with_hrefs) self.assertNotIn('next', data_with_hrefs) @@ -1345,10 +1395,25 @@ class TestingJsonSanitization(testtools.TestCase): .endswith(' '), "whitespace should be gone") -class WhenCreatingContainersUsingContainersResource(testtools.TestCase): +class WhenCreatingContainersUsingContainersResource(FunctionalTest): def setUp(self): - super(WhenCreatingContainersUsingContainersResource, self).setUp() + super( + WhenCreatingContainersUsingContainersResource, self + ).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + @property + def root(self): + self._init() + + class RootController(object): + containers = controllers.containers.ContainersController( + self.tenant_repo, self.container_repo, self.secret_repo + ) + + return RootController() + + def _init(self): self.name = 'test container name' self.type = 'generic' self.secret_refs = [ @@ -1382,60 +1447,58 @@ class WhenCreatingContainersUsingContainersResource(testtools.TestCase): self.secret_repo = mock.MagicMock() self.secret_repo.create_from.return_value = None - self.stream = mock.MagicMock() - self.container_req = {'name': self.name, 'type': self.type, 'secret_refs': self.secret_refs} - self.json = json.dumps(self.container_req) - self.stream.read.return_value = self.json - - self.req = mock.MagicMock() - self.req.stream = self.stream - - self.resp = mock.MagicMock() - self.resource = res.ContainersResource(self.tenant_repo, - self.container_repo, - self.secret_repo) - def test_should_add_new_container(self): - self.resource.on_post(self.req, self.resp, self.tenant_keystone_id) - - self.assertEqual(falcon.HTTP_202, self.resp.status) + resp = self.app.post_json( + '/%s/containers/' % self.tenant_keystone_id, + self.container_req + ) + self.assertEqual(resp.status_int, 202) args, kwargs = self.container_repo.create_from.call_args container = args[0] self.assertIsInstance(container, models.Container) def test_should_fail_container_bad_json(self): - self.stream.read.return_value = '' - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.tenant_keystone_id, + resp = self.app.post( + '/%s/containers/' % self.tenant_keystone_id, + '', + expect_errors=True ) - self.assertEqual(falcon.HTTP_400, exception.status) + self.assertEquals(resp.status_int, 400) def test_should_throw_exception_when_secret_ref_doesnt_exist(self): self.secret_repo.get.return_value = None - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_post, - self.req, self.resp, self.tenant_keystone_id, + resp = self.app.post_json( + '/%s/containers/' % self.tenant_keystone_id, + self.container_req, + expect_errors=True ) - self.assertEqual(falcon.HTTP_404, exception.status) + self.assertEqual(resp.status_int, 404) -class WhenGettingOrDeletingContainerUsingContainerResource(testtools.TestCase): +class WhenGettingOrDeletingContainerUsingContainerResource(FunctionalTest): def setUp(self): super( - WhenGettingOrDeletingContainerUsingContainerResource, - self, + WhenGettingOrDeletingContainerUsingContainerResource, self ).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + @property + def root(self): + self._init() + + class RootController(object): + containers = controllers.containers.ContainersController( + self.tenant_repo, self.container_repo, self.secret_repo + ) + + return RootController() + + def _init(self): self.tenant_keystone_id = 'keystoneid1234' self.tenant_internal_id = 'tenantid1234' @@ -1452,15 +1515,12 @@ class WhenGettingOrDeletingContainerUsingContainerResource(testtools.TestCase): self.container_repo.get.return_value = self.container self.container_repo.delete_entity_by_id.return_value = None - self.req = mock.MagicMock() - self.resp = mock.MagicMock() - - self.resource = res.ContainerResource(self.tenant_repo, - self.container_repo) + self.secret_repo = mock.MagicMock() def test_should_get_container(self): - self.resource.on_get(self.req, self.resp, self.tenant_keystone_id, - self.container.id) + self.app.get('/%s/containers/%s/' % ( + self.tenant_keystone_id, self.container.id + )) self.container_repo.get \ .assert_called_once_with(entity_id=self.container.id, @@ -1468,8 +1528,9 @@ class WhenGettingOrDeletingContainerUsingContainerResource(testtools.TestCase): suppress_exception=True) def test_should_delete_container(self): - self.resource.on_delete(self.req, self.resp, self.tenant_keystone_id, - self.container.id) + self.app.delete('/%s/containers/%s/' % ( + self.tenant_keystone_id, self.container.id + )) self.container_repo.delete_entity_by_id \ .assert_called_once_with(entity_id=self.container.id, @@ -1477,30 +1538,40 @@ class WhenGettingOrDeletingContainerUsingContainerResource(testtools.TestCase): def test_should_throw_exception_for_get_when_container_not_found(self): self.container_repo.get.return_value = None - - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_get, - self.req, self.resp, self.tenant_keystone_id, self.container.id, - ) - self.assertEqual(falcon.HTTP_404, exception.status) + resp = self.app.get('/%s/containers/%s/' % ( + self.tenant_keystone_id, self.container.id + ), expect_errors=True) + self.assertEqual(resp.status_int, 404) def test_should_throw_exception_for_delete_when_container_not_found(self): self.container_repo.delete_entity_by_id.side_effect = excep.NotFound( "Test not found exception") - exception = self.assertRaises( - falcon.HTTPError, - self.resource.on_delete, - self.req, self.resp, self.tenant_keystone_id, self.container.id, - ) - self.assertEqual(falcon.HTTP_404, exception.status) + resp = self.app.delete('/%s/containers/%s/' % ( + self.tenant_keystone_id, self.container.id + ), expect_errors=True) + self.assertEqual(resp.status_int, 404) -class WhenGettingContainersListUsingResource(testtools.TestCase): +class WhenGettingContainersListUsingResource(FunctionalTest): def setUp(self): - super(WhenGettingContainersListUsingResource, self).setUp() + super( + WhenGettingContainersListUsingResource, self + ).setUp() + self.app = TestApp(app.PecanAPI(self.root)) + @property + def root(self): + self._init() + + class RootController(object): + containers = controllers.containers.ContainersController( + self.tenant_repo, self.container_repo, self.secret_repo + ) + + return RootController() + + def _init(self): self.tenant_id = 'tenant1234' self.keystone_id = 'keystoneid1234' @@ -1519,18 +1590,16 @@ class WhenGettingContainersListUsingResource(testtools.TestCase): self.tenant_repo = mock.MagicMock() self.secret_repo = mock.MagicMock() - self.req = mock.MagicMock() - self.req.accept = 'application/json' - self.req.get_param = mock.Mock() - self.req.get_param.side_effect = [self.offset, self.limit, None, None, - None, 0] - self.resp = mock.MagicMock() - self.resource = res.ContainersResource(self.tenant_repo, - self.container_repo, - self.secret_repo) + self.params = { + 'offset': self.offset, + 'limit': self.limit, + } def test_should_get_list_containers(self): - self.resource.on_get(self.req, self.resp, self.keystone_id) + resp = self.app.get( + '/%s/containers/' % self.keystone_id, + self.params + ) self.container_repo.get_by_create_date \ .assert_called_once_with(self.keystone_id, @@ -1538,33 +1607,37 @@ class WhenGettingContainersListUsingResource(testtools.TestCase): limit_arg=self.limit, suppress_exception=True) - resp_body = jsonutils.loads(self.resp.body) - self.assertTrue('previous' in resp_body) - self.assertTrue('next' in resp_body) + self.assertTrue('previous' in resp.namespace) + self.assertTrue('next' in resp.namespace) url_nav_next = self._create_url(self.keystone_id, self.offset + self.limit, self.limit) - self.assertTrue(self.resp.body.count(url_nav_next) == 1) + self.assertTrue(resp.body.count(url_nav_next) == 1) url_nav_prev = self._create_url(self.keystone_id, 0, self.limit) - self.assertTrue(self.resp.body.count(url_nav_prev) == 1) + self.assertTrue(resp.body.count(url_nav_prev) == 1) url_hrefs = self._create_url(self.keystone_id) - self.assertTrue(self.resp.body.count(url_hrefs) == + self.assertTrue(resp.body.count(url_hrefs) == (self.num_containers + 2)) def test_response_should_include_total(self): - self.resource.on_get(self.req, self.resp, self.keystone_id) - resp_body = jsonutils.loads(self.resp.body) - self.assertIn('total', resp_body) - self.assertEqual(resp_body['total'], self.total) + resp = self.app.get( + '/%s/containers/' % self.keystone_id, + self.params + ) + self.assertIn('total', resp.namespace) + self.assertEqual(resp.namespace['total'], self.total) def test_should_handle_no_containers(self): del self.containers[:] - self.resource.on_get(self.req, self.resp, self.keystone_id) + resp = self.app.get( + '/%s/containers/' % self.keystone_id, + self.params + ) self.container_repo.get_by_create_date \ .assert_called_once_with(self.keystone_id, @@ -1572,16 +1645,15 @@ class WhenGettingContainersListUsingResource(testtools.TestCase): limit_arg=self.limit, suppress_exception=True) - resp_body = jsonutils.loads(self.resp.body) - self.assertFalse('previous' in resp_body) - self.assertFalse('next' in resp_body) + self.assertFalse('previous' in resp.namespace) + self.assertFalse('next' in resp.namespace) def _create_url(self, keystone_id, offset_arg=None, limit_arg=None): if limit_arg: offset = int(offset_arg) limit = int(limit_arg) - return '/v1/{0}/containers' \ + return '/{0}/containers' \ '?limit={1}&offset={2}'.format(keystone_id, limit, offset) else: - return '/v1/{0}/containers'.format(self.keystone_id) + return '/{0}/containers'.format(self.keystone_id) diff --git a/barbican/tests/api/test_resources_policy.py b/barbican/tests/api/test_resources_policy.py index 58d13d9b4..9e17f24b6 100644 --- a/barbican/tests/api/test_resources_policy.py +++ b/barbican/tests/api/test_resources_policy.py @@ -23,11 +23,13 @@ import os import testtools -import falcon import mock +from webob import exc from oslo.config import cfg -from barbican.api import resources as res +from barbican.api.controllers import orders +from barbican.api.controllers import secrets +from barbican.api.controllers import versions from barbican import context from barbican.openstack.common import policy @@ -41,6 +43,52 @@ TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ENFORCER = policy.Enforcer() +class TestableResource(object): + + def __init__(self, *args, **kwargs): + self.controller = self.controller_cls(*args, **kwargs) + + def on_get(self, req, resp, *args, **kwargs): + with mock.patch('pecan.request', req): + with mock.patch('pecan.response', resp): + return self.controller.index(*args, **kwargs) + + def on_post(self, req, resp, *args, **kwargs): + with mock.patch('pecan.request', req): + with mock.patch('pecan.response', resp): + return self.controller.on_post(*args, **kwargs) + + def on_put(self, req, resp, *args, **kwargs): + with mock.patch('pecan.request', req): + with mock.patch('pecan.response', resp): + return self.controller.on_put(*args, **kwargs) + + def on_delete(self, req, resp, *args, **kwargs): + with mock.patch('pecan.request', req): + with mock.patch('pecan.response', resp): + return self.controller.on_delete(*args, **kwargs) + + +class VersionResource(TestableResource): + controller_cls = versions.VersionController + + +class SecretsResource(TestableResource): + controller_cls = secrets.SecretsController + + +class SecretResource(TestableResource): + controller_cls = secrets.SecretController + + +class OrdersResource(TestableResource): + controller_cls = orders.OrdersController + + +class OrderResource(TestableResource): + controller_cls = orders.OrderController + + class BaseTestCase(testtools.TestCase): def setUp(self): @@ -61,9 +109,12 @@ class BaseTestCase(testtools.TestCase): 'roles': roles or [], 'policy_enforcer': self.policy_enforcer, } - req.env = {} - req.env['barbican.context'] = context.RequestContext(**kwargs) - req.accept = accept + req.environ = {} + req.environ['barbican.context'] = context.RequestContext(**kwargs) + if accept: + req.accept.header_value.return_value = accept + else: + req.accept = None return req @@ -81,8 +132,7 @@ class BaseTestCase(testtools.TestCase): def _assert_post_rbac_exception(self, exception, role): """Assert that we received the expected RBAC-passed exception.""" - self.assertEqual(falcon.HTTP_500, exception.status) - self.assertEqual('Read Error', exception.title) + self.assertEqual(500, exception.status_int) def _generate_get_error(self): """Falcon exception generator to throw from early-exit mocks. @@ -95,7 +145,7 @@ class BaseTestCase(testtools.TestCase): """ # The 'Read Error' clause needs to match that asserted in # _assert_post_rbac_exception() above. - return falcon.HTTPError(falcon.HTTP_500, 'Read Error') + return exc.HTTPInternalServerError(message='Read Error') def _assert_pass_rbac(self, roles, method_under_test, accept=None): """Assert that RBAC authorization rules passed for the specified roles. @@ -110,8 +160,9 @@ class BaseTestCase(testtools.TestCase): accept=accept) # Force an exception early past the RBAC passing. - self.req.stream = self._generate_stream_for_exit() - exception = self.assertRaises(falcon.HTTPError, method_under_test) + self.req.body_file = self._generate_stream_for_exit() + exception = self.assertRaises(exc.HTTPInternalServerError, + method_under_test) self._assert_post_rbac_exception(exception, role) self.setUp() # Need to re-setup @@ -128,8 +179,8 @@ class BaseTestCase(testtools.TestCase): self.req = self._generate_req(roles=[role] if role else [], accept=accept) - exception = self.assertRaises(falcon.HTTPError, method_under_test) - self.assertEqual(falcon.HTTP_403, exception.status) + exception = self.assertRaises(exc.HTTPForbidden, method_under_test) + self.assertEqual(403, exception.status_int) self.setUp() # Need to re-setup @@ -139,7 +190,7 @@ class WhenTestingVersionResource(BaseTestCase): def setUp(self): super(WhenTestingVersionResource, self).setUp() - self.resource = res.VersionResource() + self.resource = VersionResource() def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) @@ -184,13 +235,13 @@ class WhenTestingSecretsResource(BaseTestCase): ._generate_get_error()) self.secret_repo.get_by_create_date = get_by_create_date - self.resource = res.SecretsResource(crypto_manager=mock.MagicMock(), - tenant_repo=mock.MagicMock(), - secret_repo=self.secret_repo, - tenant_secret_repo=mock - .MagicMock(), - datum_repo=mock.MagicMock(), - kek_repo=mock.MagicMock()) + self.resource = SecretsResource(crypto_manager=mock.MagicMock(), + tenant_repo=mock.MagicMock(), + secret_repo=self.secret_repo, + tenant_secret_repo=mock + .MagicMock(), + datum_repo=mock.MagicMock(), + kek_repo=mock.MagicMock()) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) @@ -233,11 +284,12 @@ class WhenTestingSecretResource(BaseTestCase): self.secret_repo.get = fail_method self.secret_repo.delete_entity_by_id = fail_method - self.resource = res.SecretResource(crypto_manager=mock.MagicMock(), - tenant_repo=mock.MagicMock(), - secret_repo=self.secret_repo, - datum_repo=mock.MagicMock(), - kek_repo=mock.MagicMock()) + self.resource = SecretResource(self.secret_id, + crypto_manager=mock.MagicMock(), + tenant_repo=mock.MagicMock(), + secret_repo=self.secret_repo, + datum_repo=mock.MagicMock(), + kek_repo=mock.MagicMock()) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) @@ -276,15 +328,15 @@ class WhenTestingSecretResource(BaseTestCase): def _invoke_on_get(self): self.resource.on_get(self.req, self.resp, - self.keystone_id, self.secret_id) + self.keystone_id) def _invoke_on_put(self): self.resource.on_put(self.req, self.resp, - self.keystone_id, self.secret_id) + self.keystone_id) def _invoke_on_delete(self): self.resource.on_delete(self.req, self.resp, - self.keystone_id, self.secret_id) + self.keystone_id) class WhenTestingOrdersResource(BaseTestCase): @@ -302,9 +354,9 @@ class WhenTestingOrdersResource(BaseTestCase): ._generate_get_error()) self.order_repo.get_by_create_date = get_by_create_date - self.resource = res.OrdersResource(tenant_repo=mock.MagicMock(), - order_repo=self.order_repo, - queue_resource=mock.MagicMock()) + self.resource = OrdersResource(tenant_repo=mock.MagicMock(), + order_repo=self.order_repo, + queue_resource=mock.MagicMock()) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) @@ -347,7 +399,8 @@ class WhenTestingOrderResource(BaseTestCase): self.order_repo.get = fail_method self.order_repo.delete_entity_by_id = fail_method - self.resource = res.OrderResource(order_repo=self.order_repo) + self.resource = OrderResource(self.order_id, + order_repo=self.order_repo) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) @@ -368,9 +421,7 @@ class WhenTestingOrderResource(BaseTestCase): self._invoke_on_delete) def _invoke_on_get(self): - self.resource.on_get(self.req, self.resp, - self.keystone_id, self.order_id) + self.resource.on_get(self.req, self.resp, self.keystone_id) def _invoke_on_delete(self): - self.resource.on_delete(self.req, self.resp, - self.keystone_id, self.order_id) + self.resource.on_delete(self.req, self.resp, self.keystone_id) diff --git a/barbican/tests/tasks/test_resources.py b/barbican/tests/tasks/test_resources.py index aff22732a..65bc82899 100644 --- a/barbican/tests/tasks/test_resources.py +++ b/barbican/tests/tasks/test_resources.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import falcon import mock import testtools @@ -143,7 +142,7 @@ class WhenBeginningOrder(testtools.TestCase): ) self.assertEqual(models.States.ERROR, self.order.status) - self.assertEqual(falcon.HTTP_500, self.order.error_status_code) + self.assertEqual(500, self.order.error_status_code) self.assertEqual(u._('Create Secret failure seen - please contact ' 'site administrator.'), self.order.error_reason) diff --git a/requirements.txt b/requirements.txt index 36d31bd60..2dd2eef92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ alembic>=0.4.1 Babel>=1.3 eventlet>=0.13.0 -falcon>=0.1.6,<0.1.7 +pecan>=0.5.0 iso8601==0.1.8 jsonschema>=1.3.0,!=1.4.0 kombu>=2.4.8