Pecan: Allow unauthenticated version listing

The current API allows listings of the neturon server
version without authentication. Because pecan was treating
this like a normal controller, it was requiring keystone
authentication.

This adjusts the version listing to behave as a wrapper so
it can be placed outside of the keystone authentication
wrapper to allow anonymous queries.

Closes-Bug: #1556038
Change-Id: I9f5aa3bea0e11c5e179fc286f9fa350b3930364f
This commit is contained in:
Kevin Benton 2016-03-11 03:57:53 -08:00
parent e433c2870a
commit 21825d6cbe
5 changed files with 50 additions and 22 deletions

View File

@ -25,7 +25,7 @@ class Versions(object):
@classmethod
def factory(cls, global_config, **local_config):
return cls()
return cls(app=None)
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
@ -38,6 +38,8 @@ class Versions(object):
]
if req.path != '/':
if self.app:
return req.get_response(self.app)
language = req.best_match_language()
msg = _('Unknown API version specified')
msg = oslo_i18n.translate(msg, language)
@ -57,3 +59,6 @@ class Versions(object):
response.body = wsgi.encode_body(body)
return response
def __init__(self, app):
self.app = app

View File

@ -19,6 +19,7 @@ from oslo_middleware import cors
from oslo_middleware import request_id
import pecan
from neutron.api import versions
from neutron.common import exceptions as n_exc
from neutron.pecan_wsgi import hooks
from neutron.pecan_wsgi import startup
@ -75,6 +76,9 @@ def _wrap_app(app):
raise n_exc.InvalidConfigurationOption(
opt_name='auth_strategy', opt_value=cfg.CONF.auth_strategy)
# version can be unauthenticated so it goes outside of auth
app = versions.Versions(app)
# This should be the last middleware in the list (which results in
# it being the first in the middleware chain). This is to ensure
# that any errors thrown by other middleware, such as an auth

View File

@ -41,10 +41,12 @@ class RootController(object):
@utils.expose(generic=True)
def index(self):
builder = versions_view.get_view_builder(pecan.request)
versions = [builder.build(version) for version in _get_version_info()]
return dict(versions=versions)
# NOTE(kevinbenton): The pecan framework does not handle
# any requests to the root because they are intercepted
# by the 'version' returning wrapper.
pass
@utils.when(index, method='GET')
@utils.when(index, method='HEAD')
@utils.when(index, method='POST')
@utils.when(index, method='PATCH')

View File

@ -62,10 +62,10 @@ class TestRootController(test_functional.PecanFunctionalTest):
manager.NeutronManager.set_controller_for_resource(
_SERVICE_PLUGIN_COLLECTION, FakeServicePluginController())
def _test_method_returns_405(self, method):
def _test_method_returns_code(self, method, code=200):
api_method = getattr(self.app, method)
response = api_method(self.base_url, expect_errors=True)
self.assertEqual(response.status_int, 405)
self.assertEqual(response.status_int, code)
def test_get(self):
response = self.app.get(self.base_url)
@ -77,20 +77,12 @@ class TestRootController(test_functional.PecanFunctionalTest):
self.assertIn(attr, versions[0])
self.assertEqual(value, versions[0][attr])
def test_post(self):
self._test_method_returns_405('post')
def test_put(self):
self._test_method_returns_405('put')
def test_patch(self):
self._test_method_returns_405('patch')
def test_delete(self):
self._test_method_returns_405('delete')
def test_head(self):
self._test_method_returns_405('head')
def test_methods(self):
self._test_method_returns_code('post')
self._test_method_returns_code('patch')
self._test_method_returns_code('delete')
self._test_method_returns_code('head')
self._test_method_returns_code('put')
class TestV2Controller(TestRootController):
@ -116,6 +108,14 @@ class TestV2Controller(TestRootController):
expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_methods(self):
self._test_method_returns_code('post', 405)
self._test_method_returns_code('put', 405)
self._test_method_returns_code('patch', 405)
self._test_method_returns_code('delete', 405)
self._test_method_returns_code('head', 405)
self._test_method_returns_code('delete', 405)
class TestExtensionsController(TestRootController):
"""Test extension listing and detail reporting."""
@ -142,6 +142,14 @@ class TestExtensionsController(TestRootController):
json_body = jsonutils.loads(response.body)
self.assertEqual(test_alias, json_body['extension']['alias'])
def test_methods(self):
self._test_method_returns_code('post', 405)
self._test_method_returns_code('put', 405)
self._test_method_returns_code('patch', 405)
self._test_method_returns_code('delete', 405)
self._test_method_returns_code('head', 405)
self._test_method_returns_code('delete', 405)
class TestQuotasController(test_functional.PecanFunctionalTest):
"""Test quota management API controller."""
@ -229,6 +237,7 @@ class TestResourceController(TestRootController):
"""Test generic controller"""
# TODO(salv-orlando): This test case must not explicitly test the 'port'
# resource. Also it should validate correct plugin/resource association
base_url = '/v2.0'
def setUp(self):
super(TestResourceController, self).setUp()
@ -279,6 +288,14 @@ class TestResourceController(TestRootController):
def test_plugin_initialized(self):
self.assertIsNotNone(manager.NeutronManager._instance)
def test_methods(self):
self._test_method_returns_code('post', 405)
self._test_method_returns_code('put', 405)
self._test_method_returns_code('patch', 405)
self._test_method_returns_code('delete', 405)
self._test_method_returns_code('head', 405)
self._test_method_returns_code('delete', 405)
class TestRequestProcessing(TestResourceController):

View File

@ -73,7 +73,7 @@ class TestErrors(PecanFunctionalTest):
class TestRequestID(PecanFunctionalTest):
def test_request_id(self):
response = self.app.get('/')
response = self.app.get('/v2.0/')
self.assertIn('x-openstack-request-id', response.headers)
self.assertTrue(
response.headers['x-openstack-request-id'].startswith('req-'))
@ -88,7 +88,7 @@ class TestKeystoneAuth(PecanFunctionalTest):
pass
def test_auth_enforced(self):
response = self.app.get('/', expect_errors=True)
response = self.app.get('/v2.0/', expect_errors=True)
self.assertEqual(response.status_int, 401)