diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index d1879d8bb1..c5f57bf613 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -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 diff --git a/keystone/assignment/controllers.py b/keystone/assignment/controllers.py index 2c4975b4af..17cf8d8c40 100644 --- a/keystone/assignment/controllers.py +++ b/keystone/assignment/controllers.py @@ -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) diff --git a/keystone/auth/plugins/oauth1.py b/keystone/auth/plugins/oauth1.py index 956f9ea03e..4e52605ba5 100644 --- a/keystone/auth/plugins/oauth1.py +++ b/keystone/auth/plugins/oauth1.py @@ -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) diff --git a/keystone/common/config.py b/keystone/common/config.py index 7bcd7dd288..85c49f830c 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -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 ' diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 08fee4d89c..657d3a0fa7 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -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: diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py index e92b24973f..c6e3f9b746 100644 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -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) diff --git a/keystone/contrib/federation/controllers.py b/keystone/contrib/federation/controllers.py index 925ab910a5..97d4b5bce8 100644 --- a/keystone/contrib/federation/controllers.py +++ b/keystone/contrib/federation/controllers.py @@ -29,11 +29,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') @@ -46,7 +46,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: @@ -56,21 +56,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} @@ -135,7 +136,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 @@ -147,14 +148,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 @@ -164,13 +165,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} diff --git a/keystone/contrib/oauth1/controllers.py b/keystone/contrib/oauth1/controllers.py index a78cdb9136..2c938ba485 100644 --- a/keystone/contrib/oauth1/controllers.py +++ b/keystone/contrib/oauth1/controllers.py @@ -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(), diff --git a/keystone/contrib/oauth1/core.py b/keystone/contrib/oauth1/core.py index f0386e03b9..41d681004f 100644 --- a/keystone/contrib/oauth1/core.py +++ b/keystone/contrib/oauth1/core.py @@ -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 = {} diff --git a/keystone/contrib/revoke/controllers.py b/keystone/contrib/revoke/controllers.py index 516ee9c413..3be1c23479 100644 --- a/keystone/contrib/revoke/controllers.py +++ b/keystone/contrib/revoke/controllers.py @@ -36,6 +36,7 @@ class RevokeController(controller.V3Controller): 'links': { 'next': None, 'self': RevokeController.base_url( + context, path=context['path']) + '/events', 'previous': None} } diff --git a/keystone/controllers.py b/keystone/controllers.py index cbdfbdac17..bef1dcdcac 100644 --- a/keystone/controllers.py +++ b/keystone/controllers.py @@ -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': [ diff --git a/keystone/middleware/core.py b/keystone/middleware/core.py index 410cebdf7a..7ca9be4496 100644 --- a/keystone/middleware/core.py +++ b/keystone/middleware/core.py @@ -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.""" diff --git a/keystone/tests/test_auth.py b/keystone/tests/test_auth.py index ac14de5bfe..d1b62af01e 100644 --- a/keystone/tests/test_auth.py +++ b/keystone/tests/test_auth.py @@ -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']) diff --git a/keystone/tests/test_content_types.py b/keystone/tests/test_content_types.py index 4cb710a7ce..6e098a6227 100644 --- a/keystone/tests/test_content_types.py +++ b/keystone/tests/test_content_types.py @@ -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')) diff --git a/keystone/tests/test_v3.py b/keystone/tests/test_v3.py index a6d5c54d27..7708f7fed5 100644 --- a/keystone/tests/test_v3.py +++ b/keystone/tests/test_v3.py @@ -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: diff --git a/keystone/tests/test_v3_oauth1.py b/keystone/tests/test_v3_oauth1.py index 942dd9ebc7..b653855d9a 100644 --- a/keystone/tests/test_v3_oauth1.py +++ b/keystone/tests/test_v3_oauth1.py @@ -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']) diff --git a/keystone/tests/test_versions.py b/keystone/tests/test_versions.py index 31950391d9..004d99b51a 100644 --- a/keystone/tests/test_versions.py +++ b/keystone/tests/test_versions.py @@ -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) diff --git a/keystone/tests/test_wsgi.py b/keystone/tests/test_wsgi.py index e2875e6f87..f436c3c3ac 100644 --- a/keystone/tests/test_wsgi.py +++ b/keystone/tests/test_wsgi.py @@ -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): diff --git a/keystone/trust/controllers.py b/keystone/trust/controllers.py index 51f8fff8b2..cc3cc1f227 100644 --- a/keystone/trust/controllers.py +++ b/keystone/trust/controllers.py @@ -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}