summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Danjou <julien@danjou.info>2015-09-21 17:27:07 +0200
committerSteve Martinelli <stevemar@ca.ibm.com>2016-02-16 22:48:08 +0000
commit40c3942c12d1dd2c826d836987616838a73a64a1 (patch)
treea998701e1145451a06f1f582fe265b01f6103891
parenta320eaa90323c04082204de9b9e630fec96661aa (diff)
wsgi: fix base_url finding
The current wsgi.Application.base_url() function does not work correctly if Keystone runs on something like "http://1.2.3.4/identity" which is now a default in devstack. This patch fixes that by using wsgiref.util to parse environment variable set in WSGI mode to find the real base url and returns the correct URL. The following environment variables will be used to produce the effective base url: HTTP_HOST SERVER_NAME SERVER_PORT SCRIPT_NAME Closes-Bug: #1381961 Change-Id: I111c206a8a751ed117c6869f55f8236b29ab88a2
Notes
Notes (review): Code-Review+2: Steve Martinelli <stevemar@ca.ibm.com> Workflow+1: Steve Martinelli <stevemar@ca.ibm.com> Verified+2: Jenkins Submitted-by: Jenkins Submitted-at: Wed, 17 Feb 2016 01:59:05 +0000 Reviewed-on: https://review.openstack.org/226464 Project: openstack/keystone Branch: refs/heads/master
-rw-r--r--keystone/common/utils.py17
-rw-r--r--keystone/common/wsgi.py40
-rw-r--r--keystone/tests/unit/test_auth.py17
-rw-r--r--keystone/tests/unit/test_wsgi.py75
4 files changed, 128 insertions, 21 deletions
diff --git a/keystone/common/utils.py b/keystone/common/utils.py
index b61baea..ebe2bd4 100644
--- a/keystone/common/utils.py
+++ b/keystone/common/utils.py
@@ -577,3 +577,20 @@ def lower_case_hostname(url):
577 # Note: _replace method for named tuples is public and defined in docs 577 # Note: _replace method for named tuples is public and defined in docs
578 replaced = parsed._replace(netloc=parsed.netloc.lower()) 578 replaced = parsed._replace(netloc=parsed.netloc.lower())
579 return moves.urllib.parse.urlunparse(replaced) 579 return moves.urllib.parse.urlunparse(replaced)
580
581
582def remove_standard_port(url):
583 # remove the default ports specified in RFC2616 and 2818
584 o = moves.urllib.parse.urlparse(url)
585 separator = ':'
586 (host, separator, port) = o.netloc.partition(':')
587 if o.scheme.lower() == 'http' and port == '80':
588 # NOTE(gyee): _replace() is not a private method. It has an
589 # an underscore prefix to prevent conflict with field names.
590 # See https://docs.python.org/2/library/collections.html#
591 # collections.namedtuple
592 o = o._replace(netloc=host)
593 if o.scheme.lower() == 'https' and port == '443':
594 o = o._replace(netloc=host)
595
596 return moves.urllib.parse.urlunparse(o)
diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py
index 55752b7..0c3ea8e 100644
--- a/keystone/common/wsgi.py
+++ b/keystone/common/wsgi.py
@@ -20,6 +20,7 @@
20 20
21import copy 21import copy
22import itertools 22import itertools
23import re
23import wsgiref.util 24import wsgiref.util
24 25
25from oslo_config import cfg 26from oslo_config import cfg
@@ -376,13 +377,19 @@ class Application(BaseApplication):
376 itertools.chain(CONF.items(), CONF.eventlet_server.items())) 377 itertools.chain(CONF.items(), CONF.eventlet_server.items()))
377 378
378 url = url % substitutions 379 url = url % substitutions
380 elif 'environment' in context:
381 url = wsgiref.util.application_uri(context['environment'])
382 # remove version from the URL as it may be part of SCRIPT_NAME but
383 # it should not be part of base URL
384 url = re.sub(r'/v(3|(2\.0))/*$', '', url)
385
386 # now remove the standard port
387 url = utils.remove_standard_port(url)
379 else: 388 else:
380 # NOTE(jamielennox): If url is not set via the config file we 389 # if we don't have enough information to come up with a base URL,
381 # should set it relative to the url that the user used to get here 390 # then fall back to localhost. This should never happen in
382 # so as not to mess with version discovery. This is not perfect. 391 # production environment.
383 # host_url omits the path prefix, but there isn't another good 392 url = 'http://localhost:%d' % CONF.eventlet_server.public_port
384 # solution that will work for all urls.
385 url = context['host_url']
386 393
387 return url.rstrip('/') 394 return url.rstrip('/')
388 395
@@ -812,18 +819,15 @@ def render_exception(error, context=None, request=None, user_locale=None):
812 if isinstance(error, exception.AuthPluginException): 819 if isinstance(error, exception.AuthPluginException):
813 body['error']['identity'] = error.authentication 820 body['error']['identity'] = error.authentication
814 elif isinstance(error, exception.Unauthorized): 821 elif isinstance(error, exception.Unauthorized):
815 url = CONF.public_endpoint 822 # NOTE(gyee): we only care about the request environment in the
816 if not url: 823 # context. Also, its OK to pass the environemt as it is read-only in
817 if request: 824 # Application.base_url()
818 context = {'host_url': request.host_url} 825 local_context = {}
819 if context: 826 if request:
820 url = Application.base_url(context, 'public') 827 local_context = {'environment': request.environ}
821 else: 828 elif context and 'environment' in context:
822 url = 'http://localhost:%d' % CONF.eventlet_server.public_port 829 local_context = {'environment': context['environment']}
823 else: 830 url = Application.base_url(local_context, 'public')
824 substitutions = dict(
825 itertools.chain(CONF.items(), CONF.eventlet_server.items()))
826 url = url % substitutions
827 831
828 headers.append(('WWW-Authenticate', 'Keystone uri="%s"' % url)) 832 headers.append(('WWW-Authenticate', 'Keystone uri="%s"' % url))
829 return render_response(status=(error.code, error.title), 833 return render_response(status=(error.code, error.title),
diff --git a/keystone/tests/unit/test_auth.py b/keystone/tests/unit/test_auth.py
index 92a25b1..1b73202 100644
--- a/keystone/tests/unit/test_auth.py
+++ b/keystone/tests/unit/test_auth.py
@@ -14,6 +14,8 @@
14 14
15import copy 15import copy
16import datetime 16import datetime
17import random
18import string
17import uuid 19import uuid
18 20
19import mock 21import mock
@@ -41,7 +43,9 @@ CONF = cfg.CONF
41TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' 43TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
42DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id 44DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id
43 45
44HOST_URL = 'http://keystone:5001' 46HOST = ''.join(random.choice(string.ascii_lowercase) for x in range(
47 random.randint(5, 15)))
48HOST_URL = 'http://%s' % (HOST)
45 49
46 50
47def _build_user_auth(token=None, user_id=None, username=None, 51def _build_user_auth(token=None, user_id=None, username=None,
@@ -871,7 +875,16 @@ class AuthWithTrust(AuthTest):
871 token_id=token_id, 875 token_id=token_id,
872 token_data=self.token_provider_api.validate_token(token_id)) 876 token_data=self.token_provider_api.validate_token(token_id))
873 auth_context = authorization.token_to_auth_context(token_ref) 877 auth_context = authorization.token_to_auth_context(token_ref)
874 return {'environment': {authorization.AUTH_CONTEXT_ENV: auth_context}, 878 # NOTE(gyee): if public_endpoint and admin_endpoint are not set, which
879 # is the default, the base url will be constructed from the environment
880 # variables wsgi.url_scheme, SERVER_NAME, SERVER_PORT, and SCRIPT_NAME.
881 # We have to set them in the context so the base url can be constructed
882 # accordingly.
883 return {'environment': {authorization.AUTH_CONTEXT_ENV: auth_context,
884 'wsgi.url_scheme': 'http',
885 'SCRIPT_NAME': '/v3',
886 'SERVER_PORT': '80',
887 'SERVER_NAME': HOST},
875 'token_id': token_id, 888 'token_id': token_id,
876 'host_url': HOST_URL} 889 'host_url': HOST_URL}
877 890
diff --git a/keystone/tests/unit/test_wsgi.py b/keystone/tests/unit/test_wsgi.py
index e621cd5..564d740 100644
--- a/keystone/tests/unit/test_wsgi.py
+++ b/keystone/tests/unit/test_wsgi.py
@@ -213,7 +213,9 @@ class ApplicationTest(BaseWSGITest):
213 213
214 def test_render_exception_host(self): 214 def test_render_exception_host(self):
215 e = exception.Unauthorized(message=u'\u7f51\u7edc') 215 e = exception.Unauthorized(message=u'\u7f51\u7edc')
216 context = {'host_url': 'http://%s:5000' % uuid.uuid4().hex} 216 req = self._make_request(url='/')
217 context = {'host_url': 'http://%s:5000' % uuid.uuid4().hex,
218 'environment': req.environ}
217 resp = wsgi.render_exception(e, context=context) 219 resp = wsgi.render_exception(e, context=context)
218 220
219 self.assertEqual(http_client.UNAUTHORIZED, resp.status_int) 221 self.assertEqual(http_client.UNAUTHORIZED, resp.status_int)
@@ -238,6 +240,77 @@ class ApplicationTest(BaseWSGITest):
238 self.assertEqual({'name': u'nonexit\xe8nt'}, 240 self.assertEqual({'name': u'nonexit\xe8nt'},
239 jsonutils.loads(resp.body)) 241 jsonutils.loads(resp.body))
240 242
243 def test_base_url(self):
244 class FakeApp(wsgi.Application):
245 def index(self, context):
246 return self.base_url(context, 'public')
247 req = self._make_request(url='/')
248 # NOTE(gyee): according to wsgiref, if HTTP_HOST is present in the
249 # request environment, it will be used to construct the base url.
250 # SERVER_NAME and SERVER_PORT will be ignored. These are standard
251 # WSGI environment variables populated by the webserver.
252 req.environ.update({
253 'SCRIPT_NAME': '/identity',
254 'SERVER_NAME': '1.2.3.4',
255 'wsgi.url_scheme': 'http',
256 'SERVER_PORT': '80',
257 'HTTP_HOST': '1.2.3.4',
258 })
259 resp = req.get_response(FakeApp())
260 self.assertEqual(b"http://1.2.3.4/identity", resp.body)
261
262 # if HTTP_HOST is absent, SERVER_NAME and SERVER_PORT will be used
263 req = self._make_request(url='/')
264 del req.environ['HTTP_HOST']
265 req.environ.update({
266 'SCRIPT_NAME': '/identity',
267 'SERVER_NAME': '1.1.1.1',
268 'wsgi.url_scheme': 'http',
269 'SERVER_PORT': '1234',
270 })
271 resp = req.get_response(FakeApp())
272 self.assertEqual(b"http://1.1.1.1:1234/identity", resp.body)
273
274 # make sure keystone normalize the standard HTTP port 80 by stripping
275 # it
276 req = self._make_request(url='/')
277 req.environ.update({'HTTP_HOST': 'foo:80',
278 'SCRIPT_NAME': '/identity'})
279 resp = req.get_response(FakeApp())
280 self.assertEqual(b"http://foo/identity", resp.body)
281
282 # make sure keystone normalize the standard HTTPS port 443 by stripping
283 # it
284 req = self._make_request(url='/')
285 req.environ.update({'HTTP_HOST': 'foo:443',
286 'SCRIPT_NAME': '/identity',
287 'wsgi.url_scheme': 'https'})
288 resp = req.get_response(FakeApp())
289 self.assertEqual(b"https://foo/identity", resp.body)
290
291 # make sure non-standard port is preserved
292 req = self._make_request(url='/')
293 req.environ.update({'HTTP_HOST': 'foo:1234',
294 'SCRIPT_NAME': '/identity'})
295 resp = req.get_response(FakeApp())
296 self.assertEqual(b"http://foo:1234/identity", resp.body)
297
298 # make sure version portion of the SCRIPT_NAME, '/v2.0', is stripped
299 # from base url
300 req = self._make_request(url='/')
301 req.environ.update({'HTTP_HOST': 'foo:80',
302 'SCRIPT_NAME': '/bar/identity/v2.0'})
303 resp = req.get_response(FakeApp())
304 self.assertEqual(b"http://foo/bar/identity", resp.body)
305
306 # make sure version portion of the SCRIPT_NAME, '/v3' is stripped from
307 # base url
308 req = self._make_request(url='/')
309 req.environ.update({'HTTP_HOST': 'foo:80',
310 'SCRIPT_NAME': '/identity/v3'})
311 resp = req.get_response(FakeApp())
312 self.assertEqual(b"http://foo/identity", resp.body)
313
241 314
242class ExtensionRouterTest(BaseWSGITest): 315class ExtensionRouterTest(BaseWSGITest):
243 def test_extensionrouter_local_config(self): 316 def test_extensionrouter_local_config(self):