From 88827a895fd8606ac708b8858fa735377dca9238 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Thu, 10 Aug 2017 13:45:14 +0200 Subject: [PATCH] Allow discovery URLs to have trailing slashes The _get_discovery_url_choices generator works by taking a starting URL, splitting it on '/', and working through the parts trying to get a matching discovery document from it. It makes assumptions about what the URL might look like: it might have a project ID on the end of it, and a version before that. If the starting URL has a trailing '/', splitting the URL results in an empty string at the end of the list of parts, which is then treated as a version. The real version is left on the URL while the generator assumes it has already trimmed the URL down to an unversioned endpoint. If that version does not match the version we're seeking, the resulting discovery document will be mismatched and the generator will fail to yield the right endpoint. This patch normalizes the starting URL by removing the trailing '/', if there is one. This way every part of the split URL will be meaningful. Closes-bug: #1709658 Change-Id: I28c48f78d6f07804d6ea228f163dd37b0fcfcd58 --- keystoneauth1/discover.py | 2 +- .../unit/identity/test_identity_common.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/keystoneauth1/discover.py b/keystoneauth1/discover.py index 8003d018..4b738a01 100644 --- a/keystoneauth1/discover.py +++ b/keystoneauth1/discover.py @@ -983,7 +983,7 @@ class EndpointData(object): min_version and max_version are already normalized, so will either be None or a tuple. """ - url = urllib.parse.urlparse(self.url) + url = urllib.parse.urlparse(self.url.rstrip('/')) url_parts = url.path.split('/') # First, check to see if the catalog url ends with a project id diff --git a/keystoneauth1/tests/unit/identity/test_identity_common.py b/keystoneauth1/tests/unit/identity/test_identity_common.py index 65bb93d7..866b0e53 100644 --- a/keystoneauth1/tests/unit/identity/test_identity_common.py +++ b/keystoneauth1/tests/unit/identity/test_identity_common.py @@ -1119,6 +1119,8 @@ class CatalogHackTests(utils.TestCase): V2_URL = BASE_URL + 'v2.0' V3_URL = BASE_URL + 'v3' + PROJECT_ID = uuid.uuid4().hex + def test_getting_endpoints(self): disc = fixture.DiscoveryList(href=self.BASE_URL) self.stub_url('GET', @@ -1176,6 +1178,43 @@ class CatalogHackTests(utils.TestCase): self.assertEqual(self.V2_URL, endpoint) + def test_getting_endpoints_project_id_and_trailing_slash_in_disc_url(self): + # Test that when requesting a v3 endpoint and having a project in the + # session but only the v2 endpoint with a trailing slash in the + # catalog, we can still discover the v3 endpoint. + disc = fixture.DiscoveryList(href=self.BASE_URL) + self.stub_url('GET', + ['/'], + base_url=self.BASE_URL, + json=disc) + + # Create a project-scoped token. This will exercise the flow in the + # discovery URL sequence where a project ID exists in the token but + # there is no project ID in the URL. + token = fixture.V3Token(project_id=self.PROJECT_ID) + + # Add only a v2 endpoint with a trailing slash + service = token.add_service(self.IDENTITY) + service.add_endpoint('public', self.V2_URL + '/') + service.add_endpoint('admin', self.V2_URL + '/') + + # Auth with v3 + kwargs = {'headers': {'X-Subject-Token': self.TEST_TOKEN}} + self.stub_url('POST', + ['auth', 'tokens'], + base_url=self.V3_URL, + json=token, **kwargs) + v3_auth = identity.V3Password(self.V3_URL, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + sess = session.Session(auth=v3_auth) + + # Try to get a v3 endpoint + endpoint = sess.get_endpoint(service_type=self.IDENTITY, + interface='public', + version=(3, 0)) + self.assertEqual(self.V3_URL, endpoint) + def test_returns_original_skipping_discovery(self): token = fixture.V2Token() service = token.add_service(self.IDENTITY)