From a4ba4df7a6728f6c31976d335587698583f5e2d0 Mon Sep 17 00:00:00 2001 From: Rashmil Panchani <32737711+Rashmil-1999@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:13:06 -0500 Subject: [PATCH 1/3] Remove uid dependency (#120) * remove uid dependency * debug statements * remove comments and debug statements * add sqlite3 install command in dockerfile for lab * edit command * edit command --- CHANGELOG.md | 5 + authenticator/customauthenticator/custom.py | 148 +++++++++++--------- 2 files changed, 90 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 405a6b6..8c528a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Changed +- UID logic. We will no longer depend upon the uid parameter from LDAP in JWT token. [#119](https://github.com/IN-CORE/incore-lab/issues/119) + ## [1.7.0] - 2024-06-12 ### Changed - IN-CORE relase for 5.4.0 with pyincore 1.19.0 [#116](https://github.com/IN-CORE/incore-lab/issues/116) diff --git a/authenticator/customauthenticator/custom.py b/authenticator/customauthenticator/custom.py index d33fa48..c62a2a2 100644 --- a/authenticator/customauthenticator/custom.py +++ b/authenticator/customauthenticator/custom.py @@ -10,93 +10,93 @@ from tornado import web import requests + class CustomTokenAuthenticator(Authenticator): """ - Accept the authenticated Access Token from cookie. + Accept the authenticated Access Token from cookie. """ + auth_cookie_header = Unicode( - os.environ.get('AUTH_COOKIE_HEADER', ''), + os.environ.get("AUTH_COOKIE_HEADER", ""), config=True, help="the cookie header we put in browser to retrieve token", ) auth_username_key = Unicode( - os.environ.get('AUTH_USERNAME_KEY', ''), + os.environ.get("AUTH_USERNAME_KEY", ""), config=True, help="the key to retreive username from the json", ) - auth_uid_number_key = Unicode( - os.environ.get('AUTH_UID_NUMBER_KEY', ''), - config=True, - help="the key to retreive uid from the json", - ) - landing_page_login_url = Unicode( - os.environ.get('LANDING_PAGE_LOGIN_URL', ''), + os.environ.get("LANDING_PAGE_LOGIN_URL", ""), config=True, help="the landing page login entry", ) keycloak_url = Unicode( - os.environ.get('KEYCLOAK_URL', ''), + os.environ.get("KEYCLOAK_URL", ""), config=True, help="the URL where keycloak is installed", ) keycloak_audience = Unicode( - os.environ.get('KEYCLOAK_AUDIENCE', ''), + os.environ.get("KEYCLOAK_AUDIENCE", ""), config=True, help="the audience for keycloak to check", ) keycloak_pem_key = Unicode( - os.environ.get('KEYCLOAK_PEM_KEY', ''), + os.environ.get("KEYCLOAK_PEM_KEY", ""), config=True, help="the RSA pem key with proper header and footer (deprecated)", ) auth_group = Unicode( - os.environ.get('AUTH_GROUP', ''), + os.environ.get("AUTH_GROUP", ""), config=True, - help="the user group for incore jupyterhub authenticator" + help="the user group for incore jupyterhub authenticator", ) auth_role = Unicode( - os.environ.get('AUTH_ROLE', ''), + os.environ.get("AUTH_ROLE", ""), config=True, - help="the user role for incore jupyterhub authenticator" + help="the user role for incore jupyterhub authenticator", ) space_service_url = Unicode( - os.environ.get('SPACE_SERVICE_URL', ''), + os.environ.get("SPACE_SERVICE_URL", ""), config=True, - help="the internal space service url" + help="the internal space service url", ) quotas = None def get_handlers(self, app): return [ - (r'/', LoginHandler), - (r'/user', LoginHandler), - (r'/lab', LoginHandler), - (r'/login', LoginHandler), - (r'/logout', CustomTokenLogoutHandler), + (r"/", LoginHandler), + (r"/user", LoginHandler), + (r"/lab", LoginHandler), + (r"/login", LoginHandler), + (r"/logout", CustomTokenLogoutHandler), ] def get_keycloak_pem(self): if not self.keycloak_url: - raise web.HTTPError(500, log_message="JupyterHub is not correctly configured.") + raise web.HTTPError( + 500, log_message="JupyterHub is not correctly configured." + ) # fetch the key response = urllib.request.urlopen(self.keycloak_url) if response.code >= 200 or response <= 299: - encoding = response.info().get_content_charset('utf-8') + encoding = response.info().get_content_charset("utf-8") result = json.loads(response.read().decode(encoding)) - self.keycloak_pem_key = f"-----BEGIN PUBLIC KEY-----\n" \ - f"{result['public_key']}\n" \ - f"-----END PUBLIC KEY-----" + self.keycloak_pem_key = ( + f"-----BEGIN PUBLIC KEY-----\n" + f"{result['public_key']}\n" + f"-----END PUBLIC KEY-----" + ) else: raise web.HTTPError(500, log_message="Could not get key from keycloak.") @@ -107,15 +107,19 @@ def check_jwt_token(self, access_token): # make sure audience is set if not self.keycloak_audience: - raise web.HTTPError(403, log_message="JupyterHub is not correctly configured.") + raise web.HTTPError( + 403, log_message="JupyterHub is not correctly configured." + ) # no token in the cookie if not access_token: raise web.HTTPError(401, log_message="Please login to access IN-CORE Lab.") # make sure it is a valid token - if len(access_token.split(" ")) != 2 or access_token.split(" ")[0] != 'bearer': - raise web.HTTPError(403, log_message="Token format not valid, it has to be bearer xxxx!") + if len(access_token.split(" ")) != 2 or access_token.split(" ")[0] != "bearer": + raise web.HTTPError( + 403, log_message="Token format not valid, it has to be bearer xxxx!" + ) # decode jwt token instead of sending it to userinfo endpoint: access_token = access_token.split(" ")[1] @@ -124,24 +128,29 @@ def check_jwt_token(self, access_token): try: resp_json = jwt.decode(access_token, public_key, audience=audience) except ExpiredSignatureError: - raise web.HTTPError(403, log_message='JWT Expired Signature Error: token signature has expired') + raise web.HTTPError( + 403, + log_message="JWT Expired Signature Error: token signature has expired", + ) except JWTClaimsError: - raise web.HTTPError(403, log_message='JWT Claims Error: token signature is invalid') + raise web.HTTPError( + 403, log_message="JWT Claims Error: token signature is invalid" + ) except JWTError: - raise web.HTTPError(403, log_message='JWT Error: token signature is invalid') + raise web.HTTPError( + 403, log_message="JWT Error: token signature is invalid" + ) except Exception as e: raise web.HTTPError(403, log_message="Not a valid jwt token!") # make sure we know username if self.auth_username_key not in resp_json.keys(): - raise web.HTTPError(500, log_message=f"Required field {self.auth_username_key} does not exist in jwt token") + raise web.HTTPError( + 500, + log_message=f"Required field {self.auth_username_key} does not exist in jwt token", + ) username = resp_json[self.auth_username_key] - # make sure there is a user id - if self.auth_uid_number_key not in resp_json.keys(): - raise web.HTTPError(500, log_message=f"Required field {self.auth_uid_number_key} does not exist in jwt token") - uid = resp_json[self.auth_uid_number_key] - # get the groups/roles for the user user_groups = resp_json.get("groups", []) if "roles" in resp_json: @@ -153,39 +162,48 @@ def check_jwt_token(self, access_token): # check authorization if self.auth_group not in user_groups and self.auth_role not in user_roles: - raise web.HTTPError(403, log_message="The current user does not belongs to incore user group and " + - "cannot access incore lab. Please contact NCSA IN-CORE development team") + raise web.HTTPError( + 403, + log_message="The current user does not belongs to incore user group and " + + "cannot access incore lab. Please contact NCSA IN-CORE development team", + ) admin = False if "incore_admin" in user_groups or "incore_admin" in user_roles: admin = True - self.log.info(f"username={username} logged in with uid={uid}") + self.log.info(f"username={username} logged in") return { - 'name': username, - 'admin': admin, - 'auth_state': { - 'uid': uid, - 'groups': user_groups, - 'roles': user_roles, + "name": username, + "admin": admin, + "auth_state": { + "groups": user_groups, + "roles": user_roles, }, } async def authenticate(self, handler, data): self.log.info("Authenticate") try: - access_token = urllib.parse.unquote(handler.get_cookie(self.auth_cookie_header, "")) + access_token = urllib.parse.unquote( + handler.get_cookie(self.auth_cookie_header, "") + ) if not access_token: - raise web.HTTPError(401, log_message="Please login to access IN-CORE Lab.") + raise web.HTTPError( + 401, log_message="Please login to access IN-CORE Lab." + ) # check token and authorization user = self.check_jwt_token(access_token) return user except web.HTTPError as e: if e.log_message: - error_msg = urllib.parse.quote(e.log_message.encode('utf-8')) + error_msg = urllib.parse.quote(e.log_message.encode("utf-8")) else: - error_msg = urllib.parse.quote(f"Error {e}".encode('utf-8')) + ". Please login to access IN-CORE Lab." + error_msg = ( + urllib.parse.quote(f"Error {e}".encode("utf-8")) + + ". Please login to access IN-CORE Lab." + ) handler.redirect(f"{self.landing_page_login_url}?error={error_msg}") def find_quota(self, user, auth_state): @@ -197,22 +215,25 @@ def find_quota(self, user, auth_state): # Define the headers headers = { - "x-auth-userinfo": json.dumps({"preferred_username":user.name}), - "x-auth-usergroup": json.dumps({"groups": []}) + "x-auth-userinfo": json.dumps({"preferred_username": user.name}), + "x-auth-usergroup": json.dumps({"groups": []}), } resp = requests.get(url, headers=headers) + if resp.status_code == 200 and "incoreLab" in resp.json(): self.log.info(f"Quota for current user:{user.name}") self.log.info(json.dumps(resp.json()["incoreLab"])) return resp.json()["incoreLab"] else: - self.log.exception(f"Request failed with status code: {resp.status_code}") + self.log.exception( + f"Request failed with status code: {resp.status_code}" + ) except: self.log.exception("Could not load quota") # default quotas - return { "cpu": [ 1, 2 ], "mem": [ 2, 4 ], "disk": 4, "service": [100, 2]} + return {"cpu": [1, 2], "mem": [2, 4], "disk": 4, "service": [100, 2]} async def pre_spawn_start(self, user, spawner): auth_state = await user.get_auth_state() @@ -220,8 +241,7 @@ async def pre_spawn_start(self, user, spawner): self.log.error("No auth state") return - spawner.environment['NB_USER'] = user.name - spawner.environment['NB_UID'] = str(auth_state['uid']) + spawner.environment["NB_USER"] = user.name quota = self.find_quota(user, auth_state) if "cpu" in quota: @@ -237,6 +257,7 @@ async def pre_spawn_start(self, user, spawner): spawner.mem_guarantee = "2G" spawner.mem_limit = "4G" + # # # This is called from the jupyterlab so there is no cookies that this depends on # async def refresh_user(self, user, handler): @@ -261,8 +282,9 @@ class CustomTokenLogoutHandler(LogoutHandler): async def handle_logout(self): # remove incore token on logout self.log.info("Remove incore token on logout") - error_msg = "You have logged out of IN-CORE system from IN-CORE lab. Please login again if you want to use " \ - "IN-CORE components." + error_msg = ( + "You have logged out of IN-CORE system from IN-CORE lab. Please login again if you want to use " + "IN-CORE components." + ) self.set_cookie(self.authenticator.auth_cookie_header, "") self.redirect(f"{self.authenticator.landing_page_login_url}?error={error_msg}") - From c8864cc17adfd64736c2e215fbbfcae935a9300b Mon Sep 17 00:00:00 2001 From: Vismayak Mohanarajan Date: Fri, 9 Aug 2024 09:33:21 -0500 Subject: [PATCH 2/3] Updating base jupyter-lab image (#123) * Update Dockerfile.lab and Update CHANGELOG.md --- CHANGELOG.md | 2 ++ Dockerfile.lab | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c528a9..4203cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Changed +- Updated base image forJupyterLab to 4.1 [#122](https://github.com/IN-CORE/incore-lab/issues/122) ### Changed - UID logic. We will no longer depend upon the uid parameter from LDAP in JWT token. [#119](https://github.com/IN-CORE/incore-lab/issues/119) diff --git a/Dockerfile.lab b/Dockerfile.lab index 7e3c8cc..3b0f191 100644 --- a/Dockerfile.lab +++ b/Dockerfile.lab @@ -1,4 +1,4 @@ -FROM jupyterhub/singleuser:4.0.2 +FROM jupyterhub/singleuser:4.1 USER root RUN apt-get -qq update && apt-get install -y --no-install-recommends \ @@ -33,8 +33,8 @@ ENV INCORE=$INCORE \ RUN conda install -c conda-forge mamba # Set mamba channels -RUN mamba config --add channels conda-forge && \ - mamba config --add channels in-core +RUN conda config --add channels conda-forge && \ + conda config --add channels in-core # Install pyincore, pyincore-viz and other mamba packages RUN umask 0 && \ From 4d85ad229e4c3f5e07028d49e0f893c9e3eb118d Mon Sep 17 00:00:00 2001 From: YONG WOOK KIM Date: Thu, 15 Aug 2024 08:32:25 -0500 Subject: [PATCH 3/3] Relase 1.8.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4203cbd..c47be27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [Unreleased] +## [1.8.0] - 2024-08-21 ### Changed - Updated base image forJupyterLab to 4.1 [#122](https://github.com/IN-CORE/incore-lab/issues/122)