Add domain scoped token to session in multidomain

In order to perform identity operations in keystone v3 when the v3
policy file is used, a domain scoped token is required. Adding the
domain scoped token to the session as it remains valid until the user
logs out.

The domain scoped token is sizeable, so a check to make sure the
session backend used is not signed cookies, as this will overflow
the cookie.

Additionally, errors around getting and storing the domain scoped
token are logged, but doesn't block authentication, as it only blocks
identity operations.

A call to delete the domain token is made on logout.

Support for the case of a user with a domain role but no project roles
is now supported as well. That is a user can log in with only scoping
to a domain. This allows domain admins to be able to configure identity
without requiring a project role.

Implements: blueprint domain-scoped-tokens
Change-Id: I0ed1737cdd80dc143f1df94700e311351d5d3b24
This commit is contained in:
David Lyle 2014-12-10 11:16:38 -07:00 committed by Dan Nguyen
parent a4496c8cb7
commit 517de5f664
5 changed files with 152 additions and 29 deletions

View File

@ -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)

View File

@ -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'],

View File

@ -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()

View File

@ -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

View File

@ -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)