From 2a5e82712cf49179027b7ddce27aaa2601494836 Mon Sep 17 00:00:00 2001 From: Ryan Wehrle Date: Fri, 23 Feb 2024 16:14:13 -0500 Subject: [PATCH 1/7] Add jwt package; Set username based on token --- pyproject.toml | 1 + src/ansys/hps/client/client.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 019f4102a..ea4c440d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "setuptools;python_version>='3.12'", # Python3.12 doesn't include setuptools automatically "backoff>=2.0.0", "pydantic>=1.10.0", + "PyJWT>=2.8.0" ] [project.optional-dependencies] diff --git a/src/ansys/hps/client/client.py b/src/ansys/hps/client/client.py index 7f423cb2d..6d7caa257 100644 --- a/src/ansys/hps/client/client.py +++ b/src/ansys/hps/client/client.py @@ -25,6 +25,7 @@ from typing import Union import warnings +import jwt import requests from .auth.authenticate import authenticate @@ -190,6 +191,13 @@ def __init__( verify=self.verify, ) self.access_token = tokens["access_token"] + + try: + parsed_jwt = jwt.decode(self.access_token, options={"verify_signature": False}) + self.username = parsed_jwt.get("preferred_username", self.username) + except: + log.warning("Could not retrieve username from access token") + # client credentials flow does not return a refresh token self.refresh_token = tokens.get("refresh_token", None) From a086998a7b8122863cd4d765038e54fb229849d0 Mon Sep 17 00:00:00 2001 From: Ryan Wehrle Date: Tue, 27 Feb 2024 08:42:33 -0500 Subject: [PATCH 2/7] Fix log statement punctuation; Raise exception when username passed does not match preferred_username --- src/ansys/hps/client/client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ansys/hps/client/client.py b/src/ansys/hps/client/client.py index 6d7caa257..1126fca42 100644 --- a/src/ansys/hps/client/client.py +++ b/src/ansys/hps/client/client.py @@ -30,7 +30,7 @@ from .auth.authenticate import authenticate from .connection import create_session -from .exceptions import raise_for_status +from .exceptions import HPSError, raise_for_status from .warnings import UnverifiedHTTPSRequestsWarning log = logging.getLogger(__name__) @@ -194,9 +194,14 @@ def __init__( try: parsed_jwt = jwt.decode(self.access_token, options={"verify_signature": False}) - self.username = parsed_jwt.get("preferred_username", self.username) + parsed_username = parsed_jwt.get("preferred_username", None) + if self.username != None and self.username != parsed_username: + raise HPSError( + "Username and preferred_username from access token do not match." + ) + self.username = parsed_username except: - log.warning("Could not retrieve username from access token") + log.warning("Could not retrieve preferred_username from access token.") # client credentials flow does not return a refresh token self.refresh_token = tokens.get("refresh_token", None) From 59ae967b3a28ed2e83fc27d6e1ac534713a13fca Mon Sep 17 00:00:00 2001 From: Ryan Wehrle Date: Tue, 27 Feb 2024 09:15:41 -0500 Subject: [PATCH 3/7] Move exception --- src/ansys/hps/client/client.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ansys/hps/client/client.py b/src/ansys/hps/client/client.py index 1126fca42..23c7240ee 100644 --- a/src/ansys/hps/client/client.py +++ b/src/ansys/hps/client/client.py @@ -192,16 +192,20 @@ def __init__( ) self.access_token = tokens["access_token"] + parsed_username = None + try: parsed_jwt = jwt.decode(self.access_token, options={"verify_signature": False}) - parsed_username = parsed_jwt.get("preferred_username", None) + parsed_username = parsed_jwt["preferred_username"] + except: + log.warning("Could not retrieve preferred_username from access token.") + + if parsed_username != None: if self.username != None and self.username != parsed_username: raise HPSError( "Username and preferred_username from access token do not match." ) self.username = parsed_username - except: - log.warning("Could not retrieve preferred_username from access token.") # client credentials flow does not return a refresh token self.refresh_token = tokens.get("refresh_token", None) From 12497281261fafb7c1a3e9ffdc5c24336eb242a7 Mon Sep 17 00:00:00 2001 From: Ryan Wehrle Date: Tue, 27 Feb 2024 09:58:14 -0500 Subject: [PATCH 4/7] Add usernames to exception message --- src/ansys/hps/client/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ansys/hps/client/client.py b/src/ansys/hps/client/client.py index 23c7240ee..858a7ca8b 100644 --- a/src/ansys/hps/client/client.py +++ b/src/ansys/hps/client/client.py @@ -203,7 +203,11 @@ def __init__( if parsed_username != None: if self.username != None and self.username != parsed_username: raise HPSError( - "Username and preferred_username from access token do not match." + ( + f"Username: '{self.username}' and " + f"preferred_username: '{parsed_username}' " + "from access token do not match." + ) ) self.username = parsed_username From c14c6dd3398a26c56601d62fdf02b7813ea23480 Mon Sep 17 00:00:00 2001 From: Ryan Wehrle Date: Tue, 27 Feb 2024 11:43:08 -0500 Subject: [PATCH 5/7] Surround parameter name with quotes --- src/ansys/hps/client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/hps/client/client.py b/src/ansys/hps/client/client.py index 858a7ca8b..dcf0efc17 100644 --- a/src/ansys/hps/client/client.py +++ b/src/ansys/hps/client/client.py @@ -198,7 +198,7 @@ def __init__( parsed_jwt = jwt.decode(self.access_token, options={"verify_signature": False}) parsed_username = parsed_jwt["preferred_username"] except: - log.warning("Could not retrieve preferred_username from access token.") + log.warning("Could not retrieve 'preferred_username' from access token.") if parsed_username != None: if self.username != None and self.username != parsed_username: From af635719fa2fd825e9958caa199db847b4127e7a Mon Sep 17 00:00:00 2001 From: Ryan Wehrle Date: Wed, 28 Feb 2024 11:57:34 -0500 Subject: [PATCH 6/7] Fix none checks; Move scope of username logic --- src/ansys/hps/client/client.py | 39 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/ansys/hps/client/client.py b/src/ansys/hps/client/client.py index dcf0efc17..32a32959b 100644 --- a/src/ansys/hps/client/client.py +++ b/src/ansys/hps/client/client.py @@ -191,29 +191,28 @@ def __init__( verify=self.verify, ) self.access_token = tokens["access_token"] - - parsed_username = None - - try: - parsed_jwt = jwt.decode(self.access_token, options={"verify_signature": False}) - parsed_username = parsed_jwt["preferred_username"] - except: - log.warning("Could not retrieve 'preferred_username' from access token.") - - if parsed_username != None: - if self.username != None and self.username != parsed_username: - raise HPSError( - ( - f"Username: '{self.username}' and " - f"preferred_username: '{parsed_username}' " - "from access token do not match." - ) - ) - self.username = parsed_username - # client credentials flow does not return a refresh token self.refresh_token = tokens.get("refresh_token", None) + parsed_username = None + + try: + parsed_jwt = jwt.decode(self.access_token, options={"verify_signature": False}) + parsed_username = parsed_jwt["preferred_username"] + except: + log.warning("Could not retrieve preferred_username from access token.") + + if parsed_username is not None: + if self.username is not None and self.username != parsed_username: + raise HPSError( + ( + f"Username: '{self.username}' and " + f"preferred_username: '{parsed_username}' " + "from access token do not match." + ) + ) + self.username = parsed_username + self.session = create_session( self.access_token, verify=self.verify, From 71d11120daecc5599fd14d36c18487ed32b4bcbf Mon Sep 17 00:00:00 2001 From: Ryan Wehrle Date: Wed, 28 Feb 2024 12:41:59 -0500 Subject: [PATCH 7/7] Add tests for pref username logic --- tests/test_client.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 9d40df55e..3f88425e1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -27,6 +27,7 @@ import requests from ansys.hps.client import Client +from ansys.hps.client.exceptions import HPSError log = logging.getLogger(__name__) @@ -76,3 +77,40 @@ def test_authentication_workflows(url, username, password): assert client2.access_token is not None assert client2.refresh_token != client0.refresh_token client2.refresh_access_token() + + +def test_authentication_username(url, username, password, keycloak_client): + + # Password workflow + client0 = Client(url, username, password) + assert client0.username == username + + # Impersonation + realm_clients = keycloak_client.get_clients() + rep_impersonation_client = next( + (x for x in realm_clients if x["clientId"] == "rep-impersonation"), None + ) + assert rep_impersonation_client is not None + client1 = Client( + url=url, + client_id=rep_impersonation_client["clientId"], + client_secret=rep_impersonation_client["secret"], + ) + assert client1.username == "service-account-rep-impersonation" + + +def test_authentication_username_exception(url, username, keycloak_client): + + # Impersonation + realm_clients = keycloak_client.get_clients() + rep_impersonation_client = next( + (x for x in realm_clients if x["clientId"] == "rep-impersonation"), None + ) + assert rep_impersonation_client is not None + with pytest.raises(HPSError): + Client( + url=url, + username=username, + client_id=rep_impersonation_client["clientId"], + client_secret=rep_impersonation_client["secret"], + )