Implement the REST API with pecan.

Change-Id: I60665f8273a61303be98912e249b153d784a221b
This commit is contained in:
Ryan Petrello 2014-04-21 14:10:15 -07:00 committed by Steve Heyman
parent 2e414eb031
commit daa0517b63
14 changed files with 1640 additions and 1343 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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}

View File

@ -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'

View File

@ -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}

View File

@ -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__
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -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)

View File

@ -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