Make region and project sticky

This change will make the region and project "sticky" in that whatever is selected
will remain selected.  When users select other projects or login/logout the region will
stay what the user last selected, and users will try to be returned to the last used
project

Change-Id: I8b38ab2cb8b616ad6976aa8167b8209926054df4
Closes-Bug: 1357047
Closes-Bug: 1389401
This commit is contained in:
eric 2014-09-04 14:48:25 -06:00 committed by eric
parent aeab556f8b
commit 4ceb57d02b
5 changed files with 131 additions and 68 deletions

View File

@ -112,46 +112,63 @@ class KeystoneBackend(object):
self.check_auth_expiry(unscoped_auth_ref)
# Check if token is automatically scoped to default_project
# grab the project from this token, to use as a default
# if no recent_project is found in the cookie
token_proj_id = None
if unscoped_auth_ref.project_scoped:
auth_ref = unscoped_auth_ref
else:
# For now we list all the user's projects and iterate through.
token_proj_id = unscoped_auth_ref.get('project',
{}).get('id')
# We list all the user's projects
try:
if utils.get_keystone_version() < 3:
projects = client.tenants.list()
else:
client.management_url = auth_url
projects = client.projects.list(
user=unscoped_auth_ref.user_id)
except (keystone_exceptions.ClientException,
keystone_exceptions.AuthorizationFailure) as exc:
msg = _('Unable to retrieve authorized projects.')
raise exceptions.KeystoneAuthException(msg)
# Abort if there are no projects for this user
if not projects:
msg = _('You are not authorized for any projects.')
raise exceptions.KeystoneAuthException(msg)
# the recent project id a user might have set in a cookie
recent_project = None
if request:
recent_project = request.COOKIES.get('recent_project',
token_proj_id)
# if a most recent project was found, try using it first
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:
try:
if utils.get_keystone_version() < 3:
projects = client.tenants.list()
else:
client.management_url = auth_url
projects = client.projects.list(
user=unscoped_auth_ref.user_id)
client = keystone_client.Client(
tenant_id=project.id,
token=unscoped_auth_ref.auth_token,
auth_url=auth_url,
insecure=insecure,
cacert=ca_cert,
debug=settings.DEBUG)
auth_ref = client.auth_ref
break
except (keystone_exceptions.ClientException,
keystone_exceptions.AuthorizationFailure) as exc:
msg = _('Unable to retrieve authorized projects.')
raise exceptions.KeystoneAuthException(msg)
keystone_exceptions.AuthorizationFailure):
auth_ref = None
# Abort if there are no projects for this user
if not projects:
msg = _('You are not authorized for any projects.')
raise exceptions.KeystoneAuthException(msg)
while projects:
project = projects.pop()
try:
client = keystone_client.Client(
tenant_id=project.id,
token=unscoped_auth_ref.auth_token,
auth_url=auth_url,
insecure=insecure,
cacert=ca_cert,
debug=settings.DEBUG)
auth_ref = client.auth_ref
break
except (keystone_exceptions.ClientException,
keystone_exceptions.AuthorizationFailure):
auth_ref = None
if auth_ref is None:
msg = _("Unable to authenticate to any available projects.")
raise exceptions.KeystoneAuthException(msg)
if auth_ref is None:
msg = _("Unable to authenticate to any available projects.")
raise exceptions.KeystoneAuthException(msg)
# Check expiry for our new scoped token.
self.check_auth_expiry(auth_ref)

View File

@ -131,7 +131,7 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_tenants(user, tenants)
self._mock_scoped_client_for_tenant(unscoped, self.data.tenant_two.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.tenant_one.id)
self.mox.ReplayAll()
@ -157,8 +157,8 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_tenants(user, tenants)
self._mock_client_token_auth_failure(unscoped, self.data.tenant_two.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.tenant_one.id)
self._mock_client_token_auth_failure(unscoped, self.data.tenant_one.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.tenant_two.id)
self.mox.ReplayAll()
url = reverse('login')
@ -171,6 +171,14 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
response = self.client.post(url, form_data)
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
def test_login_w_bad_region_cookie(self):
self.client.cookies['services_region'] = "bad_region"
self._login()
self.assertNotEqual("bad_region",
self.client.session['services_region'])
self.assertEqual("RegionOne",
self.client.session['services_region'])
def test_no_enabled_tenants(self):
tenants = [self.data.tenant_one, self.data.tenant_two]
user = self.data.user
@ -178,8 +186,8 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_tenants(user, tenants)
self._mock_client_token_auth_failure(unscoped, self.data.tenant_two.id)
self._mock_client_token_auth_failure(unscoped, self.data.tenant_one.id)
self._mock_client_token_auth_failure(unscoped, self.data.tenant_two.id)
self.mox.ReplayAll()
url = reverse('login')
@ -289,7 +297,7 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_tenants(user, tenants)
self._mock_scoped_client_for_tenant(unscoped, self.data.tenant_two.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.tenant_one.id)
self._mock_scoped_client_for_tenant(scoped, tenant.id,
url=sc.url_for(endpoint_type=et))
self.mox.ReplayAll()
@ -332,7 +340,7 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_tenants(user, tenants)
self._mock_scoped_client_for_tenant(unscoped, self.data.tenant_two.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.tenant_one.id)
self.mox.ReplayAll()
@ -364,6 +372,7 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
self.assertEqual(self.client.session['services_region'], region)
self.assertEqual(self.client.cookies['services_region'].value, region)
def test_switch_region_with_next(self, next=None):
self.test_switch_region(next='/next_url')
@ -500,7 +509,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_projects(user, projects)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_two.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id)
self.mox.ReplayAll()
@ -522,8 +531,8 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_projects(user, projects)
self._mock_client_token_auth_failure(unscoped,
self.data.project_two.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id)
self.data.project_one.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_two.id)
self.mox.ReplayAll()
url = reverse('login')
@ -544,10 +553,10 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_projects(user, projects)
self._mock_client_token_auth_failure(unscoped,
self.data.project_two.id)
self._mock_client_token_auth_failure(unscoped,
self.data.project_one.id)
self._mock_client_token_auth_failure(unscoped,
self.data.project_two.id)
self.mox.ReplayAll()
url = reverse('login')
@ -638,7 +647,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_projects(user, projects)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_two.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id)
self._mock_scoped_client_for_tenant(
unscoped,
project.id,
@ -683,7 +692,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase):
form_data = self.get_form_data(user)
self._mock_unscoped_client_list_projects(user, projects)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_two.id)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id)
self.mox.ReplayAll()

View File

@ -35,6 +35,9 @@ def set_session_from_user(request, user):
def create_user_from_token(request, token, endpoint, services_region=None):
# if the region is provided, use that, otherwise use the preferred region
svc_region = services_region or \
utils.default_services_region(token.serviceCatalog, request)
return User(id=token.user['id'],
token=token,
user=token.user['name'],
@ -50,7 +53,7 @@ def create_user_from_token(request, token, endpoint, services_region=None):
service_catalog=token.serviceCatalog,
roles=token.roles,
endpoint=endpoint,
services_region=services_region)
services_region=svc_region)
class Token(object):
@ -180,8 +183,7 @@ class User(models.AnonymousUser):
self.project_id = project_id or tenant_id
self.project_name = project_name or tenant_name
self.service_catalog = service_catalog
self._services_region = (services_region or
self.default_services_region())
self._services_region = services_region
self.roles = roles or []
self.endpoint = endpoint
self.enabled = enabled
@ -286,19 +288,6 @@ class User(models.AnonymousUser):
def authorized_tenants(self, tenant_list):
self._authorized_tenants = tenant_list
def default_services_region(self):
"""Returns the first endpoint region for first non-identity service.
Extracted from the service catalog.
"""
if self.service_catalog:
for service in self.service_catalog:
if service['type'] == 'identity':
continue
for endpoint in service['endpoints']:
return endpoint['region']
return None
@property
def services_region(self):
return self._services_region

View File

@ -13,6 +13,7 @@
import datetime
import functools
import logging
from django.conf import settings
from django.contrib import auth
@ -25,6 +26,8 @@ from keystoneclient.v3 import client as client_v3
from six.moves.urllib import parse as urlparse
LOG = logging.getLogger(__name__)
_PROJECT_CACHE = {}
_TOKEN_TIMEOUT_MARGIN = getattr(settings, 'TOKEN_TIMEOUT_MARGIN', 0)
@ -195,3 +198,43 @@ def get_project_list(*args, **kwargs):
projects.sort(key=lambda project: project.name.lower())
return projects
def default_services_region(service_catalog, request=None):
"""Returns the first endpoint region for first non-identity service.
Extracted from the service catalog.
"""
if service_catalog:
available_regions = [endpoint['region'] for service
in service_catalog for endpoint
in service['endpoints']
if service['type'] != 'identity']
if not available_regions:
# this is very likely an incomplete keystone setup
LOG.warning('No regions could be found excluding identity.')
available_regions = [endpoint['region'] for service
in service_catalog for endpoint
in service['endpoints']]
if not available_regions:
# this is a critical problem and it's not clear how this occurs
LOG.error('No regions can be found in the service catalog.')
return None
selected_region = None
if request:
selected_region = request.COOKIES.get('services_region',
available_regions[0])
if selected_region not in available_regions:
selected_region = available_regions[0]
return selected_region
return None
def set_response_cookie(response, cookie_name, cookie_value):
"""a common policy of setting cookies for last used project
and region, can be reused in other locations.
this method will set the cookie to expire in 365 days.
"""
now = timezone.now()
expire_date = now + datetime.timedelta(days=365)
response.set_cookie(cookie_name, cookie_value, expires=expire_date)

View File

@ -10,7 +10,6 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import django
@ -196,7 +195,10 @@ def switch(request, tenant_id, redirect_field_name=auth.REDIRECT_FIELD_NAME):
user = auth_user.create_user_from_token(
request, auth_user.Token(auth_ref), endpoint)
auth_user.set_session_from_user(request, user)
return shortcuts.redirect(redirect_to)
response = shortcuts.redirect(redirect_to)
utils.set_response_cookie(response, 'recent_project',
request.user.project_id)
return response
@login_required
@ -216,4 +218,7 @@ def switch_region(request, region_name,
if not is_safe_url(url=redirect_to, host=request.get_host()):
redirect_to = settings.LOGIN_REDIRECT_URL
return shortcuts.redirect(redirect_to)
response = shortcuts.redirect(redirect_to)
utils.set_response_cookie(response, 'services_region',
request.session['services_region'])
return response