276 lines
11 KiB
Diff
276 lines
11 KiB
Diff
From 3e528e26f17593bb2c1a148768367f194adfa343 Mon Sep 17 00:00:00 2001
|
|
From: Kam Nasim <kam.nasim@windriver.com>
|
|
Date: Wed, 30 May 2018 11:01:33 -0400
|
|
Subject: [PATCH] Distributed Keystone for Distributed Cloud -
|
|
Horizon
|
|
|
|
In Distributed Cloud, Keystone is now running on each Subcloud.
|
|
Switching to Subcloud region now requires Openstack Auth to retrieve an
|
|
Unscoped token from the switched Region and reinitialize the django
|
|
session and cookie data with token data retrieved from the Subcloud.
|
|
|
|
Since Subcloud's Keystone doesn't contain the Identity endpoint for the
|
|
Central Region, there was no way to go back in Horizon from a subcloud
|
|
region to the SystemController region. We achieve this by caching the
|
|
SystemController endpoint in the Django Session at the time of login
|
|
---
|
|
openstack_auth/user.py | 8 ++++-
|
|
openstack_auth/utils.py | 47 ++++++++++++++++++++++++-----
|
|
openstack_auth/views.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++-
|
|
3 files changed, 123 insertions(+), 10 deletions(-)
|
|
|
|
diff --git a/openstack_auth/user.py b/openstack_auth/user.py
|
|
index f486bfa..39e3e34 100644
|
|
--- a/openstack_auth/user.py
|
|
+++ b/openstack_auth/user.py
|
|
@@ -29,6 +29,7 @@ from openstack_auth import utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
_TOKEN_HASH_ENABLED = getattr(settings, 'OPENSTACK_TOKEN_HASH_ENABLED', True)
|
|
+_DC_MODE = getattr(settings, 'DC_MODE', False)
|
|
|
|
|
|
def set_session_from_user(request, user):
|
|
@@ -387,12 +388,17 @@ class User(models.AbstractBaseUser, models.AnonymousUser):
|
|
if self.service_catalog:
|
|
for service in self.service_catalog:
|
|
service_type = service.get('type')
|
|
- if service_type is None or service_type == 'identity':
|
|
+ if service_type is None:
|
|
continue
|
|
for endpoint in service.get('endpoints', []):
|
|
region = utils.get_endpoint_region(endpoint)
|
|
if region not in regions:
|
|
regions.append(region)
|
|
+ # If we are in Distributed Cloud mode, then ensure that
|
|
+ # SystemController region is present in the Region Selection
|
|
+ if _DC_MODE and utils.DC_SYSTEMCONTROLLER_REGION not in regions:
|
|
+ regions.append(utils.DC_SYSTEMCONTROLLER_REGION)
|
|
+
|
|
return regions
|
|
|
|
def save(*args, **kwargs):
|
|
diff --git a/openstack_auth/utils.py b/openstack_auth/utils.py
|
|
index 9a55790..a2107a0 100644
|
|
--- a/openstack_auth/utils.py
|
|
+++ b/openstack_auth/utils.py
|
|
@@ -18,7 +18,6 @@ import keyring
|
|
import logging
|
|
import os
|
|
import time
|
|
-import time
|
|
import re
|
|
|
|
from django.conf import settings
|
|
@@ -40,6 +39,10 @@ LOG = logging.getLogger(__name__)
|
|
|
|
_TOKEN_TIMEOUT_MARGIN = getattr(settings, 'TOKEN_TIMEOUT_MARGIN', 0)
|
|
|
|
+# Distributed Cloud Region Definitions
|
|
+DC_SYSTEMCONTROLLER_REGION = "SystemController"
|
|
+DC_LOCAL_REGION = "RegionOne"
|
|
+
|
|
"""
|
|
We need the request object to get the user, so we'll slightly modify the
|
|
existing django.contrib.auth.get_user method. To do so we update the
|
|
@@ -346,7 +349,8 @@ def clean_up_auth_url(auth_url):
|
|
scheme, netloc, re.sub(r'/auth.*', '', path), '', ''))
|
|
|
|
|
|
-def get_token_auth_plugin(auth_url, token, project_id=None, domain_name=None):
|
|
+def get_token_auth_plugin(auth_url, token, project_id=None, project_name=None,
|
|
+ domain_name=None):
|
|
if get_keystone_version() >= 3:
|
|
if domain_name:
|
|
return v3_auth.Token(auth_url=auth_url,
|
|
@@ -354,9 +358,13 @@ def get_token_auth_plugin(auth_url, token, project_id=None, domain_name=None):
|
|
domain_name=domain_name,
|
|
reauthenticate=False)
|
|
else:
|
|
+ # If project ID is defined then use that
|
|
+ # otherwise we expect the project_name
|
|
+ # to be set
|
|
return v3_auth.Token(auth_url=auth_url,
|
|
token=token,
|
|
project_id=project_id,
|
|
+ project_name=project_name,
|
|
reauthenticate=False)
|
|
else:
|
|
return v2_auth.Token(auth_url=auth_url,
|
|
@@ -440,7 +448,7 @@ def set_response_cookie(response, cookie_name, cookie_value):
|
|
|
|
def delete_response_cookie(response, cookie_name):
|
|
""" Common function for deleting a cookie from the response.
|
|
-
|
|
+
|
|
Deletes the cookie of the given cookie_name. Fails silently
|
|
if cookie name doesn't exist
|
|
"""
|
|
@@ -459,6 +467,30 @@ def get_endpoint_region(endpoint):
|
|
return endpoint.get('region_id') or endpoint.get('region')
|
|
|
|
|
|
+def get_internal_identity_endpoints(service_catalog, region_filter=""):
|
|
+ """Retrieve Internal Identity endpoints organized by region
|
|
+ """
|
|
+ endpoint_dict = {}
|
|
+ for service in service_catalog:
|
|
+ service_type = service.get('type')
|
|
+ if service_type is None or service_type != 'identity':
|
|
+ continue
|
|
+ for endpoint in service.get('endpoints', []):
|
|
+ # only retrieve internal endpoints
|
|
+ if endpoint['interface'] != 'internal':
|
|
+ continue
|
|
+ region = get_endpoint_region(endpoint)
|
|
+ # If Region Filter is set then only retrieve
|
|
+ # that specific region
|
|
+ if region_filter and region != region_filter:
|
|
+ continue
|
|
+ if region not in endpoint_dict:
|
|
+ endpoint_dict[region] = endpoint['url']
|
|
+ if region_filter:
|
|
+ break
|
|
+ return endpoint_dict
|
|
+
|
|
+
|
|
def using_cookie_backed_sessions():
|
|
engine = getattr(settings, 'SESSION_ENGINE', '')
|
|
return "signed_cookies" in engine
|
|
@@ -514,7 +546,6 @@ def get_admin_permissions():
|
|
return {get_role_permission(role) for role in get_admin_roles()}
|
|
|
|
|
|
-
|
|
def get_client_ip(request):
|
|
"""Return client ip address using SECURE_PROXY_ADDR_HEADER variable.
|
|
If not present then consider using HTTP_X_FORWARDED_FOR from.
|
|
@@ -535,10 +566,10 @@ def get_client_ip(request):
|
|
request.META.get('REMOTE_ADDR')
|
|
)
|
|
else:
|
|
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
- if x_forwarded_for:
|
|
- return (x_forwarded_for.split(',')[0])
|
|
- else:
|
|
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
+ if x_forwarded_for:
|
|
+ return (x_forwarded_for.split(',')[0])
|
|
+ else:
|
|
return request.META.get('REMOTE_ADDR')
|
|
|
|
|
|
diff --git a/openstack_auth/views.py b/openstack_auth/views.py
|
|
index a9d84df..ad339c4 100644
|
|
--- a/openstack_auth/views.py
|
|
+++ b/openstack_auth/views.py
|
|
@@ -48,6 +48,8 @@ except AttributeError:
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
+_DC_MODE = getattr(settings, 'DC_MODE', False)
|
|
+
|
|
|
|
@sensitive_post_parameters()
|
|
@csrf_protect
|
|
@@ -130,7 +132,14 @@ def login(request, template_name=None, extra_context=None, **kwargs):
|
|
' in %s minutes') %
|
|
expiration_time).replace(':', ' Hours and ')
|
|
messages.warning(request, msg)
|
|
-
|
|
+
|
|
+ # If we are in Distributed Cloud mode, and System Controller
|
|
+ # region, then also store the Region Endpoint, as this will
|
|
+ # be used by Subcloud Region Selector (switch_region()) to go
|
|
+ # back to the Central Region
|
|
+ if _DC_MODE:
|
|
+ request.session['SystemController_endpoint'] = login_region
|
|
+
|
|
# WRS: add login user name to handle HORIZON session timeout
|
|
utils.set_response_cookie(res, 'login_user',
|
|
request.user.username)
|
|
@@ -270,6 +279,60 @@ def switch_region(request, region_name,
|
|
LOG.debug('Switching services region to %s for user "%s".'
|
|
% (region_name, request.user.username))
|
|
|
|
+ # If this is a Distributed Cloud deployment then the Region may
|
|
+ # correspond to a Subcloud, and we need to invalidate the existing
|
|
+ # token as it will no longer be valid in this Subcloud
|
|
+ if _DC_MODE:
|
|
+ region_auth_url = None
|
|
+ request.session['region_name'] = region_name
|
|
+ request.session['login_region'] = region_name
|
|
+ endpoint_dict = utils.get_internal_identity_endpoints(
|
|
+ request.user.service_catalog, region_filter=region_name)
|
|
+
|
|
+ try:
|
|
+ region_auth_url = endpoint_dict[region_name]
|
|
+ except KeyError as e:
|
|
+ # If we were on a subcloud, then the SystemController Identity
|
|
+ # endpoint will not be available, therefore retrieve it from
|
|
+ # the session (cached at login)
|
|
+ if region_name == utils.DC_SYSTEMCONTROLLER_REGION:
|
|
+ region_auth_url = request.session.get(
|
|
+ 'SystemController_endpoint', None)
|
|
+
|
|
+ if not region_auth_url:
|
|
+ msg = _('Cannot switch to subcloud %s, no Identity available '
|
|
+ 'for subcloud with the provided credentials(%s)' %
|
|
+ (region_name, request.user.username))
|
|
+ raise exceptions.KeystoneAuthException(msg)
|
|
+
|
|
+ passwordPlugin = plugin.PasswordPlugin()
|
|
+ unscoped_auth = passwordPlugin.get_plugin(
|
|
+ auth_url=region_auth_url,
|
|
+ username=request.user.username,
|
|
+ password=utils.get_user_password(request.user.username),
|
|
+ user_domain_name=request.user.user_domain_name)
|
|
+ if not unscoped_auth:
|
|
+ msg = _('Cannot switch to subcloud %s authentication backend %s '
|
|
+ 'with the provided credentials(%s)' %
|
|
+ (region_name, region_auth_url, request.user.username))
|
|
+ raise exceptions.KeystoneAuthException(msg)
|
|
+ unscoped_auth_ref = passwordPlugin.get_access_info(unscoped_auth)
|
|
+ try:
|
|
+ request.user = auth.authenticate(
|
|
+ request=request, auth_url=unscoped_auth.auth_url,
|
|
+ token=unscoped_auth_ref.auth_token)
|
|
+ except exceptions.KeystoneAuthException as exc:
|
|
+ msg = 'Switching to Subcloud failed: %s' % six.text_type(exc)
|
|
+ res = django_http.HttpResponseRedirect(settings.LOGIN_URL)
|
|
+ res.set_cookie('logout_reason', msg, max_age=10)
|
|
+ return res
|
|
+ auth_user.set_session_from_user(request, request.user)
|
|
+ request.session['_auth_user_id'] = request.user.id
|
|
+ message = (
|
|
+ _('Switch to Subcloud "%(region_name)s"'
|
|
+ 'successful.') % {'region_name': region_name})
|
|
+ messages.success(request, message)
|
|
+
|
|
redirect_to = request.GET.get(redirect_field_name, '')
|
|
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
|
redirect_to = settings.LOGIN_REDIRECT_URL
|
|
@@ -277,6 +340,19 @@ def switch_region(request, region_name,
|
|
response = shortcuts.redirect(redirect_to)
|
|
utils.set_response_cookie(response, 'services_region',
|
|
request.session['services_region'])
|
|
+
|
|
+ # If we were in Distributed Cloud mode then we need to refresh the
|
|
+ # Project list as well since Identity Service provider has changed
|
|
+ if _DC_MODE:
|
|
+ # Refresh the project list stored in the cookie, along with
|
|
+ # the project switch event
|
|
+ utils.set_response_cookie(response, 'recent_project',
|
|
+ request.user.project_id)
|
|
+ tenants = request.user.authorized_tenants
|
|
+ tenants = map(lambda x: x.to_dict(), tenants)
|
|
+ utils.delete_response_cookie(response, 'authorized_tenants')
|
|
+ utils.set_response_cookie(response, 'authorized_tenants', tenants)
|
|
+
|
|
return response
|
|
|
|
|
|
--
|
|
1.8.3.1
|
|
|