From f0c7f27af61c2d638b2b75e6e58e5f60554c64ff Mon Sep 17 00:00:00 2001 From: Elvin Tubillara Date: Fri, 18 Nov 2016 15:19:20 -0600 Subject: [PATCH] Add K2K Auth Dropdown This adds auth functionality to the Auth Drop down. A new K2K django auth plugin has been added (With the intent to do K2K at Login Time). Session variables have been added so horizon can display the names of the Keystone Providers. An endpoint was also added that allows the user to switch keystone providers. Change-Id: I75b1a10a3b40b5544b60f6fdc060e0070c585977 Implements: blueprint k2k-horizon --- openstack_auth/backend.py | 3 + openstack_auth/plugin/__init__.py | 4 +- openstack_auth/plugin/k2k.py | 104 +++++++++++ openstack_auth/tests/data_v3.py | 49 +++-- openstack_auth/tests/settings.py | 2 + openstack_auth/tests/tests.py | 297 +++++++++++++++++++++++++++--- openstack_auth/urls.py | 5 +- openstack_auth/utils.py | 51 +++++ openstack_auth/views.py | 74 ++++++++ 9 files changed, 544 insertions(+), 45 deletions(-) create mode 100644 openstack_auth/plugin/k2k.py diff --git a/openstack_auth/backend.py b/openstack_auth/backend.py index 9ada58d1..c8acd216 100644 --- a/openstack_auth/backend.py +++ b/openstack_auth/backend.py @@ -190,6 +190,9 @@ class KeystoneBackend(object): services_region=region_name) if request is not None: + # if no k2k providers exist then the function returns quickly + utils.store_initial_k2k_session(auth_url, request, scoped_auth_ref, + unscoped_auth_ref) request.session['unscoped_token'] = unscoped_token if domain_auth_ref: # check django session engine, if using cookies, this will not diff --git a/openstack_auth/plugin/__init__.py b/openstack_auth/plugin/__init__.py index 64caeace..c664bed7 100644 --- a/openstack_auth/plugin/__init__.py +++ b/openstack_auth/plugin/__init__.py @@ -13,8 +13,10 @@ from openstack_auth.plugin.base import * # noqa from openstack_auth.plugin.password import * # noqa from openstack_auth.plugin.token import * # noqa +from openstack_auth.plugin.k2k import * # noqa __all__ = ['BasePlugin', 'PasswordPlugin', - 'TokenPlugin'] + 'TokenPlugin', + 'K2KAuthPlugin'] diff --git a/openstack_auth/plugin/k2k.py b/openstack_auth/plugin/k2k.py new file mode 100644 index 00000000..03a85f0e --- /dev/null +++ b/openstack_auth/plugin/k2k.py @@ -0,0 +1,104 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ +from keystoneauth1.identity import v3 as v3_auth + +from openstack_auth import exceptions +from openstack_auth.plugin import base +from openstack_auth import utils + +LOG = logging.getLogger(__name__) + +__all__ = ['K2KAuthPlugin'] + + +class K2KAuthPlugin(base.BasePlugin): + + def get_plugin(self, service_provider=None, auth_url=None, plugins=[], + **kwargs): + """Authenticate using keystone to keystone federation. + + This plugin uses other v3 plugins to authenticate a user to a + identity provider in order to authenticate the user to a service + provider + + :param service_provider: service provider ID + :param auth_url: Keystone auth url + :param plugins: list of openstack_auth plugins to check + :returns Keystone2Keystone keystone auth plugin + """ + + # service_provider being None prevents infinite recursion + if utils.get_keystone_version() < 3 or not service_provider: + return None + + keystone_idp_id = getattr(settings, 'KEYSTONE_PROVIDER_IDP_ID', + 'localkeystone') + if service_provider == keystone_idp_id: + return None + + for plugin in plugins: + unscoped_idp_auth = plugin.get_plugin(plugins=plugins, + auth_url=auth_url, **kwargs) + if unscoped_idp_auth: + break + else: + LOG.debug('Could not find base authentication backend for ' + 'K2K plugin with the provided credentials.') + return None + + idp_exception = None + scoped_idp_auth = None + unscoped_auth_ref = base.BasePlugin.get_access_info( + self, unscoped_idp_auth) + try: + scoped_idp_auth, __ = self.get_project_scoped_auth( + unscoped_idp_auth, unscoped_auth_ref) + except exceptions.KeystoneAuthException as idp_excp: + idp_exception = idp_excp + + if not scoped_idp_auth or idp_exception: + msg = 'Identity provider authentication Failed.' + raise exceptions.KeystoneAuthException(msg) + + session = utils.get_session() + + if scoped_idp_auth.get_sp_auth_url(session, service_provider) is None: + msg = _('Could not find service provider ID on Keystone.') + raise exceptions.KeystoneAuthException(msg) + + unscoped_auth = v3_auth.Keystone2Keystone( + base_plugin=scoped_idp_auth, + service_provider=service_provider) + return unscoped_auth + + def get_access_info(self, unscoped_auth): + """Get the access info object + + We attempt to get the auth ref. If it fails and if the K2K auth plugin + was being used then we will prepend a message saying that the error was + on the service provider side. + :param: unscoped_auth: Keystone auth plugin for unscoped user + :returns: keystoneclient.access.AccessInfo object + """ + try: + unscoped_auth_ref = base.BasePlugin.get_access_info( + self, unscoped_auth) + except exceptions.KeystoneAuthException as excp: + msg = _('Service provider authentication failed. %s') + raise exceptions.KeystoneAuthException(msg % str(excp)) + return unscoped_auth_ref diff --git a/openstack_auth/tests/data_v3.py b/openstack_auth/tests/data_v3.py index 70530914..1d2ef82b 100644 --- a/openstack_auth/tests/data_v3.py +++ b/openstack_auth/tests/data_v3.py @@ -55,7 +55,8 @@ class TestResponse(requests.Response): return self._text -def generate_test_data(pki=False): +def generate_test_data(pki=False, service_providers=False, + endpoint='localhost'): '''Builds a set of test_data data as returned by Keystone V2.''' test_data = TestDataContainer() @@ -64,19 +65,19 @@ def generate_test_data(pki=False): 'id': uuid.uuid4().hex, 'endpoints': [ { - 'url': 'http://admin.localhost:35357/v3', + 'url': 'http://admin.%s:35357/v3' % endpoint, 'region': 'RegionOne', 'interface': 'admin', 'id': uuid.uuid4().hex, }, { - 'url': 'http://internal.localhost:5000/v3', + 'url': 'http://internal.%s:5000/v3' % endpoint, 'region': 'RegionOne', 'interface': 'internal', 'id': uuid.uuid4().hex }, { - 'url': 'http://public.localhost:5000/v3', + 'url': 'http://public.%s:5000/v3' % endpoint, 'region': 'RegionOne', 'interface': 'public', 'id': uuid.uuid4().hex @@ -131,43 +132,43 @@ def generate_test_data(pki=False): 'id': uuid.uuid4().hex, 'endpoints': [ { - 'url': ('http://nova-admin.localhost:8774/v2.0/%s' - % (project_dict_1['id'])), + 'url': ('http://nova-admin.%s:8774/v2.0/%s' + % (endpoint, project_dict_1['id'])), 'region': 'RegionOne', 'interface': 'admin', 'id': uuid.uuid4().hex, }, { - 'url': ('http://nova-internal.localhost:8774/v2.0/%s' - % (project_dict_1['id'])), + 'url': ('http://nova-internal.%s:8774/v2.0/%s' + % (endpoint, project_dict_1['id'])), 'region': 'RegionOne', 'interface': 'internal', 'id': uuid.uuid4().hex }, { - 'url': ('http://nova-public.localhost:8774/v2.0/%s' - % (project_dict_1['id'])), + 'url': ('http://nova-public.%s:8774/v2.0/%s' + % (endpoint, project_dict_1['id'])), 'region': 'RegionOne', 'interface': 'public', 'id': uuid.uuid4().hex }, { - 'url': ('http://nova2-admin.localhost:8774/v2.0/%s' - % (project_dict_1['id'])), + 'url': ('http://nova2-admin.%s:8774/v2.0/%s' + % (endpoint, project_dict_1['id'])), 'region': 'RegionTwo', 'interface': 'admin', 'id': uuid.uuid4().hex, }, { - 'url': ('http://nova2-internal.localhost:8774/v2.0/%s' - % (project_dict_1['id'])), + 'url': ('http://nova2-internal.%s:8774/v2.0/%s' + % (endpoint, project_dict_1['id'])), 'region': 'RegionTwo', 'interface': 'internal', 'id': uuid.uuid4().hex }, { - 'url': ('http://nova2-public.localhost:8774/v2.0/%s' - % (project_dict_1['id'])), + 'url': ('http://nova2-public.%s:8774/v2.0/%s' + % (endpoint, project_dict_1['id'])), 'region': 'RegionTwo', 'interface': 'public', 'id': uuid.uuid4().hex @@ -218,6 +219,19 @@ def generate_test_data(pki=False): } } + sp_list = None + if service_providers: + test_data.sp_auth_url = 'http://service_provider_endp:5000/v3' + test_data.service_provider_id = 'k2kserviceprovider' + # The access info for the identity provider + # should return a list of service providers + sp_list = [ + {'auth_url': test_data.sp_auth_url, + 'id': test_data.service_provider_id, + 'sp_url': 'https://k2kserviceprovider/sp_url'} + ] + scoped_token_dict['token']['service_providers'] = sp_list + test_data.scoped_access_info = access.create( resp=auth_response, body=scoped_token_dict @@ -264,6 +278,9 @@ def generate_test_data(pki=False): } } + if service_providers: + unscoped_token_dict['token']['service_providers'] = sp_list + test_data.unscoped_access_info = access.create( resp=auth_response, body=unscoped_token_dict diff --git a/openstack_auth/tests/settings.py b/openstack_auth/tests/settings.py index 17f59e6f..9590d51c 100644 --- a/openstack_auth/tests/settings.py +++ b/openstack_auth/tests/settings.py @@ -70,3 +70,5 @@ TEMPLATES = [ 'APP_DIRS': True, }, ] + +AUTH_USER_MODEL = 'openstack_auth.User' diff --git a/openstack_auth/tests/tests.py b/openstack_auth/tests/tests.py index c58aacc0..6016d736 100644 --- a/openstack_auth/tests/tests.py +++ b/openstack_auth/tests/tests.py @@ -76,15 +76,18 @@ class OpenStackAuthTestsMixin(object): plugin.get_access(mox.IsA(session.Session)).AndRaise(exc) def _mock_scoped_client_for_tenant(self, auth_ref, tenant_id, url=None, - client=True): + client=True, token=None): if url is None: url = settings.OPENSTACK_KEYSTONE_URL + if not token: + token = self.data.unscoped_access_info.auth_token + plugin = self._create_token_auth( tenant_id, - token=self.data.unscoped_access_info.auth_token, + token=token, url=url) - + self.scoped_token_auth = plugin plugin.get_access(mox.IsA(session.Session)).AndReturn(auth_ref) if client: return self.ks_client_module.Client( @@ -98,6 +101,33 @@ class OpenStackAuthTestsMixin(object): 'username': user.name} +class OpenStackAuthFederatedTestsMixin(object): + """Common functions for federation""" + def _mock_unscoped_federated_list_projects(self, client, projects): + client.federation = self.mox.CreateMockAnything() + client.federation.projects = self.mox.CreateMockAnything() + client.federation.projects.list().AndReturn(projects) + + def _mock_unscoped_token_client(self, unscoped, auth_url=None, + client=True): + if not auth_url: + auth_url = settings.OPENSTACK_KEYSTONE_URL + plugin = self._create_token_auth( + None, + token=unscoped.auth_token, + url=auth_url) + plugin.get_access(mox.IsA(session.Session)).AndReturn(unscoped) + plugin.auth_url = auth_url + if client: + return self.ks_client_module.Client( + session=mox.IsA(session.Session), + auth=plugin) + + def _mock_federated_client_list_projects(self, unscoped, projects): + client = self._mock_unscoped_token_client(unscoped) + self._mock_unscoped_federated_list_projects(client, projects) + + class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase): def setUp(self): @@ -431,7 +461,9 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase): self.assertEqual(tenant_list, expected_tenants) -class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): +class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, + OpenStackAuthFederatedTestsMixin, + test.TestCase): def _mock_unscoped_client_list_projects(self, user, projects): client = self._mock_unscoped_client(user) @@ -532,6 +564,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): self.mox.StubOutClassWithMocks(v3_auth, 'Token') self.mox.StubOutClassWithMocks(v3_auth, 'Password') self.mox.StubOutClassWithMocks(client_v3, 'Client') + self.mox.StubOutClassWithMocks(v3_auth, 'Keystone2Keystone') def test_login(self): projects = [self.data.project_one, self.data.project_two] @@ -774,6 +807,234 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): def test_switch_region_with_next(self, next=None): self.test_switch_region(next='/next_url') + def test_switch_keystone_provider_remote_fail(self): + auth_url = settings.OPENSTACK_KEYSTONE_URL + target_provider = 'k2kserviceprovider' + self.data = data_v3.generate_test_data(service_providers=True) + self.sp_data = data_v3.generate_test_data(endpoint='http://sp2') + projects = [self.data.project_one, self.data.project_two] + user = self.data.user + unscoped = self.data.unscoped_access_info + form_data = self.get_form_data(user) + + # mock authenticate + self._mock_unscoped_and_domain_list_projects(user, projects) + self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) + + # mock switch + plugin = v3_auth.Token(auth_url=auth_url, + token=unscoped.auth_token, + project_id=None, + reauthenticate=False) + plugin.get_access(mox.IsA(session.Session) + ).AndReturn(self.data.unscoped_access_info) + plugin.auth_url = auth_url + client = self.ks_client_module.Client(session=mox.IsA(session.Session), + auth=plugin) + + self._mock_unscoped_list_projects(client, user, projects) + plugin = self._create_token_auth( + self.data.project_one.id, + token=self.data.unscoped_access_info.auth_token, + url=settings.OPENSTACK_KEYSTONE_URL) + plugin.get_access(mox.IsA(session.Session)).AndReturn( + settings.OPENSTACK_KEYSTONE_URL) + plugin.get_sp_auth_url( + mox.IsA(session.Session), target_provider + ).AndReturn('https://k2kserviceprovider/sp_url') + + # let the K2K plugin fail when logging in + plugin = v3_auth.Keystone2Keystone( + base_plugin=plugin, service_provider=target_provider) + plugin.get_access(mox.IsA(session.Session)).AndRaise( + keystone_exceptions.AuthorizationFailure) + self.mox.ReplayAll() + + # Log in + url = reverse('login') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + response = self.client.post(url, form_data) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) + + # Switch + url = reverse('switch_keystone_provider', args=[target_provider]) + form_data['keystone_provider'] = target_provider + response = self.client.get(url, form_data, follow=True) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) + + # Assert that provider has not changed because of failure + self.assertEqual(self.client.session['keystone_provider_id'], + 'localkeystone') + # These should never change + self.assertEqual(self.client.session['k2k_base_unscoped_token'], + unscoped.auth_token) + self.assertEqual(self.client.session['k2k_auth_url'], auth_url) + + def test_switch_keystone_provider_remote(self): + auth_url = settings.OPENSTACK_KEYSTONE_URL + target_provider = 'k2kserviceprovider' + self.data = data_v3.generate_test_data(service_providers=True) + self.sp_data = data_v3.generate_test_data(endpoint='http://sp2') + projects = [self.data.project_one, self.data.project_two] + user = self.data.user + unscoped = self.data.unscoped_access_info + form_data = self.get_form_data(user) + + # mock authenticate + self._mock_unscoped_and_domain_list_projects(user, projects) + self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) + + # mock switch + plugin = v3_auth.Token(auth_url=auth_url, + token=unscoped.auth_token, + project_id=None, + reauthenticate=False) + plugin.get_access(mox.IsA(session.Session)).AndReturn( + self.data.unscoped_access_info) + + plugin.auth_url = auth_url + client = self.ks_client_module.Client(session=mox.IsA(session.Session), + auth=plugin) + + self._mock_unscoped_list_projects(client, user, projects) + plugin = self._create_token_auth( + self.data.project_one.id, + token=self.data.unscoped_access_info.auth_token, + url=settings.OPENSTACK_KEYSTONE_URL) + plugin.get_access(mox.IsA(session.Session)).AndReturn( + settings.OPENSTACK_KEYSTONE_URL) + + plugin.get_sp_auth_url( + mox.IsA(session.Session), target_provider + ).AndReturn('https://k2kserviceprovider/sp_url') + plugin = v3_auth.Keystone2Keystone(base_plugin=plugin, + service_provider=target_provider) + plugin.get_access(mox.IsA(session.Session)). \ + AndReturn(self.sp_data.unscoped_access_info) + plugin.auth_url = 'http://service_provider_endp:5000/v3' + + # mock authenticate for service provider + sp_projects = [self.sp_data.project_one, self.sp_data.project_two] + sp_unscoped = self.sp_data.federated_unscoped_access_info + client = self._mock_unscoped_token_client(sp_unscoped, plugin.auth_url) + self._mock_unscoped_federated_list_projects(client, sp_projects) + self._mock_scoped_client_for_tenant(sp_unscoped, + self.sp_data.project_one.id, + url=plugin.auth_url, + token=sp_unscoped.auth_token) + + self.mox.ReplayAll() + + # Log in + url = reverse('login') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + response = self.client.post(url, form_data) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) + + # Switch + url = reverse('switch_keystone_provider', args=[target_provider]) + form_data['keystone_provider'] = target_provider + response = self.client.get(url, form_data, follow=True) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) + + # Assert keystone provider has changed + self.assertEqual(self.client.session['keystone_provider_id'], + target_provider) + # These should not change + self.assertEqual(self.client.session['k2k_base_unscoped_token'], + unscoped.auth_token) + self.assertEqual(self.client.session['k2k_auth_url'], auth_url) + + def test_switch_keystone_provider_local(self): + auth_url = settings.OPENSTACK_KEYSTONE_URL + self.data = data_v3.generate_test_data(service_providers=True) + keystone_provider = 'localkeystone' + projects = [self.data.project_one, self.data.project_two] + user = self.data.user + unscoped = self.data.unscoped_access_info + form_data = self.get_form_data(user) + + # mock authenticate + self._mock_unscoped_and_domain_list_projects(user, projects) + self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) + self._mock_unscoped_token_client(unscoped, + auth_url=auth_url, + client=False) + client = self._mock_unscoped_token_client(unscoped, auth_url) + self._mock_unscoped_list_projects(client, user, projects) + self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) + + self.mox.ReplayAll() + + # Log in + url = reverse('login') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + response = self.client.post(url, form_data) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) + + # Switch + url = reverse('switch_keystone_provider', args=[keystone_provider]) + form_data['keystone_provider'] = keystone_provider + response = self.client.get(url, form_data, follow=True) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) + + # Assert nothing has changed since we are going from local to local + self.assertEqual(self.client.session['keystone_provider_id'], + keystone_provider) + self.assertEqual(self.client.session['k2k_base_unscoped_token'], + unscoped.auth_token) + self.assertEqual(self.client.session['k2k_auth_url'], auth_url) + + def test_switch_keystone_provider_local_fail(self): + auth_url = settings.OPENSTACK_KEYSTONE_URL + self.data = data_v3.generate_test_data(service_providers=True) + keystone_provider = 'localkeystone' + projects = [self.data.project_one, self.data.project_two] + user = self.data.user + unscoped = self.data.unscoped_access_info + form_data = self.get_form_data(user) + + # mock authenticate + self._mock_unscoped_and_domain_list_projects(user, projects) + self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) + + # Let using the base token for logging in fail + plugin = v3_auth.Token(auth_url=auth_url, + token=unscoped.auth_token, + project_id=None, + reauthenticate=False) + plugin.get_access(mox.IsA(session.Session)). \ + AndRaise(keystone_exceptions.AuthorizationFailure) + plugin.auth_url = auth_url + self.mox.ReplayAll() + + # Log in + url = reverse('login') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + response = self.client.post(url, form_data) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) + + # Switch + url = reverse('switch_keystone_provider', args=[keystone_provider]) + form_data['keystone_provider'] = keystone_provider + response = self.client.get(url, form_data, follow=True) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) + + # Assert + self.assertEqual(self.client.session['keystone_provider_id'], + keystone_provider) + self.assertEqual(self.client.session['k2k_base_unscoped_token'], + unscoped.auth_token) + self.assertEqual(self.client.session['k2k_auth_url'], auth_url) + def test_tenant_sorting(self): projects = [self.data.project_two, self.data.project_one] expected_projects = [self.data.project_one, self.data.project_two] @@ -791,7 +1052,9 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): self.assertEqual(project_list, expected_projects) -class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, test.TestCase): +class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, + OpenStackAuthFederatedTestsMixin, + test.TestCase): def _create_token_auth(self, project_id=None, token=None, url=None): if not token: @@ -805,26 +1068,6 @@ class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, test.TestCase): project_id=project_id, reauthenticate=False) - def _mock_unscoped_client(self, unscoped): - plugin = self._create_token_auth( - None, - token=unscoped.auth_token, - url=settings.OPENSTACK_KEYSTONE_URL) - plugin.get_access(mox.IsA(session.Session)).AndReturn(unscoped) - plugin.auth_url = settings.OPENSTACK_KEYSTONE_URL - - return self.ks_client_module.Client(session=mox.IsA(session.Session), - auth=plugin) - - def _mock_unscoped_federated_list_projects(self, client, projects): - client.federation = self.mox.CreateMockAnything() - client.federation.projects = self.mox.CreateMockAnything() - client.federation.projects.list().AndReturn(projects) - - def _mock_unscoped_client_list_projects(self, unscoped, projects): - client = self._mock_unscoped_client(unscoped) - self._mock_unscoped_federated_list_projects(client, projects) - def setUp(self): super(OpenStackAuthTestsWebSSO, self).setUp() @@ -908,7 +1151,7 @@ class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, test.TestCase): token = unscoped.auth_token form_data = {'token': token} - self._mock_unscoped_client_list_projects(unscoped, projects) + self._mock_federated_client_list_projects(unscoped, projects) self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) self.mox.ReplayAll() @@ -927,7 +1170,7 @@ class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, test.TestCase): token = unscoped.auth_token form_data = {'token': token} - self._mock_unscoped_client_list_projects(unscoped, projects) + self._mock_federated_client_list_projects(unscoped, projects) self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) self.mox.ReplayAll() diff --git a/openstack_auth/urls.py b/openstack_auth/urls.py index a7d9dac1..12d7cfb0 100644 --- a/openstack_auth/urls.py +++ b/openstack_auth/urls.py @@ -26,7 +26,10 @@ urlpatterns = [ name='switch_tenants'), url(r'^switch_services_region/(?P[^/]+)/$', views.switch_region, - name='switch_services_region') + name='switch_services_region'), + url(r'^switch_keystone_provider/(?P[^/]+)/$', + views.switch_keystone_provider, + name='switch_keystone_provider') ] if utils.is_websso_enabled(): diff --git a/openstack_auth/utils.py b/openstack_auth/utils.py index c0507261..0d5ee110 100644 --- a/openstack_auth/utils.py +++ b/openstack_auth/utils.py @@ -498,3 +498,54 @@ def get_client_ip(request): request.META.get('REMOTE_ADDR') ) return request.META.get('REMOTE_ADDR') + + +def store_initial_k2k_session(auth_url, request, scoped_auth_ref, + unscoped_auth_ref): + """Stores session variables if there are k2k service providers + + This stores variables related to Keystone2Keystone federation. This + function gets skipped if there are no Keystone service providers. + An unscoped token to the identity provider keystone gets stored + so that it can be used to do federated login into the service + providers when switching keystone providers. + The settings file can be configured to set the display name + of the local (identity provider) keystone by setting + KEYSTONE_PROVIDER_IDP_NAME. The KEYSTONE_PROVIDER_IDP_ID settings + variable is used for comparison against the service providers. + It should not conflict with any of the service provider ids. + + :param auth_url: base token auth url + :param request: Django http request object + :param scoped_auth_ref: Scoped Keystone access info object + :param unscoped_auth_ref: Unscoped Keystone access info object + """ + keystone_provider_id = request.session.get('keystone_provider_id', None) + if keystone_provider_id: + return None + + providers = getattr(scoped_auth_ref, 'service_providers', None) + if providers: + providers = getattr(providers, '_service_providers', None) + + if providers: + keystone_idp_name = getattr(settings, 'KEYSTONE_PROVIDER_IDP_NAME', + 'Local Keystone') + keystone_idp_id = getattr( + settings, 'KEYSTONE_PROVIDER_IDP_ID', 'localkeystone') + keystone_identity_provider = {'name': keystone_idp_name, + 'id': keystone_idp_id} + # (edtubill) We will use the IDs as the display names + # We may want to be able to set display names in the future. + keystone_providers = [ + {'name': provider_id, 'id': provider_id} + for provider_id in providers] + + keystone_providers.append(keystone_identity_provider) + + # We treat the Keystone idp ID as None + request.session['keystone_provider_id'] = keystone_idp_id + request.session['keystone_providers'] = keystone_providers + request.session['k2k_base_unscoped_token'] =\ + unscoped_auth_ref.auth_token + request.session['k2k_auth_url'] = auth_url diff --git a/openstack_auth/views.py b/openstack_auth/views.py index 8831403b..ce51fa69 100644 --- a/openstack_auth/views.py +++ b/openstack_auth/views.py @@ -31,6 +31,8 @@ import six from openstack_auth import exceptions from openstack_auth import forms +from openstack_auth import plugin + # This is historic and is added back in to not break older versions of # Horizon, fix to Horizon to remove this requirement was committed in # Juno @@ -241,3 +243,75 @@ def switch_region(request, region_name, utils.set_response_cookie(response, 'services_region', request.session['services_region']) return response + + +@login_required +def switch_keystone_provider(request, keystone_provider=None, + redirect_field_name=auth.REDIRECT_FIELD_NAME): + """Switches the user's keystone provider using K2K Federation + + If keystone_provider is given then we switch the user to + the keystone provider using K2K federation. Otherwise if keystone_provider + is None then we switch the user back to the Identity Provider Keystone + which a non federated token auth will be used. + """ + base_token = request.session.get('k2k_base_unscoped_token', None) + k2k_auth_url = request.session.get('k2k_auth_url', None) + keystone_providers = request.session.get('keystone_providers', None) + + if not base_token or not k2k_auth_url: + msg = _('K2K Federation not setup for this session') + raise exceptions.KeystoneAuthException(msg) + + redirect_to = request.GET.get(redirect_field_name, '') + if not is_safe_url(url=redirect_to, host=request.get_host()): + redirect_to = settings.LOGIN_REDIRECT_URL + + unscoped_auth_ref = None + keystone_idp_id = getattr( + settings, 'KEYSTONE_PROVIDER_IDP_ID', 'localkeystone') + + if keystone_provider == keystone_idp_id: + current_plugin = plugin.TokenPlugin() + unscoped_auth = current_plugin.get_plugin(auth_url=k2k_auth_url, + token=base_token) + else: + # Switch to service provider using K2K federation + plugins = [plugin.TokenPlugin()] + current_plugin = plugin.K2KAuthPlugin() + + unscoped_auth = current_plugin.get_plugin( + auth_url=k2k_auth_url, service_provider=keystone_provider, + plugins=plugins, token=base_token) + + try: + # Switch to identity provider using token auth + unscoped_auth_ref = current_plugin.get_access_info(unscoped_auth) + except exceptions.KeystoneAuthException as exc: + msg = 'Switching to Keystone Provider %s has failed. %s' \ + % (keystone_provider, (six.text_type(exc))) + messages.error(request, msg) + + if unscoped_auth_ref: + try: + request.user = auth.authenticate( + request=request, auth_url=unscoped_auth.auth_url, + token=unscoped_auth_ref.auth_token) + except exceptions.KeystoneAuthException as exc: + msg = 'Keystone provider switch failed: %s' % six.text_type(exc) + res = django_http.HttpResponseRedirect(settings.LOGIN_URL) + res.set_cookie('logout_reason', msg, max_age=10) + return res + auth.login(request, request.user) + auth_user.set_session_from_user(request, request.user) + request.session['keystone_provider_id'] = keystone_provider + request.session['keystone_providers'] = keystone_providers + request.session['k2k_base_unscoped_token'] = base_token + request.session['k2k_auth_url'] = k2k_auth_url + message = ( + _('Switch to Keystone Provider "%(keystone_provider)s"' + 'successful.') % {'keystone_provider': keystone_provider}) + messages.success(request, message) + + response = shortcuts.redirect(redirect_to) + return response