diff --git a/openstack_auth/backend.py b/openstack_auth/backend.py index c630fb0a..29ec87e0 100644 --- a/openstack_auth/backend.py +++ b/openstack_auth/backend.py @@ -129,15 +129,47 @@ class KeystoneBackend(object): # 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] - # Abort if there are no projects for this user - if not projects: + # Abort if there are no projects for this user and a valid domain + # token has not been obtained + # + # The valid use cases for a user login are: + # Keystone v2: user must have a role on a project and be able + # to obtain a project scoped token + # Keystone v3: 1) user can obtain a domain scoped token (user + # has a role on the domain they authenticated to), + # only, no roles on a project + # 2) user can obtain a domain scoped token and has + # a role on a project in the domain they + # authenticated to (and can obtain a project scoped + # token) + # 3) user cannot obtain a domain scoped token, but can + # obtain a project scoped token + if not projects 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 @@ -172,8 +204,15 @@ class KeystoneBackend(object): else: break else: - msg = _("Unable to authenticate to any available projects.") - raise exceptions.KeystoneAuthException(msg) + # 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) @@ -189,6 +228,19 @@ class KeystoneBackend(object): if request is not None: request.session['unscoped_token'] = unscoped_token + if domain_auth_ref: + # check django session engine, if using cookies, this will not + # work, as it will overflow the cookie so don't add domain + # scoped token to the session and put error in the log + if utils.using_cookie_backed_sessions(): + LOG.error('Using signed cookies as SESSION_ENGINE with ' + 'OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT is ' + 'enabled. This disables the ability to ' + 'perform identity operations due to cookie size ' + 'constraints.') + else: + request.session['domain_token'] = domain_auth_ref + request.user = user timeout = getattr(settings, "SESSION_TIMEOUT", 3600) token_life = user.token.expires - datetime.datetime.now(pytz.utc) diff --git a/openstack_auth/tests/data_v3.py b/openstack_auth/tests/data_v3.py index 9018561c..73d16cc6 100644 --- a/openstack_auth/tests/data_v3.py +++ b/openstack_auth/tests/data_v3.py @@ -216,6 +216,31 @@ def generate_test_data(): body=scoped_token_dict ) + domain_token_dict = { + 'token': { + 'methods': ['password'], + 'expires_at': expiration, + 'domain': { + 'id': domain_dict['id'], + 'name': domain_dict['name'], + }, + 'user': { + 'id': user_dict['id'], + 'name': user_dict['name'], + 'domain': { + 'id': domain_dict['id'], + 'name': domain_dict['name'] + } + }, + 'roles': [role_dict], + 'catalog': [keystone_service, nova_service] + } + } + test_data.domain_scoped_access_info = access.AccessInfo.factory( + resp=auth_response, + body=domain_token_dict + ) + unscoped_token_dict = { 'token': { 'methods': ['password'], diff --git a/openstack_auth/tests/tests.py b/openstack_auth/tests/tests.py index 09014291..95928f88 100644 --- a/openstack_auth/tests/tests.py +++ b/openstack_auth/tests/tests.py @@ -485,6 +485,32 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): client.projects.list(user=user.id).AndRaise( keystone_exceptions.AuthorizationFailure) + def _mock_unscoped_and_domain_list_projects(self, user, projects): + client = self._mock_unscoped_client(user) + self._mock_scoped_for_domain(projects) + self._mock_unscoped_list_projects(client, user, projects) + + def _mock_scoped_for_domain(self, projects): + url = settings.OPENSTACK_KEYSTONE_URL + + plugin = self._create_token_auth( + project_id=None, + domain_name=DEFAULT_DOMAIN, + token=self.data.unscoped_access_info.auth_token, + url=url) + + plugin.get_access(mox.IsA(session.Session)).AndReturn( + self.data.domain_scoped_access_info) + + # if no projects or no enabled projects for user, but domain scoped + # token client auth gets set to domain scoped auth otherwise it's set + # to the project scoped auth and that happens in a different mock + enabled_projects = [project for project in projects if project.enabled] + if not projects or not enabled_projects: + return self.ks_client_module.Client( + session=mox.IsA(session.Session), + auth=plugin) + def _create_password_auth(self, username=None, password=None, url=None): if not username: username = self.data.user.name @@ -501,17 +527,24 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): user_domain_name=DEFAULT_DOMAIN, unscoped=True) - def _create_token_auth(self, project_id, token=None, url=None): + def _create_token_auth(self, project_id, token=None, url=None, + domain_name=None): if not token: token = self.data.unscoped_access_info.auth_token if not url: url = settings.OPENSTACK_KEYSTONE_URL - return auth_v3.Token(auth_url=url, - token=token, - project_id=project_id, - reauthenticate=False) + if domain_name: + return auth_v3.Token(auth_url=url, + token=token, + domain_name=domain_name, + reauthenticate=False) + else: + return auth_v3.Token(auth_url=url, + token=token, + project_id=project_id, + reauthenticate=False) def setUp(self): super(OpenStackAuthTestsV3, self).setUp() @@ -527,7 +560,6 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): self.data = data_v3.generate_test_data() self.ks_client_module = client_v3 - settings.OPENSTACK_API_VERSIONS['identity'] = 3 settings.OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v3" @@ -542,7 +574,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): unscoped = self.data.unscoped_access_info form_data = self.get_form_data(user) - self._mock_unscoped_client_list_projects(user, projects) + self._mock_unscoped_and_domain_list_projects(user, projects) self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) self.mox.ReplayAll() @@ -565,7 +597,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): unscoped = self.data.unscoped_access_info form_data = self.get_form_data(user) - self._mock_unscoped_client_list_projects(user, projects) + self._mock_unscoped_and_domain_list_projects(user, projects) self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id) self.mox.ReplayAll() @@ -585,7 +617,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): form_data = self.get_form_data(user) - self._mock_unscoped_client_list_projects(user, projects) + self._mock_unscoped_and_domain_list_projects(user, projects) self.mox.ReplayAll() url = reverse('login') @@ -596,15 +628,13 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): # POST to the page to log in. response = self.client.post(url, form_data) - self.assertTemplateUsed(response, 'auth/login.html') - self.assertContains(response, - 'You are not authorized for any projects.') + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) def test_no_projects(self): user = self.data.user - form_data = self.get_form_data(user) - self._mock_unscoped_client_list_projects(user, []) + + self._mock_unscoped_and_domain_list_projects(user, []) self.mox.ReplayAll() url = reverse('login') @@ -615,9 +645,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): # POST to the page to log in. response = self.client.post(url, form_data) - self.assertTemplateUsed(response, 'auth/login.html') - self.assertContains(response, - 'You are not authorized for any projects.') + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL) def test_fail_projects(self): user = self.data.user @@ -692,7 +720,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): form_data = self.get_form_data(user) - self._mock_unscoped_client_list_projects(user, projects) + self._mock_unscoped_and_domain_list_projects(user, projects) self._mock_scoped_client_for_tenant(scoped, self.data.project_one.id) self._mock_scoped_client_for_tenant( scoped, @@ -738,7 +766,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase): sc = self.data.service_catalog form_data = self.get_form_data(user) - self._mock_unscoped_client_list_projects(user, projects) + self._mock_unscoped_and_domain_list_projects(user, projects) self._mock_scoped_client_for_tenant(scoped, self.data.project_one.id) self.mox.ReplayAll() diff --git a/openstack_auth/utils.py b/openstack_auth/utils.py index 1bc212ed..6f3e8d9f 100644 --- a/openstack_auth/utils.py +++ b/openstack_auth/utils.py @@ -298,13 +298,18 @@ def fix_auth_url_version(auth_url): return auth_url -def get_token_auth_plugin(auth_url, token, project_id=None): +def get_token_auth_plugin(auth_url, token, project_id=None, domain_name=None): if get_keystone_version() >= 3: - return v3_auth.Token(auth_url=auth_url, - token=token, - project_id=project_id, - reauthenticate=False) - + if domain_name: + return v3_auth.Token(auth_url=auth_url, + token=token, + domain_name=domain_name, + reauthenticate=False) + else: + return v3_auth.Token(auth_url=auth_url, + token=token, + project_id=project_id, + reauthenticate=False) else: return v2_auth.Token(auth_url=auth_url, token=token, @@ -388,3 +393,8 @@ def get_endpoint_region(endpoint): Keystone V2 and V3. """ return endpoint.get('region_id') or endpoint.get('region') + + +def using_cookie_backed_sessions(): + engine = getattr(settings, 'SESSION_ENGINE', '') + return "signed_cookies" in engine diff --git a/openstack_auth/views.py b/openstack_auth/views.py index 52f2d482..65feccba 100644 --- a/openstack_auth/views.py +++ b/openstack_auth/views.py @@ -165,9 +165,17 @@ def logout(request, login_url=None, **kwargs): {'username': request.user.username} LOG.info(msg) endpoint = request.session.get('region_endpoint') + + # delete the project scoped token token = request.session.get('token') if token and endpoint: delete_token(endpoint=endpoint, token_id=token.id) + + # delete the domain scoped token if set + domain_token = request.session.get('domain_token') + if domain_token and endpoint: + delete_token(endpoint=endpoint, token_id=domain_token.auth_token) + """ Securely logs a user out. """ return django_auth_views.logout_then_login(request, login_url=login_url, **kwargs)