Tweak Sahara to make version discovery easier
Or at least, tweak Sahara to be more accomodating to keystoneauth's way of doing version discovery. * Don't require auth to do version discovery * Make project ID in URL optional for APIv1 (and also for APIv2, but don't go around advertising that... *wink*) Change-Id: Idb6f734aee26cab5bd629963a66ba01c92760864 Closes-Bug: #1744350
This commit is contained in:
parent
58816b4c44
commit
b1503b36c4
@ -1,5 +1,5 @@
|
||||
[pipeline:sahara]
|
||||
pipeline = cors http_proxy_to_wsgi request_id acl auth_validator_v2 sahara_api
|
||||
pipeline = cors http_proxy_to_wsgi request_id versions acl auth_validator sahara_api
|
||||
|
||||
[composite:sahara_api]
|
||||
use = egg:Paste#urlmap
|
||||
@ -22,15 +22,18 @@ paste.filter_factory = oslo_middleware.request_id:RequestId.factory
|
||||
[filter:acl]
|
||||
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
|
||||
|
||||
# this filter is given as a reference for v1-only deployments
|
||||
# [filter:auth_validator]
|
||||
# paste.filter_factory = sahara.api.middleware.auth_valid:AuthValidator.factory
|
||||
|
||||
[filter:auth_validator_v2]
|
||||
paste.filter_factory = sahara.api.middleware.auth_valid:AuthValidatorV2.factory
|
||||
[filter:auth_validator]
|
||||
paste.filter_factory = sahara.api.middleware.auth_valid:AuthValidator.factory
|
||||
|
||||
[filter:debug]
|
||||
paste.filter_factory = oslo_middleware.debug:Debug.factory
|
||||
|
||||
[filter:http_proxy_to_wsgi]
|
||||
paste.filter_factory = oslo_middleware:HTTPProxyToWSGI.factory
|
||||
|
||||
[filter:versions]
|
||||
paste.filter_factory = sahara.api.middleware.version_discovery:VersionResponseMiddlewareV2.factory
|
||||
|
||||
# this filter is given as a reference for v1-only deployments
|
||||
#[filter:versions]
|
||||
#paste.filter_factory = sahara.api.middleware.version_discovery:VersionResponseMiddlewareV1.factory
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
other:
|
||||
- The presence of project ID in Sahara APIv1 paths is now optional.
|
@ -16,6 +16,7 @@
|
||||
from oslo_log import log as logging
|
||||
from oslo_middleware import base
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import uuidutils
|
||||
import webob
|
||||
import webob.exc as ex
|
||||
|
||||
@ -48,55 +49,16 @@ class AuthValidator(base.Middleware):
|
||||
path = req.environ['PATH_INFO']
|
||||
if path != '/':
|
||||
try:
|
||||
version, url_tenant, rest = strutils.split_path(path, 3, 3,
|
||||
True)
|
||||
version, possibly_url_tenant, rest = (
|
||||
strutils.split_path(path, 2, 3, True)
|
||||
)
|
||||
except ValueError:
|
||||
LOG.warning("Incorrect path: {path}".format(path=path))
|
||||
raise ex.HTTPNotFound(_("Incorrect path"))
|
||||
|
||||
if token_tenant != url_tenant:
|
||||
LOG.debug("Unauthorized: token tenant != requested tenant")
|
||||
raise ex.HTTPUnauthorized(
|
||||
_('Token tenant != requested tenant'))
|
||||
return self.application
|
||||
|
||||
|
||||
class AuthValidatorV2(base.Middleware):
|
||||
|
||||
"""Handles token auth results and tenants."""
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
"""Ensures valid path and tenant
|
||||
|
||||
Handle incoming requests by checking tenant info from the
|
||||
headers and url ({tenant_id} url attribute), if using v1 or v1.1
|
||||
APIs. If using the v2 API, this function just makes sure that
|
||||
keystonemiddleware has populated the WSGI environment.
|
||||
|
||||
Pass request downstream on success.
|
||||
Reject request if tenant_id from headers is not equal to the
|
||||
tenant_id from url in the case of v1.
|
||||
"""
|
||||
path = req.environ['PATH_INFO']
|
||||
if path != '/':
|
||||
token_tenant = req.environ.get("HTTP_X_TENANT_ID")
|
||||
if not token_tenant:
|
||||
LOG.warning("Can't get tenant_id from env")
|
||||
raise ex.HTTPServiceUnavailable()
|
||||
|
||||
try:
|
||||
if path.startswith('/v2'):
|
||||
version, rest = strutils.split_path(path, 2, 2, True)
|
||||
else:
|
||||
version, requested_tenant, rest = strutils.split_path(
|
||||
path, 3, 3, True)
|
||||
except ValueError:
|
||||
LOG.warning("Incorrect path: {path}".format(path=path))
|
||||
raise ex.HTTPNotFound(_("Incorrect path"))
|
||||
|
||||
if path.startswith('/v1'):
|
||||
if token_tenant != requested_tenant:
|
||||
if uuidutils.is_uuid_like(possibly_url_tenant):
|
||||
url_tenant = possibly_url_tenant
|
||||
if token_tenant != url_tenant:
|
||||
LOG.debug("Unauthorized: token tenant != requested tenant")
|
||||
raise ex.HTTPUnauthorized(
|
||||
_('Token tenant != requested tenant'))
|
||||
|
@ -28,26 +28,13 @@ from sahara.utils import api as api_utils
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def build_app(version_response=None):
|
||||
def build_app():
|
||||
"""App builder (wsgi).
|
||||
|
||||
Entry point for Sahara REST API server
|
||||
"""
|
||||
app = flask.Flask('sahara.api')
|
||||
|
||||
version_response = (version_response or
|
||||
{
|
||||
"versions": [
|
||||
{"id": "v1.0", "status": "SUPPORTED"},
|
||||
{"id": "v1.1", "status": "CURRENT"}
|
||||
]
|
||||
})
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def version_list():
|
||||
context.set_ctx(None)
|
||||
return api_utils.render(version_response)
|
||||
|
||||
@app.teardown_request
|
||||
def teardown_request(_ex=None):
|
||||
context.set_ctx(None)
|
||||
@ -78,14 +65,7 @@ def build_v2_app():
|
||||
|
||||
Entry point for Experimental V2 Sahara REST API server
|
||||
"""
|
||||
version_response = {
|
||||
"versions": [
|
||||
{"id": "v1.0", "status": "SUPPORTED"},
|
||||
{"id": "v1.1", "status": "CURRENT"},
|
||||
{"id": "v2", "status": "EXPERIMENTAL"}
|
||||
]
|
||||
}
|
||||
app = build_app(version_response)
|
||||
app = build_app()
|
||||
|
||||
api_v2.register_blueprints(app, url_prefix='/v2')
|
||||
|
||||
|
73
sahara/api/middleware/version_discovery.py
Normal file
73
sahara/api/middleware/version_discovery.py
Normal file
@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2018 Massachusetts Open Cloud
|
||||
#
|
||||
# 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 re
|
||||
|
||||
from oslo_middleware import base
|
||||
from oslo_serialization import jsonutils
|
||||
import webob
|
||||
import webob.dec
|
||||
|
||||
|
||||
class VersionResponseMiddlewareV1(base.Middleware):
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
"""Respond to a request for all Sahara API versions."""
|
||||
path = req.environ['PATH_INFO']
|
||||
if re.match(r"^/*$", path):
|
||||
response = webob.Response(request=req, status=300,
|
||||
content_type="application/json")
|
||||
response.body = jsonutils.dumps(self._get_versions(req))
|
||||
return response
|
||||
else:
|
||||
return self.application
|
||||
|
||||
def _get_versions(self, req):
|
||||
"""Populate the version response with APIv1 stuff."""
|
||||
version_response = {
|
||||
"versions": [
|
||||
{"id": "v1.0",
|
||||
"status": "SUPPORTED",
|
||||
"links": self._get_links("1.0", req)
|
||||
},
|
||||
{"id": "v1.1",
|
||||
"status": "CURRENT",
|
||||
"links": self._get_links("1.1", req)
|
||||
}
|
||||
]
|
||||
}
|
||||
return version_response
|
||||
|
||||
@staticmethod
|
||||
def _get_links(version, req):
|
||||
href = "%s/v%s/" % (req.host_url, version)
|
||||
return [{"rel": "self", "href": href}]
|
||||
|
||||
|
||||
class VersionResponseMiddlewareV2(VersionResponseMiddlewareV1):
|
||||
|
||||
def _get_versions(self, req):
|
||||
"""Populate the version response with APIv1 and APIv2 stuff."""
|
||||
version_response = (
|
||||
super(VersionResponseMiddlewareV2, self)._get_versions(req)
|
||||
)
|
||||
version_response["versions"].append(
|
||||
{"id": "v2",
|
||||
"status": "EXPERIMENTAL",
|
||||
"links": self._get_links("2", req)
|
||||
}
|
||||
)
|
||||
return version_response
|
@ -21,6 +21,9 @@ from sahara.tests.unit import base as test_base
|
||||
|
||||
class AuthValidatorTest(test_base.SaharaTestCase):
|
||||
|
||||
tid1 = '8f9f0c8c4c634d6280e785b44a10b8ab'
|
||||
tid2 = '431c8ab1f14f4607bdfc17e05b3924d1'
|
||||
|
||||
@staticmethod
|
||||
@webob.dec.wsgify
|
||||
def application(req):
|
||||
@ -30,71 +33,55 @@ class AuthValidatorTest(test_base.SaharaTestCase):
|
||||
super(AuthValidatorTest, self).setUp()
|
||||
self.app = auth_valid.AuthValidator(self.application)
|
||||
|
||||
def test_auth_ok(self):
|
||||
req = webob.Request.blank("/v1.1/tid/clusters", accept="text/plain",
|
||||
def test_auth_ok_project_id_in_url(self):
|
||||
req = webob.Request.blank("/v1.1/%s/clusters" % self.tid1,
|
||||
accept="text/plain",
|
||||
method="GET",
|
||||
environ={"HTTP_X_TENANT_ID": "tid"})
|
||||
environ={"HTTP_X_TENANT_ID": self.tid1})
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(200, res.status_code)
|
||||
|
||||
def test_auth_ok_no_project_id_in_url(self):
|
||||
req = webob.Request.blank("/v1.1/clusters",
|
||||
accept="text/plain",
|
||||
method="GET",
|
||||
environ={"HTTP_X_TENANT_ID": self.tid1})
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(200, res.status_code)
|
||||
|
||||
def test_auth_ok_without_path(self):
|
||||
req = webob.Request.blank("/", accept="text/plain", method="GET",
|
||||
environ={"HTTP_X_TENANT_ID": "tid"})
|
||||
environ={"HTTP_X_TENANT_ID": self.tid1})
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(200, res.status_code)
|
||||
|
||||
def test_auth_without_header(self):
|
||||
req = webob.Request.blank("/v1.1/tid/clusters", accept="text/plain",
|
||||
def test_auth_without_environ(self):
|
||||
req = webob.Request.blank("/v1.1/%s/clusters" % self.tid1,
|
||||
accept="text/plain",
|
||||
method="GET")
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(503, res.status_code)
|
||||
|
||||
def test_auth_with_wrong_url(self):
|
||||
req = webob.Request.blank("/v1.1", accept="text/plain", method="GET",
|
||||
environ={"HTTP_X_TENANT_ID": "tid"})
|
||||
environ={"HTTP_X_TENANT_ID": self.tid1})
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(404, res.status_code)
|
||||
|
||||
def test_auth_different_tenant(self):
|
||||
req = webob.Request.blank("/v1.1/tid1/clusters", accept="text/plain",
|
||||
req = webob.Request.blank("/v1.1/%s/clusters" % self.tid1,
|
||||
accept="text/plain",
|
||||
method="GET",
|
||||
environ={"HTTP_X_TENANT_ID": "tid2"})
|
||||
environ={"HTTP_X_TENANT_ID": self.tid2})
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(401, res.status_code)
|
||||
|
||||
|
||||
class AuthValidatorV2Test(test_base.SaharaTestCase):
|
||||
|
||||
@staticmethod
|
||||
@webob.dec.wsgify
|
||||
def application(req):
|
||||
return "Banana"
|
||||
|
||||
def setUp(self):
|
||||
super(AuthValidatorV2Test, self).setUp()
|
||||
self.app = auth_valid.AuthValidatorV2(self.application)
|
||||
|
||||
def test_auth_ok(self):
|
||||
req = webob.Request.blank("/v2/tid/clusters", accept="text/plain",
|
||||
def test_auth_tenant_id_in_url_v2(self):
|
||||
# NOTE(jfreud): we expect AuthValidator to let this case pass through
|
||||
# although Flask will reject it with a 404 further down the pipeline
|
||||
req = webob.Request.blank("/v2/%s/clusters" % self.tid1,
|
||||
accept="text/plain",
|
||||
method="GET",
|
||||
environ={"HTTP_X_TENANT_ID": "tid"})
|
||||
environ={"HTTP_X_TENANT_ID": self.tid1})
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(200, res.status_code)
|
||||
|
||||
def test_auth_ok_without_path(self):
|
||||
req = webob.Request.blank("/", accept="text/plain", method="GET",
|
||||
environ={"HTTP_X_TENANT_ID": "tid"})
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(200, res.status_code)
|
||||
|
||||
def test_auth_without_environ(self):
|
||||
req = webob.Request.blank("/v2/tid/clusters", accept="text/plain",
|
||||
method="GET")
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(503, res.status_code)
|
||||
|
||||
def test_auth_with_wrong_url(self):
|
||||
req = webob.Request.blank("/v2", accept="text/plain", method="GET",
|
||||
environ={"HTTP_X_TENANT_ID": "tid"})
|
||||
res = req.get_response(self.app)
|
||||
self.assertEqual(404, res.status_code)
|
||||
|
@ -76,7 +76,7 @@ class Rest(flask.Blueprint):
|
||||
if status:
|
||||
flask.request.status_code = status
|
||||
|
||||
kwargs.pop("tenant_id")
|
||||
kwargs.pop("tenant_id", None)
|
||||
req_id = flask.request.environ.get(oslo_req_id.ENV_REQUEST_ID)
|
||||
auth_plugin = flask.request.environ.get('keystone.token_auth')
|
||||
ctx = context.Context(
|
||||
@ -103,6 +103,8 @@ class Rest(flask.Blueprint):
|
||||
return internal_error(500, 'Internal Server Error', e)
|
||||
|
||||
f_rule = "/<tenant_id>" + rule
|
||||
self.add_url_rule(rule, endpoint, handler, **options)
|
||||
self.add_url_rule(rule + '.json', endpoint, handler, **options)
|
||||
self.add_url_rule(f_rule, endpoint, handler, **options)
|
||||
self.add_url_rule(f_rule + '.json', endpoint, handler, **options)
|
||||
|
||||
@ -131,6 +133,7 @@ class RestV2(Rest):
|
||||
if status:
|
||||
flask.request.status_code = status
|
||||
|
||||
kwargs.pop("tenant_id", None)
|
||||
req_id = flask.request.environ.get(oslo_req_id.ENV_REQUEST_ID)
|
||||
auth_plugin = flask.request.environ.get('keystone.token_auth')
|
||||
ctx = context.Context(
|
||||
@ -156,8 +159,11 @@ class RestV2(Rest):
|
||||
except Exception as e:
|
||||
return internal_error(500, 'Internal Server Error', e)
|
||||
|
||||
f_rule = "/<tenant_id>" + rule
|
||||
self.add_url_rule(rule, endpoint, handler, **options)
|
||||
self.add_url_rule(rule + '.json', endpoint, handler, **options)
|
||||
self.add_url_rule(f_rule, endpoint, handler, **options)
|
||||
self.add_url_rule(f_rule + '.json', endpoint, handler, **options)
|
||||
|
||||
return func
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user