From 597e6d79b40d7e063446f8fe9b265390d76b69ce Mon Sep 17 00:00:00 2001 From: Elvin Tubillara Date: Tue, 27 Dec 2016 15:39:25 -0600 Subject: [PATCH] Refactor project and domain scoping Moves the project and domain logic into their own functions inside the plugin object. Change-Id: I3aa026364443220c9b3fa38ec306fed4d9e878cc --- openstack_auth/backend.py | 106 +++++++------------------------ openstack_auth/plugin/base.py | 113 ++++++++++++++++++++++++++++++++++ openstack_auth/tests/tests.py | 2 + 3 files changed, 136 insertions(+), 85 deletions(-) diff --git a/openstack_auth/backend.py b/openstack_auth/backend.py index 9509f45c..9ada58d1 100644 --- a/openstack_auth/backend.py +++ b/openstack_auth/backend.py @@ -20,7 +20,6 @@ import pytz from django.conf import settings from django.utils.module_loading import import_string # noqa from django.utils.translation import ugettext_lazy as _ -from keystoneauth1 import exceptions as keystone_exceptions from openstack_auth import exceptions from openstack_auth import user as auth_user @@ -110,51 +109,23 @@ class KeystoneBackend(object): 'configuration error that should be addressed.') raise exceptions.KeystoneAuthException(msg) - session = utils.get_session() - keystone_client_class = utils.get_keystone_client().Client - - try: - unscoped_auth_ref = unscoped_auth.get_access(session) - except keystone_exceptions.ConnectFailure as exc: - LOG.error(str(exc)) - msg = _('Unable to establish connection to keystone endpoint.') - raise exceptions.KeystoneAuthException(msg) - except (keystone_exceptions.Unauthorized, - keystone_exceptions.Forbidden, - keystone_exceptions.NotFound) as exc: - LOG.debug(str(exc)) - raise exceptions.KeystoneAuthException(_('Invalid credentials.')) - except (keystone_exceptions.ClientException, - keystone_exceptions.AuthorizationFailure) as exc: - msg = _("An error occurred authenticating. " - "Please try again later.") - LOG.debug(str(exc)) - raise exceptions.KeystoneAuthException(msg) + # the recent project id a user might have set in a cookie + recent_project = None + request = kwargs.get('request') + if request: + # Grab recent_project found in the cookie, try to scope + # to the last project used. + recent_project = request.COOKIES.get('recent_project') + unscoped_auth_ref = plugin.get_access_info(unscoped_auth) # Check expiry for our unscoped auth ref. self.check_auth_expiry(unscoped_auth_ref) - # 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 - if utils.get_keystone_version() >= 3 and 'user_domain_name' in kwargs: - try: - token = unscoped_auth_ref.auth_token - domain_auth = utils.get_token_auth_plugin( - auth_url, - token, - domain_name=kwargs['user_domain_name']) - domain_auth_ref = domain_auth.get_access(session) - except Exception: - LOG.debug('Error getting domain scoped token.', exc_info=True) - - projects = plugin.list_projects(session, - unscoped_auth, - unscoped_auth_ref) - # Attempt to scope only to enabled projects - projects = [project for project in projects if project.enabled] + domain_name = kwargs.get('user_domain_name', None) + domain_auth, domain_auth_ref = plugin.get_domain_scoped_auth( + unscoped_auth, unscoped_auth_ref, domain_name) + scoped_auth, scoped_auth_ref = plugin.get_project_scoped_auth( + unscoped_auth, unscoped_auth_ref, recent_project=recent_project) # Abort if there are no projects for this user and a valid domain # token has not been obtained @@ -171,54 +142,17 @@ class KeystoneBackend(object): # token) # 3) user cannot obtain a domain scoped token, but can # obtain a project scoped token - if not projects and not domain_auth_ref: + if not scoped_auth_ref and domain_auth_ref: + # if the user can't obtain a project scoped token, set the scoped + # token to be the domain token, if valid + scoped_auth = domain_auth + scoped_auth_ref = domain_auth_ref + elif not scoped_auth_ref and not domain_auth_ref: msg = _('You are not authorized for any projects.') if utils.get_keystone_version() >= 3: msg = _('You are not authorized for any projects or domains.') raise exceptions.KeystoneAuthException(msg) - # the recent project id a user might have set in a cookie - recent_project = None - request = kwargs.get('request') - - if request: - # Grab recent_project found in the cookie, try to scope - # to the last project used. - recent_project = request.COOKIES.get('recent_project') - - # if a most recent project was found, try using it first - if recent_project: - for pos, project in enumerate(projects): - if project.id == recent_project: - # move recent project to the beginning - projects.pop(pos) - projects.insert(0, project) - break - - for project in projects: - token = unscoped_auth_ref.auth_token - scoped_auth = utils.get_token_auth_plugin(auth_url, - token=token, - project_id=project.id) - - try: - scoped_auth_ref = scoped_auth.get_access(session) - except (keystone_exceptions.ClientException, - keystone_exceptions.AuthorizationFailure): - pass - else: - break - else: - # if the user can't obtain a project scoped token, set the scoped - # token to be the domain token, if valid - if domain_auth_ref: - scoped_auth = domain_auth - scoped_auth_ref = domain_auth_ref - else: - # if no domain or project token for user, abort - msg = _("Unable to authenticate to any available projects.") - raise exceptions.KeystoneAuthException(msg) - # Check expiry for our new scoped token. self.check_auth_expiry(scoped_auth_ref) @@ -276,6 +210,8 @@ class KeystoneBackend(object): session_time = min(timeout, int(token_life.total_seconds())) request.session.set_expiry(session_time) + keystone_client_class = utils.get_keystone_client().Client + session = utils.get_session() scoped_client = keystone_client_class(session=session, auth=scoped_auth) diff --git a/openstack_auth/plugin/base.py b/openstack_auth/plugin/base.py index da87365c..71704217 100644 --- a/openstack_auth/plugin/base.py +++ b/openstack_auth/plugin/base.py @@ -11,6 +11,7 @@ # under the License. import abc +import logging from django.utils.translation import ugettext_lazy as _ from keystoneauth1 import exceptions as keystone_exceptions @@ -21,6 +22,7 @@ import six from openstack_auth import exceptions from openstack_auth import utils +LOG = logging.getLogger(__name__) __all__ = ['BasePlugin'] @@ -95,3 +97,114 @@ class BasePlugin(object): keystone_exceptions.AuthorizationFailure): msg = _('Unable to retrieve authorized projects.') raise exceptions.KeystoneAuthException(msg) + + def get_access_info(self, keystone_auth): + """Get the access info from an unscoped auth + + This function provides the base functionality that the + plugins will use to authenticate and get the access info object. + + :param keystone_auth: keystoneauth1 identity plugin + :raises: exceptions.KeystoneAuthException on auth failure + :returns: keystoneclient.access.AccessInfo + """ + session = utils.get_session() + + try: + unscoped_auth_ref = keystone_auth.get_access(session) + except keystone_exceptions.ConnectFailure as exc: + LOG.error(str(exc)) + msg = _('Unable to establish connection to keystone endpoint.') + raise exceptions.KeystoneAuthException(msg) + except (keystone_exceptions.Unauthorized, + keystone_exceptions.Forbidden, + keystone_exceptions.NotFound) as exc: + LOG.debug(str(exc)) + raise exceptions.KeystoneAuthException(_('Invalid credentials.')) + except (keystone_exceptions.ClientException, + keystone_exceptions.AuthorizationFailure) as exc: + msg = _("An error occurred authenticating. " + "Please try again later.") + LOG.debug(str(exc)) + raise exceptions.KeystoneAuthException(msg) + return unscoped_auth_ref + + def get_project_scoped_auth(self, unscoped_auth, unscoped_auth_ref, + recent_project=None): + """Get the project scoped keystone auth and access info + + This function returns a project scoped keystone token plugin + and AccessInfo object. + + :param unscoped_auth: keystone auth plugin + :param unscoped_auth_ref: keystoneclient.access.AccessInfo` or None. + :param recent_project: project that we should try to scope to + :return: keystone token auth plugin, AccessInfo object + """ + auth_url = unscoped_auth.auth_url + session = utils.get_session() + + projects = self.list_projects( + session, unscoped_auth, unscoped_auth_ref) + # Attempt to scope only to enabled projects + projects = [project for project in projects if project.enabled] + + # if a most recent project was found, try using it first + if recent_project: + for pos, project in enumerate(projects): + if project.id == recent_project: + # move recent project to the beginning + projects.pop(pos) + projects.insert(0, project) + break + + scoped_auth = None + scoped_auth_ref = None + for project in projects: + token = unscoped_auth_ref.auth_token + scoped_auth = utils.get_token_auth_plugin(auth_url, + token=token, + project_id=project.id) + try: + scoped_auth_ref = scoped_auth.get_access(session) + except (keystone_exceptions.ClientException, + keystone_exceptions.AuthorizationFailure): + pass + else: + break + + return scoped_auth, scoped_auth_ref + + def get_domain_scoped_auth(self, unscoped_auth, unscoped_auth_ref, + domain_name=None): + """Get the domain scoped keystone auth and access info + + This function returns a domain scoped keystone token plugin + and AccessInfo object. + + :param unscoped_auth: keystone auth plugin + :param unscoped_auth_ref: keystoneclient.access.AccessInfo` or None. + :param domain_name: domain that we should try to scope to + :return: keystone token auth plugin, AccessInfo object + """ + session = utils.get_session() + auth_url = unscoped_auth.auth_url + + if not domain_name or utils.get_keystone_version() < 3: + return None, None + + # 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: + 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 Exception: + LOG.debug('Error getting domain scoped token.', exc_info=True) + return domain_auth, domain_auth_ref diff --git a/openstack_auth/tests/tests.py b/openstack_auth/tests/tests.py index f2dbe557..c58aacc0 100644 --- a/openstack_auth/tests/tests.py +++ b/openstack_auth/tests/tests.py @@ -55,6 +55,7 @@ class OpenStackAuthTestsMixin(object): plugin = self._create_password_auth() plugin.get_access(mox.IsA(session.Session)). \ AndReturn(self.data.unscoped_access_info) + plugin.auth_url = settings.OPENSTACK_KEYSTONE_URL return self.ks_client_module.Client(session=mox.IsA(session.Session), auth=plugin) @@ -810,6 +811,7 @@ class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, test.TestCase): 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)