From 1a7e61ed51a54f166572c462e7b47797aa0bc95c Mon Sep 17 00:00:00 2001 From: Kam Nasim Date: Wed, 30 May 2018 13:26:35 -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. Story: 2002842 Task: 22785 Change-Id: I274e8b0756e0f24321a108c6c1a0a5d6178e0c7a Signed-off-by: Jack Ding --- .../0006-meta-Distributed-Keystone.patch | 24 ++ .../centos/meta_patches/PATCH_ORDER | 1 + .../patches/0004-Distributed-Keystone.patch | 293 ++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 openstack/python-django-openstack-auth/centos/meta_patches/0006-meta-Distributed-Keystone.patch create mode 100644 openstack/python-django-openstack-auth/centos/patches/0004-Distributed-Keystone.patch diff --git a/openstack/python-django-openstack-auth/centos/meta_patches/0006-meta-Distributed-Keystone.patch b/openstack/python-django-openstack-auth/centos/meta_patches/0006-meta-Distributed-Keystone.patch new file mode 100644 index 00000000..247adeab --- /dev/null +++ b/openstack/python-django-openstack-auth/centos/meta_patches/0006-meta-Distributed-Keystone.patch @@ -0,0 +1,24 @@ +From 186e684a33b3db1c5a5fc010cdb63b459351fcb7 Mon Sep 17 00:00:00 2001 +From: Kam Nasim +Date: Wed, 30 May 2018 11:28:49 -0400 +Subject: [PATCH] meta patch for distributed keystone + +--- + SPECS/python-django-openstack-auth.spec | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/SPECS/python-django-openstack-auth.spec b/SPECS/python-django-openstack-auth.spec +index 21e4bde..f925d87 100644 +--- a/SPECS/python-django-openstack-auth.spec ++++ b/SPECS/python-django-openstack-auth.spec +@@ -18,6 +18,7 @@ Source0: https://tarballs.openstack.org/django_openstack_auth/django_open + Patch0001: 0001-Pike-rebase-for-openstack-auth.patch + Patch0002: 0002-disable-token-validation-per-auth-request.patch + Patch0003: 0003-cache-authorized-tenants-in-cookie-to-improve-performance.patch ++Patch0004: 0004-Distributed-Keystone.patch + + BuildArch: noarch + +-- +1.8.3.1 + diff --git a/openstack/python-django-openstack-auth/centos/meta_patches/PATCH_ORDER b/openstack/python-django-openstack-auth/centos/meta_patches/PATCH_ORDER index 9df29a6a..3993db44 100644 --- a/openstack/python-django-openstack-auth/centos/meta_patches/PATCH_ORDER +++ b/openstack/python-django-openstack-auth/centos/meta_patches/PATCH_ORDER @@ -3,3 +3,4 @@ 0003-meta-roll-in-TIS-patches.patch 0004-meta-disable-token-validation-per-auth-req.patch 0005-meta-cache-authorized-tenants-in-cookie-to-improve-performance.patch +0006-meta-Distributed-Keystone.patch diff --git a/openstack/python-django-openstack-auth/centos/patches/0004-Distributed-Keystone.patch b/openstack/python-django-openstack-auth/centos/patches/0004-Distributed-Keystone.patch new file mode 100644 index 00000000..3ee927d8 --- /dev/null +++ b/openstack/python-django-openstack-auth/centos/patches/0004-Distributed-Keystone.patch @@ -0,0 +1,293 @@ +From 3e528e26f17593bb2c1a148768367f194adfa343 Mon Sep 17 00:00:00 2001 +From: Kam Nasim +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/forms.py | 4 +-- + openstack_auth/user.py | 8 ++++- + openstack_auth/utils.py | 47 ++++++++++++++++++++++++----- + openstack_auth/views.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++- + 4 files changed, 125 insertions(+), 12 deletions(-) + +diff --git a/openstack_auth/forms.py b/openstack_auth/forms.py +index 90e281b..4834ab2 100644 +--- a/openstack_auth/forms.py ++++ b/openstack_auth/forms.py +@@ -153,10 +153,10 @@ class Login(django_auth_forms.AuthenticationForm): + # since this user logged in successfully, clear its + # lockout status + utils.clear_user_lockout(username) +- ++ + # handle user login + utils.handle_user_login(username, password) +- ++ + except exceptions.KeystoneAuthException as exc: + if getattr(exc,"invalidCredentials", False): + msg = 'Login failed for user "%(username)s", remote address '\ +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 +