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