Add V3 JSON Home support to GET /
The server wasn't returning a JSON Home response for GET / when the Accept header is `application/json-home`. By returning the V3 JSON Home response for GET / a V3 client that supports JSON Home can GET either /v3 or / and use the response. The identity API should be able to be set to /. Closes-Bug: #1366589 Change-Id: I3191a85acf9d2f582f6b48a164cf5ac2bf84a8cf
This commit is contained in:
parent
12655bf172
commit
0b67673034
@ -20,7 +20,6 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from oslo import i18n
|
from oslo import i18n
|
||||||
from paste import deploy
|
|
||||||
import pbr.version
|
import pbr.version
|
||||||
|
|
||||||
|
|
||||||
@ -50,6 +49,7 @@ from keystone import config
|
|||||||
from keystone.i18n import _
|
from keystone.i18n import _
|
||||||
from keystone.openstack.common import service
|
from keystone.openstack.common import service
|
||||||
from keystone.openstack.common import systemd
|
from keystone.openstack.common import systemd
|
||||||
|
from keystone import service as keystone_service
|
||||||
|
|
||||||
|
|
||||||
CONF = config.CONF
|
CONF = config.CONF
|
||||||
@ -73,7 +73,7 @@ class ServerWrapper(object):
|
|||||||
|
|
||||||
|
|
||||||
def create_server(conf, name, host, port, workers):
|
def create_server(conf, name, host, port, workers):
|
||||||
app = deploy.loadapp('config:%s' % conf, name=name)
|
app = keystone_service.loadapp('config:%s' % conf, name)
|
||||||
server = environment.Server(app, host=host, port=port,
|
server = environment.Server(app, host=host, port=port,
|
||||||
keepalive=CONF.tcp_keepalive,
|
keepalive=CONF.tcp_keepalive,
|
||||||
keepidle=CONF.tcp_keepidle)
|
keepidle=CONF.tcp_keepidle)
|
||||||
|
@ -16,7 +16,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from oslo import i18n
|
from oslo import i18n
|
||||||
from paste import deploy
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE(dstanek): i18n.enable_lazy() must be called before
|
# NOTE(dstanek): i18n.enable_lazy() must be called before
|
||||||
@ -32,6 +31,7 @@ from keystone.common import environment
|
|||||||
from keystone.common import sql
|
from keystone.common import sql
|
||||||
from keystone import config
|
from keystone import config
|
||||||
from keystone.openstack.common import log
|
from keystone.openstack.common import log
|
||||||
|
from keystone import service
|
||||||
|
|
||||||
|
|
||||||
CONF = config.CONF
|
CONF = config.CONF
|
||||||
@ -55,7 +55,6 @@ drivers = backends.load_backends()
|
|||||||
# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
|
# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
|
||||||
# The following is a reference to Python Paste Deploy documentation
|
# The following is a reference to Python Paste Deploy documentation
|
||||||
# http://pythonpaste.org/deploy/
|
# http://pythonpaste.org/deploy/
|
||||||
application = deploy.loadapp('config:%s' % config.find_paste_config(),
|
application = service.loadapp('config:%s' % config.find_paste_config(), name)
|
||||||
name=name)
|
|
||||||
|
|
||||||
dependency.resolve_future_dependencies()
|
dependency.resolve_future_dependencies()
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
def build_v3_resource_relation(resource_name):
|
def build_v3_resource_relation(resource_name):
|
||||||
return ('http://docs.openstack.org/api/openstack-identity/3/rel/%s' %
|
return ('http://docs.openstack.org/api/openstack-identity/3/rel/%s' %
|
||||||
resource_name)
|
resource_name)
|
||||||
@ -49,3 +52,13 @@ class Parameters(object):
|
|||||||
ROLE_ID = build_v3_parameter_relation('role_id')
|
ROLE_ID = build_v3_parameter_relation('role_id')
|
||||||
SERVICE_ID = build_v3_parameter_relation('service_id')
|
SERVICE_ID = build_v3_parameter_relation('service_id')
|
||||||
USER_ID = build_v3_parameter_relation('user_id')
|
USER_ID = build_v3_parameter_relation('user_id')
|
||||||
|
|
||||||
|
|
||||||
|
def translate_urls(json_home, new_prefix):
|
||||||
|
"""Given a JSON Home document, sticks new_prefix on each of the urls."""
|
||||||
|
|
||||||
|
for dummy_rel, resource in six.iteritems(json_home['resources']):
|
||||||
|
if 'href' in resource:
|
||||||
|
resource['href'] = new_prefix + resource['href']
|
||||||
|
elif 'href-template' in resource:
|
||||||
|
resource['href-template'] = new_prefix + resource['href-template']
|
||||||
|
@ -12,9 +12,13 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import webob
|
||||||
|
|
||||||
from keystone.common import extension
|
from keystone.common import extension
|
||||||
|
from keystone.common import json_home
|
||||||
from keystone.common import wsgi
|
from keystone.common import wsgi
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
|
from keystone.openstack.common import jsonutils
|
||||||
from keystone.openstack.common import log
|
from keystone.openstack.common import log
|
||||||
|
|
||||||
|
|
||||||
@ -25,6 +29,31 @@ MEDIA_TYPE_XML = 'application/vnd.openstack.identity-%s+xml'
|
|||||||
|
|
||||||
_VERSIONS = []
|
_VERSIONS = []
|
||||||
|
|
||||||
|
# NOTE(blk-u): latest_app will be set by keystone.service.loadapp(). It gets
|
||||||
|
# set to the application that was just loaded. In the case of keystone-all,
|
||||||
|
# loadapp() gets called twice, once for the public app and once for the admin
|
||||||
|
# app. In the case of httpd/keystone, loadapp() gets called once for the public
|
||||||
|
# app if this is the public instance or loadapp() gets called for the admin app
|
||||||
|
# if it's the admin instance.
|
||||||
|
# This is used to fetch the /v3 JSON Home response. The /v3 JSON Home response
|
||||||
|
# is the same whether it's the admin or public service so either admin or
|
||||||
|
# public works.
|
||||||
|
latest_app = None
|
||||||
|
|
||||||
|
|
||||||
|
def request_v3_json_home(new_prefix):
|
||||||
|
if 'v3' not in _VERSIONS:
|
||||||
|
# No V3 support, so return an empty JSON Home document.
|
||||||
|
return {'resources': {}}
|
||||||
|
|
||||||
|
req = webob.Request.blank(
|
||||||
|
'/v3', headers={'Accept': 'application/json-home'})
|
||||||
|
v3_json_home_str = req.get_response(latest_app).body
|
||||||
|
v3_json_home = jsonutils.loads(v3_json_home_str)
|
||||||
|
json_home.translate_urls(v3_json_home, new_prefix)
|
||||||
|
|
||||||
|
return v3_json_home
|
||||||
|
|
||||||
|
|
||||||
class Extensions(wsgi.Application):
|
class Extensions(wsgi.Application):
|
||||||
"""Base extensions controller to be extended by public and admin API's."""
|
"""Base extensions controller to be extended by public and admin API's."""
|
||||||
@ -144,6 +173,14 @@ class Version(wsgi.Application):
|
|||||||
return versions
|
return versions
|
||||||
|
|
||||||
def get_versions(self, context):
|
def get_versions(self, context):
|
||||||
|
|
||||||
|
req_mime_type = v3_mime_type_best_match(context)
|
||||||
|
if req_mime_type == MimeTypes.JSON_HOME:
|
||||||
|
v3_json_home = request_v3_json_home('/v3')
|
||||||
|
return wsgi.render_response(
|
||||||
|
body=v3_json_home,
|
||||||
|
headers=(('Content-Type', MimeTypes.JSON_HOME),))
|
||||||
|
|
||||||
versions = self._get_versions_list(context)
|
versions = self._get_versions_list(context)
|
||||||
return wsgi.render_response(status=(300, 'Multiple Choices'), body={
|
return wsgi.render_response(status=(300, 'Multiple Choices'), body={
|
||||||
'versions': {
|
'versions': {
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from paste import deploy
|
||||||
import routes
|
import routes
|
||||||
|
|
||||||
from keystone import assignment
|
from keystone import assignment
|
||||||
@ -36,6 +37,14 @@ CONF = config.CONF
|
|||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def loadapp(conf, name):
|
||||||
|
# NOTE(blk-u): Save the application being loaded in the controllers module.
|
||||||
|
# This is similar to how public_app_factory() and v3_app_factory()
|
||||||
|
# register the version with the controllers module.
|
||||||
|
controllers.latest_app = deploy.loadapp(conf, name=name)
|
||||||
|
return controllers.latest_app
|
||||||
|
|
||||||
|
|
||||||
def fail_gracefully(f):
|
def fail_gracefully(f):
|
||||||
"""Logs exceptions and aborts."""
|
"""Logs exceptions and aborts."""
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
|
@ -29,7 +29,6 @@ import fixtures
|
|||||||
from oslo.config import fixture as config_fixture
|
from oslo.config import fixture as config_fixture
|
||||||
import oslotest.base as oslotest
|
import oslotest.base as oslotest
|
||||||
from oslotest import mockpatch
|
from oslotest import mockpatch
|
||||||
from paste import deploy
|
|
||||||
import six
|
import six
|
||||||
from testtools import testcase
|
from testtools import testcase
|
||||||
import webob
|
import webob
|
||||||
@ -53,6 +52,7 @@ from keystone import exception
|
|||||||
from keystone.i18n import _
|
from keystone.i18n import _
|
||||||
from keystone import notifications
|
from keystone import notifications
|
||||||
from keystone.openstack.common import log
|
from keystone.openstack.common import log
|
||||||
|
from keystone import service
|
||||||
from keystone.tests import ksfixtures
|
from keystone.tests import ksfixtures
|
||||||
|
|
||||||
# NOTE(dstanek): Tests inheriting from TestCase depend on having the
|
# NOTE(dstanek): Tests inheriting from TestCase depend on having the
|
||||||
@ -603,7 +603,7 @@ class TestCase(BaseTestCase):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
def loadapp(self, config, name='main'):
|
def loadapp(self, config, name='main'):
|
||||||
return deploy.loadapp(self._paste_config(config), name=name)
|
return service.loadapp(self._paste_config(config), name=name)
|
||||||
|
|
||||||
def client(self, app, *args, **kw):
|
def client(self, app, *args, **kw):
|
||||||
return TestClient(app, *args, **kw)
|
return TestClient(app, *args, **kw)
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
import functools
|
import functools
|
||||||
import random
|
import random
|
||||||
|
|
||||||
@ -556,22 +557,35 @@ class VersionTestCase(tests.TestCase):
|
|||||||
data = jsonutils.loads(resp.body)
|
data = jsonutils.loads(resp.body)
|
||||||
self.assertEqual(data, v2_only_response)
|
self.assertEqual(data, v2_only_response)
|
||||||
|
|
||||||
def test_json_home_v3(self):
|
def _test_json_home(self, path, exp_json_home_data):
|
||||||
# If the request is /v3 and the Accept header is application/json-home
|
|
||||||
# then the server responds with a JSON Home document.
|
|
||||||
|
|
||||||
client = self.client(self.public_app)
|
client = self.client(self.public_app)
|
||||||
resp = client.get('/v3', headers={'Accept': 'application/json-home'})
|
resp = client.get(path, headers={'Accept': 'application/json-home'})
|
||||||
|
|
||||||
self.assertThat(resp.status, tt_matchers.Equals('200 OK'))
|
self.assertThat(resp.status, tt_matchers.Equals('200 OK'))
|
||||||
self.assertThat(resp.headers['Content-Type'],
|
self.assertThat(resp.headers['Content-Type'],
|
||||||
tt_matchers.Equals('application/json-home'))
|
tt_matchers.Equals('application/json-home'))
|
||||||
|
|
||||||
|
self.assertThat(jsonutils.loads(resp.body),
|
||||||
|
tt_matchers.Equals(exp_json_home_data))
|
||||||
|
|
||||||
|
def test_json_home_v3(self):
|
||||||
|
# If the request is /v3 and the Accept header is application/json-home
|
||||||
|
# then the server responds with a JSON Home document.
|
||||||
|
|
||||||
exp_json_home_data = {
|
exp_json_home_data = {
|
||||||
'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED}
|
'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED}
|
||||||
|
|
||||||
self.assertThat(jsonutils.loads(resp.body),
|
self._test_json_home('/v3', exp_json_home_data)
|
||||||
tt_matchers.Equals(exp_json_home_data))
|
|
||||||
|
def test_json_home_root(self):
|
||||||
|
# If the request is / and the Accept header is application/json-home
|
||||||
|
# then the server responds with a JSON Home document.
|
||||||
|
|
||||||
|
exp_json_home_data = copy.deepcopy({
|
||||||
|
'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED})
|
||||||
|
json_home.translate_urls(exp_json_home_data, '/v3')
|
||||||
|
|
||||||
|
self._test_json_home('/', exp_json_home_data)
|
||||||
|
|
||||||
def test_accept_type_handling(self):
|
def test_accept_type_handling(self):
|
||||||
# Accept headers with multiple types and qvalues are handled.
|
# Accept headers with multiple types and qvalues are handled.
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
from testtools import matchers
|
from testtools import matchers
|
||||||
|
|
||||||
from keystone.common import json_home
|
from keystone.common import json_home
|
||||||
@ -57,3 +59,33 @@ class JsonHomeTest(tests.BaseTestCase):
|
|||||||
'http://docs.openstack.org/api/openstack-identity/3/ext/%s/%s/'
|
'http://docs.openstack.org/api/openstack-identity/3/ext/%s/%s/'
|
||||||
'param/%s' % (extension_name, extension_version, parameter_name))
|
'param/%s' % (extension_name, extension_version, parameter_name))
|
||||||
self.assertThat(relation, matchers.Equals(exp_relation))
|
self.assertThat(relation, matchers.Equals(exp_relation))
|
||||||
|
|
||||||
|
def test_translate_urls(self):
|
||||||
|
href_rel = self.getUniqueString()
|
||||||
|
href = self.getUniqueString()
|
||||||
|
href_template_rel = self.getUniqueString()
|
||||||
|
href_template = self.getUniqueString()
|
||||||
|
href_vars = {self.getUniqueString(): self.getUniqueString()}
|
||||||
|
original_json_home = {
|
||||||
|
'resources': {
|
||||||
|
href_rel: {'href': href},
|
||||||
|
href_template_rel: {
|
||||||
|
'href-template': href_template,
|
||||||
|
'href-vars': href_vars}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_json_home = copy.deepcopy(original_json_home)
|
||||||
|
new_prefix = self.getUniqueString()
|
||||||
|
json_home.translate_urls(new_json_home, new_prefix)
|
||||||
|
|
||||||
|
exp_json_home = {
|
||||||
|
'resources': {
|
||||||
|
href_rel: {'href': new_prefix + href},
|
||||||
|
href_template_rel: {
|
||||||
|
'href-template': new_prefix + href_template,
|
||||||
|
'href-vars': href_vars}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertThat(new_json_home, matchers.Equals(exp_json_home))
|
||||||
|
Loading…
Reference in New Issue
Block a user