Change the default version discovery URLs

The default discovery URLs for when the admin_endpoint and
public_endpoint configuration values are unset is to point to the
localhost. This is wrong in all but the most trivial cases.

It also has the problem of not being able to distinguish for the public
service whether it was accessed via the 'public' or 'private' endpoint,
meaning that all clients that correctly do discovery will end up routing
to the public URL.

The most sensible default is to simply use the requested URL as the
basis for pointing to the versioned endpoints as it at least assumes
that the endpoint is accessible relative to the location used to arrive
on the page.

As mentioned in comments this is not a perfect solution. HOST_URL is the
URL not including path (ie http://server:port) so we do not have access
to the prefix automatically. Unfortunately the way keystone uses these
endpoints I don't see a way of improving that without a more substantial
redesign.

This patch is ugly because our layers are so intertwined. It should be
nicer with pecan.

DocImpact: Changes the default values of admin_endpoint and
public_endpoint and how they are used. In most situations now these
values should be ignored in configuration.

Change-Id: Ia6d9fbeb60ada661dc2052c9bd51db7a1dc8cd4b
Closes-Bug: #1288009
This commit is contained in:
Jamie Lennox 2014-03-05 12:06:30 +10:00
parent f42b8083cd
commit 7a760caa5d
19 changed files with 230 additions and 108 deletions

View File

@ -36,12 +36,24 @@
# The base public endpoint URL for keystone that are
# advertised to clients (NOTE: this does NOT affect how
# keystone listens for connections). (string value)
# keystone listens for connections) (string value).
# Defaults to the base host URL of the request. Eg a
# request to http://server:5000/v2.0/users will
# default to http://server:5000. You should only need
# to set this value if the base URL contains a path
# (eg /prefix/v2.0) or the endpoint should be found on
# a different server.
#public_endpoint=http://localhost:%(public_port)s/
# The base admin endpoint URL for keystone that are advertised
# to clients (NOTE: this does NOT affect how keystone listens
# for connections). (string value)
# for connections) (string value).
# Defaults to the base host URL of the request. Eg a
# request to http://server:35357/v2.0/users will
# default to http://server:35357. You should only need
# to set this value if the base URL contains a path
# (eg /prefix/v2.0) or the endpoint should be found on
# a different server.
#admin_endpoint=http://localhost:%(admin_port)s/
# onready allows you to send a notification when the process

View File

@ -574,7 +574,7 @@ class RoleAssignmentV3(controller.V3Controller):
# the wrapper as have already included the links in the entities
pass
def _format_entity(self, entity):
def _format_entity(self, context, entity):
"""Format an assignment entity for API response.
The driver layer returns entities as dicts containing the ids of the
@ -641,16 +641,17 @@ class RoleAssignmentV3(controller.V3Controller):
else:
target_link = '/domains/%s' % entity['domain_id']
formatted_entity.setdefault('links', {})
formatted_entity['links']['assignment'] = (
self.base_url('%(target)s/%(actor)s/roles/%(role)s%(suffix)s' % {
'target': target_link,
'actor': actor_link,
'role': entity['role_id'],
'suffix': suffix}))
path = '%(target)s/%(actor)s/roles/%(role)s%(suffix)s' % {
'target': target_link,
'actor': actor_link,
'role': entity['role_id'],
'suffix': suffix}
formatted_entity['links']['assignment'] = self.base_url(context, path)
return formatted_entity
def _expand_indirect_assignments(self, refs):
def _expand_indirect_assignments(self, context, refs):
"""Processes entity list into all-direct assignments.
For any group role assignments in the list, create a role assignment
@ -710,7 +711,7 @@ class RoleAssignmentV3(controller.V3Controller):
user_entry = copy.deepcopy(template)
user_entry['user'] = {'id': user['id']}
user_entry['links']['membership'] = (
self.base_url('/groups/%s/users/%s' %
self.base_url(context, '/groups/%s/users/%s' %
(group_id, user['id'])))
return user_entry
@ -727,6 +728,7 @@ class RoleAssignmentV3(controller.V3Controller):
project_entry['scope']['project'] = {'id': project_id}
project_entry['links']['assignment'] = (
self.base_url(
context,
'/OS-INHERIT/domains/%s/users/%s/roles/%s'
'/inherited_to_projects' % (
domain_id, project_entry['user']['id'],
@ -746,12 +748,13 @@ class RoleAssignmentV3(controller.V3Controller):
project_entry['user'] = {'id': user_id}
project_entry['scope']['project'] = {'id': project_id}
project_entry['links']['assignment'] = (
self.base_url('/OS-INHERIT/domains/%s/groups/%s/roles/%s'
self.base_url(context,
'/OS-INHERIT/domains/%s/groups/%s/roles/%s'
'/inherited_to_projects' % (
domain_id, group_id,
project_entry['role']['id'])))
project_entry['links']['membership'] = (
self.base_url('/groups/%s/users/%s' %
self.base_url(context, '/groups/%s/users/%s' %
(group_id, user_id)))
return project_entry
@ -862,14 +865,15 @@ class RoleAssignmentV3(controller.V3Controller):
hints = self.build_driver_hints(context, filters)
refs = self.assignment_api.list_role_assignments()
formatted_refs = (
[self._format_entity(x) for x in refs
[self._format_entity(context, x) for x in refs
if self._filter_inherited(x)])
if ('effective' in context['query_string'] and
self._query_filter_is_true(
context['query_string']['effective'])):
formatted_refs = self._expand_indirect_assignments(formatted_refs)
formatted_refs = self._expand_indirect_assignments(context,
formatted_refs)
return self.wrap_collection(context, formatted_refs, hints=hints)

View File

@ -13,6 +13,7 @@
# under the License.
from keystone import auth
from keystone.common import controller
from keystone.common import dependency
from keystone.contrib.oauth1 import core as oauth
from keystone.contrib.oauth1 import validator
@ -54,7 +55,7 @@ class OAuth(auth.AuthMethodHandler):
if now > expires:
raise exception.Unauthorized(_('Access token is expired'))
url = oauth.rebuild_url(context['path'])
url = controller.V3Controller.base_url(context, context['path'])
access_verifier = oauth.ResourceEndpoint(
request_validator=validator.OAuthValidator(),
token_generator=oauth.token_generator)

View File

@ -50,15 +50,25 @@ FILE_OPTIONS = {
help='The port number which the public service listens '
'on.'),
cfg.StrOpt('public_endpoint',
default='http://localhost:%(public_port)s/',
help='The base public endpoint URL for keystone that are '
'advertised to clients (NOTE: this does NOT affect '
'how keystone listens for connections).'),
'how keystone listens for connections). '
'Defaults to the base host URL of the request. Eg a '
'request to http://server:5000/v2.0/users will '
'default to http://server:5000. You should only need '
'to set this value if the base URL contains a path '
'(eg /prefix/v2.0) or the endpoint should be found on '
'a different server.'),
cfg.StrOpt('admin_endpoint',
default='http://localhost:%(admin_port)s/',
help='The base admin endpoint URL for keystone that are '
'advertised to clients (NOTE: this does NOT affect '
'how keystone listens for connections).'),
'how keystone listens for connections). '
'Defaults to the base host URL of the request. Eg a '
'request to http://server:35357/v2.0/users will '
'default to http://server:35357. You should only need '
'to set this value if the base URL contains a path '
'(eg /prefix/v2.0) or the endpoint should be found on '
'a different server.'),
cfg.StrOpt('onready',
help='onready allows you to send a notification when the '
'process is ready to serve For example, to have it '

View File

@ -292,28 +292,21 @@ class V3Controller(wsgi.Application):
get_member_from_driver = None
@classmethod
def base_url(cls, path=None):
endpoint = CONF.public_endpoint % CONF
def base_url(cls, context, path=None):
endpoint = super(V3Controller, cls).base_url(context, 'public')
if not path:
path = cls.collection_name
# allow a missing trailing slash in the config
if endpoint[-1] != '/':
endpoint += '/'
url = endpoint + 'v3'
if path:
return url + path
else:
return url + '/' + cls.collection_name
return '%s/%s/%s' % (endpoint, 'v3', path.lstrip('/'))
@classmethod
def _add_self_referential_link(cls, ref):
def _add_self_referential_link(cls, context, ref):
ref.setdefault('links', {})
ref['links']['self'] = cls.base_url() + '/' + ref['id']
ref['links']['self'] = cls.base_url(context) + '/' + ref['id']
@classmethod
def wrap_member(cls, context, ref):
cls._add_self_referential_link(ref)
cls._add_self_referential_link(context, ref)
return {cls.member_name: ref}
@classmethod
@ -349,7 +342,7 @@ class V3Controller(wsgi.Application):
container = {cls.collection_name: refs}
container['links'] = {
'next': None,
'self': cls.base_url(path=context['path']),
'self': cls.base_url(context, path=context['path']),
'previous': None}
if list_limited:

View File

@ -185,6 +185,7 @@ class Application(BaseApplication):
context['query_string'] = dict(six.iteritems(req.params))
context['headers'] = dict(six.iteritems(req.headers))
context['path'] = req.environ['PATH_INFO']
context['host_url'] = req.host_url
params = req.environ.get(PARAMS_ENV, {})
#authentication and authorization attributes are set as environment
#values by the container and processed by the pipeline. the complete
@ -208,17 +209,21 @@ class Application(BaseApplication):
LOG.warning(
_('Authorization failed. %(exception)s from %(remote_addr)s'),
{'exception': e, 'remote_addr': req.environ['REMOTE_ADDR']})
return render_exception(e, user_locale=best_match_language(req))
return render_exception(e, context=context,
user_locale=best_match_language(req))
except exception.Error as e:
LOG.warning(e)
return render_exception(e, user_locale=best_match_language(req))
return render_exception(e, context=context,
user_locale=best_match_language(req))
except TypeError as e:
LOG.exception(e)
return render_exception(exception.ValidationError(e),
context=context,
user_locale=best_match_language(req))
except Exception as e:
LOG.exception(e)
return render_exception(exception.UnexpectedError(exception=e),
context=context,
user_locale=best_match_language(req))
if result is None:
@ -302,6 +307,22 @@ class Application(BaseApplication):
return token_ref.get('trust_id')
@classmethod
def base_url(cls, context, endpoint_type):
url = CONF['%s_endpoint' % endpoint_type]
if url:
url = url % CONF
else:
# NOTE(jamielennox): if url is not set via the config file we
# should set it relative to the url that the user used to get here
# so as not to mess with version discovery. This is not perfect.
# host_url omits the path prefix, but there isn't another good
# solution that will work for all urls.
url = context['host_url']
return url.rstrip('/')
class Middleware(Application):
"""Base WSGI middleware.
@ -370,15 +391,17 @@ class Middleware(Application):
return self.process_response(request, response)
except exception.Error as e:
LOG.warning(e)
return render_exception(e,
return render_exception(e, request=request,
user_locale=best_match_language(request))
except TypeError as e:
LOG.exception(e)
return render_exception(exception.ValidationError(e),
request=request,
user_locale=best_match_language(request))
except Exception as e:
LOG.exception(e)
return render_exception(exception.UnexpectedError(exception=e),
request=request,
user_locale=best_match_language(request))
@ -475,9 +498,10 @@ class Router(object):
"""
match = req.environ['wsgiorg.routing_args'][1]
if not match:
return render_exception(
exception.NotFound(_('The resource could not be found.')),
user_locale=best_match_language(req))
msg = _('The resource could not be found.')
return render_exception(exception.NotFound(msg),
request=req,
user_locale=best_match_language(req))
app = match['controller']
return app
@ -571,7 +595,7 @@ def render_response(body=None, status=None, headers=None):
headerlist=headers)
def render_exception(error, user_locale=None):
def render_exception(error, context=None, request=None, user_locale=None):
"""Forms a WSGI response based on the current error."""
error_message = error.args[0]
@ -590,9 +614,18 @@ def render_exception(error, user_locale=None):
if isinstance(error, exception.AuthPluginException):
body['error']['identity'] = error.authentication
elif isinstance(error, exception.Unauthorized):
headers.append(('WWW-Authenticate',
'Keystone uri="%s"' % (
CONF.public_endpoint % CONF)))
url = CONF.public_endpoint
if not url:
if request:
context = {'host_url': request.host_url}
if context:
url = Application.base_url(context, 'public')
else:
url = 'http://localhost:%d' % CONF.public_port
else:
url = url % CONF
headers.append(('WWW-Authenticate', 'Keystone uri="%s"' % url))
return render_response(status=(error.code, error.title),
body=body,
headers=headers)

View File

@ -28,11 +28,11 @@ class _ControllerBase(controller.V3Controller):
"""Base behaviors for federation controllers."""
@classmethod
def base_url(cls, path=None):
def base_url(cls, context, path=None):
"""Construct a path and pass it to V3Controller.base_url method."""
path = '/OS-FEDERATION/' + cls.collection_name
return controller.V3Controller.base_url(path=path)
return super(_ControllerBase, cls).base_url(context, path=path)
@dependency.requires('federation_api')
@ -45,7 +45,7 @@ class IdentityProvider(_ControllerBase):
_public_parameters = frozenset(['id', 'enabled', 'description', 'links'])
@classmethod
def _add_related_links(cls, ref):
def _add_related_links(cls, context, ref):
"""Add URLs for entities related with Identity Provider.
Add URLs pointing to:
@ -55,21 +55,22 @@ class IdentityProvider(_ControllerBase):
ref.setdefault('links', {})
base_path = ref['links'].get('self')
if base_path is None:
base_path = '/'.join([IdentityProvider.base_url(), ref['id']])
base_path = '/'.join([IdentityProvider.base_url(context),
ref['id']])
for name in ['protocols']:
ref['links'][name] = '/'.join([base_path, name])
@classmethod
def _add_self_referential_link(cls, ref):
def _add_self_referential_link(cls, context, ref):
id = ref.get('id')
self_path = '/'.join([cls.base_url(), id])
self_path = '/'.join([cls.base_url(context), id])
ref.setdefault('links', {})
ref['links']['self'] = self_path
@classmethod
def wrap_member(cls, context, ref):
cls._add_self_referential_link(ref)
cls._add_related_links(ref)
cls._add_self_referential_link(context, ref)
cls._add_related_links(context, ref)
ref = cls.filter_params(ref)
return {cls.member_name: ref}
@ -134,7 +135,7 @@ class FederationProtocol(_ControllerBase):
_mutable_parameters = frozenset(['mapping_id'])
@classmethod
def _add_self_referential_link(cls, ref):
def _add_self_referential_link(cls, context, ref):
"""Add 'links' entry to the response dictionary.
Calls IdentityProvider.base_url() class method, as it constructs
@ -146,14 +147,14 @@ class FederationProtocol(_ControllerBase):
ref.setdefault('links', {})
base_path = ref['links'].get('identity_provider')
if base_path is None:
base_path = [IdentityProvider.base_url(), ref['idp_id']]
base_path = [IdentityProvider.base_url(context), ref['idp_id']]
base_path = '/'.join(base_path)
self_path = [base_path, 'protocols', ref['id']]
self_path = '/'.join(self_path)
ref['links']['self'] = self_path
@classmethod
def _add_related_links(cls, ref):
def _add_related_links(cls, context, ref):
"""Add new entries to the 'links' subdictionary in the response.
Adds 'identity_provider' key with URL pointing to related identity
@ -163,13 +164,14 @@ class FederationProtocol(_ControllerBase):
"""
ref.setdefault('links', {})
base_path = '/'.join([IdentityProvider.base_url(), ref['idp_id']])
base_path = '/'.join([IdentityProvider.base_url(context),
ref['idp_id']])
ref['links']['identity_provider'] = base_path
@classmethod
def wrap_member(cls, context, ref):
cls._add_related_links(ref)
cls._add_self_referential_link(ref)
cls._add_related_links(context, ref)
cls._add_self_referential_link(context, ref)
ref = cls.filter_params(ref)
return {cls.member_name: ref}

View File

@ -35,13 +35,13 @@ class ConsumerCrudV3(controller.V3Controller):
member_name = 'consumer'
@classmethod
def base_url(cls, path=None):
def base_url(cls, context, path=None):
"""Construct a path and pass it to V3Controller.base_url method."""
# NOTE(stevemar): Overriding path to /OS-OAUTH1/consumers so that
# V3Controller.base_url handles setting the self link correctly.
path = '/OS-OAUTH1/' + cls.collection_name
return controller.V3Controller.base_url(path=path)
return controller.V3Controller.base_url(context, path=path)
@controller.protected()
def create_consumer(self, context, consumer):
@ -90,13 +90,14 @@ class AccessTokenCrudV3(controller.V3Controller):
access_token = self.oauth_api.get_access_token(access_token_id)
if access_token['authorizing_user_id'] != user_id:
raise exception.NotFound()
access_token = self._format_token_entity(access_token)
access_token = self._format_token_entity(context, access_token)
return AccessTokenCrudV3.wrap_member(context, access_token)
@controller.protected()
def list_access_tokens(self, context, user_id):
refs = self.oauth_api.list_access_tokens(user_id)
formatted_refs = ([self._format_token_entity(x) for x in refs])
formatted_refs = ([self._format_token_entity(context, x)
for x in refs])
return AccessTokenCrudV3.wrap_collection(context, formatted_refs)
@controller.protected()
@ -107,7 +108,7 @@ class AccessTokenCrudV3(controller.V3Controller):
return self.oauth_api.delete_access_token(
user_id, access_token_id)
def _format_token_entity(self, entity):
def _format_token_entity(self, context, entity):
formatted_entity = entity.copy()
access_token_id = formatted_entity['id']
@ -124,7 +125,7 @@ class AccessTokenCrudV3(controller.V3Controller):
'access_token_id': access_token_id})
formatted_entity.setdefault('links', {})
formatted_entity['links']['roles'] = (self.base_url(url))
formatted_entity['links']['roles'] = (self.base_url(context, url))
return formatted_entity
@ -185,7 +186,7 @@ class OAuthControllerV3(controller.V3Controller):
raise exception.ValidationError(
attribute='requested_project_id', target='request')
url = oauth1.rebuild_url(context['path'])
url = self.base_url(context, context['path'])
req_headers = {'Requested-Project-Id': requested_project_id}
req_headers.update(headers)
@ -250,7 +251,7 @@ class OAuthControllerV3(controller.V3Controller):
if now > expires:
raise exception.Unauthorized(_('Request token is expired'))
url = oauth1.rebuild_url(context['path'])
url = self.base_url(context, context['path'])
access_verifier = oauth1.AccessTokenEndpoint(
request_validator=validator.OAuthValidator(),

View File

@ -106,17 +106,6 @@ def filter_token(access_token_ref):
return access_token_ref
def rebuild_url(path):
endpoint = CONF.public_endpoint % CONF
# allow a missing trailing slash in the config
if endpoint[-1] != '/':
endpoint += '/'
url = endpoint + 'v3'
return url + path
def get_oauth_headers(headers):
parameters = {}

View File

@ -36,6 +36,7 @@ class RevokeController(controller.V3Controller):
'links': {
'next': None,
'self': RevokeController.base_url(
context,
path=context['path']) + '/events',
'previous': None}
}

View File

@ -69,12 +69,10 @@ class Version(wsgi.Application):
super(Version, self).__init__()
def _get_identity_url(self, version='v2.0'):
def _get_identity_url(self, context, version):
"""Returns a URL to keystone's own endpoint."""
url = CONF['%s_endpoint' % self.endpoint_url_type] % CONF
if url[-1] != '/':
url += '/'
return '%s%s/' % (url, version)
url = self.base_url(context, self.endpoint_url_type)
return '%s/%s/' % (url, version)
def _get_versions_list(self, context):
"""The list of versions is dependent on the context."""
@ -87,7 +85,7 @@ class Version(wsgi.Application):
'links': [
{
'rel': 'self',
'href': self._get_identity_url(version='v2.0'),
'href': self._get_identity_url(context, 'v2.0'),
}, {
'rel': 'describedby',
'type': 'text/html',
@ -120,7 +118,7 @@ class Version(wsgi.Application):
'links': [
{
'rel': 'self',
'href': self._get_identity_url(version='v3'),
'href': self._get_identity_url(context, 'v3'),
}
],
'media-types': [

View File

@ -118,7 +118,7 @@ class JsonBodyMiddleware(wsgi.Middleware):
if request.content_type not in ('application/json', ''):
e = exception.ValidationError(attribute='application/json',
target='Content-Type header')
return wsgi.render_exception(e)
return wsgi.render_exception(e, request=request)
params_parsed = {}
try:
@ -126,7 +126,7 @@ class JsonBodyMiddleware(wsgi.Middleware):
except ValueError:
e = exception.ValidationError(attribute='valid JSON',
target='request body')
return wsgi.render_exception(e)
return wsgi.render_exception(e, request=request)
finally:
if not params_parsed:
params_parsed = {}
@ -166,7 +166,7 @@ class XmlBodyMiddleware(wsgi.Middleware):
LOG.exception('Serializer failed')
e = exception.ValidationError(attribute='valid XML',
target='request body')
return wsgi.render_exception(e)
return wsgi.render_exception(e, request=request)
def process_response(self, request, response):
"""Transform the response from JSON to XML."""

View File

@ -35,6 +35,8 @@ CONF = config.CONF
TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id
HOST_URL = 'http://keystone:5001'
def _build_user_auth(token=None, user_id=None, username=None,
password=None, tenant_id=None, tenant_name=None,
@ -674,7 +676,8 @@ class AuthWithTrust(AuthTest):
auth_context = authorization.token_to_auth_context(
token_ref['token_data'])
return {'environment': {authorization.AUTH_CONTEXT_ENV: auth_context},
'token_id': token_id}
'token_id': token_id,
'host_url': HOST_URL}
def create_trust(self, expires_at=None, impersonation=True):
username = self.trustor['name']
@ -723,9 +726,9 @@ class AuthWithTrust(AuthTest):
role_ids = [self.role_browser['id'], self.role_member['id']]
self.assertTrue(timeutils.parse_strtime(self.new_trust['expires_at'],
fmt=TIME_FORMAT))
self.assertIn('http://localhost:5000/v3/OS-TRUST/',
self.assertIn('%s/v3/OS-TRUST/' % HOST_URL,
self.new_trust['links']['self'])
self.assertIn('http://localhost:5000/v3/OS-TRUST/',
self.assertIn('%s/v3/OS-TRUST/' % HOST_URL,
self.new_trust['roles_links']['self'])
for role in self.new_trust['roles']:
@ -743,7 +746,8 @@ class AuthWithTrust(AuthTest):
expires_at="Z")
def test_get_trust(self):
context = {'token_id': self.unscoped_token['access']['token']['id']}
context = {'token_id': self.unscoped_token['access']['token']['id'],
'host_url': HOST_URL}
trust = self.trust_controller.get_trust(context,
self.new_trust['id'])['trust']
self.assertEqual(self.trustor['id'], trust['trustor_user_id'])

View File

@ -681,7 +681,16 @@ class CoreApiTests(object):
r = self.public_request(
path='/v2.0/tenants',
expected_status=401)
self.assertEqual('Keystone uri="%s"' % (CONF.public_endpoint % CONF),
self.assertEqual('Keystone uri="http://localhost"',
r.headers.get('WWW-Authenticate'))
def test_www_authenticate_header_host(self):
test_url = 'http://%s:4187' % uuid.uuid4().hex
self.config_fixture.config(public_endpoint=test_url)
r = self.public_request(
path='/v2.0/tenants',
expected_status=401)
self.assertEqual('Keystone uri="%s"' % test_url,
r.headers.get('WWW-Authenticate'))

View File

@ -17,6 +17,7 @@ import uuid
from lxml import etree
import six
from testtools import matchers
from keystone import auth
from keystone.common import authorization
@ -395,19 +396,17 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase):
def assertValidListLinks(self, links):
self.assertIsNotNone(links)
self.assertIsNotNone(links.get('self'))
self.assertIn(CONF.public_endpoint % CONF, links['self'])
self.assertThat(links['self'], matchers.StartsWith('http://localhost'))
self.assertIn('next', links)
if links['next'] is not None:
self.assertIn(
CONF.public_endpoint % CONF,
links['next'])
self.assertThat(links['next'],
matchers.StartsWith('http://localhost'))
self.assertIn('previous', links)
if links['previous'] is not None:
self.assertIn(
CONF.public_endpoint % CONF,
links['previous'])
self.assertThat(links['previous'],
matchers.StartsWith('http://localhost'))
def assertValidListResponse(self, resp, key, entity_validator, ref=None,
expected_length=None, keys_to_check=None):
@ -467,7 +466,8 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase):
self.assertIsNotNone(entity.get('links'))
self.assertIsNotNone(entity['links'].get('self'))
self.assertIn(CONF.public_endpoint % CONF, entity['links']['self'])
self.assertThat(entity['links']['self'],
matchers.StartsWith('http://localhost'))
self.assertIn(entity['id'], entity['links']['self'])
if ref:

View File

@ -51,7 +51,7 @@ class OAuth1Tests(test_v3.RestfulTestCase):
super(OAuth1Tests, self).setUp()
# Now that the app has been served, we can query CONF values
self.base_url = (CONF.public_endpoint % CONF) + "v3"
self.base_url = 'http://localhost/v3'
self.controller = controllers.OAuthControllerV3()
def _create_single_consumer(self):
@ -153,7 +153,7 @@ class ConsumerCRUDTests(OAuth1Tests):
consumer = self._create_single_consumer()
consumer_id = consumer['id']
resp = self.get(self.CONSUMER_URL + '/%s' % consumer_id)
self_url = [CONF.public_endpoint % CONF, 'v3', self.CONSUMER_URL,
self_url = ['http://localhost/v3', self.CONSUMER_URL,
'/', consumer_id]
self_url = ''.join(self_url)
self.assertEqual(resp.result['consumer']['links']['self'], self_url)
@ -164,7 +164,7 @@ class ConsumerCRUDTests(OAuth1Tests):
resp = self.get(self.CONSUMER_URL)
entities = resp.result['consumers']
self.assertIsNotNone(entities)
self_url = [CONF.public_endpoint % CONF, 'v3', self.CONSUMER_URL]
self_url = ['http://localhost/v3', self.CONSUMER_URL]
self_url = ''.join(self_url)
self.assertEqual(resp.result['links']['self'], self_url)
self.assertValidListLinks(resp.result['links'])

View File

@ -118,6 +118,10 @@ class VersionTestCase(tests.TestCase):
self.public_app = self.loadapp('keystone', 'main')
self.admin_app = self.loadapp('keystone', 'admin')
self.config_fixture.config(
public_endpoint='http://localhost:%(public_port)d',
admin_endpoint='http://localhost:%(admin_port)d')
fixture = self.useFixture(moxstubout.MoxStubout())
self.stubs = fixture.stubs
@ -161,6 +165,25 @@ class VersionTestCase(tests.TestCase):
version, 'http://localhost:%s/v2.0/' % CONF.admin_port)
self.assertEqual(data, expected)
def test_use_site_url_if_endpoint_unset(self):
self.config_fixture.config(public_endpoint=None, admin_endpoint=None)
for app in (self.public_app, self.admin_app):
client = self.client(app)
resp = client.get('/')
self.assertEqual(resp.status_int, 300)
data = jsonutils.loads(resp.body)
expected = VERSIONS_RESPONSE
for version in expected['versions']['values']:
# localhost happens to be the site url for tests
if version['id'] == 'v3.0':
self._paste_in_port(
version, 'http://localhost/v3/')
elif version['id'] == 'v2.0':
self._paste_in_port(
version, 'http://localhost/v2.0/')
self.assertEqual(data, expected)
def test_public_version_v2(self):
client = self.client(self.public_app)
resp = client.get('/v2.0/')
@ -181,6 +204,17 @@ class VersionTestCase(tests.TestCase):
'http://localhost:%s/v2.0/' % CONF.admin_port)
self.assertEqual(data, expected)
def test_use_site_url_if_endpoint_unset_v2(self):
self.config_fixture.config(public_endpoint=None, admin_endpoint=None)
for app in (self.public_app, self.admin_app):
client = self.client(app)
resp = client.get('/v2.0/')
self.assertEqual(resp.status_int, 200)
data = jsonutils.loads(resp.body)
expected = v2_VERSION_RESPONSE
self._paste_in_port(expected['version'], 'http://localhost/v2.0/')
self.assertEqual(data, expected)
def test_public_version_v3(self):
client = self.client(self.public_app)
resp = client.get('/v3/')
@ -201,6 +235,17 @@ class VersionTestCase(tests.TestCase):
'http://localhost:%s/v3/' % CONF.admin_port)
self.assertEqual(data, expected)
def test_use_site_url_if_endpoint_unset_v3(self):
self.config_fixture.config(public_endpoint=None, admin_endpoint=None)
for app in (self.public_app, self.admin_app):
client = self.client(app)
resp = client.get('/v3/')
self.assertEqual(resp.status_int, 200)
data = jsonutils.loads(resp.body)
expected = v3_VERSION_RESPONSE
self._paste_in_port(expected['version'], 'http://localhost/v3/')
self.assertEqual(data, expected)
def test_v2_disabled(self):
self.stubs.Set(controllers, '_VERSIONS', ['v3'])
client = self.client(self.public_app)
@ -331,6 +376,10 @@ vnd.openstack.identity-v3+xml"/>
self.public_app = self.loadapp('keystone', 'main')
self.admin_app = self.loadapp('keystone', 'admin')
self.config_fixture.config(
public_endpoint='http://localhost:%(public_port)d',
admin_endpoint='http://localhost:%(admin_port)d')
fixture = self.useFixture(moxstubout.MoxStubout())
self.stubs = fixture.stubs
@ -355,6 +404,14 @@ vnd.openstack.identity-v3+xml"/>
expected = self.VERSIONS_RESPONSE % dict(port=CONF.admin_port)
self.assertThat(data, matchers.XMLEquals(expected))
def test_use_site_url_if_endpoint_unset(self):
client = self.client(self.public_app)
resp = client.get('/', headers=self.REQUEST_HEADERS)
self.assertEqual(resp.status_int, 300)
data = resp.body
expected = self.VERSIONS_RESPONSE % dict(port=CONF.public_port)
self.assertThat(data, matchers.XMLEquals(expected))
def test_public_version_v2(self):
client = self.client(self.public_app)
resp = client.get('/v2.0/', headers=self.REQUEST_HEADERS)

View File

@ -14,6 +14,7 @@
import gettext
import socket
import uuid
from babel import localedata
import mock
@ -114,6 +115,13 @@ class ApplicationTest(BaseWSGITest):
resp = wsgi.render_exception(e)
self.assertEqual(resp.status_int, 401)
def test_render_exception_host(self):
e = exception.Unauthorized(message=u'\u7f51\u7edc')
context = {'host_url': 'http://%s:5000' % uuid.uuid4().hex}
resp = wsgi.render_exception(e, context=context)
self.assertEqual(resp.status_int, 401)
class ExtensionRouterTest(BaseWSGITest):
def test_extensionrouter_local_config(self):

View File

@ -53,13 +53,13 @@ class TrustV3(controller.V3Controller):
member_name = "trust"
@classmethod
def base_url(cls, path=None):
def base_url(cls, context, path=None):
"""Construct a path and pass it to V3Controller.base_url method."""
# NOTE(stevemar): Overriding path to /OS-TRUST/trusts so that
# V3Controller.base_url handles setting the self link correctly.
path = '/OS-TRUST/' + cls.collection_name
return controller.V3Controller.base_url(path=path)
return super(TrustV3, cls).base_url(context, path=path)
def _get_user_id(self, context):
if 'token_id' in context:
@ -99,7 +99,7 @@ class TrustV3(controller.V3Controller):
trust_full_roles.append(full_role)
trust['roles'] = trust_full_roles
trust['roles_links'] = {
'self': (self.base_url() + "/%s/roles" % trust['id']),
'self': (self.base_url(context) + "/%s/roles" % trust['id']),
'next': None,
'previous': None}