Repository: rucio/rucio

Issue:
Make m-VO compatible with WebUI
Motivation
----------

Currently m-VO isn't supported when using the WebUI, as VO cannot be selected at login and the VO is not passed by the function calls from within the WebUI.


Modification
------------

Add a cookie for VO, and add it to the headers used in function calls made by the `RucioClient`.
Add additional logic to the process of account selection so we automatically choose the VO where possible (based on the identity and optionally provided account). If the VO cannot be uniquely determined (e.g. one person has access to the account `root` at two VOs) then display a list of the appropriate VOs for them to choose from.


Discussion:

Resolving patch:
diff --git a/lib/rucio/web/ui/common/utils.py b/lib/rucio/web/ui/common/utils.py
--- a/lib/rucio/web/ui/common/utils.py
+++ b/lib/rucio/web/ui/common/utils.py
@@ -11,6 +11,7 @@
 # - Jaroslav Guenther, <jaroslav.guenther@cern.ch>, 2019-2020
 # - Eli Chadwick, <eli.chadwick@stfc.ac.uk>, 2020
 # - Benedikt Ziemons <benedikt.ziemons@cern.ch>, 2020
+# - Patrick Austin <patrick.austin@stfc.ac.uk>, 2020
 #
 # PY3K COMPATIBLE
 #
@@ -27,9 +28,10 @@
 
 from rucio import version
 from rucio.api import authentication as auth, identity
-from rucio.api.account import get_account_info, list_account_attributes
-from rucio.common.config import config_get
-from rucio.db.sqla.constants import AccountType
+from rucio.api.account import account_exists, get_account_info, list_account_attributes
+from rucio.common.config import config_get, config_get_bool
+from rucio.core import identity as identity_core, vo as vo_core
+from rucio.db.sqla.constants import AccountType, IdentityType
 
 
 if sys.version_info > (3, 0):
@@ -73,6 +75,7 @@
     except:
         AUTH_ISSUERS = []
 
+MULTI_VO = config_get_bool('common', 'multi_vo', raise_exception=False, default=False)
 
 # excluded characters for injected JavaScript variables
 VARIABLE_VALUE_REGEX = re.compile(r"^[\w\- /=,.+*#()\[\]]*$", re.UNICODE)
@@ -139,17 +142,32 @@ def __to_js(var, value):
     return '<script type="text/javascript">var %s = "%s";</script>' % (var, value)
 
 
-def select_account_name(identitystr, identity_type):
+def select_account_name(identitystr, identity_type, vo=None):
     """
-    Looks for account corresponding to the provided identity.
+    Looks for account (and VO if not known) corresponding to the provided identity.
     :param identitystr: identity string
     :param identity_type: identity_type e.g. x509, saml, oidc, userpass
-    :returns: None or account string
+    :returns: Tuple of None or account string, None or VO string or list of VO strings
     """
-    accounts = identity.list_accounts_for_identity(identitystr, identity_type)
     ui_account = None
+    if not MULTI_VO:
+        vo = 'def'
+    if vo is not None:
+        accounts = identity.list_accounts_for_identity(identitystr, identity_type)
+    else:
+        internal_accounts = identity_core.list_accounts_for_identity(identitystr, IdentityType.from_sym(identity_type))
+        accounts = [account.external for account in internal_accounts]
+        vos = [account.vo for account in internal_accounts]
+        if vos:
+            vos = list(set(vos))
+            # If we only have 1 VO that matches the identity use that, otherwise return all possible VOs so the user can choose
+            if len(vos) == 1:
+                vo = vos[0]
+            else:
+                return None, vos
+
     if len(accounts) == 0:
-        return None
+        return None, vo
     # check if ui_account param is set
     if 'ui_account' in input():
         ui_account = input()['ui_account']
@@ -158,14 +176,14 @@ def select_account_name(identitystr, identity_type):
         ui_account = input()['account']
     if ui_account:
         if ui_account not in accounts:
-            return None
+            return None, vo
         else:
-            return ui_account
+            return ui_account, vo
     else:
         # try to set the default account to the user account, if not available take the first account.
         def_account = accounts[0]
         for account in accounts:
-            account_info = get_account_info(account)
+            account_info = get_account_info(account, vo=vo)
             if account_info.account_type == AccountType.USER:
                 def_account = account
                 break
@@ -173,7 +191,7 @@ def select_account_name(identitystr, identity_type):
         if (selected_account):
             def_account = selected_account
         ui_account = def_account
-    return ui_account
+    return ui_account, vo
 
 
 def get_token(token_method, acc=None, vo=None, idt=None, pwd=None):
@@ -230,10 +248,11 @@ def access_granted(valid_token_dict, rendered_tpl=None):
     :returns: rendered base temmplate with rendered_tpl content
     """
     js_account = __to_js('account', valid_token_dict['account'])
+    js_vo = __to_js('vo', valid_token_dict['vo'])
     js_token = __to_js('token', valid_token_dict['token'])
     rucio_ui_version = version.version_string()
     policy = config_get('policy', 'permission')
-    return RENDERER.base(js_token, js_account, rucio_ui_version, policy, rendered_tpl)
+    return RENDERER.base(js_token, js_account, js_vo, MULTI_VO, rucio_ui_version, policy, rendered_tpl)
 
 
 def finalize_auth(token, identity_type, cookie_dict_extra=None):
@@ -249,7 +268,7 @@ def finalize_auth(token, identity_type, cookie_dict_extra=None):
     if not valid_token_dict:
         return RENDERER.problem("It was not possible to validate and finalize your login with the provided token.")
     try:
-        attribs = list_account_attributes(valid_token_dict['account'])
+        attribs = list_account_attributes(valid_token_dict['account'], valid_token_dict['vo'])
         accounts = identity.list_accounts_for_identity(valid_token_dict['identity'], identity_type)
         accvalues = ""
         for acc in accounts:
@@ -261,7 +280,8 @@ def finalize_auth(token, identity_type, cookie_dict_extra=None):
                        'rucio-auth-token-created-at': long(time()),
                        'rucio-available-accounts': accounts,
                        'rucio-account-attr': dumps(attribs),
-                       'rucio-selected-account': valid_token_dict['account']}
+                       'rucio-selected-account': valid_token_dict['account'],
+                       'rucio-selected-vo': valid_token_dict['vo']}
         if cookie_dict_extra and isinstance(cookie_dict_extra, dict):
             cookie_dict.update(cookie_dict_extra)
         set_cookies(cookie_dict)
@@ -270,6 +290,20 @@ def finalize_auth(token, identity_type, cookie_dict_extra=None):
         return RENDERER.problem("It was not possible to validate and finalize your login with the provided token.")
 
 
+def get_vo_descriptions(vos):
+    """
+    Gets the description for each VO in the list.
+    :param vos: List of 3 character VO strings
+    :returns: List of tuples containing VO string, VO description
+    """
+    all_vos = vo_core.list_vos()
+    vos_with_desc = []
+    for vo in all_vos:
+        if vo['vo'] in vos:
+            vos_with_desc.append((vo['vo'], vo['description']))
+    return vos_with_desc
+
+
 # AUTH_TYPE SPECIFIC METHODS FOLLOW:
 
 
@@ -285,16 +319,51 @@ def x509token_auth(data=None):
     dn = ctx.env.get('SSL_CLIENT_S_DN')
     if not dn.startswith('/'):
         dn = '/%s' % '/'.join(dn.split(',')[::-1])
+    if not MULTI_VO:
+        ui_vo = 'def'
+    elif hasattr(data, 'vo') and data.vo:
+        ui_vo = data.vo
+    else:
+        ui_vo = None
     if hasattr(data, 'account') and data.account:
         ui_account = data.account
     else:
-        ui_account = select_account_name(dn, 'x509')
-    msg = "<br><br>Your certificate (%s) is not mapped to (possibly any) rucio account: %s." % (html_escape(dn), html_escape(ui_account))
+        ui_account = None
+
+    if ui_account is None and ui_vo is None:
+        ui_account, ui_vo = select_account_name(dn, 'x509', ui_vo)
+    elif ui_account is None:
+        ui_account, _ = select_account_name(dn, 'x509', ui_vo)
+    elif ui_vo is None:
+        _, ui_vo = select_account_name(dn, 'x509', ui_vo)
+
+    # Try to eliminate VOs based on the account name (if we have one), if we still have multiple options let the user select one
+    if type(ui_vo) is list:
+        if ui_account:
+            valid_vos = []
+            for vo in ui_vo:
+                if account_exists(ui_account, vo):
+                    valid_vos.append(vo)
+            if len(valid_vos) == 0:
+                return RENDERER.problem(('<br><br>Your certificate (%s) is not mapped to (possibly any) rucio account: %s at any VO.' % (html_escape(dn), html_escape(ui_account))))
+            elif len(valid_vos) == 1:
+                ui_vo = valid_vos[0]
+            else:
+                vos_with_desc = get_vo_descriptions(valid_vos)
+                return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, vos_with_desc)
+        else:
+            vos_with_desc = get_vo_descriptions(ui_vo)
+            return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, vos_with_desc)
+
+    if MULTI_VO:
+        msg = "<br><br>Your certificate (%s) is not mapped to (possibly any) rucio account: %s at VO: %s." % (html_escape(dn), html_escape(ui_account), html_escape(ui_vo))
+    else:
+        msg = "<br><br>Your certificate (%s) is not mapped to (possibly any) rucio account: %s." % (html_escape(dn), html_escape(ui_account))
     msg += "<br><br><font color=\"red\">First, please make sure it is correctly registered in <a href=\"https://voms2.cern.ch:8443/voms/atlas\">VOMS</a> and be patient until it has been fully propagated through the system.</font>"
     msg += "<br><br>Then, if it is still not working please contact <a href=\"mailto:atlas-adc-ddm-support@cern.ch\">DDM Support</a>."
     if not ui_account:
         return RENDERER.problem(msg)
-    token = get_token(auth.get_auth_token_x509, acc=ui_account, idt=dn)
+    token = get_token(auth.get_auth_token_x509, acc=ui_account, vo=ui_vo, idt=dn)
     if not token:
         return RENDERER.problem(msg)
     return finalize_auth(token, 'x509')
@@ -303,7 +372,7 @@ def x509token_auth(data=None):
 def userpass_auth(data, rendered_tpl):
     """
     Manages login via Rucio USERPASS method.
-    :param data: data object containing account, username and password string
+    :param data: data object containing account, VO (if multi_vo), username and password string
     :param rendered_tpl: rendered page template
     :returns: final page or a page with an error message
     """
@@ -313,15 +382,55 @@ def userpass_auth(data, rendered_tpl):
         # if user tries to access a page through URL without logging in, then redirect to login page.
         if rendered_tpl:
             return RENDERER.login(None)
+        if not MULTI_VO:
+            ui_vo = 'def'
+        elif hasattr(data, 'vo') and data.vo:
+            ui_vo = data.vo
+        else:
+            ui_vo = None
         if hasattr(data, 'account') and data.account:
             ui_account = data.account
         else:
-            ui_account = select_account_name(data.username, 'userpass')
+            ui_account = None
+
+        if ui_account is None and ui_vo is None:
+            ui_account, ui_vo = select_account_name(data.username, 'userpass', ui_vo)
+        elif ui_account is None:
+            ui_account, _ = select_account_name(data.username, 'userpass', ui_vo)
+        elif ui_vo is None:
+            _, ui_vo = select_account_name(data.username, 'userpass', ui_vo)
+
+        # Try to eliminate VOs based on the account name (if we have one), if we still have multiple options let the user select one
+        if type(ui_vo) is list:
+            if ui_account:
+                valid_vos = []
+                for vo in ui_vo:
+                    if account_exists(ui_account, vo):
+                        valid_vos.append(vo)
+
+                if len(valid_vos) == 0:
+                    return RENDERER.problem(('Cannot find any Rucio account %s associated with identity %s at any VO.' % (html_escape(ui_account), html_escape(data.username))))
+                elif len(valid_vos) == 1:
+                    ui_vo = valid_vos[0]
+                else:
+                    vos_with_desc = get_vo_descriptions(valid_vos)
+                    return RENDERER.login(ui_account, None, vos_with_desc)
+            else:
+                vos_with_desc = get_vo_descriptions(ui_vo)
+                return RENDERER.login(None, None, vos_with_desc)
+
         if not ui_account:
-            return RENDERER.problem(('Cannot get find any account associated with %s identity.' % (html_escape(data.username))))
-        token = get_token(auth.get_auth_token_user_pass, acc=ui_account, idt=data.username, pwd=data.password.encode("ascii"))
+            if MULTI_VO:
+                return RENDERER.problem(('Cannot get find any account associated with %s identity at VO %s.' % (html_escape(data.username), html_escape(ui_vo))))
+            else:
+                return RENDERER.problem(('Cannot get find any account associated with %s identity.' % (html_escape(data.username))))
+        token = get_token(auth.get_auth_token_user_pass, acc=ui_account, vo=ui_vo, idt=data.username, pwd=data.password.encode("ascii"))
         if not token:
-            return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s.') % (html_escape(data.username), html_escape(ui_account)))
+            if MULTI_VO:
+                return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s at VO %s.') % (html_escape(data.username), html_escape(ui_account), html_escape(ui_vo)))
+            else:
+                return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s.') % (html_escape(data.username), html_escape(ui_account)))
+
     return finalize_auth(token, 'userpass')
 
 
@@ -337,22 +446,59 @@ def saml_auth(method, data=None):
     req = prepare_webpy_request(ctx.env, dict(input()))
     samlauth = OneLogin_Saml2_Auth(req, custom_base_path=SAML_PATH)
     saml_user_data = cookies().get('saml-user-data')
-    ui_account = None
+    if not MULTI_VO:
+        ui_vo = 'def'
+    elif hasattr(data, 'vo') and data.vo:
+        ui_vo = data.vo
+    else:
+        ui_vo = None
     if hasattr(data, 'account') and data.account:
         ui_account = data.account
+    else:
+        ui_account = None
+
     if method == "GET":
         # If user data is not present, redirect to IdP for authentication
         if not saml_user_data:
             return seeother(samlauth.login())
         # If user data is present but token is not valid, create a new one
         saml_nameid = cookies().get('saml-nameid')
+        if ui_account is None and ui_vo is None:
+            ui_account, ui_vo = select_account_name(saml_nameid, 'saml', ui_vo)
+        elif ui_account is None:
+            ui_account, _ = select_account_name(saml_nameid, 'saml', ui_vo)
+        elif ui_vo is None:
+            _, ui_vo = select_account_name(saml_nameid, 'saml', ui_vo)
+
+        # Try to eliminate VOs based on the account name (if we have one), if we still have multiple options let the user select one
+        if type(ui_vo) is list:
+            if ui_account:
+                valid_vos = []
+                for vo in ui_vo:
+                    if account_exists(ui_account, vo):
+                        valid_vos.append(vo)
+                if len(valid_vos) == 0:
+                    return RENDERER.problem(('Cannot find any Rucio account %s associated with identity %s at any VO.' % (html_escape(ui_account), html_escape(saml_nameid))))
+                elif len(valid_vos) == 1:
+                    ui_vo = valid_vos[0]
+                else:
+                    vos_with_desc = get_vo_descriptions(valid_vos)
+                    return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, vos_with_desc)
+            else:
+                vos_with_desc = get_vo_descriptions(ui_vo)
+                return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, vos_with_desc)
+
         if not ui_account:
-            ui_account = select_account_name(saml_nameid, 'saml')
-        if not ui_account:
-            return RENDERER.problem('Cannot get find any account associated with %s identity.' % (html_escape(saml_nameid)))
-        token = get_token(auth.get_auth_token_saml, acc=ui_account, idt=saml_nameid)
+            if MULTI_VO:
+                return RENDERER.problem('Cannot get find any account associated with %s identity at VO %s.' % (html_escape(saml_nameid), html_escape(ui_vo)))
+            else:
+                return RENDERER.problem('Cannot get find any account associated with %s identity.' % (html_escape(saml_nameid)))
+        token = get_token(auth.get_auth_token_saml, acc=ui_account, vo=ui_vo, idt=saml_nameid)
         if not token:
-            return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s.') % (html_escape(saml_nameid), html_escape(ui_account)))
+            if MULTI_VO:
+                return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s at VO %s.') % (html_escape(saml_nameid), html_escape(ui_account), html_escape(ui_vo)))
+            else:
+                return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s.') % (html_escape(saml_nameid), html_escape(ui_account)))
         return finalize_auth(token, 'saml')
 
     # If method is POST, check the received SAML response and redirect to home if valid
@@ -366,13 +512,42 @@ def saml_auth(method, data=None):
             cookie_extra['saml-session-index'] = samlauth.get_session_index()
             # WHY THIS ATTEMPTS TO GET A NEW TOKEN ?
             # WE SHOULD HAVE IT/GET IT FROM COOKIE OR DB AND JUST REDIRECT, NO ?
+            if ui_account is None and ui_vo is None:
+                ui_account, ui_vo = select_account_name(saml_nameid, 'saml', ui_vo)
+            elif ui_account is None:
+                ui_account, _ = select_account_name(saml_nameid, 'saml', ui_vo)
+            elif ui_vo is None:
+                _, ui_vo = select_account_name(saml_nameid, 'saml', ui_vo)
+
+            # Try to eliminate VOs based on the account name (if we have one), if we still have multiple options let the user select one
+            if type(ui_vo) is list:
+                if ui_account:
+                    valid_vos = []
+                    for vo in ui_vo:
+                        if account_exists(ui_account, vo):
+                            valid_vos.append(vo)
+                    if len(valid_vos) == 0:
+                        return RENDERER.problem(('Cannot find any Rucio account %s associated with identity %s at any VO.' % (html_escape(ui_account), html_escape(saml_nameid))))
+                    elif len(valid_vos) == 1:
+                        ui_vo = valid_vos[0]
+                    else:
+                        vos_with_desc = get_vo_descriptions(valid_vos)
+                        return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, vos_with_desc)
+                else:
+                    vos_with_desc = get_vo_descriptions(ui_vo)
+                    return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, vos_with_desc)
+
             if not ui_account:
-                ui_account = select_account_name(saml_nameid, 'saml')
-            if not ui_account:
-                return RENDERER.problem('Cannot get find any account associated with %s identity.' % (html_escape(saml_nameid)))
-            token = get_token(auth.get_auth_token_saml, acc=ui_account, idt=saml_nameid)
+                if MULTI_VO:
+                    return RENDERER.problem('Cannot get find any account associated with %s identity at VO %s.' % (html_escape(saml_nameid), html_escape(ui_vo)))
+                else:
+                    return RENDERER.problem('Cannot get find any account associated with %s identity.' % (html_escape(saml_nameid)))
+            token = get_token(auth.get_auth_token_saml, acc=ui_account, vo=ui_vo, idt=saml_nameid)
             if not token:
-                return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s.') % (html_escape(saml_nameid), html_escape(ui_account)))
+                if MULTI_VO:
+                    return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s at VO %s.') % (html_escape(saml_nameid), html_escape(ui_account), html_escape(ui_vo)))
+                else:
+                    return RENDERER.problem(('Cannot get auth token. It is possible that the presented identity %s is not mapped to any Rucio account %s.') % (html_escape(saml_nameid), html_escape(ui_account)))
             return finalize_auth(token, 'saml', cookie_extra)
 
         return RENDERER.problem("Not authenticated")
@@ -380,16 +555,30 @@ def saml_auth(method, data=None):
     return RENDERER.problem("Error while processing SAML")
 
 
-def oidc_auth(account, issuer):
+def oidc_auth(account, issuer, ui_vo=None):
     """
     Open ID Connect Login
     :param account: Rucio account string
     :param issuer: issuer key (e.g. xdc, wlcg) as in the idpsecrets.json file
+    :param ui_vo: 3 character string to identify the VO, if None will attempt to deduce it from `account`
     :returns: rendered final page or a page with error message
     """
-
     if not account:
         account = 'webui'
+    if ui_vo is None:
+        all_vos = [vo['vo'] for vo in vo_core.list_vos()]
+        valid_vos = []
+        for vo in all_vos:
+            if account_exists(account, vo):
+                valid_vos.append(vo)
+        if len(valid_vos) == 0:
+            return RENDERER.problem(('Cannot find any Rucio account %s at any VO.' % html_escape(account)))
+        elif len(valid_vos) == 1:
+            ui_vo = valid_vos[0]
+        else:
+            vos_with_desc = get_vo_descriptions(valid_vos)
+            return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, vos_with_desc)
+
     if not issuer:
         return RENDERER.problem("Please provide IdP issuer.")
     kwargs = {'audience': None,
@@ -400,7 +589,7 @@ def oidc_auth(account, issuer):
               'refresh_lifetime': None,
               'ip': None,
               'webhome': ctx.realhome + '/oidc_final'}
-    auth_url = auth.get_auth_oidc(account, **kwargs)
+    auth_url = auth.get_auth_oidc(account, vo=ui_vo, **kwargs)
     if not auth_url:
         return RENDERER.problem("It was not possible to get the OIDC authentication url from the Rucio auth server. "
                                 + "In case you provided your account name, make sure it is known to Rucio.")   # NOQA: W503
@@ -423,7 +612,7 @@ def authenticate(rendered_tpl):
 
     # login without any known server config
     if not AUTH_TYPE:
-        return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT)
+        return RENDERER.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, None)
     # for AUTH_TYPE predefined by the server continue
     else:
         if AUTH_TYPE == 'userpass':
diff --git a/lib/rucio/web/ui/main.py b/lib/rucio/web/ui/main.py
--- a/lib/rucio/web/ui/main.py
+++ b/lib/rucio/web/ui/main.py
@@ -23,6 +23,7 @@
 # - Stefan Prenner <stefan.prenner@cern.ch>, 2018
 # - Hannes Hansen <hannes.jakob.hansen@cern.ch>, 2019
 # - Ruturaj Gujar <ruturaj.gujar23@gmail.com>, 2019
+# - Patrick Austin <patrick.austin@stfc.ac.uk>, 2020
 #
 # PY3K COMPATIBLE
 
@@ -37,7 +38,7 @@
 from web import cookies
 
 from rucio.api.authentication import get_auth_token_x509
-from rucio.common.config import config_get
+from rucio.common.config import config_get, config_get_bool
 from rucio.common.utils import generate_http_error
 from rucio.web.ui.common.utils import get_token, authenticate, userpass_auth, x509token_auth, saml_auth, oidc_auth, finalize_auth, AUTH_ISSUERS, SAML_SUPPORT
 
@@ -83,6 +84,7 @@
 
 )
 
+MULTI_VO = config_get_bool('common', 'multi_vo', raise_exception=False, default=False)
 POLICY = config_get('policy', 'permission')
 
 ATLAS_URLS = ()
@@ -191,7 +193,7 @@ def GET(self):  # pylint:disable=no-self-use,invalid-name
                 raise generate_http_error(401, 'CannotAuthenticate', 'Cannot get token')
         else:
             render = template.render(join(dirname(__file__), 'templates/'))
-            return render.select_login_method(AUTH_ISSUERS, SAML_SUPPORT)
+            return render.select_login_method(AUTH_ISSUERS, SAML_SUPPORT, None)
 
 
 class X509(object):
@@ -208,7 +210,13 @@ def GET(self):  # pylint:disable=no-self-use,invalid-name
         """ GET """
         data = param_input()
         try:
-            return oidc_auth(data.account, data.issuer)
+            if not MULTI_VO:
+                ui_vo = 'def'
+            elif hasattr(data, 'vo') and data.vo:
+                ui_vo = data.vo
+            else:
+                ui_vo = None
+            return oidc_auth(data.account, data.issuer, ui_vo)
         except:
             raise generate_http_error(401, 'CannotAuthenticate', 'Cannot get token OIDC auth url from the server.')
 
@@ -354,7 +362,11 @@ def GET(self):
             account = data.account
         else:
             account = None
-        return render.login(account)
+        if hasattr(data, 'vo') and data.vo:
+            vo = data.vo
+        else:
+            vo = None
+        return render.login(account, vo, None)
 
     def POST(self):
         """ POST """


Test patch: