From ca3166707b2b8d121d4bf75dcea32ddfd3a442f1 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Thu, 20 Oct 2016 21:59:55 +0200 Subject: [PATCH] Allow federated users to auth with domain scope When a federated user logs in, openstack_auth receives an unscoped token and no user_domain_name parameter. Currently, if the federated user has a role in one or more domains, but no roles in any projects, openstack_auth prevents authorization and denies the user's login with the error "You are not authorized for any projects or domains." This is a problem because first, it's inaccurate, as the user is authorized for at least one domain, and second, a keystone administrator may want to give federated users access to a domain without any projects in it, for example so delegate the creation of projects to the federated users themselves. This patch allows federated users without project roles to log in by looking up domains as well as projects when attempting to scope the token. This lookup is skipped if the domain was passed as part of the request. This patch also slightly restructures the OpenStackAuthTestsWebSSO and OpenStackAuthTestsV3 tests because mox needs to simulate only one instance of the plugin but two instances of the client objects for every call to authenticate(). Closes-bug: #1649101 Change-Id: I151218ff28c0728898ed5315d63dd8122ce3b166 --- openstack_auth/plugin/base.py | 38 +++++++++++++++++---- openstack_auth/tests/tests.py | 63 ++++++++++++++++++++++++++++------- 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/openstack_auth/plugin/base.py b/openstack_auth/plugin/base.py index 5172329..4b520b4 100644 --- a/openstack_auth/plugin/base.py +++ b/openstack_auth/plugin/base.py @@ -98,6 +98,18 @@ class BasePlugin(object): msg = _('Unable to retrieve authorized projects.') raise exceptions.KeystoneAuthException(msg) + def list_domains(self, session, auth_plugin, auth_ref=None): + try: + if self.keystone_version >= 3: + client = v3_client.Client(session=session, auth=auth_plugin) + return client.auth.domains() + else: + return [] + except (keystone_exceptions.ClientException, + keystone_exceptions.AuthorizationFailure): + msg = _('Unable to retrieve authorized domains.') + raise exceptions.KeystoneAuthException(msg) + def get_access_info(self, keystone_auth): """Get the access info from an unscoped auth @@ -190,22 +202,36 @@ class BasePlugin(object): session = utils.get_session() auth_url = unscoped_auth.auth_url - if not domain_name or utils.get_keystone_version() < 3: + if utils.get_keystone_version() < 3: return None, None + if domain_name: + domains = [domain_name] + else: + domains = self.list_domains(session, + unscoped_auth, + unscoped_auth_ref) + domains = [domain.name for domain in domains if domain.enabled] # domain support can require domain scoped tokens to perform # identity operations depending on the policy files being used # for keystone. domain_auth = None domain_auth_ref = None - try: + for domain_name in domains: token = unscoped_auth_ref.auth_token domain_auth = utils.get_token_auth_plugin( auth_url, token, domain_name=domain_name) - domain_auth_ref = domain_auth.get_access(session) - except (keystone_exceptions.ClientException, - keystone_exceptions.AuthorizationFailure): - LOG.debug('Error getting domain scoped token.', exc_info=True) + try: + domain_auth_ref = domain_auth.get_access(session) + except (keystone_exceptions.ClientException, + keystone_exceptions.AuthorizationFailure): + pass + else: + if len(domains) > 1: + LOG.info("More than one valid domain found for user %s," + " scoping to %s" % + (unscoped_auth_ref.user_id, domain_name)) + break return domain_auth, domain_auth_ref diff --git a/openstack_auth/tests/tests.py b/openstack_auth/tests/tests.py index 45adf2e..4790f73 100644 --- a/openstack_auth/tests/tests.py +++ b/openstack_auth/tests/tests.py @@ -108,8 +108,27 @@ class OpenStackAuthFederatedTestsMixin(object): client.federation.projects = self.mox.CreateMockAnything() client.federation.projects.list().AndReturn(projects) + def _mock_unscoped_list_domains(self, client, domains): + client.auth = self.mox.CreateMockAnything() + client.auth.domains().AndReturn(domains) + def _mock_unscoped_token_client(self, unscoped, auth_url=None, - client=True): + client=True, plugin=None): + if not auth_url: + auth_url = settings.OPENSTACK_KEYSTONE_URL + if unscoped and not plugin: + 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_plugin(self, unscoped, auth_url=None): if not auth_url: auth_url = settings.OPENSTACK_KEYSTONE_URL plugin = self._create_token_auth( @@ -117,16 +136,17 @@ class OpenStackAuthFederatedTestsMixin(object): 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) + plugin.auth_url = settings.OPENSTACK_KEYSTONE_URL + return plugin - def _mock_federated_client_list_projects(self, unscoped, projects): - client = self._mock_unscoped_token_client(unscoped) + def _mock_federated_client_list_projects(self, unscoped_auth, projects): + client = self._mock_unscoped_token_client(None, plugin=unscoped_auth) self._mock_unscoped_federated_list_projects(client, projects) + def _mock_federated_client_list_domains(self, unscoped_auth, domains): + client = self._mock_unscoped_token_client(None, plugin=unscoped_auth) + self._mock_unscoped_list_domains(client, domains) + class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase): @@ -885,6 +905,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, 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] + domains = [] user = self.data.user unscoped = self.data.unscoped_access_info form_data = self.get_form_data(user) @@ -925,7 +946,13 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, # 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) + sp_unscoped_auth = self._mock_plugin(sp_unscoped, + auth_url=plugin.auth_url) + client = self._mock_unscoped_token_client(None, plugin.auth_url, + plugin=sp_unscoped_auth) + self._mock_unscoped_list_domains(client, domains) + client = self._mock_unscoped_token_client(None, plugin.auth_url, + plugin=sp_unscoped_auth) self._mock_unscoped_federated_list_projects(client, sp_projects) self._mock_scoped_client_for_tenant(sp_unscoped, self.sp_data.project_one.id, @@ -961,6 +988,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, self.data = data_v3.generate_test_data(service_providers=True) keystone_provider = 'localkeystone' projects = [self.data.project_one, self.data.project_two] + domains = [] user = self.data.user unscoped = self.data.unscoped_access_info form_data = self.get_form_data(user) @@ -971,7 +999,12 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, self._mock_unscoped_token_client(unscoped, auth_url=auth_url, client=False) - client = self._mock_unscoped_token_client(unscoped, auth_url) + unscoped_auth = self._mock_plugin(unscoped) + client = self._mock_unscoped_token_client(None, auth_url=auth_url, + plugin=unscoped_auth) + self._mock_unscoped_list_domains(client, domains) + client = self._mock_unscoped_token_client(None, auth_url=auth_url, + plugin=unscoped_auth) self._mock_unscoped_list_projects(client, user, projects) self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) @@ -1154,11 +1187,14 @@ class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, def test_websso_login(self): projects = [self.data.project_one, self.data.project_two] + domains = [] unscoped = self.data.federated_unscoped_access_info token = unscoped.auth_token + unscoped_auth = self._mock_plugin(unscoped) form_data = {'token': token} - self._mock_federated_client_list_projects(unscoped, projects) + self._mock_federated_client_list_domains(unscoped_auth, domains) + self._mock_federated_client_list_projects(unscoped_auth, projects) self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) self.mox.ReplayAll() @@ -1173,11 +1209,14 @@ class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, settings.OPENSTACK_KEYSTONE_URL = 'http://auth.openstack.org:5000/v3' projects = [self.data.project_one, self.data.project_two] + domains = [] unscoped = self.data.federated_unscoped_access_info token = unscoped.auth_token + unscoped_auth = self._mock_plugin(unscoped) form_data = {'token': token} - self._mock_federated_client_list_projects(unscoped, projects) + self._mock_federated_client_list_domains(unscoped_auth, domains) + self._mock_federated_client_list_projects(unscoped_auth, projects) self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) self.mox.ReplayAll()