From 5e8d1abcff2ff908e57c92cc32bbadfbffa41cf4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:43:21 +0200 Subject: [PATCH 01/24] capture 401 earlier --- ayon_api/server_api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7b60d68a7..73c45c8f3 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -765,6 +765,7 @@ def validate_token(self) -> bool: except UnauthorizedError: self._token_is_valid = False + self.close_session() finally: self._token_validation_started = False @@ -1222,6 +1223,11 @@ def _do_rest_request(self, function, url, **kwargs): ): self.validate_token() + if self._token_is_valid is False: + raise UnauthorizedError( + "Authentication token was invalidated." + ) + if "headers" not in kwargs: kwargs["headers"] = self.get_headers() @@ -1297,6 +1303,14 @@ def _do_rest_request(self, function, url, **kwargs): if new_response is not None: return new_response + if ( + response is not None + and self._token_is_valid + and response.status_code == 401 + ): + self._token_is_valid = False + self.close_session() + new_response = RestApiResponse(response) self.log.debug(f"Response {str(new_response)}") return new_response From 9a74717948595fb8b06ec6c7ac7f4d83a2f0d27b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:43:39 +0200 Subject: [PATCH 02/24] fix return statement in finally --- ayon_api/server_api.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 73c45c8f3..cdabfb268 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -187,9 +187,8 @@ def as_user(self, username: Optional[str]) -> Generator[None, None, None]: user_id = uuid.uuid4().hex self._user_ids.append(user_id) self._users_by_id[user_id] = username - try: - yield - finally: + + def _cleanup(): self._users_by_id.pop(user_id, None) if not self._user_ids: return @@ -208,6 +207,11 @@ def as_user(self, username: Optional[str]) -> Generator[None, None, None]: new_last_user = self._users_by_id.get(self._user_ids[-1]) self._last_user = new_last_user + try: + yield + finally: + _cleanup() + class ServerAPI( InstallersAPI, From c60e8b5da8e27685b7c1ffdf684f3fc2ee7d6a1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:47:19 +0200 Subject: [PATCH 03/24] support cert and verify in token validation functions --- ayon_api/utils.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index d2cb88570..d486db3cf 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -658,6 +658,9 @@ def logout_from_server( def get_user_by_token( url: str, token: str, + *, + verify: str | bool | None = None, + cert: str | None = None, timeout: float | None = None, ) -> dict[str, Any] | None: """Get user information by url and token. @@ -675,6 +678,12 @@ def get_user_by_token( if timeout is None: timeout = get_default_timeout() + if verify is None: + verify = os.environ.get("AYON_CA_FILE") or True + + if cert is None: + cert = os.environ.get("AYON_CERT_FILE") or None + base_headers = { "Content-Type": "application/json", } @@ -688,6 +697,8 @@ def get_user_by_token( f"{url}/api/users/me", headers=headers, timeout=timeout, + verify=verify, + cert=cert, ) if response.status_code == 200: return response.json() @@ -697,6 +708,9 @@ def get_user_by_token( def is_token_valid( url: str, token: str, + *, + verify: str | bool | None = None, + cert: str | None = None, timeout: float | None = None, ) -> bool: """Check if token is valid. @@ -713,7 +727,13 @@ def is_token_valid( bool: True if token is valid. """ - if get_user_by_token(url, token, timeout=timeout): + if get_user_by_token( + url, + token, + verify=verify, + cert=cert, + timeout=timeout + ): return True return False From 557c5b69f1a07a2aaa2cc46dfb779ae92e72a450 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:10:24 +0200 Subject: [PATCH 04/24] fix token validation --- ayon_api/server_api.py | 49 +++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index cdabfb268..487214116 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -67,6 +67,7 @@ get_media_mime_type_for_stream, get_machine_name, fill_own_attribs, + is_token_valid, ) from ._api_helpers import ( InstallersAPI, @@ -758,19 +759,27 @@ def validate_server_availability(self): ) def validate_token(self) -> bool: + if self._access_token is None: + self._token_is_valid = False + self.close_session() + return False + + self._token_validation_started = True try: - self._token_validation_started = True # TODO add other possible validations # - existence of 'user' key in info # - validate that 'site_id' is in 'sites' in info - self.get_info() - self.get_user() - self._token_is_valid = True - - except UnauthorizedError: + self._get_server_info() + self._token_is_valid = is_token_valid( + self.base_url, + self._access_token, + verify=self._ssl_verify, + cert=self._cert + ) + except Exception: self._token_is_valid = False self.close_session() - + self.log.error("Failed to validate token.", exc_info=True) finally: self._token_validation_started = False return self._token_is_valid @@ -866,6 +875,9 @@ def get_info(self) -> dict[str, Any]: dict[str, Any]: Information from server. """ + if self._session is None: + return self._get_server_info() + response = self.get("info") response.raise_for_status() return response.data @@ -1213,10 +1225,19 @@ def _logout(self): logout_from_server(self._base_url, self._access_token) def _do_rest_request(self, function, url, **kwargs): + if ( + self._session is not None + and self._token_is_valid is False + ): + raise UnauthorizedError( + "Authentication token was invalidated." + ) + kwargs.setdefault("timeout", self.timeout) max_retries = kwargs.get("max_retries", self.max_retries) if max_retries < 1: max_retries = 1 + if self._session is None: # Validate token if was not yet validated # - ignore validation if we're in middle of @@ -1227,11 +1248,6 @@ def _do_rest_request(self, function, url, **kwargs): ): self.validate_token() - if self._token_is_valid is False: - raise UnauthorizedError( - "Authentication token was invalidated." - ) - if "headers" not in kwargs: kwargs["headers"] = self.get_headers() @@ -1573,6 +1589,15 @@ def _endpoint_to_url( base_url = self._rest_url if use_rest else self._base_url return f"{base_url}/{endpoint}" + def _get_server_info(self) -> dict[str, Any]: + """Get server info without a session.""" + response = requests.get( + f"{self._rest_url}/info", + cert=self._cert, + verify=self._ssl_verify, + ) + response.raise_for_status() + return response.json() def _download_file_to_stream( self, endpoint: str, From ce3a33f11a7ed020c5fe74ca72de3d4cf8f891a9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:10:54 +0200 Subject: [PATCH 05/24] move private methods below public ones --- ayon_api/server_api.py | 273 ++++++++++++++++++++--------------------- 1 file changed, 136 insertions(+), 137 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 487214116..d9ed5faa8 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -951,29 +951,6 @@ def links_graphql_support_data(self) -> bool: ) return self._links_graphql_support_data - def _get_user_info(self) -> Optional[dict[str, Any]]: - if self._access_token is None: - return None - - if self._access_token_is_service is not None: - response = self.get("users/me") - if response.status == 200: - return response.data - return None - - self._access_token_is_service = False - response = self.get("users/me") - if response.status == 200: - return response.data - - self._access_token_is_service = True - response = self.get("users/me") - if response.status == 200: - return response.data - - self._access_token_is_service = None - return None - def get_users( self, project_name: Optional[str] = None, @@ -1221,120 +1198,6 @@ def logout(self, soft: bool = False): self._logout() self.reset_token() - def _logout(self): - logout_from_server(self._base_url, self._access_token) - - def _do_rest_request(self, function, url, **kwargs): - if ( - self._session is not None - and self._token_is_valid is False - ): - raise UnauthorizedError( - "Authentication token was invalidated." - ) - - kwargs.setdefault("timeout", self.timeout) - max_retries = kwargs.get("max_retries", self.max_retries) - if max_retries < 1: - max_retries = 1 - - if self._session is None: - # Validate token if was not yet validated - # - ignore validation if we're in middle of - # validation - if ( - self._token_is_valid is None - and not self._token_validation_started - ): - self.validate_token() - - if "headers" not in kwargs: - kwargs["headers"] = self.get_headers() - - if isinstance(function, RequestType): - function = self._base_functions_mapping[function] - - elif isinstance(function, RequestType): - function = self._session_functions_mapping[function] - - response = None - new_response = None - for retry_idx in reversed(range(max_retries)): - try: - response = function(url, **kwargs) - - # Usually these mean, try later. - # 502: returned by the proxy: nginx - # 503: returned by the server: if no capacity - if response.status_code in {502, 503}: - new_response = RestApiResponse(response) - self.log.warning( - "Server returned %s status code." - " Retrying with longer delay...", - response.status_code - ) - if retry_idx != 0: - time.sleep(2) - continue - break - - except ConnectionRefusedError: - if retry_idx == 0: - self.log.warning( - "Connection error happened.", exc_info=True - ) - - # Server may be restarting - new_response = RestApiResponse( - None, - { - "detail": ( - "Unable to connect the server. Connection refused" - ) - } - ) - - except requests.exceptions.Timeout: - # Connection timed out - new_response = RestApiResponse( - None, - {"detail": "Connection timed out."} - ) - - except requests.exceptions.ConnectionError: - # Log warning only on last attempt - if retry_idx == 0: - self.log.warning( - "Connection error happened.", exc_info=True - ) - - new_response = RestApiResponse( - None, - { - "detail": ( - "Unable to connect the server. Connection error" - ) - } - ) - - if retry_idx != 0: - time.sleep(0.1) - - if new_response is not None: - return new_response - - if ( - response is not None - and self._token_is_valid - and response.status_code == 401 - ): - self._token_is_valid = False - self.close_session() - - new_response = RestApiResponse(response) - self.log.debug(f"Response {str(new_response)}") - return new_response - def raw_post(self, entrypoint: str, **kwargs): url = self._endpoint_to_url(entrypoint) self.log.debug(f"Executing [POST] {url}") @@ -1589,6 +1452,9 @@ def _endpoint_to_url( base_url = self._rest_url if use_rest else self._base_url return f"{base_url}/{endpoint}" + def _logout(self): + logout_from_server(self._base_url, self._access_token) + def _get_server_info(self) -> dict[str, Any]: """Get server info without a session.""" response = requests.get( @@ -1598,6 +1464,139 @@ def _get_server_info(self) -> dict[str, Any]: ) response.raise_for_status() return response.json() + + def _get_user_info(self) -> Optional[dict[str, Any]]: + if self._access_token is None: + return None + + if self._access_token_is_service is not None: + response = self.get("users/me") + if response.status == 200: + return response.data + return None + + self._access_token_is_service = False + response = self.get("users/me") + if response.status == 200: + return response.data + + self._access_token_is_service = True + response = self.get("users/me") + if response.status == 200: + return response.data + + self._access_token_is_service = None + return None + + def _do_rest_request(self, function, url, **kwargs): + kwargs.setdefault("timeout", self.timeout) + max_retries = kwargs.get("max_retries", self.max_retries) + if max_retries < 1: + max_retries = 1 + + if self._token_is_valid is False: + raise UnauthorizedError( + "Authentication token was invalidated." + ) + + if self._session is None: + # Validate token if was not yet validated + # - ignore validation if we're in middle of + # validation + if ( + self._token_is_valid is None + and not self._token_validation_started + ): + self.validate_token() + + if "headers" not in kwargs: + kwargs["headers"] = self.get_headers() + + if isinstance(function, RequestType): + function = self._base_functions_mapping[function] + + elif isinstance(function, RequestType): + function = self._session_functions_mapping[function] + + response = None + new_response = None + for retry_idx in reversed(range(max_retries)): + try: + response = function(url, **kwargs) + + # Usually these mean, try later. + # 502: returned by the proxy: nginx + # 503: returned by the server: if no capacity + if response.status_code in {502, 503}: + new_response = RestApiResponse(response) + self.log.warning( + "Server returned %s status code." + " Retrying with longer delay...", + response.status_code + ) + if retry_idx != 0: + time.sleep(2) + continue + break + + except ConnectionRefusedError: + if retry_idx == 0: + self.log.warning( + "Connection error happened.", exc_info=True + ) + + # Server may be restarting + new_response = RestApiResponse( + None, + { + "detail": ( + "Unable to connect the server. Connection refused" + ) + } + ) + + except requests.exceptions.Timeout: + # Connection timed out + new_response = RestApiResponse( + None, + {"detail": "Connection timed out."} + ) + + except requests.exceptions.ConnectionError: + # Log warning only on last attempt + if retry_idx == 0: + self.log.warning( + "Connection error happened.", exc_info=True + ) + + new_response = RestApiResponse( + None, + { + "detail": ( + "Unable to connect the server. Connection error" + ) + } + ) + + if retry_idx != 0: + time.sleep(0.1) + + if new_response is not None: + return new_response + + if ( + response is not None + and self._token_is_valid + and response.status_code == 401 + ): + self._token_is_valid = False + self.close_session() + self._trigger_on_invalidate_callbacks() + + new_response = RestApiResponse(response) + self.log.debug(f"Response {str(new_response)}") + return new_response + def _download_file_to_stream( self, endpoint: str, From b80b51ac2044c39290ef95ec3d92525faabba00e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:05:39 +0200 Subject: [PATCH 06/24] handle if user service or not with utils function 'get_user_info_by_token' --- ayon_api/server_api.py | 11 +++-- ayon_api/utils.py | 94 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index d9ed5faa8..7a6131566 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -67,7 +67,7 @@ get_media_mime_type_for_stream, get_machine_name, fill_own_attribs, - is_token_valid, + get_user_info_by_token, ) from ._api_helpers import ( InstallersAPI, @@ -770,12 +770,17 @@ def validate_token(self) -> bool: # - existence of 'user' key in info # - validate that 'site_id' is in 'sites' in info self._get_server_info() - self._token_is_valid = is_token_valid( + user_info = get_user_info_by_token( self.base_url, self._access_token, verify=self._ssl_verify, cert=self._cert ) + self._token_is_valid = user_info.is_valid + is_service = None + if user_info.is_valid: + is_service = user_info.is_service + self._access_token_is_service = is_service except Exception: self._token_is_valid = False self.close_session() @@ -787,7 +792,7 @@ def validate_token(self) -> bool: def set_token(self, token: Optional[str]): self.reset_token() self._access_token = token - self.get_user() + self.validate_token() def reset_token(self): self._access_token = None diff --git a/ayon_api/utils.py b/ayon_api/utils.py index d486db3cf..40505e817 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -3,6 +3,7 @@ import os import re import datetime +from dataclasses import dataclass, field import copy import logging import json @@ -655,19 +656,32 @@ def logout_from_server( ) -def get_user_by_token( +@dataclass +class UserInfo: + """User information.""" + is_valid: bool = False + is_service: bool = False + content: bytes = b"" + data: dict[str, Any] = field(default_factory=dict) + + +def get_user_info_by_token( url: str, token: str, *, verify: str | bool | None = None, cert: str | None = None, timeout: float | None = None, -) -> dict[str, Any] | None: +) -> UserInfo: """Get user information by url and token. Args: url (str): Server url. token (str): User's token. + verify (str | bool | None): SSL verification for request. Value from + 'AYON_CA_FILE' environment variable is used if not specified. + cert (str | None): SSL certificate for request. Value from + 'AYON_CERT_FILE' environment variable is used if not specified. timeout (float | None): Timeout for request. Value from 'get_default_timeout' is used if not specified. @@ -675,6 +689,10 @@ def get_user_by_token( dict[str, Any] | None: User information if url and token are valid. """ + output = UserInfo() + if not token: + return output + if timeout is None: timeout = get_default_timeout() @@ -687,9 +705,9 @@ def get_user_by_token( base_headers = { "Content-Type": "application/json", } - for header_value in ( - {"Authorization": f"Bearer {token}"}, - {"X-Api-Key": token}, + for header_value, is_service in ( + ({"Authorization": f"Bearer {token}"}, False), + ({"X-Api-Key": token}, True), ): headers = base_headers.copy() headers.update(header_value) @@ -700,18 +718,61 @@ def get_user_by_token( verify=verify, cert=cert, ) - if response.status_code == 200: - return response.json() + try: + data = response.json() + except Exception: + data = {} + + output = UserInfo( + is_valid=response.status_code == 200, + is_service=is_service, + data=data, + content=response.content, + ) + if output.is_valid: + break + return output + + +def get_user_by_token( + url: str, + token: str, + timeout: float | None = None, + *, + verify: str | bool | None = None, + cert: str | None = None, +) -> dict[str, Any] | None: + """Get user information by url and token. + + Args: + url (str): Server url. + token (str): User's token. + timeout (float | None): Timeout for request. Value from + 'get_default_timeout' is used if not specified. + verify (str | bool | None): SSL verification for request. Value from + 'AYON_CA_FILE' environment variable is used if not specified. + cert (str | None): SSL certificate for request. Value from + 'AYON_CERT_FILE' environment variable is used if not specified. + + Returns: + dict[str, Any] | None: User information if url and token are valid. + + """ + user_info = get_user_info_by_token( + url, token, timeout=timeout, verify=verify, cert=cert, + ) + if user_info.is_valid: + return user_info.data return None def is_token_valid( url: str, token: str, + timeout: float | None = None, *, verify: str | bool | None = None, cert: str | None = None, - timeout: float | None = None, ) -> bool: """Check if token is valid. @@ -722,20 +783,19 @@ def is_token_valid( token (str): User's token. timeout (float | None): Timeout for request. Value from 'get_default_timeout' is used if not specified. + verify (str | bool | None): SSL verification for request. Value from + 'AYON_CA_FILE' environment variable is used if not specified. + cert (str | None): SSL certificate for request. Value from + 'AYON_CERT_FILE' environment variable is used if not specified. Returns: bool: True if token is valid. """ - if get_user_by_token( - url, - token, - verify=verify, - cert=cert, - timeout=timeout - ): - return True - return False + user_info = get_user_info_by_token( + url, token, timeout=timeout, verify=verify, cert=cert + ) + return user_info.is_valid def validate_url( From d36a0e9f21ab05286e2a99640010df2ef68957a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:14:12 +0200 Subject: [PATCH 07/24] wrap token information to dataclass and added better handling of it --- ayon_api/_api.py | 6 +- ayon_api/server_api.py | 170 ++++++++++++++++++++++------------------- 2 files changed, 96 insertions(+), 80 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 5c843360f..541c22948 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -129,10 +129,10 @@ def login(self, username: str, password: str): login is skipped. """ - previous_token = self._access_token + previous_token = self._token_info.token super().login(username, password) - if self.has_valid_token and previous_token != self._access_token: - os.environ[SERVER_API_ENV_KEY] = self._access_token + if self.has_valid_token and previous_token != self._token_info.token: + os.environ[SERVER_API_ENV_KEY] = self._token_info.token @staticmethod def get_url(): diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7a6131566..0a935dfb7 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -6,6 +6,7 @@ from __future__ import annotations import copy +from dataclasses import dataclass import os import re import io @@ -214,6 +215,13 @@ def _cleanup(): _cleanup() +@dataclass +class TokenInfo: + token: str | None = None + is_valid: bool | None = None + is_service: bool | None = None + + class ServerAPI( InstallersAPI, DependencyPackagesAPI, @@ -295,7 +303,7 @@ def __init__( self._rest_url: str = f"{base_url}/api" self._graphql_url: str = f"{base_url}/graphql" self._log: logging.Logger = logging.getLogger(self.__class__.__name__) - self._access_token: Optional[str] = token + # Allow to have 'site_id' to 'None' if site_id is NOT_SET: site_id = get_default_site_id() @@ -327,9 +335,8 @@ def __init__( self._ssl_verify = ssl_verify self._cert = cert - self._access_token_is_service = None - self._token_is_valid = None - self._token_validation_started = False + self._token_info = TokenInfo(token=token) + self._server_available = None self._server_version = None self._server_version_tuple = None @@ -356,7 +363,7 @@ def __init__( self._as_user_stack = _AsUserStack() # Create session - if self._access_token and create_session: + if self._token_info.token and create_session: self.validate_server_availability() self.create_session() @@ -503,7 +510,7 @@ def access_token(self) -> Optional[str]: Optional[str]: Token string or None if not authorized yet. """ - return self._access_token + return self.self._token_info.token def is_service_user(self) -> bool: """Check if connection is using service API key. @@ -514,7 +521,7 @@ def is_service_user(self) -> bool: """ if not self.has_valid_token: raise ValueError("User is not logged in.") - return bool(self._access_token_is_service) + return bool(self._token_info.is_service) def get_site_id(self) -> Optional[str]: """Site id used for connection. @@ -683,7 +690,7 @@ def set_default_service_username(self, username: Optional[str] = None): "Authentication of connection did not happen yet." ) - if not self._access_token_is_service: + if not self._token_info.is_service: raise ValueError( "Can't set service username. API key is not a service token." ) @@ -717,7 +724,7 @@ def as_username( "Authentication of connection did not happen yet." ) - if not self._access_token_is_service: + if not self._token_info.is_service: if ignore_service_error: yield None return @@ -745,12 +752,12 @@ def is_server_available(self) -> bool: @property def has_valid_token(self) -> bool: - if self._access_token is None: + if self._token_info.token is None: return False - if self._token_is_valid is None: + if self._token_info.is_valid is None: self.validate_token() - return self._token_is_valid + return self._token_info.is_valid def validate_server_availability(self): if not self.is_server_available: @@ -759,44 +766,44 @@ def validate_server_availability(self): ) def validate_token(self) -> bool: - if self._access_token is None: - self._token_is_valid = False + if self._token_info.token is None: + self._token_info.is_valid = False self.close_session() return False - self._token_validation_started = True try: # TODO add other possible validations # - existence of 'user' key in info # - validate that 'site_id' is in 'sites' in info self._get_server_info() + user_info = get_user_info_by_token( self.base_url, - self._access_token, + self._token_info.token, verify=self._ssl_verify, cert=self._cert ) - self._token_is_valid = user_info.is_valid + self._token_info.is_valid = user_info.is_valid is_service = None if user_info.is_valid: is_service = user_info.is_service - self._access_token_is_service = is_service + self._token_info.is_service = is_service + except Exception: - self._token_is_valid = False + self._token_info.is_valid = False self.close_session() self.log.error("Failed to validate token.", exc_info=True) - finally: - self._token_validation_started = False - return self._token_is_valid + + return self._token_info.is_valid def set_token(self, token: Optional[str]): self.reset_token() - self._access_token = token + self._token_info.token = token self.validate_token() def reset_token(self): - self._access_token = None - self._token_is_valid = None + self._token_info.token = None + self._token_info.is_valid = None self.close_session() def create_session( @@ -1128,14 +1135,14 @@ def get_headers( if self._sender is not None: headers["x-sender"] = self._sender - if self._access_token: - if self._access_token_is_service: - headers["X-Api-Key"] = self._access_token + if self._token_info.token: + if self._token_info.is_service: + headers["X-Api-Key"] = self._token_info.token username = self._as_user_stack.username if username: headers["X-as-user"] = username else: - headers["Authorization"] = f"Bearer {self._access_token}" + headers["Authorization"] = f"Bearer {self._token_info.token}" return headers def login( @@ -1146,7 +1153,7 @@ def login( Args: username (str): Username. password (str): Password. - create_session (Optional[bool]): Create session after login. + create_session (bool): Create session after login. Default: True. Raises: @@ -1170,26 +1177,25 @@ def login( self.validate_server_availability() - self._token_validation_started = True - - try: - response = self.post( - "auth/login", - name=username, - password=password - ) - if response.status_code != 200: - _detail = response.data.get("detail") - details = "" - if _detail: - details = f" {_detail}" - - raise AuthenticationError(f"Login failed {details}") + response = self.post( + "auth/login", + name=username, + password=password, + handle_invalid_token=False, + ) + if response.status_code != 200: + _detail = response.data.get("detail") + details = "" + if _detail: + details = f" {_detail}" - finally: - self._token_validation_started = False + raise AuthenticationError(f"Login failed {details}") - self._access_token = response["token"] + self._token_info.token = response["token"] + # Should be valid if was just loged in + self._token_info.is_valid = True + # Service token can't be obtained by login, so it is not service token + self._token_info.is_service = False if not self.has_valid_token: raise AuthenticationError("Invalid credentials") @@ -1198,7 +1204,7 @@ def login( self.create_session() def logout(self, soft: bool = False): - if self._access_token: + if self._token_info.token: if not soft: self._logout() self.reset_token() @@ -1458,7 +1464,7 @@ def _endpoint_to_url( return f"{base_url}/{endpoint}" def _logout(self): - logout_from_server(self._base_url, self._access_token) + logout_from_server(self._base_url, self._token_info.token) def _get_server_info(self) -> dict[str, Any]: """Get server info without a session.""" @@ -1471,46 +1477,57 @@ def _get_server_info(self) -> dict[str, Any]: return response.json() def _get_user_info(self) -> Optional[dict[str, Any]]: - if self._access_token is None: + if self._token_info.token is None: return None - if self._access_token_is_service is not None: - response = self.get("users/me") - if response.status == 200: - return response.data - return None + if self._token_info.is_service is None: + if self._token_info.is_valid is False: + return None - self._access_token_is_service = False - response = self.get("users/me") - if response.status == 200: - return response.data + self.validate_token() + + if self._token_info.is_valid is False: + return None - self._access_token_is_service = True response = self.get("users/me") if response.status == 200: return response.data - - self._access_token_is_service = None return None - def _do_rest_request(self, function, url, **kwargs): + def _do_rest_request( + self, + function: Any, + url: str, + *, + handle_invalid_token: bool = True, + **kwargs + ): kwargs.setdefault("timeout", self.timeout) max_retries = kwargs.get("max_retries", self.max_retries) if max_retries < 1: max_retries = 1 - if self._token_is_valid is False: - raise UnauthorizedError( - "Authentication token was invalidated." + if handle_invalid_token and self._token_info.is_valid is False: + # Return a fake error response if the token is known to be invalid. + # Added to prevent DDOS attack on server when many requests + # with invalid token are send. It is better to return error + # immediately without trying to send a request to server. + # NOTE maybe store last know response data and re-use it? + detail = "Access token is missing" + if self._token_info.is_service: + detail = "Invalid API key" + new_response = RestApiResponse( + None, + {"code": 401, "detail": detail} ) + new_response.status = 401 + return new_response if self._session is None: # Validate token if was not yet validated - # - ignore validation if we're in middle of - # validation if ( - self._token_is_valid is None - and not self._token_validation_started + handle_invalid_token + and self._token_info.is_valid is None ): self.validate_token() @@ -1589,16 +1606,15 @@ def _do_rest_request(self, function, url, **kwargs): if new_response is not None: return new_response + new_response = RestApiResponse(response) if ( - response is not None - and self._token_is_valid - and response.status_code == 401 + handle_invalid_token + and new_response.status_code == 401 + and self._token_info.is_valid ): - self._token_is_valid = False + self._token_info.is_valid = False self.close_session() - self._trigger_on_invalidate_callbacks() - new_response = RestApiResponse(response) self.log.debug(f"Response {str(new_response)}") return new_response From bba0f17cc09d933a66989bd5e4cd7cea576a6994 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:39:03 +0200 Subject: [PATCH 08/24] few overall fixes --- ayon_api/server_api.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 0a935dfb7..d7e93eaf2 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -887,7 +887,7 @@ def get_info(self) -> dict[str, Any]: dict[str, Any]: Information from server. """ - if self._session is None: + if self._token_info.is_valid is None: return self._get_server_info() response = self.get("info") @@ -1464,7 +1464,8 @@ def _endpoint_to_url( return f"{base_url}/{endpoint}" def _logout(self): - logout_from_server(self._base_url, self._token_info.token) + if self._token_info.is_valid: + logout_from_server(self._base_url, self._token_info.token) def _get_server_info(self) -> dict[str, Any]: """Get server info without a session.""" @@ -1477,18 +1478,17 @@ def _get_server_info(self) -> dict[str, Any]: return response.json() def _get_user_info(self) -> Optional[dict[str, Any]]: - if self._token_info.token is None: + if ( + self._token_info.token is None + or self._token_info.is_valid is False + ): return None if self._token_info.is_service is None: + self.validate_token() if self._token_info.is_valid is False: return None - self.validate_token() - - if self._token_info.is_valid is False: - return None - response = self.get("users/me") if response.status == 200: return response.data From 1b01a9d7fbdc43c198b477a21fbbcefc694ac049 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:57:56 +0200 Subject: [PATCH 09/24] mark few functions for deprecation --- ayon_api/utils.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 40505e817..35ef5262c 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import os import re import datetime @@ -16,6 +17,7 @@ from urllib.parse import urlparse, urlencode, ParseResult import typing from typing import Any, Iterable +import warnings from enum import IntEnum import requests @@ -68,6 +70,38 @@ ) ) +@dataclass +class _TimeoutWrapInfo: + func = None + args_pos = 2 + + +def _timeout_kwarg_deprecation(arg): + """Decorator to add timeout kwarg to function.""" + # TODO remove this deprecation + wrap_info = _TimeoutWrapInfo() + + def wrapper(*args, **kwargs): + if len(args) > wrap_info.args_pos: + warnings.warn( + "Timeout was passed as a positional argument please" + " use timeout=... keyword argument instead. This will stop" + " working in future versions on ayon-api.", + category=FutureWarning, + stacklevel=2, + ) + return wrap_info.func(*args, **kwargs) + + if not isinstance(arg, int): + wrap_info.func = arg + return functools.wraps(arg)(wrapper) + + wrap_info.args_pos = arg + def main_wrapper(func): + wrap_info.func = func + return functools.wraps(func)(wrapper) + return main_wrapper + class SortOrder(IntEnum): """Sort order for GraphQl requests.""" @@ -588,6 +622,7 @@ def _try_connect_to_server( return None +@_timeout_kwarg_deprecation(3) def login_to_server( url: str, username: str, @@ -629,6 +664,7 @@ def login_to_server( return token +@_timeout_kwarg_deprecation def logout_from_server( url: str, token: str, @@ -734,6 +770,7 @@ def get_user_info_by_token( return output +@_timeout_kwarg_deprecation def get_user_by_token( url: str, token: str, @@ -766,6 +803,7 @@ def get_user_by_token( return None +@_timeout_kwarg_deprecation def is_token_valid( url: str, token: str, @@ -798,6 +836,7 @@ def is_token_valid( return user_info.is_valid +@_timeout_kwarg_deprecation(1) def validate_url( url: str, timeout: int | None = None, From a7ad9da4eccb5ad8011842d58b0aee863e06ff6c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:00:35 +0200 Subject: [PATCH 10/24] pass timeout --- ayon_api/server_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index d7e93eaf2..ee6a56baa 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -781,7 +781,8 @@ def validate_token(self) -> bool: self.base_url, self._token_info.token, verify=self._ssl_verify, - cert=self._cert + cert=self._cert, + timeout=self.timeout, ) self._token_info.is_valid = user_info.is_valid is_service = None @@ -1473,6 +1474,7 @@ def _get_server_info(self) -> dict[str, Any]: f"{self._rest_url}/info", cert=self._cert, verify=self._ssl_verify, + timeout=self.timeout, ) response.raise_for_status() return response.json() From 2cb728d6bd211c7b157f95d721d44d76ccc48be5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:00:54 +0200 Subject: [PATCH 11/24] fix typo --- ayon_api/server_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index ee6a56baa..7c5c80739 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1193,7 +1193,7 @@ def login( raise AuthenticationError(f"Login failed {details}") self._token_info.token = response["token"] - # Should be valid if was just loged in + # Should be valid if was just logged in self._token_info.is_valid = True # Service token can't be obtained by login, so it is not service token self._token_info.is_service = False From 9eb311e54359678ba22abfc2dcfd396e4111aef7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:22:33 +0200 Subject: [PATCH 12/24] store the unauthorized response --- ayon_api/server_api.py | 12 ++++++++---- ayon_api/utils.py | 10 ++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7c5c80739..a3641aa5b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -220,6 +220,7 @@ class TokenInfo: token: str | None = None is_valid: bool | None = None is_service: bool | None = None + unauthorized_response: requests.Response | None = None class ServerAPI( @@ -768,6 +769,7 @@ def validate_server_availability(self): def validate_token(self) -> bool: if self._token_info.token is None: self._token_info.is_valid = False + self._token_info.unauthorized_response = None self.close_session() return False @@ -785,6 +787,7 @@ def validate_token(self) -> bool: timeout=self.timeout, ) self._token_info.is_valid = user_info.is_valid + self._token_info.unauthorized_response = user_info.response is_service = None if user_info.is_valid: is_service = user_info.is_service @@ -792,6 +795,7 @@ def validate_token(self) -> bool: except Exception: self._token_info.is_valid = False + self._token_info.unauthorized_response = None self.close_session() self.log.error("Failed to validate token.", exc_info=True) @@ -1514,10 +1518,9 @@ def _do_rest_request( # Added to prevent DDOS attack on server when many requests # with invalid token are send. It is better to return error # immediately without trying to send a request to server. - # NOTE maybe store last know response data and re-use it? - detail = "Access token is missing" - if self._token_info.is_service: - detail = "Invalid API key" + if self._token_info.unauthorized_response is not None: + return RestApiResponse(self._token_info.unauthorized_response) + new_response = RestApiResponse( None, {"code": 401, "detail": detail} @@ -1615,6 +1618,7 @@ def _do_rest_request( and self._token_info.is_valid ): self._token_info.is_valid = False + self._token_info.unauthorized_response = response self.close_session() self.log.debug(f"Response {str(new_response)}") diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 35ef5262c..6e5a4b388 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -697,8 +697,7 @@ class UserInfo: """User information.""" is_valid: bool = False is_service: bool = False - content: bytes = b"" - data: dict[str, Any] = field(default_factory=dict) + response: requests.Response | None = None def get_user_info_by_token( @@ -754,16 +753,11 @@ def get_user_info_by_token( verify=verify, cert=cert, ) - try: - data = response.json() - except Exception: - data = {} output = UserInfo( is_valid=response.status_code == 200, is_service=is_service, - data=data, - content=response.content, + response=response, ) if output.is_valid: break From 2a3ce1658e94e3e35f3925b876f4bf3f5dc1d9a7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:24:09 +0200 Subject: [PATCH 13/24] mark ayon api errors with prefix --- ayon_api/server_api.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a3641aa5b..205de2c23 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1523,7 +1523,7 @@ def _do_rest_request( new_response = RestApiResponse( None, - {"code": 401, "detail": detail} + {"code": 401, "detail": "AYON api error: Invalid API key"} ) new_response.status = 401 return new_response @@ -1569,7 +1569,8 @@ def _do_rest_request( except ConnectionRefusedError: if retry_idx == 0: self.log.warning( - "Connection error happened.", exc_info=True + "AYON api error: Connection error happened.", + exc_info=True, ) # Server may be restarting @@ -1577,7 +1578,8 @@ def _do_rest_request( None, { "detail": ( - "Unable to connect the server. Connection refused" + "AYON api error: Unable to connect the server." + " Connection refused" ) } ) @@ -1586,21 +1588,23 @@ def _do_rest_request( # Connection timed out new_response = RestApiResponse( None, - {"detail": "Connection timed out."} + {"detail": "AYON api error: Connection timed out."} ) except requests.exceptions.ConnectionError: # Log warning only on last attempt if retry_idx == 0: self.log.warning( - "Connection error happened.", exc_info=True + "AYON api error: Connection error happened.", + exc_info=True ) new_response = RestApiResponse( None, { "detail": ( - "Unable to connect the server. Connection error" + "AYON api error: Unable to connect the server." + " Connection error." ) } ) From 09a58ce5de339e55f3ff9548440b388fa7e33dc3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:24:45 +0200 Subject: [PATCH 14/24] reset is_service too --- ayon_api/server_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 205de2c23..92540ec15 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -808,6 +808,7 @@ def set_token(self, token: Optional[str]): def reset_token(self): self._token_info.token = None + self._token_info.is_service = None self._token_info.is_valid = None self.close_session() From 347293d011b233aa8342b4104dbb1ebd0421575f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:25:04 +0200 Subject: [PATCH 15/24] change how errors are handled in rest api response --- ayon_api/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 6e5a4b388..b00185964 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -207,11 +207,11 @@ def ok(self) -> bool: def raise_for_status(self, message=None): if self._response is None: if self._data and self._data.get("detail"): + if self.status_code == 401: + raise UnauthorizedError(self._data["detail"]) raise ServerError(self._data["detail"]) raise ValueError("Response is not available.") - if self.status_code == 401: - raise UnauthorizedError("Missing or invalid authentication token") try: self._response.raise_for_status() except requests.exceptions.HTTPError as exc: @@ -232,6 +232,8 @@ def raise_for_status(self, message=None): detail = self.data.get("detail") if detail: message = f"{message} ({detail})" + if self.status_code == 401: + raise UnauthorizedError(message, exc.response) raise HTTPRequestError(message, exc.response) def __enter__(self, *args, **kwargs): From 7358aedbfc669b47ff8a587f04d3984927e263ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:33:32 +0200 Subject: [PATCH 16/24] use raw_post when using 'handle_invalid_token' --- ayon_api/server_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 92540ec15..220ef7cc0 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1183,10 +1183,12 @@ def login( self.validate_server_availability() - response = self.post( + response = self.raw_post( "auth/login", - name=username, - password=password, + json=dict( + name=username, + password=password, + ), handle_invalid_token=False, ) if response.status_code != 200: From 78f38427371058eed2a484f7af636f1adb57b52b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:39:21 +0200 Subject: [PATCH 17/24] fix doubled self --- ayon_api/server_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 220ef7cc0..d09601e80 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -511,7 +511,7 @@ def access_token(self) -> Optional[str]: Optional[str]: Token string or None if not authorized yet. """ - return self.self._token_info.token + return self._token_info.token def is_service_user(self) -> bool: """Check if connection is using service API key. From 0485ad6f347a7a787020d45a2dfbc783550c6572 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:41:45 +0200 Subject: [PATCH 18/24] pass timeout to requests request --- ayon_api/server_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index d09601e80..cfefc3289 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -746,7 +746,8 @@ def is_server_available(self) -> bool: response = requests.get( self._base_url, cert=self._cert, - verify=self._ssl_verify + verify=self._ssl_verify, + timeout=self.timeout, ) self._server_available = response.status_code == 200 return self._server_available From 7d2c512ec570d82269c16fe91f18f6d3a40510d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:41:57 +0200 Subject: [PATCH 19/24] fix return type in 'get_user_info_by_token' --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index b00185964..fb7f8cc32 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -723,7 +723,7 @@ def get_user_info_by_token( 'get_default_timeout' is used if not specified. Returns: - dict[str, Any] | None: User information if url and token are valid. + UserInfo: User information if url and token are valid. """ output = UserInfo() From 865861ac5d6ef6aee6b546a5088a5dabf0f87a05 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:46:58 +0200 Subject: [PATCH 20/24] unset more attributes --- ayon_api/server_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index cfefc3289..60a5d11dc 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -796,6 +796,7 @@ def validate_token(self) -> bool: except Exception: self._token_info.is_valid = False + self._token_info.is_service = None self._token_info.unauthorized_response = None self.close_session() self.log.error("Failed to validate token.", exc_info=True) @@ -811,6 +812,7 @@ def reset_token(self): self._token_info.token = None self._token_info.is_service = None self._token_info.is_valid = None + self._token_info.unauthorized_response = None self.close_session() def create_session( From 196e7cbba5a8b5032371736afc3307cf2a136395 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:49:20 +0200 Subject: [PATCH 21/24] remove unsed import --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index fb7f8cc32..ab2e4c7dd 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -4,7 +4,7 @@ import os import re import datetime -from dataclasses import dataclass, field +from dataclasses import dataclass import copy import logging import json From a1798328ff1efffed7856d16d76795131b38e494 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:54:36 +0200 Subject: [PATCH 22/24] do not add token to headers if is not valid --- ayon_api/server_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 60a5d11dc..e442c6f6a 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1144,7 +1144,7 @@ def get_headers( if self._sender is not None: headers["x-sender"] = self._sender - if self._token_info.token: + if self._token_info.token and self._token_info.is_valid is not False: if self._token_info.is_service: headers["X-Api-Key"] = self._token_info.token username = self._as_user_stack.username From 79cb24296b49d0fb84608a1fc25cd2beeae6cb27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:11:11 +0200 Subject: [PATCH 23/24] don't use raw requests for server info --- ayon_api/server_api.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index e442c6f6a..7347ca231 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -896,10 +896,15 @@ def get_info(self) -> dict[str, Any]: dict[str, Any]: Information from server. """ - if self._token_info.is_valid is None: - return self._get_server_info() + handle_invalid_token = ( + self._token_info.token + and self._token_info.is_valid + ) - response = self.get("info") + response = self.raw_get( + "info", + handle_invalid_token=handle_invalid_token, + ) response.raise_for_status() return response.data @@ -1480,14 +1485,12 @@ def _logout(self): def _get_server_info(self) -> dict[str, Any]: """Get server info without a session.""" - response = requests.get( - f"{self._rest_url}/info", - cert=self._cert, - verify=self._ssl_verify, - timeout=self.timeout, + response = self.raw_get( + "info", + handle_invalid_token=False, ) response.raise_for_status() - return response.json() + return response.data def _get_user_info(self) -> Optional[dict[str, Any]]: if ( From b4323d3866b26794f853b385719a072486d79ae7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:50:11 +0200 Subject: [PATCH 24/24] do not safe-guard token validation --- ayon_api/server_api.py | 46 ++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7347ca231..ee16d8e86 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -774,32 +774,26 @@ def validate_token(self) -> bool: self.close_session() return False - try: - # TODO add other possible validations - # - existence of 'user' key in info - # - validate that 'site_id' is in 'sites' in info - self._get_server_info() - - user_info = get_user_info_by_token( - self.base_url, - self._token_info.token, - verify=self._ssl_verify, - cert=self._cert, - timeout=self.timeout, - ) - self._token_info.is_valid = user_info.is_valid - self._token_info.unauthorized_response = user_info.response - is_service = None - if user_info.is_valid: - is_service = user_info.is_service - self._token_info.is_service = is_service - - except Exception: - self._token_info.is_valid = False - self._token_info.is_service = None - self._token_info.unauthorized_response = None - self.close_session() - self.log.error("Failed to validate token.", exc_info=True) + # TODO add other possible validations + # - existence of 'user' key in info + # - validate that 'site_id' is in 'sites' in info + + # Check server url + self._get_server_info() + + user_info = get_user_info_by_token( + self.base_url, + self._token_info.token, + verify=self._ssl_verify, + cert=self._cert, + timeout=self.timeout, + ) + self._token_info.is_valid = user_info.is_valid + self._token_info.unauthorized_response = user_info.response + is_service = None + if user_info.is_valid: + is_service = user_info.is_service + self._token_info.is_service = is_service return self._token_info.is_valid