From 12e098be622de707fc8e73d6d735ae93b5747c17 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:17:38 +0100 Subject: [PATCH] Store username of access token on validation Token validation already requests 'users/me', so keep the username from the response and expose it as 'ServerAPI.access_token_username'. Use it in 'login' and 'init_service' instead of refetching the user. Co-Authored-By: Claude Opus 5.5 --- ayon_api/_api.py | 8 +++++--- ayon_api/server_api.py | 34 ++++++++++++++++++++++++++-------- ayon_api/utils.py | 2 ++ tests/test_token_validation.py | 20 ++++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index acdd838d9..c9083e65e 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -23,7 +23,7 @@ SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY, ) -from .exceptions import FailedServiceInit +from .exceptions import FailedServiceInit, UnauthorizedError from ._api_helpers.base import _PLACEHOLDER from .utils import ( NOT_SET, @@ -262,8 +262,10 @@ def init_service( if connect: print("Connecting to server \"{}\"".format(server_url)) con = GlobalContext.get_server_api_connection() - user = con.get_user() - print("Logged in as user \"{}\"".format(user["name"])) + username = con.access_token_username + if username is None: + raise UnauthorizedError("User is not authorized.") + print("Logged in as user \"{}\"".format(username)) def init_service(*args, **kwargs): diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index baaaaa731..5cc23cc3e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -291,6 +291,7 @@ class TokenInfo: token: str | None = None is_valid: bool | None = None is_service: bool | None = None + username: str | None = None unauthorized_response: requests.Response | None = None @@ -615,6 +616,24 @@ def access_token(self) -> str | None: """ return self._token_info.token + @property + def access_token_username(self) -> str | None: + """Name of user who owns the access token. + + The value is stored on token validation, so it does not require + additional request. It is not affected by 'as_username', use + 'get_user' to get user the requests are made as. + + Returns: + str | None: Username or None if token is not valid. + + """ + if not self.has_valid_token: + return None + if self._token_info.username is None: + self.validate_token() + return self._token_info.username + def is_service_user(self) -> bool: """Check if connection is using service API key. @@ -925,10 +944,12 @@ def _request_token_user_info(self) -> tuple[UserInfo, bool]: def _store_token_validation(self, user_info: UserInfo) -> None: self._token_info.is_valid = user_info.is_valid self._token_info.unauthorized_response = user_info.response - is_service = None + is_service = username = None if user_info.is_valid: is_service = user_info.is_service + username = user_info.username self._token_info.is_service = is_service + self._token_info.username = username def set_token(self, token: str | None) -> None: self._reset_token() @@ -943,6 +964,7 @@ def reset_token(self) -> None: def _reset_token(self) -> None: self._token_info.token = None self._token_info.is_service = None + self._token_info.username = None self._token_info.is_valid = None self._token_info.unauthorized_response = None self.close_session() @@ -1440,13 +1462,7 @@ def login( """ if self.has_valid_token: - try: - user_info = self.get_user(fill_attrib=False) - except UnauthorizedError: - user_info = {} - - current_username = user_info.get("name") - if current_username == username: + if self.access_token_username == username: self.close_session() if create_session: self.create_session() @@ -1478,6 +1494,8 @@ def login( # Service token can't be obtained by login, so it is not # service token self._token_info.is_service = False + user = response.data.get("user") or {} + self._token_info.username = user.get("name") if not self.has_valid_token: raise AuthenticationError("Invalid credentials") diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 803eafbb4..aeaabb2f9 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -704,6 +704,7 @@ class UserInfo: """User information.""" is_valid: bool = False is_service: bool = False + username: str | None = None response: requests.Response | None = None @@ -779,6 +780,7 @@ def get_user_info_by_token( response=response, ) if output.is_valid: + output.username = response.json().get("name") break return output diff --git a/tests/test_token_validation.py b/tests/test_token_validation.py index dd9b665ae..472725125 100644 --- a/tests/test_token_validation.py +++ b/tests/test_token_validation.py @@ -121,6 +121,7 @@ def test_user_info_by_token(token, expected_requests, is_service): ) assert user_info.is_valid assert user_info.is_service is is_service + assert user_info.username == "user" assert server.requests == expected_requests @@ -138,6 +139,25 @@ def test_connection_validates_with_single_request(server, token, auth): assert con._session is server.sessions[0] +@pytest.mark.parametrize( + "token, auth", [(USER_TOKEN, "bearer"), (API_KEY, "x-api-key")] +) +def test_access_token_username_from_validation(server, token, auth): + con = ServerAPI(URL, token) + + assert con.access_token_username == "user" + assert server.requests == [("/api/users/me", auth)] + + con.reset_token() + assert con.access_token_username is None + + +def test_invalid_token_has_no_username(server): + con = ServerAPI(URL, "d" * 32) + + assert con.access_token_username is None + + def test_global_connection_validates_with_single_request( server, monkeypatch ):