Repository: getindata/kedro-kubeflow
Pull request #185 (2022-09-21 07:11:02)

Support latest changes in Dex login screens
#### Description

Latest kubeflow (1.6) changed Dex login screens and broke obtaining Dex authservice session. This PR accommodates the new Dex login flow.

##### PR Checklist
- [ ] Tests added
- [ ] [Changelog](CHANGELOG.md) updated 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6b52b5..d1551c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## [Unreleased]
 
+-   Support latest changes in Dex login screens
 -   Improvements in build process (migrated build tool to poetry, quality gate switched to sonarcloud)
 
 ## [0.7.0] - 2022-08-22
diff --git a/kedro_kubeflow/auth.py b/kedro_kubeflow/auth.py
index 36ad561..5bf3969 100644
--- a/kedro_kubeflow/auth.py
+++ b/kedro_kubeflow/auth.py
@@ -1,3 +1,4 @@
+import html
 import logging
 import os
 import re
@@ -49,6 +50,12 @@ class AuthHandler(object):
         finally:
             return jwt_token
 
+    # DEX supports Resource Owner Password Credentials Grant only for LDAP connectors.
+    # And it doesn't work for Open ID Connect, e.g. Google (Moreover,
+    # Google doesn't support such a flow at all).
+    # Here is a hacky workaround - we imitate a web browser to navigate to the login page and
+    # proceed with 'Log in with Email'. It does work,
+    # but it is prone to errors if anything substantial changes in the way DEX handles login screens.
     def obtain_dex_authservice_session(self, kfp_api):
         if DEX_USERNAME not in os.environ or DEX_PASSWORD not in os.environ:
             self.log.debug(
@@ -57,21 +64,34 @@ class AuthHandler(object):
             return None
 
         s = requests.Session()
+        kfp_url_parts = urlsplit(kfp_api)
+
+        def extract_url(regexp, response: str):
+            form_url = re.search(regexp, response)
+
+            if form_url:
+                return urlunsplit(
+                    [
+                        kfp_url_parts.scheme,
+                        kfp_url_parts.netloc,
+                        html.unescape(form_url.group(0)),
+                        None,
+                        None,
+                    ]
+                )
+            return None
+
         r = s.get(kfp_api)
-        form_relative_url = re.search(
-            '/dex/auth/local\\?req=([^"]*)', r.text
-        ).group(0)
 
-        kfp_url_parts = urlsplit(kfp_api)
-        form_absolute_url = urlunsplit(
-            [
-                kfp_url_parts.scheme,
-                kfp_url_parts.netloc,
-                form_relative_url,
-                None,
-                None,
-            ]
-        )
+        login_url = extract_url('/dex/auth/local\\?req=[^"]*', r.text)
+
+        if not login_url:
+
+            login_form_url = extract_url('/dex/auth/local\\?[^"]*', r.text)
+
+            r = s.get(login_form_url)
+
+            login_url = extract_url('/dex/auth/local/login\\?[^"]*', r.text)
 
         headers = {
             "Content-Type": "application/x-www-form-urlencoded",
@@ -81,5 +101,5 @@ class AuthHandler(object):
             "password": os.environ[DEX_PASSWORD],
         }
 
-        s.post(form_absolute_url, headers=headers, data=data)
+        s.post(login_url, headers=headers, data=data)
         return s.cookies.get_dict()["authservice_session"]


Test patch:
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 8f140db..34be392 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -113,3 +113,37 @@ class TestAuthHandler(unittest.TestCase):
             responses.calls[1].request.body
             == "login=user%40example.com&password=pa%24%24"
         )
+
+    @responses.activate
+    def test_should_get_cookie_from_dex_secured_system_with_kubeflow_1_6(self):
+        # given
+        os.environ["DEX_USERNAME"] = "user@example.com"
+        os.environ["DEX_PASSWORD"] = "pa$$"
+        responses.add(
+            responses.GET,
+            "https://kubeflow.local/pipeline",
+            body='<a href="/dex/auth/local?client_id=kubeflow-oidc-authservice&amp;redirect_uri=%2Flogin%2Foidc&amp;response_type=code&amp;scope=profile&#43;email&#43;groups&#43;openid&amp;state=MTY2MzY3Njk0OHxFd3dBRUZVeGFtdEhaR2RPYVZsRE5XWmpTbkU9fNHbrYsoZfJQGtNhqLlcn_B7MiGljHAAOUoyvZD-53Bg" target="_self">',  # noqa: E501
+        )
+        responses.add(
+            responses.GET,
+            "https://kubeflow.local/dex/auth/local?client_id=kubeflow-oidc-authservice&redirect_uri=%2Flogin%2Foidc&response_type=code&scope=profile+email+groups+openid&state=MTY2MzY3Njk0OHxFd3dBRUZVeGFtdEhaR2RPYVZsRE5XWmpTbkU9fNHbrYsoZfJQGtNhqLlcn_B7MiGljHAAOUoyvZD-53Bg",  # noqa: E501
+            body='<a href="/dex/auth/local/login?back=%2Fdex%2Fauth%3Fclient_id%3Dkubeflow-oidc-authservice%26redirect_uri%3D%252Flogin%252Foidc%26response_type%3Dcode%26scope%3Dprofile%2Bemail%2Bgroups%2Bopenid%26state%3DMTY2MzY3Njk0OHxFd3dBRUZVeGFtdEhaR2RPYVZsRE5XWmpTbkU9fNHbrYsoZfJQGtNhqLlcn_B7MiGljHAAOUoyvZD-53Bg&amp;state=o4odhdlneub2iczv2657sa2gv" target="_self">',  # noqa: E501
+        )
+
+        responses.add(
+            responses.POST,
+            "https://kubeflow.local/dex/auth/local/login?back=%2Fdex%2Fauth%3Fclient_id%3Dkubeflow-oidc-authservice%26redirect_uri%3D%252Flogin%252Foidc%26response_type%3Dcode%26scope%3Dprofile%2Bemail%2Bgroups%2Bopenid%26state%3DMTY2MzY3Njk0OHxFd3dBRUZVeGFtdEhaR2RPYVZsRE5XWmpTbkU9fNHbrYsoZfJQGtNhqLlcn_B7MiGljHAAOUoyvZD-53Bg&state=o4odhdlneub2iczv2657sa2gv",  # noqa: E501
+            headers={"Set-cookie": "authservice_session=sessionID"},
+        )
+
+        # when
+        session = AuthHandler().obtain_dex_authservice_session(
+            "https://kubeflow.local/pipeline"
+        )
+
+        # then
+        assert session == "sessionID"
+        assert (
+            responses.calls[2].request.body
+            == "login=user%40example.com&password=pa%24%24"
+        )