Implement the REST API with pecan.
Change-Id: I60665f8273a61303be98912e249b153d784a221b
This commit is contained in:
parent
2e414eb031
commit
daa0517b63
@ -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)
|
||||
|
@ -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
|
||||
|
97
barbican/api/controllers/__init__.py
Normal file
97
barbican/api/controllers/__init__.py
Normal 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
|
154
barbican/api/controllers/containers.py
Normal file
154
barbican/api/controllers/containers.py
Normal 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}
|
118
barbican/api/controllers/hrefs.py
Normal file
118
barbican/api/controllers/hrefs.py
Normal 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
|
188
barbican/api/controllers/orders.py
Normal file
188
barbican/api/controllers/orders.py
Normal 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}
|
27
barbican/api/controllers/performance.py
Normal file
27
barbican/api/controllers/performance.py
Normal 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'
|
252
barbican/api/controllers/secrets.py
Normal file
252
barbican/api/controllers/secrets.py
Normal 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}
|
35
barbican/api/controllers/versions.py
Normal file
35
barbican/api/controllers/versions.py
Normal 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__
|
||||
}
|
@ -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
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user