Use api-paste.ini for loading middleware

Changes:
* use api-paste.ini config for loading middleware
* refactoring middlewares to support loading via pastedeploy
* use debug middleware from oslo_middleware library instead log_exchange
  middleware

Closes-bug: #1503983
Closes-bug: #1361360

Change-Id: I444c1799ef53dbb19a601e51dd95cd8509fb1c0c
This commit is contained in:
Sergey Reshetnyak 2015-10-07 16:24:35 +03:00
parent f8e6907299
commit 64583e4b83
11 changed files with 137 additions and 170 deletions

View File

@ -10,6 +10,8 @@ include sahara/db/migration/alembic_migrations/versions/*.py
include sahara/db/migration/alembic_migrations/versions/README
include sahara/db/templates/README.rst
include etc/sahara/api-paste.ini
recursive-include sahara/locale *
recursive-include sahara/db/migration/alembic_migrations *

View File

@ -76,6 +76,8 @@ function configure_sahara {
cp -p $SAHARA_DIR/etc/sahara/policy.json $SAHARA_CONF_DIR
fi
cp -p $SAHARA_DIR/etc/sahara/api-paste.ini $SAHARA_CONF_DIR
# Create auth cache dir
sudo install -d -o $STACK_USER -m 700 $SAHARA_AUTH_CACHE_DIR
rm -rf $SAHARA_AUTH_CACHE_DIR/*

View File

@ -250,3 +250,12 @@ Example 2. Disallow image registry manipulations to non-admin users.
"data-processing:images:add_tags": "role:admin",
"data-processing:images:remove_tags": "role:admin"
}
API configuration
-----------------
Sahara uses the ``api-paste.ini`` file to configure the data processing API
service. For middleware injection sahara uses pastedeploy library. The location
of the api-paste file is controlled by the ``api_paste_config`` parameter in
the ``[default]`` section. By default sahara will search for a
``api-paste.ini`` file in the same directory as the configuration file.

28
etc/sahara/api-paste.ini Normal file
View File

@ -0,0 +1,28 @@
[pipeline:sahara]
pipeline = request_id cors acl auth_validator sahara_api
[composite:sahara_api]
use = egg:Paste#urlmap
/: sahara_apiv11
[app:sahara_apiv11]
paste.app_factory = sahara.api.middleware.sahara_middleware:Router.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
allow_headers=X-Auth-Token,X-Server-Management-Url
allow_methods=GET,PUT,POST,DELETE,PATCH
allowed_origin=
expose_headers=X-Auth-Token,X-Server-Management-Url
[filter:request_id]
paste.filter_factory = oslo_middleware.request_id:RequestId.factory
[filter:acl]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
[filter:auth_validator]
paste.filter_factory = sahara.api.middleware.auth_valid:AuthValidator.factory
[filter:debug]
paste.filter_factory = oslo_middleware.debug:Debug.factory

View File

@ -17,7 +17,6 @@
import functools
from keystonemiddleware import auth_token
from oslo_config import cfg
from oslo_policy import policy
@ -45,8 +44,3 @@ def enforce(rule):
return handler
return decorator
def wrap(app):
"""Wrap wsgi application with ACL check."""
return auth_token.AuthProtocol(app, {})

View File

@ -14,6 +14,8 @@
# limitations under the License.
from oslo_log import log as logging
from oslo_middleware import base
import webob
import webob.exc as ex
from sahara.i18n import _
@ -24,13 +26,12 @@ import sahara.openstack.commons as commons
LOG = logging.getLogger(__name__)
class AuthValidator(object):
class AuthValidator(base.Middleware):
"""Handles token auth results and tenants."""
def __init__(self, app):
self.app = app
def __call__(self, env, start_response):
@webob.dec.wsgify
def __call__(self, req):
"""Ensures that tenants in url and token are equal.
Handle incoming request by checking tenant info prom the headers and
@ -40,30 +41,20 @@ class AuthValidator(object):
Reject request if tenant_id from headers not equals to tenant_id from
url.
"""
token_tenant = env['HTTP_X_TENANT_ID']
token_tenant = req.environ.get("HTTP_X_TENANT_ID")
if not token_tenant:
LOG.warning(_LW("Can't get tenant_id from env"))
resp = ex.HTTPServiceUnavailable()
return resp(env, start_response)
raise ex.HTTPServiceUnavailable()
path = env['PATH_INFO']
path = req.environ['PATH_INFO']
if path != '/':
version, url_tenant, rest = commons.split_path(path, 3, 3, True)
if not version or not url_tenant or not rest:
LOG.warning(_LW("Incorrect path: {path}").format(path=path))
resp = ex.HTTPNotFound(_("Incorrect path"))
return resp(env, start_response)
raise ex.HTTPNotFound(_("Incorrect path"))
if token_tenant != url_tenant:
LOG.debug("Unauthorized: token tenant != requested tenant")
resp = ex.HTTPUnauthorized(
raise ex.HTTPUnauthorized(
_('Token tenant != requested tenant'))
return resp(env, start_response)
return self.app(env, start_response)
def wrap(app):
"""Wrap wsgi application with auth validator check."""
return AuthValidator(app)
return self.application

View File

@ -1,67 +0,0 @@
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# 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.
# It's based on debug middleware from oslo-incubator
"""Debug middleware"""
from __future__ import print_function
import sys
from oslo_middleware import base
import six
import webob.dec
class LogExchange(base.Middleware):
"""Helper class that returns debug information.
Can be inserted into any WSGI application chain to get information about
the request and response.
"""
@webob.dec.wsgify
def __call__(self, req):
print(("*" * 40) + " REQUEST ENVIRON")
for key, value in req.environ.items():
print(key, "=", value)
if req.is_body_readable:
print(('*' * 40) + " REQUEST BODY")
if req.content_type == 'application/json':
print(req.json)
else:
print(req.body)
print()
resp = req.get_response(self.application)
print(("*" * 40) + " RESPONSE HEADERS")
for (key, value) in six.iteritems(resp.headers):
print(key, "=", value)
print()
resp.app_iter = self.print_generator(resp.app_iter)
return resp
@staticmethod
def print_generator(app_iter):
"""Prints the contents of a wrapper string iterator when iterated."""
print(("*" * 40) + " RESPONSE BODY")
for part in app_iter:
sys.stdout.write(part)
sys.stdout.flush()
yield part
print()

View File

@ -0,0 +1,79 @@
# Copyright (c) 2015 Mirantis 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.
import flask
from oslo_config import cfg
import six
from werkzeug import exceptions as werkzeug_exceptions
from sahara.api import v10 as api_v10
from sahara.api import v11 as api_v11
from sahara import context
from sahara.utils import api as api_utils
CONF = cfg.CONF
def build_app():
"""App builder (wsgi).
Entry point for Sahara REST API server
"""
app = flask.Flask('sahara.api')
@app.route('/', methods=['GET'])
def version_list():
context.set_ctx(None)
return api_utils.render({
"versions": [
{"id": "v1.0", "status": "SUPPORTED"},
{"id": "v1.1", "status": "CURRENT"}
]
})
@app.teardown_request
def teardown_request(_ex=None):
context.set_ctx(None)
app.register_blueprint(api_v10.rest, url_prefix='/v1.0')
app.register_blueprint(api_v10.rest, url_prefix='/v1.1')
app.register_blueprint(api_v11.rest, url_prefix='/v1.1')
def make_json_error(ex):
status_code = (ex.code
if isinstance(ex, werkzeug_exceptions.HTTPException)
else 500)
description = (ex.description
if isinstance(ex, werkzeug_exceptions.HTTPException)
else str(ex))
return api_utils.render({'error': status_code,
'error_message': description},
status=status_code)
for code in six.iterkeys(werkzeug_exceptions.default_exceptions):
app.error_handler_spec[None][code] = make_json_error
return app
class Router(object):
def __call__(self, environ, response):
return self.app(environ, response)
@classmethod
def factory(cls, global_config, **local_config):
cls.app = build_app()
return cls(**local_config)

View File

@ -41,7 +41,6 @@ if os.path.exists(os.path.join(possible_topdir,
oslo_i18n.enable_lazy()
from sahara.api import acl
import sahara.main as server
from sahara.service import ops
@ -49,11 +48,6 @@ from sahara.service import ops
def main():
server.setup_common(possible_topdir, 'engine')
# NOTE(apavlov): acl.wrap is called here to set up auth_uri value
# in context by using keystone functionality (mostly to avoid
# code duplication).
acl.wrap(None)
server.setup_sahara_engine()
ops_server = ops.OpsServer()

View File

@ -15,6 +15,8 @@
import itertools
# loading keystonemiddleware opts because sahara uses these options in code
from keystonemiddleware import opts # noqa
from oslo_config import cfg
from oslo_config import types
from oslo_log import log

View File

@ -15,23 +15,14 @@
import os
import flask
from oslo_config import cfg
from oslo_log import log
import oslo_middleware.cors as cors_middleware
from oslo_middleware import request_id
from oslo_service import systemd
import six
from oslo_service import wsgi as oslo_wsgi
import stevedore
from werkzeug import exceptions as werkzeug_exceptions
from sahara.api import acl
from sahara.api.middleware import auth_valid
from sahara.api.middleware import log_exchange
from sahara.api import v10 as api_v10
from sahara.api import v11 as api_v11
from sahara import config
from sahara import context
from sahara.i18n import _LI
from sahara.i18n import _LW
from sahara.plugins import base as plugins_base
@ -39,7 +30,6 @@ from sahara.service import api as service_api
from sahara.service.edp import api as edp_api
from sahara.service import ops as service_ops
from sahara.service import periodic
from sahara.utils import api as api_utils
from sahara.utils.openstack import cinder
from sahara.utils import remote
from sahara.utils import rpc as messaging
@ -113,65 +103,8 @@ def setup_auth_policy():
def make_app():
"""App builder (wsgi)
Entry point for Sahara REST API server
"""
app = flask.Flask('sahara.api')
@app.route('/', methods=['GET'])
def version_list():
context.set_ctx(None)
return api_utils.render({
"versions": [
{"id": "v1.0", "status": "SUPPORTED"},
{"id": "v1.1", "status": "CURRENT"}
]
})
@app.teardown_request
def teardown_request(_ex=None):
context.set_ctx(None)
app.register_blueprint(api_v10.rest, url_prefix='/v1.0')
app.register_blueprint(api_v10.rest, url_prefix='/v1.1')
app.register_blueprint(api_v11.rest, url_prefix='/v1.1')
def make_json_error(ex):
status_code = (ex.code
if isinstance(ex, werkzeug_exceptions.HTTPException)
else 500)
description = (ex.description
if isinstance(ex, werkzeug_exceptions.HTTPException)
else str(ex))
return api_utils.render({'error': status_code,
'error_message': description},
status=status_code)
for code in six.iterkeys(werkzeug_exceptions.default_exceptions):
app.error_handler_spec[None][code] = make_json_error
if CONF.debug and not CONF.log_exchange:
LOG.debug('Logging of request/response exchange could be enabled using'
' flag --log-exchange')
# Create a CORS wrapper, and attach sahara-specific defaults that must be
# included in all CORS responses.
app.wsgi_app = cors_middleware.CORS(app.wsgi_app, CONF)
app.wsgi_app.set_latent(
allow_headers=['X-Auth-Token', 'X-Server-Management-Url'],
allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
expose_headers=['X-Auth-Token', 'X-Server-Management-Url']
)
if CONF.log_exchange:
app.wsgi_app = log_exchange.LogExchange.factory(CONF)(app.wsgi_app)
app.wsgi_app = auth_valid.wrap(app.wsgi_app)
app.wsgi_app = acl.wrap(app.wsgi_app)
app.wsgi_app = request_id.RequestId(app.wsgi_app)
return app
app_loader = oslo_wsgi.Loader(CONF)
return app_loader.load_app("sahara")
def _load_driver(namespace, name):